Skip to content

Commit 0fe2187

Browse files
GaetanVDB07itomek
andauthored
Improve Agent UI active agent context (#1578)
## Summary Updates the Agent UI chat view so an empty chat reflects the selected agent, and adds a compact active-agent indicator to the task header. ## Why The Agent UI previously showed a generic empty state even when a specific agent was selected. That made it harder to confirm which agent would handle the next prompt and hid useful metadata like the agent description, starter prompts, tags, and tool count. ## Linked issue Closes #1526 ## Changes - Resolve the displayed agent from the session agent or active agent selection. - Show agent-specific empty-state icon, title, description, conversation starters, and capability summary. - Add a compact active-agent header indicator with the agent icon, name, and capability summary. - Add a focused Vitest coverage case for the agent-aware empty state and header indicator. ## Test plan - [x] `npm run test` from `src/gaia/apps/webui` passed: 4 files, 41 tests. - [x] `npm run build` from `src/gaia/apps/webui` passed. - [x] `git diff --check` passed. - [x] `python util/lint.py --all` was run. Black, isort, Flake8, import validation, dependabot validation, and doc version checks passed. The all-repo Pylint gate reported existing Python analyzer errors unrelated to this Agent UI change: - `src/gaia/agents/base/console.py:933` - `src/gaia/installer/lemonade_installer.py:630` - `src/gaia/mcp/servers/agent_ui_mcp.py:470` - [x] `python -m pytest tests/unit` was run after installing the local dev/API/MCP extras. The suite collected 5424 tests, then hit unrelated baseline/env failures and timed out after 10 minutes at 78%. ## Checklist - [x] I have linked a GitHub issue above (`Closes #N` / `Fixes #N` / `Refs #N`). - [x] I have described **why** this change is being made, not just what changed. - [x] I have run linting and tests locally (`python util/lint.py --all`, `pytest tests/unit/`), with the repo-wide Python baseline notes included above. - [x] Documentation not updated; this is a small contextual UI affordance with no setup, API, or workflow documentation change. Co-authored-by: Tomasz Iniewicz <itomek@users.noreply.github.com>
1 parent a55e23b commit 0fe2187

3 files changed

Lines changed: 213 additions & 8 deletions

File tree

src/gaia/apps/webui/src/components/ChatView.css

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,50 @@
4242
.task-header-left { display: flex; align-items: center; gap: 8px; min-width: 0; }
4343
.task-header-right { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
4444

45+
.active-agent-indicator {
46+
display: inline-flex;
47+
align-items: center;
48+
gap: 7px;
49+
min-width: 0;
50+
max-width: 260px;
51+
padding: 4px 9px;
52+
border-radius: var(--radius-sm);
53+
border: 1px solid var(--border);
54+
background: var(--bg-tertiary);
55+
color: var(--text-secondary);
56+
}
57+
58+
.active-agent-icon {
59+
flex-shrink: 0;
60+
color: var(--amd-red);
61+
}
62+
63+
.active-agent-copy {
64+
display: flex;
65+
flex-direction: column;
66+
min-width: 0;
67+
gap: 1px;
68+
line-height: 1.1;
69+
}
70+
71+
.active-agent-name {
72+
overflow: hidden;
73+
text-overflow: ellipsis;
74+
white-space: nowrap;
75+
font-size: 11px;
76+
font-weight: 650;
77+
color: var(--text-primary);
78+
}
79+
80+
.active-agent-capabilities {
81+
overflow: hidden;
82+
text-overflow: ellipsis;
83+
white-space: nowrap;
84+
font-size: 9px;
85+
font-family: var(--font-mono);
86+
color: var(--text-muted);
87+
}
88+
4589
.task-title {
4690
font-size: 13px;
4791
font-weight: 600;
@@ -260,6 +304,11 @@
260304
opacity: 0.3;
261305
}
262306

307+
.empty-task-icon.agent-aware {
308+
color: var(--amd-red);
309+
opacity: 0.8;
310+
}
311+
263312
.empty-task-title {
264313
font-size: 20px;
265314
font-weight: 700;
@@ -273,11 +322,27 @@
273322
font-size: 14px;
274323
font-family: var(--font-sans);
275324
color: var(--text-muted);
276-
margin-bottom: 32px;
325+
margin-bottom: 14px;
277326
line-height: 1.75;
278327
max-width: 440px;
279328
}
280329

330+
.empty-task-desc-spaced {
331+
margin-bottom: 32px;
332+
}
333+
334+
.empty-task-capabilities {
335+
margin-bottom: 28px;
336+
padding: 4px 10px;
337+
border: 1px solid var(--border);
338+
border-radius: var(--radius-sm);
339+
background: var(--bg-tertiary);
340+
color: var(--text-muted);
341+
font-size: 10px;
342+
font-family: var(--font-mono);
343+
white-space: nowrap;
344+
}
345+
281346
.empty-task-suggestions {
282347
display: flex;
283348
flex-wrap: wrap;

src/gaia/apps/webui/src/components/ChatView.tsx

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import type { GaiaNotification } from '../types/agent';
1010
import * as api from '../services/api';
1111
import { log } from '../utils/logger';
1212
import { bugReportUrl } from './UnsupportedFeature';
13-
import type { Message, StreamEvent, AgentStep, Attachment, Session } from '../types';
13+
import { getAgentIcon } from './agentIcons';
14+
import type { Message, StreamEvent, AgentStep, Attachment, Session, AgentInfo } from '../types';
1415

1516
import './ChatView.css';
1617
import DashboardProgress from './DashboardProgress';
@@ -23,6 +24,17 @@ const EMPTY_SUGGESTIONS = [
2324
'Show my recent files',
2425
];
2526

27+
function formatAgentCapabilities(agent?: AgentInfo): string {
28+
if (!agent) return '';
29+
const parts: string[] = [];
30+
if (typeof agent.tools_count === 'number' && agent.tools_count > 0) {
31+
parts.push(`${agent.tools_count} ${agent.tools_count === 1 ? 'tool' : 'tools'}`);
32+
}
33+
const tags = (agent.tags ?? []).filter(Boolean).slice(0, 3);
34+
if (tags.length > 0) parts.push(tags.join(', '));
35+
return parts.join(' | ');
36+
}
37+
2638
/**
2739
* Safety-net regex to strip raw tool-call JSON from streaming content.
2840
*
@@ -195,6 +207,15 @@ export function ChatView({ sessionId, onCreateAgent, onAgentChange }: ChatViewPr
195207
// Resolve human-readable agent names for the message header
196208
const sessionAgentName = agents.find((a) => a.id === session?.agent_type)?.name;
197209
const activeAgentName = agents.find((a) => a.id === activeAgentId)?.name;
210+
const displayedAgent = useMemo(
211+
() => agents.find((a) => a.id === displayedAgentId),
212+
[agents, displayedAgentId],
213+
);
214+
const displayedAgentCapabilities = useMemo(
215+
() => formatAgentCapabilities(displayedAgent),
216+
[displayedAgent],
217+
);
218+
const DisplayedAgentIcon = getAgentIcon(displayedAgent?.icon);
198219

199220
// Close agent picker on outside click
200221
useEffect(() => {
@@ -1294,6 +1315,9 @@ export function ChatView({ sessionId, onCreateAgent, onAgentChange }: ChatViewPr
12941315
};
12951316

12961317
const showEmptyState = !isLoadingMessages && messages.length === 0 && !isStreaming;
1318+
const emptyStateSuggestions = displayedAgent?.conversation_starters?.length
1319+
? displayedAgent.conversation_starters
1320+
: EMPTY_SUGGESTIONS;
12971321

12981322
// Pre-compute per-message latency: time from preceding user message to each
12991323
// assistant message. O(N) single pass, avoids repeated backward scans in render.
@@ -1347,6 +1371,23 @@ export function ChatView({ sessionId, onCreateAgent, onAgentChange }: ChatViewPr
13471371
)}
13481372
</div>
13491373
<div className="task-header-right">
1374+
{displayedAgent && (
1375+
<div
1376+
className="active-agent-indicator"
1377+
aria-label="Active agent"
1378+
title={displayedAgentCapabilities
1379+
? `${displayedAgent.name}: ${displayedAgentCapabilities}`
1380+
: displayedAgent.name}
1381+
>
1382+
<DisplayedAgentIcon size={13} className="active-agent-icon" />
1383+
<span className="active-agent-copy">
1384+
<span className="active-agent-name">{displayedAgent.name}</span>
1385+
{displayedAgentCapabilities && (
1386+
<span className="active-agent-capabilities">{displayedAgentCapabilities}</span>
1387+
)}
1388+
</span>
1389+
</div>
1390+
)}
13501391
<span
13511392
className={`model-badge ${!systemStatus?.model_loaded ? 'no-model' : ''}`}
13521393
title={
@@ -1495,15 +1536,24 @@ export function ChatView({ sessionId, onCreateAgent, onAgentChange }: ChatViewPr
14951536

14961537
{showEmptyState && (
14971538
<div className="empty-task">
1498-
<div className="empty-task-icon">
1499-
<MessageSquare size={36} strokeWidth={1.2} />
1539+
<div className={`empty-task-icon${displayedAgent ? ' agent-aware' : ''}`}>
1540+
{displayedAgent ? (
1541+
<DisplayedAgentIcon size={36} strokeWidth={1.2} />
1542+
) : (
1543+
<MessageSquare size={36} strokeWidth={1.2} />
1544+
)}
15001545
</div>
1501-
<h4 className="empty-task-title">What can I help you with?</h4>
1502-
<p className="empty-task-desc">
1503-
Ask about your documents, search files, or analyze data &mdash; powered by local AI.
1546+
<h4 className="empty-task-title">{displayedAgent?.name || 'What can I help you with?'}</h4>
1547+
<p className={`empty-task-desc${displayedAgentCapabilities ? '' : ' empty-task-desc-spaced'}`}>
1548+
{displayedAgent?.description || (
1549+
<>Ask about your documents, search files, or analyze data &mdash; powered by local AI.</>
1550+
)}
15041551
</p>
1552+
{displayedAgentCapabilities && (
1553+
<div className="empty-task-capabilities">{displayedAgentCapabilities}</div>
1554+
)}
15051555
<div className="empty-task-suggestions">
1506-
{EMPTY_SUGGESTIONS.map((s) => (
1556+
{emptyStateSuggestions.map((s) => (
15071557
<button
15081558
key={s}
15091559
className="empty-task-chip"
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
import { render, screen, within } from '@testing-library/react';
5+
import { beforeEach, describe, expect, it, vi } from 'vitest';
6+
import { ChatView } from '../ChatView';
7+
import { useChatStore } from '../../stores/chatStore';
8+
import type { AgentInfo, Session } from '../../types';
9+
import * as api from '../../services/api';
10+
11+
vi.mock('../../services/api');
12+
13+
const mockedApi = vi.mocked(api);
14+
15+
const SESSION: Session = {
16+
id: 'session-1',
17+
title: 'New Task',
18+
created_at: '2026-06-10T00:00:00Z',
19+
updated_at: '2026-06-10T00:00:00Z',
20+
model: 'qwen',
21+
system_prompt: null,
22+
message_count: 0,
23+
document_ids: [],
24+
agent_type: 'doc',
25+
};
26+
27+
const DOC_AGENT: AgentInfo = {
28+
id: 'doc',
29+
name: 'Doc Agent',
30+
description: 'Search and answer questions from indexed documents.',
31+
source: 'builtin',
32+
conversation_starters: [
33+
'Find contract clauses',
34+
'Summarize this folder',
35+
],
36+
models: [],
37+
tags: ['rag', 'files'],
38+
icon: 'file-text',
39+
tools_count: 3,
40+
};
41+
42+
beforeEach(() => {
43+
vi.clearAllMocks();
44+
mockedApi.getMessages.mockResolvedValue({ messages: [], total: 0 });
45+
mockedApi.listDocuments.mockResolvedValue({
46+
documents: [],
47+
total: 0,
48+
total_size_bytes: 0,
49+
total_chunks: 0,
50+
});
51+
52+
Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', {
53+
configurable: true,
54+
value: vi.fn(),
55+
});
56+
57+
useChatStore.setState({
58+
agents: [DOC_AGENT],
59+
activeAgentId: 'doc',
60+
sessions: [SESSION],
61+
currentSessionId: SESSION.id,
62+
messages: [],
63+
documents: [],
64+
isStreaming: false,
65+
streamingContent: '',
66+
agentSteps: [],
67+
isLoadingMessages: false,
68+
pendingPrompt: null,
69+
systemStatus: null,
70+
});
71+
});
72+
73+
describe('ChatView agent metadata', () => {
74+
it('uses the active agent metadata for the empty state and header indicator', async () => {
75+
render(<ChatView sessionId={SESSION.id} />);
76+
77+
expect(await screen.findByRole('heading', { name: 'Doc Agent' })).toBeInTheDocument();
78+
expect(screen.getByText('Search and answer questions from indexed documents.')).toBeInTheDocument();
79+
expect(screen.getByRole('button', { name: 'Find contract clauses' })).toBeInTheDocument();
80+
expect(screen.getByRole('button', { name: 'Summarize this folder' })).toBeInTheDocument();
81+
expect(screen.queryByRole('button', { name: 'Summarize a document' })).not.toBeInTheDocument();
82+
83+
const capabilitySummaries = screen.getAllByText('3 tools | rag, files');
84+
expect(capabilitySummaries.length).toBeGreaterThan(0);
85+
86+
const headerIndicator = screen.getByLabelText('Active agent');
87+
expect(within(headerIndicator).getByText('Doc Agent')).toBeInTheDocument();
88+
expect(within(headerIndicator).getByText('3 tools | rag, files')).toBeInTheDocument();
89+
});
90+
});

0 commit comments

Comments
 (0)