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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "mcp-cli"
version = "0.18"
version = "0.19"
description = "A cli for the Model Context Provider"
requires-python = ">=3.11"
readme = "README.md"
Expand Down
46 changes: 37 additions & 9 deletions src/mcp_cli/dashboard/bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,12 +236,25 @@ async def on_app_launched(self, app_info: Any) -> None:
),
"timestamp": _now(),
}
self._running_apps[app_info.tool_name] = payload
# Key by resource_uri when available so different tools sharing
# the same resource replace each other rather than accumulating.
app_key = app_info.resource_uri or app_info.tool_name
self._running_apps[app_key] = payload
await self._broadcast(_envelope("APP_LAUNCHED", payload))

async def on_app_closed(self, tool_name: str) -> None:
"""Notify dashboard that an MCP App closed."""
self._running_apps.pop(tool_name, None)
# Try removing by tool_name first, then scan by resource_uri
if tool_name not in self._running_apps:
to_remove = [
k
for k, v in self._running_apps.items()
if v.get("tool_name") == tool_name
]
for k in to_remove:
self._running_apps.pop(k, None)
else:
self._running_apps.pop(tool_name, None)
await self._broadcast(
_envelope(
"APP_CLOSED",
Expand Down Expand Up @@ -280,7 +293,10 @@ async def _on_client_connected(self, ws: Any) -> None:
if self._view_registry:
await ws.send(
_json.dumps(
_envelope("VIEW_REGISTRY", {"views": self._view_registry})
_envelope(
"VIEW_REGISTRY",
{"agent_id": self.agent_id, "views": self._view_registry},
)
)
)
# CONFIG_STATE (model, provider, servers, system prompt preview)
Expand Down Expand Up @@ -614,8 +630,8 @@ async def _handle_switch_session(self, msg: dict[str, Any]) -> None:
if ctx.conversation_history:
try:
ctx.save_session()
except Exception:
pass
except Exception as exc:
logger.warning("Failed to save current session before switch: %s", exc)

# Clear and load the target session
try:
Expand Down Expand Up @@ -757,9 +773,19 @@ async def request_tool_approval(
"timestamp": _now(),
}
# Create a future that the tool processor can await
fut: asyncio.Future[bool] = asyncio.get_running_loop().create_future()
loop = asyncio.get_running_loop()
fut: asyncio.Future[bool] = loop.create_future()
self._pending_approvals[call_id] = fut
await self._broadcast(_envelope("TOOL_APPROVAL_REQUEST", payload))

# Auto-deny after 5 minutes if no response (prevents hanging forever)
def _timeout() -> None:
if not fut.done():
logger.warning("Tool approval for %s timed out after 5m", tool_name)
fut.set_result(False)
self._pending_approvals.pop(call_id, None)

loop.call_later(300, _timeout)
return fut

async def _handle_tool_approval_response(self, msg: dict[str, Any]) -> None:
Expand Down Expand Up @@ -1064,9 +1090,11 @@ def _build_activity_history(self) -> list[dict[str, Any]] | None:
"result": result_content,
"error": None,
"success": True,
"arguments": self._serialise(arguments)
if arguments
else None,
"arguments": (
self._serialise(arguments)
if arguments
else None
),
},
}
)
Expand Down
3 changes: 2 additions & 1 deletion src/mcp_cli/dashboard/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ async def broadcast(self, msg: dict[str, Any]) -> None:
for client in list(self._clients):
try:
await client.send(payload)
except Exception:
except Exception as exc:
logger.debug("Failed to send to client, removing: %s", exc)
dead.append(client)
for c in dead:
self._clients.discard(c)
Expand Down
14 changes: 10 additions & 4 deletions src/mcp_cli/dashboard/static/css/drawers.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@
}
#settings-panel textarea:focus { border-color: var(--dash-accent); outline: none; }
#settings-panel .btn-row { display: flex; gap: 6px; margin-top: 6px; }
#settings-panel .btn-sm {
.btn-sm {
background: transparent; border: 1px solid var(--dash-border); color: var(--dash-fg);
padding: 3px 10px; border-radius: var(--dash-radius); cursor: pointer; font-size: 11px;
font-family: var(--dash-font-ui);
}
#settings-panel .btn-sm:hover { background: var(--dash-bg-hover); }
#settings-panel .btn-sm.primary { background: var(--dash-accent); color: var(--dash-bg); border-color: var(--dash-accent); }
#settings-panel .btn-sm.primary:hover { opacity: 0.9; }
.btn-sm:hover { background: var(--dash-bg-hover); }
.btn-sm.primary { background: var(--dash-accent); color: var(--dash-bg); border-color: var(--dash-accent); }
.btn-sm.primary:hover { opacity: 0.9; }
.server-list { list-style: none; padding: 0; margin: 4px 0 0 0; }
.server-item {
display: flex; align-items: center; gap: 6px; padding: 4px 0;
Expand Down Expand Up @@ -88,3 +88,9 @@
.session-actions button:hover { color: var(--dash-fg); background: var(--dash-bg-hover); }
.session-actions button.delete:hover { color: var(--dash-error); }
.session-empty { color: var(--dash-fg-muted); font-size: 12px; text-align: center; padding: 16px 0; }

/* ── Focus styles ─────────────────────────────────────────────────── */
.btn-sm:focus-visible { outline: 2px solid var(--dash-accent); outline-offset: -2px; }
.session-item:focus-visible { outline: 2px solid var(--dash-accent); outline-offset: -2px; }
#settings-panel select:focus-visible,
#settings-panel textarea:focus-visible { outline: 2px solid var(--dash-accent); outline-offset: -2px; }
4 changes: 4 additions & 0 deletions src/mcp_cli/dashboard/static/css/grid.css
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,7 @@
z-index: 10;
}
.resize-handle-row:hover, .resize-handle-row.dragging { background: var(--dash-accent); }

/* ── Focus styles ─────────────────────────────────────────────────── */
.panel-btn:focus-visible,
.panel-view-toggle:focus-visible { outline: 2px solid var(--dash-accent); outline-offset: -2px; }
4 changes: 4 additions & 0 deletions src/mcp_cli/dashboard/static/css/notifications.css
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,7 @@
}
#approval-dialog .btn-approve:hover { opacity: 0.9; }
#approval-dialog .btn-deny:hover { background: var(--dash-bg-hover); }

/* ── Focus styles ─────────────────────────────────────────────────── */
.btn-approve:focus-visible,
.btn-deny:focus-visible { outline: 2px solid var(--dash-accent); outline-offset: -2px; }
13 changes: 11 additions & 2 deletions src/mcp_cli/dashboard/static/css/responsive.css
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@
font-size: 14px;
}
.sidebar-section-btn {
min-width: 28px;
min-height: 28px;
min-width: 36px;
min-height: 36px;
font-size: 13px;
padding: 4px 6px;
}
Expand Down Expand Up @@ -127,3 +127,12 @@
font-size: 18px;
}
}

/* ── Reduced motion ───────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
7 changes: 6 additions & 1 deletion src/mcp_cli/dashboard/static/css/sidebar.css
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,6 @@ body.mobile-sidebar #sidebar-resize-handle { display: none; }
}
.sidebar-section-btn:hover { background: var(--dash-bg-hover); color: var(--dash-fg); }
.sidebar-section.maximized { flex: 100 !important; }
.sidebar-section:not(.maximized):not(.expanded) { }
/* When any section is maximized, collapse siblings */
#sidebar-view-slot:has(.sidebar-section.maximized) .sidebar-section:not(.maximized) {
flex: 0 0 auto !important;
Expand Down Expand Up @@ -237,6 +236,12 @@ body.mobile-sidebar #sidebar-panel {
body.mobile-sidebar #sidebar-panel.open { display: flex; }
body.mobile-sidebar #sidebar-header { display: flex; }

/* ── Focus styles ─────────────────────────────────────────────────── */
#sidebar-toggle:focus-visible,
#sidebar-close:focus-visible { outline: 2px solid var(--dash-accent); outline-offset: -2px; }
.sidebar-section-header:focus-visible { outline: 2px solid var(--dash-accent); outline-offset: -2px; }
.sidebar-section-btn:focus-visible { outline: 2px solid var(--dash-accent); outline-offset: -2px; }

/* ── Container-aware layout (ResizeObserver classes for IDE embedding) */
body.container-narrow .grid-row { flex-direction: column; }
body.container-narrow .resize-handle-col { display: none; }
Expand Down
10 changes: 7 additions & 3 deletions src/mcp_cli/dashboard/static/css/toolbar.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
align-items: center;
gap: 12px;
padding: 0 12px;
height: 36px;
min-height: 36px;
height: var(--dash-toolbar-height);
min-height: var(--dash-toolbar-height);
background: var(--dash-bg-surface);
border-bottom: 1px solid var(--dash-border);
flex-shrink: 0;
Expand Down Expand Up @@ -78,7 +78,6 @@
font-family: var(--dash-font-ui);
cursor: pointer;
max-width: 140px;
outline: none;
}
.tb-select:hover { border-color: var(--dash-accent); }
.tb-label {
Expand All @@ -92,3 +91,8 @@
align-items: center;
gap: 4px;
}

/* ── Focus styles ─────────────────────────────────────────────────── */
.tb-btn:focus-visible,
.tb-select:focus-visible { outline: 2px solid var(--dash-accent); outline-offset: -2px; }
.dropdown-item:focus-visible { outline: 2px solid var(--dash-accent); outline-offset: -2px; background: var(--dash-bg-hover); }
2 changes: 0 additions & 2 deletions src/mcp_cli/dashboard/static/css/variables.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,8 @@ html, body { height: 100%; overflow: hidden; }
--dash-font-ui: 'Inter',-apple-system,sans-serif;
--dash-font-size: 13px;
--dash-radius: 6px;
--dash-spacing: 8px;
--dash-toolbar-height: 36px;
--dash-touch-target: 44px;
--dash-panel-min-width: 200px;
--dash-panel-min-height: 150px;
}

Expand Down
13 changes: 9 additions & 4 deletions src/mcp_cli/dashboard/static/js/apps.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ import {
viewRegistry, viewPool, popoutWindows,
setViewRegistry,
} from './state.js';
import { buildSidebarSections } from './sidebar.js';
import { syncViewPositions, rebuildAddPanelMenu } from './layout.js';
import { showToast } from './utils.js';

// Late-binding for sidebar deps to avoid direct import from sidebar.js
let _buildSidebarSections = null;
export function setAppDeps(deps) {
_buildSidebarSections = deps.buildSidebarSections;
}

export function handleAppLaunched(payload) {
const toolName = payload.tool_name;
const appUrl = payload.url;
Expand Down Expand Up @@ -64,7 +69,7 @@ export function handleAppLaunched(payload) {
}

// Rebuild sidebar sections so the data-view-id attributes update
buildSidebarSections();
if (_buildSidebarSections) _buildSidebarSections();
rebuildAddPanelMenu();
requestAnimationFrame(() => syncViewPositions());
showToast('info', 'View updated: ' + prettyName);
Expand All @@ -85,7 +90,7 @@ export function handleAppLaunched(payload) {
rebuildAddPanelMenu();

// Add to sidebar as collapsible section (not a grid panel)
buildSidebarSections();
if (_buildSidebarSections) _buildSidebarSections();

// Auto-expand the new app section
const newSection = document.querySelector(`.sidebar-section[data-view-id="${viewId}"]`);
Expand Down Expand Up @@ -119,7 +124,7 @@ export function handleAppClosed(payload) {
popoutWindows.delete(viewId);

// Rebuild sidebar sections (app section disappears)
buildSidebarSections();
if (_buildSidebarSections) _buildSidebarSections();
rebuildAddPanelMenu();
requestAnimationFrame(() => syncViewPositions());
showToast('info', 'App closed: ' + payload.tool_name);
Expand Down
7 changes: 2 additions & 5 deletions src/mcp_cli/dashboard/static/js/export.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
'use strict';

import { viewPool } from './state.js';
import { postToIframe } from './views.js';
import { sendToBridge } from './websocket.js';
import { showToast } from './utils.js';

export function exportConversation(format) {
Expand Down Expand Up @@ -57,9 +55,8 @@ export function collectConversationMessages() {
}
return messages;
} catch (e) {
// Cross-origin — fall back to asking the bridge
sendToBridge({ type: 'REQUEST_EXPORT' });
showToast('info', 'Export requested from server');
// Cross-origin iframe — cannot read messages directly
showToast('warning', 'Cannot export: cross-origin view');
return [];
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/mcp_cli/dashboard/static/js/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { buildOverflowMenu, wireToolbarEvents } from './toolbar.js';
import { wireConfigEvents } from './config.js';
import { wireApprovalEvents } from './approval.js';
import { setViewDeps, setupViewMessageListener } from './views.js';
import { setAppDeps } from './apps.js';

// ── Wire up late-binding deps to break circular imports ───────────

Expand All @@ -45,6 +46,11 @@ setLayoutDeps({
buildSidebarSections,
});

// apps.js needs sidebar.js functions
setAppDeps({
buildSidebarSections,
});

// utils.js closeAllDrawers needs closeSidebar from sidebar.js
setCloseSidebarFn(closeSidebar);

Expand Down
32 changes: 25 additions & 7 deletions src/mcp_cli/dashboard/static/js/layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
'use strict';

import {
panels, panelCounter, layoutConfig, viewRegistry, viewPool, popoutWindows,
panels, layoutConfig, viewRegistry, viewPool, popoutWindows,
PROTOCOL, VERSION, focusedAgentId, themes, activeTheme,
setPanels, incPanelCounter, setLayoutConfig,
isSidebarView, _sidebarOpen,
Expand All @@ -17,6 +17,13 @@ import {
} from './views.js';
import { themeToCSS } from './theme.js';

// ── Cached overlay element ───────────────────────────────────────
let _cachedOverlay = null;
function getOverlay() {
if (!_cachedOverlay) _cachedOverlay = document.getElementById('view-overlay');
return _cachedOverlay;
}

// ── Late-binding for sidebar deps to avoid circular imports ───────
let _buildSidebarSections = null;

Expand Down Expand Up @@ -235,7 +242,7 @@ export function makeColHandle(rowEl, idx) {
const handle = document.createElement('div');
handle.className = 'resize-handle-col';
handle.dataset.handleType = 'col';
const overlay = document.getElementById('view-overlay');
const overlay = getOverlay();

makeDraggable(handle, {
onStart(x, _y) {
Expand Down Expand Up @@ -283,7 +290,7 @@ export function makeColHandle(rowEl, idx) {
export function makeRowHandle(root, idx) {
const handle = document.createElement('div');
handle.className = 'resize-handle-row';
const overlay = document.getElementById('view-overlay');
const overlay = getOverlay();

makeDraggable(handle, {
onStart(_x, y) {
Expand Down Expand Up @@ -338,7 +345,7 @@ export function notifyResize(panelId) {

// ── View overlay positioning — positions iframes over panel body slots ──
export function syncViewPositions() {
const overlay = document.getElementById('view-overlay');
const overlay = getOverlay();
if (!overlay) return;
const overlayRect = overlay.getBoundingClientRect();
const inMobileSidebar = document.body.classList.contains('mobile-sidebar');
Expand Down Expand Up @@ -414,11 +421,22 @@ export function buildLayoutMenu() {
}

export function applyPreset(name) {
// Grid presets control the left panel only; sidebar views (activity, tools, etc.) are always in the right sidebar
const presets = {
'Minimal': { rows: [{ height: '100%', columns: [{ width: '100%', view: 'builtin:agent-terminal' }] }] },
'Standard': { rows: [{ height: '100%', columns: [{ width: '100%', view: 'builtin:agent-terminal' }] }] },
'Full': { rows: [{ height: '100%', columns: [{ width: '100%', view: 'builtin:agent-terminal' }] }] },
'Standard': { rows: [{ height: '100%', columns: [
{ width: '70%', view: 'builtin:agent-terminal' },
{ width: '30%', view: 'builtin:tool-browser' },
] }] },
'Full': { rows: [
{ height: '70%', columns: [
{ width: '70%', view: 'builtin:agent-terminal' },
{ width: '30%', view: 'builtin:plan-viewer' },
] },
{ height: '30%', columns: [
{ width: '50%', view: 'builtin:tool-browser' },
{ width: '50%', view: 'builtin:activity-stream' },
] },
] },
};
const layout = presets[name];
if (layout) { setLayoutConfig(layout); renderLayout(layout); }
Expand Down
Loading
Loading