Skip to content

Commit 0a466ad

Browse files
committed
feat: dashboard SSE streaming progress (#1007)
1 parent d582278 commit 0a466ad

6 files changed

Lines changed: 118 additions & 4 deletions

File tree

src/gaia/agents/base/tools.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ def tool(
2020
func: Callable = None,
2121
*,
2222
atomic: bool = False,
23+
display_label: str | None = None,
2324
**kwargs, # pylint: disable=unused-argument
2425
) -> Callable:
2526
"""
@@ -67,14 +68,17 @@ def decorator(f: Callable) -> Callable:
6768

6869
params[name] = param_info
6970

70-
# Register the tool with atomic metadata
71-
_TOOL_REGISTRY[tool_name] = {
71+
# Register the tool with atomic metadata and optional display label
72+
entry = {
7273
"name": tool_name,
7374
"description": f.__doc__ or "",
7475
"parameters": params,
7576
"function": f,
7677
"atomic": atomic,
7778
}
79+
if display_label:
80+
entry["display_label"] = display_label
81+
_TOOL_REGISTRY[tool_name] = entry
7882

7983
# Return the function unchanged
8084
return f
@@ -110,6 +114,23 @@ def get_tool_display_name(tool_name: str) -> str:
110114
return tool.get("display_name", tool_name)
111115

112116

117+
def get_tool_display_label(tool_name: str) -> str:
118+
"""Return human-friendly display label for a tool if available.
119+
120+
Falls back to `display_name` (for MCP tools) and finally the raw tool name.
121+
"""
122+
tool = _TOOL_REGISTRY.get(tool_name)
123+
if not tool:
124+
return tool_name
125+
# Preferred explicit label
126+
if tool.get("display_label"):
127+
return tool["display_label"]
128+
# MCP display_name compatibility
129+
if tool.get("display_name"):
130+
return tool["display_name"]
131+
return tool_name
132+
133+
113134
def get_tool_metadata(tool_name: str):
114135
"""Return the full registry entry for a tool, or ``None`` if not found.
115136

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

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useNotificationStore, ALWAYS_ALLOW_TOOLS_KEY } from '../stores/notifica
99
import type { GaiaNotification } from '../types/agent';
1010
import * as api from '../services/api';
1111
import { log } from '../utils/logger';
12+
import ProgressStrip from './ProgressStrip';
1213
import { getSessionHash } from '../utils/format';
1314
import { bugReportUrl } from './UnsupportedFeature';
1415
import type { Message, StreamEvent, AgentStep, Attachment, Session } from '../types';
@@ -121,8 +122,8 @@ function agentEventToStep(event: StreamEvent, stepIdRef: React.MutableRefObject<
121122
case 'tool_start':
122123
return {
123124
id, type: 'tool',
124-
// Label is determined by AgentActivity based on tool name
125-
label: 'Using tool',
125+
// Prefer server-provided human-friendly label when available
126+
label: event.display_label || 'Using tool',
126127
tool: event.tool,
127128
detail: event.detail,
128129
active: true, timestamp: ts,
@@ -175,6 +176,8 @@ export function ChatView({ sessionId, onCreateAgent, onAgentChange }: ChatViewPr
175176
agents, activeAgentId, setActiveAgentId,
176177
} = useChatStore();
177178

179+
const surfacedCards = useChatStore((s) => s.surfacedCards);
180+
178181
const { addNotification } = useNotificationStore();
179182
const pendingPrompt = useChatStore((s) => s.pendingPrompt);
180183

@@ -192,6 +195,8 @@ export function ChatView({ sessionId, onCreateAgent, onAgentChange }: ChatViewPr
192195
const [attachments, setAttachments] = useState<Attachment[]>([]);
193196
const [docsExpanded, setDocsExpanded] = useState(false);
194197
const [deletingMsgId, setDeletingMsgId] = useState<number | null>(null);
198+
// Progress strip state for streaming tool progress
199+
const [progress, setProgress] = useState<{ label?: string; detail?: string; latencyMs?: number; active?: boolean }>({ active: false });
195200
// Agent picker dropdown state
196201
const [agentPickerOpen, setAgentPickerOpen] = useState(false);
197202
const agentPickerRef = useRef<HTMLDivElement>(null);
@@ -838,6 +843,16 @@ export function ChatView({ sessionId, onCreateAgent, onAgentChange }: ChatViewPr
838843
};
839844
}
840845
updateLastToolStep(updates);
846+
// Stream incremental surfaced cards for retrieval/classify results
847+
if (event.result_data?.chunks && event.result_data.chunks.length > 0) {
848+
const addCard = useChatStore.getState().addSurfacedCard;
849+
for (const c of event.result_data.chunks) {
850+
addCard({ id: `${Date.now()}-${Math.random().toString(36).slice(2,8)}`, preview: c.preview, content: c.content, source: c.source, score: c.score });
851+
}
852+
}
853+
if (typeof event.latency_ms === 'number') {
854+
setProgress((p) => ({ ...p, latencyMs: event.latency_ms }));
855+
}
841856
return;
842857
}
843858
// Tool args update the last TOOL step with detail
@@ -911,6 +926,7 @@ export function ChatView({ sessionId, onCreateAgent, onAgentChange }: ChatViewPr
911926
addAgentStep(step);
912927
if (event.type === 'tool_start') {
913928
toolOccurredRef.current = true;
929+
setProgress({ label: event.display_label || event.tool || 'Using tool', detail: event.detail, active: true });
914930
}
915931
}
916932
},
@@ -954,6 +970,9 @@ export function ChatView({ sessionId, onCreateAgent, onAgentChange }: ChatViewPr
954970
clearStreamContent();
955971
clearAgentSteps();
956972

973+
// Clear progress strip
974+
setProgress((p) => ({ ...p, active: false }));
975+
957976
// Refocus input so user can immediately type the next message
958977
if (inputRef.current) inputRef.current.focus();
959978

@@ -1128,6 +1147,19 @@ export function ChatView({ sessionId, onCreateAgent, onAgentChange }: ChatViewPr
11281147
}).catch(() => {
11291148
log.ui.warn('Clipboard write failed');
11301149
});
1150+
// Keep a reference so UI can cancel the stream
1151+
abortRef.current = controller;
1152+
1153+
const handleCancel = () => {
1154+
try {
1155+
if (abortRef.current) {
1156+
abortRef.current.abort();
1157+
}
1158+
} catch (err) {
1159+
console.error('Failed to abort stream', err);
1160+
}
1161+
setProgress((p) => ({ ...p, active: false }));
1162+
};
11311163
}, [sessionId]);
11321164

11331165
// Title editing
@@ -1297,6 +1329,28 @@ export function ChatView({ sessionId, onCreateAgent, onAgentChange }: ChatViewPr
12971329
</div>
12981330
</header>
12991331

1332+
{/* Progress strip (shows current tool/status during streaming) */}
1333+
{progress?.active && (
1334+
<div style={{ padding: '8px 12px' }}>
1335+
<ProgressStrip label={progress.label} detail={progress.detail} latencyMs={progress.latencyMs} active={progress.active} onCancel={() => { if (abortRef.current) abortRef.current.abort(); setProgress((p) => ({ ...p, active: false })); }} />
1336+
</div>
1337+
)}
1338+
1339+
{/* Surfaced incremental cards (streamed from tool_result events) */}
1340+
{surfacedCards && surfacedCards.length > 0 && (
1341+
<div className="surfaced-panel">
1342+
<div className="surfaced-header">Surfaced</div>
1343+
<div className="surfaced-cards">
1344+
{surfacedCards.map((c: any) => (
1345+
<div key={c.id} className="surfaced-card">
1346+
<div className="surfaced-preview">{c.preview}</div>
1347+
<div className="surfaced-source">{c.source}</div>
1348+
</div>
1349+
))}
1350+
</div>
1351+
</div>
1352+
)}
1353+
13001354
{/* Indexed documents context bar — shows only docs attached to this session */}
13011355
{sessionDocs.length > 0 && (() => {
13021356
// Sort by most recently accessed (last_accessed_at), falling back to indexed_at

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

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React from 'react';
2+
import './ProgressStrip.css';
3+
4+
interface Props {
5+
label?: string;
6+
detail?: string;
7+
latencyMs?: number;
8+
onCancel?: () => void;
9+
active?: boolean;
10+
}
11+
12+
export default function ProgressStrip({ label, detail, latencyMs, onCancel, active }: Props) {
13+
return (
14+
<div className={`progress-strip ${active ? 'active' : 'idle'}`} role="region" aria-live="polite">
15+
<div className="progress-main">
16+
<div className="progress-label">{label || 'Working'}</div>
17+
{detail && <div className="progress-detail">{detail}</div>}
18+
</div>
19+
<div className="progress-meta">
20+
{typeof latencyMs === 'number' && <div className="progress-latency">{latencyMs} ms</div>}
21+
{onCancel && (
22+
<button className="btn-icon progress-cancel" onClick={onCancel} aria-label="Cancel">
23+
Cancel
24+
</button>
25+
)}
26+
</div>
27+
</div>
28+
);
29+
}

src/gaia/apps/webui/src/stores/chatStore.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,12 @@ export const useChatStore = create<ChatState>((set, get) => ({
204204
documents: [],
205205
setDocuments: (docs) => set({ documents: docs }),
206206

207+
// Surfaced cards (incremental results shown on the dashboard)
208+
surfacedCards: [],
209+
setSurfacedCards: (cards: any[]) => set({ surfacedCards: cards }),
210+
addSurfacedCard: (card: any) => set((state) => ({ surfacedCards: [...state.surfacedCards, card] })),
211+
clearSurfacedCards: () => set({ surfacedCards: [] }),
212+
207213
// Connection / system status
208214
systemStatus: null,
209215
backendConnected: true, // Assume connected until proven otherwise

src/gaia/ui/sse_handler.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,9 @@ def print_tool_usage(self, tool_name: str):
216216
mcp_server = meta.get("_mcp_server")
217217
if mcp_server:
218218
event["mcp_server"] = mcp_server
219+
# Optional human-friendly label for frontend progress strips
220+
if meta.get("display_label"):
221+
event["display_label"] = meta.get("display_label")
219222
self._emit(event)
220223

221224
def print_tool_complete(self):

0 commit comments

Comments
 (0)