Skip to content
Open
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 src/gaia/agents/base/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def tool(
func: Callable = None,
*,
atomic: bool = False,
display_label: str | None = None,
**kwargs, # pylint: disable=unused-argument
) -> Callable:
"""
Expand Down Expand Up @@ -74,6 +75,7 @@ def decorator(f: Callable) -> Callable:
"parameters": params,
"function": f,
"atomic": atomic,
"display_label": display_label,
}

# Return the function unchanged
Expand Down Expand Up @@ -110,6 +112,18 @@ def get_tool_display_name(tool_name: str) -> str:
return tool.get("display_name", tool_name)


def get_tool_display_label(tool_name: str) -> str:
"""Return a user-facing label for the tool suitable for UI progress strips.

Prefers the explicit `display_label` provided on the decorator, falls
back to the registry `description`, then finally to the raw tool name.
"""
tool = _TOOL_REGISTRY.get(tool_name)
if not tool:
return None
return tool.get("display_label")


def get_tool_metadata(tool_name: str):
"""Return the full registry entry for a tool, or ``None`` if not found.

Expand Down
10 changes: 10 additions & 0 deletions src/gaia/apps/webui/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { log } from '../utils/logger';
import { bugReportUrl } from './UnsupportedFeature';
import type { Message, StreamEvent, AgentStep, Attachment, Session } from '../types';
import './ChatView.css';
import DashboardProgress from './DashboardProgress';


const EMPTY_SUGGESTIONS = [
Expand Down Expand Up @@ -184,6 +185,7 @@ export function ChatView({ sessionId, onCreateAgent, onAgentChange }: ChatViewPr
const [docsExpanded, setDocsExpanded] = useState(false);
const [deletingMsgId, setDeletingMsgId] = useState<number | null>(null);
const [policyToast, setPolicyToast] = useState<{ tool: string; receiptId?: string } | null>(null);
const [showDashboardProgress, setShowDashboardProgress] = useState(false);
// Agent picker dropdown state
const [agentPickerOpen, setAgentPickerOpen] = useState(false);
const agentPickerRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -1263,6 +1265,11 @@ export function ChatView({ sessionId, onCreateAgent, onAgentChange }: ChatViewPr
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
>
{showDashboardProgress && (
<div className="dashboard-overlay">
<DashboardProgress sessionId={sessionId} onClose={() => setShowDashboardProgress(false)} />
</div>
)}
{/* Header */}
<header className="chat-header">
<div className="chat-header-left">
Expand Down Expand Up @@ -1315,6 +1322,9 @@ export function ChatView({ sessionId, onCreateAgent, onAgentChange }: ChatViewPr
<button className="btn-icon-sm" onClick={handleExport} title="Export" aria-label="Export chat">
<Download size={15} />
</button>
<button className="btn-icon-sm" onClick={() => setShowDashboardProgress((s) => !s)} title="Refresh dashboard" aria-label="Refresh dashboard">
<ArrowDown size={15} />
</button>
<button
className={`notification-center-trigger ${notificationUnreadCount > 0 ? 'has-unread' : ''}`}
onClick={() => setNotificationPanelVisible(!showNotificationPanel)}
Expand Down
128 changes: 128 additions & 0 deletions src/gaia/apps/webui/src/components/DashboardProgress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
// SPDX-License-Identifier: MIT

import React, { useEffect, useRef, useState, useCallback } from 'react';
import { X, Loader2, Trash2 } from 'lucide-react';
import { AgentActivity } from './AgentActivity';
import * as api from '../services/api';
import { log } from '../utils/logger';
import type { StreamEvent, AgentStep } from '../types';
import './ChatView.css';

interface DashboardProgressProps {
sessionId: string;
onClose?: () => void;
}

/**
* Minimal dashboard progress strip that starts a streaming chat request
* and renders agent activity + surfaced tool_result cards as they arrive.
*/
export default function DashboardProgress({ sessionId, onClose }: DashboardProgressProps) {
const [running, setRunning] = useState(false);
const [steps, setSteps] = useState<AgentStep[]>([]);
const [surfaced, setSurfaced] = useState<Array<{ id: number; title?: string; summary?: string }>>([]);
const stepIdRef = useRef(0);
const abortRef = useRef<AbortController | null>(null);

const pushStep = useCallback((step: AgentStep) => {
setSteps((s) => [...s, step]);
}, []);

useEffect(() => {
return () => {
// Cleanup on unmount
if (abortRef.current) {
abortRef.current.abort();
abortRef.current = null;
}
};
}, []);

const start = useCallback(() => {
if (running) return;
setRunning(true);
setSteps([]);
setSurfaced([]);

const controller = api.sendMessageStream(sessionId, 'dashboard:refresh', {
onChunk: (evt: StreamEvent) => {
// ignore raw text chunks
},
onAgentEvent: (evt: StreamEvent) => {
const id = ++stepIdRef.current;
const ts = Date.now();
if (evt.type === 'tool_start') {
pushStep({ id, type: 'tool', label: 'Using tool', tool: evt.tool, detail: evt.detail, active: true, timestamp: ts });
} else if (evt.type === 'thinking') {
pushStep({ id, type: 'thinking', label: 'Thinking', detail: evt.content, active: true, timestamp: ts });
} else if (evt.type === 'tool_result') {
// Mark previous tool step inactive and append result
pushStep({ id, type: 'tool', label: evt.title || 'Tool result', detail: evt.summary, active: false, success: evt.success, timestamp: ts });
// Add surfaced card item for animation
setSurfaced((s) => [...s, { id, title: evt.title, summary: evt.summary }]);
} else if (evt.type === 'status') {
pushStep({ id, type: 'status', label: evt.message || 'Working', detail: evt.message, active: evt.status === 'working', timestamp: ts });
}
},
onDone: (evt) => {
log.stream.info('Dashboard stream done', evt);
setRunning(false);
},
onError: (err) => {
log.stream.error('Dashboard stream error', err);
setRunning(false);
}
});

abortRef.current = controller;
}, [sessionId, running, pushStep]);

const handleCancel = useCallback(async () => {
try {
await api.cancelStream(sessionId);
setRunning(false);
if (abortRef.current) {
abortRef.current.abort();
abortRef.current = null;
}
} catch (err) {
log.stream.error('Cancel request failed', err);
}
}, [sessionId]);

return (
<div className="dashboard-progress">
<div className="dashboard-progress-bar">
<div className="dashboard-progress-left">
<Loader2 className={`spin ${running ? 'visible' : 'hidden'}`} />
<strong className="dashboard-title">Refresh progress</strong>
</div>
<div className="dashboard-controls">
<button className="btn btn-plain" onClick={() => start()} disabled={running} title="Start refresh">Start</button>
<button className="btn btn-plain" onClick={handleCancel} disabled={!running} title="Cancel refresh">Cancel</button>
<button className="btn btn-plain" onClick={() => { if (onClose) onClose(); }} title="Close">Close</button>
</div>
</div>

<div className="dashboard-body">
<div className="dashboard-steps">
<AgentActivity steps={steps} isActive={running} variant="inline" />
</div>
<div className="dashboard-surfaced">
<h4>Surfaced</h4>
<div className="surfaced-list">
{surfaced.map((c) => (
<div key={c.id} className="surfaced-card">
<div className="surfaced-card-body">
<div className="surfaced-card-title">{c.title || 'Result'}</div>
<div className="surfaced-card-summary">{c.summary}</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
);
}
5 changes: 5 additions & 0 deletions src/gaia/apps/webui/src/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,11 @@ export async function confirmTool(sessionId: string, approved: boolean): Promise
return apiFetch('POST', '/chat/confirm-tool', { session_id: sessionId, approved });
}

/** Cancel an active streaming chat session (sets SSE handler cancelled flag). */
export async function cancelStream(sessionId: string): Promise<{ cancelled: boolean }> {
return apiFetch('POST', '/chat/cancel', { session_id: sessionId });
}

// -- Documents -----------------------------------------------------------------

export async function listDocuments(): Promise<{ documents: Document[]; total: number; total_size_bytes: number; total_chunks: number }> {
Expand Down
22 changes: 22 additions & 0 deletions src/gaia/ui/routers/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,25 @@ async def confirm_tool(request: ToolConfirmRequest):
)
handler.resolve_tool_confirmation(request.approved)
return {"status": "ok", "approved": request.approved}


class CancelStreamRequest(BaseModel):
session_id: str


@router.post("/api/chat/cancel")
async def cancel_stream(request: CancelStreamRequest):
"""Cancel an active streaming chat session by setting its SSE handler cancelled flag.

This allows the frontend's Cancel button to gracefully request cancellation
without tearing down the HTTP connection.
"""
from .._chat_helpers import _active_sse_handlers

handler = _active_sse_handlers.get(request.session_id)
if not handler:
raise HTTPException(
status_code=404, detail="No active chat session found for this session ID"
)
handler.cancelled.set()
return {"status": "ok", "cancelled": True}
7 changes: 5 additions & 2 deletions src/gaia/ui/sse_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from typing import Any, Dict, List, Optional

from gaia.agents.base.console import OutputHandler
from gaia.agents.base.tools import get_tool_metadata
from gaia.agents.base.tools import get_tool_display_label, get_tool_metadata

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -205,10 +205,13 @@ def print_tool_usage(self, tool_name: str):
self._tool_count += 1
self._last_tool_name = tool_name
self._tool_start_time = time.monotonic()
# Prefer an explicit display label from the tool registry; fall back
# to the legacy description map in this module when none is provided.
detail_label = get_tool_display_label(tool_name) or _tool_description(tool_name)
event = {
"type": "tool_start",
"tool": tool_name,
"detail": _tool_description(tool_name),
"detail": detail_label,
}
# Attach MCP server name if this is an MCP tool.
# _mcp_server is set by MCPTool.to_gaia_format() during registration
Expand Down
1 change: 0 additions & 1 deletion tests/unit/connectors/test_disconnect_clears_grants.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

from __future__ import annotations

import os
from typing import Any, Dict
from unittest.mock import patch

Expand Down
Loading