diff --git a/LAYOUT_SPEC.md b/LAYOUT_SPEC.md new file mode 100644 index 0000000..c927b1c --- /dev/null +++ b/LAYOUT_SPEC.md @@ -0,0 +1,115 @@ +# Dashboard Layout V2 — Spec + +## Current Layout + +``` +┌─────────────────────────────────────────────────────┐ +│ Header (logo, status, toolbar buttons) │ +├─────────────────────────────────────────────────────┤ +│ TabBar (connection tabs) │ +├──────────┬──────────────────────┬───────────────────┤ +│ Left: │ Center: │ Right: │ +│ MCP │ Screencast / MCP │ Globals Panel │ +│ Prims │ (when no session) │ │ +│ ├──────────────────────┤ │ +│ │ Bottom Panel: │ │ +│ │ Logs|Events|Agent │ │ +│ │ (side by side) │ │ +└──────────┴──────────────────────┴───────────────────┘ +``` + +## Target Layout + +``` +┌─────────────────────────────────────────────────────┐ +│ Header (logo, status, toolbar buttons) │ +├─────────────────────────────────────────────────────┤ +│ TabBar (connection tabs) │ +├──────────┬──────────────────────┬───────────────────┤ +│ Left: │ Center: │ Right: │ +│ MCP │ Screencast │ [Agent|Events| │ +│ Prims │ — OR — │ Logs] tabs │ +│ (no │ Tamagotchi │ (one at a time) │ +│ title) │ placeholder │ Default: Agent │ +│ ├──────────────────────┤ │ +│ │ Globals bar │ │ +│ │ (only if UI visible, │ │ +│ │ collapsible) │ │ +└──────────┴──────────────────────┴───────────────────┘ +``` + +## Changes Required + +### 1. Right Panel — Tabbed Events/Logs/Agent + +**Move** the current BottomPanel content (Logs, Events, Agent) into a **right sidebar panel**. + +- Tabs at top: `Agent` | `Events` | `Logs` — only **one visible at a time** (radio-style tabs, not toggles) +- Default active tab: **Agent** +- The entire right panel is **collapsible** (like the current left panel) +- Persist active tab to localStorage +- Tab style: match existing tab styling pattern (teal active, muted inactive) +- Show counts in tabs: `Agent (5)`, `Events (12)`, `Logs (3)` +- Include a "Clear" button in the panel header +- The panel should be resizable (like left panel uses `useResizablePanelWidth`) + +### 2. Left Panel — MCP Primitives (Always Left, No Title) + +- **Remove** the "MCP Primitives" title/header row from McpPrimitivesPanel when `position="left"` +- Keep the collapse button — move it somewhere sensible (e.g., top of tabs row) +- **Always render on the left**, whether or not there's an active session + - Currently: when no session, MCP Primitives renders in the center. Remove that center variant. + - The left panel should always be there (collapsed or not) +- Keep existing resizable behavior + +### 3. Globals/Environment Bar — Below Center Stage + +- **Move** GlobalsPanel from right sidebar to a **horizontal bar below the screencast area** +- Only visible when a **UI widget is active** (screencast is streaming) +- **Collapsible** — toggle from within the center stage frame (small toggle button at bottom edge of screencast) +- When collapsed: just a thin bar or button to re-expand +- Horizontal layout: key-value pairs flowing left-to-right +- Persist collapsed state to localStorage + +### 4. No-UI Placeholder — Animated Tamagotchi Star + +When no active widget/screencast, show a placeholder in the center area instead of the screencast: + +- **Animated star character** (Sirius ⭐) with eyes and mouth — tamagotchi style +- CSS animation: gentle floating/bobbing, blinking eyes +- Below the star: message text **"No active widget yet — ask your agent to test"** +- Muted colors, subtle animation (not distracting) +- The star should be an SVG with simple face features (two dot eyes, small smile) + +### 5. Toolbar Button Updates + +- Remove the current bottom-panel toggle button (LogsIcon) from Toolbar — bottom panel no longer exists +- Remove the globals toggle button (GlobalsIcon) from Toolbar — globals is now part of center stage +- Keep the primitives toggle (PrimitivesIcon) for the left panel +- Add a **right panel toggle** (new icon — could reuse GlobalsIcon rotated, or a new sidebar-right icon) +- Update Toolbar props accordingly + +### 6. Remove Bottom Panel + +- The BottomPanel component itself can be kept but is no longer used as a bottom panel +- Its content (LogsPanel, EventsPanel, AgentPanel) moves into the new RightPanel component +- Remove the resize handle between center and bottom +- Remove `useResizablePanel` usage for bottom panel height + +## File Impact + +- `InspectorDashboard.tsx` — Major restructure of layout +- `components/Toolbar.tsx` — Update button set +- `components/McpPrimitivesPanel.tsx` — Remove header when position="left" +- `components/GlobalsPanel.tsx` — Rework to horizontal bar layout +- `components/BottomPanel.tsx` — Refactor into RightPanel or deprecate +- **NEW** `components/RightPanel.tsx` — Tabbed sidebar (Agent/Events/Logs) +- **NEW** `components/NoWidgetPlaceholder.tsx` — Tamagotchi star animation +- `styles.ts` — New/updated styles + +## Style Notes + +- All existing color scheme stays (#0d0e0e backgrounds, #2d2f2f borders, #20b2aa teal accents) +- Tab styling matches existing pattern in McpPrimitivesPanel +- Smooth transitions on collapse/expand (existing 0.25s ease pattern) +- Star SVG should use teal (#20b2aa) as accent color diff --git a/packages/inspector/src/dashboard/assets/logo.png b/packages/inspector/src/dashboard/assets/logo.png index 3310f76..bcc6829 100644 Binary files a/packages/inspector/src/dashboard/assets/logo.png and b/packages/inspector/src/dashboard/assets/logo.png differ diff --git a/packages/inspector/src/dashboard/assets/sirius-star.png b/packages/inspector/src/dashboard/assets/sirius-star.png new file mode 100644 index 0000000..bcc6829 Binary files /dev/null and b/packages/inspector/src/dashboard/assets/sirius-star.png differ diff --git a/packages/inspector/src/dashboard/react/InspectorDashboard.tsx b/packages/inspector/src/dashboard/react/InspectorDashboard.tsx index 97c6d8c..9a100cd 100644 --- a/packages/inspector/src/dashboard/react/InspectorDashboard.tsx +++ b/packages/inspector/src/dashboard/react/InspectorDashboard.tsx @@ -12,7 +12,6 @@ import { useLogStream, type LogEntry } from "./hooks/useLogStream"; import { useEventStream } from "./hooks/useEventStream"; import { useAgentEventStream } from "./hooks/useAgentEventStream"; import type { InspectorEvent, AgnosticInspectorEvent } from "../../types"; -import { useResizablePanel } from "./hooks/useResizablePanel"; import { useResizablePanelWidth } from "./hooks/useResizablePanelWidth"; import { useGlobals, type GlobalsState } from "./hooks/useGlobals"; import { useConnections } from "./hooks/useConnections"; @@ -22,27 +21,16 @@ import { ConnectionBar } from "./components/ConnectionBar"; import { TabBar } from "./components/TabBar"; import { GlobalsPanel } from "./components/GlobalsPanel"; import { McpPrimitivesPanel } from "./components/McpPrimitivesPanel"; -import { BottomPanel, type PanelVisibility } from "./components/BottomPanel"; +import { RightPanel } from "./components/RightPanel"; +import { NoWidgetPlaceholder } from "./components/NoWidgetPlaceholder"; import { styles } from "./styles"; import logoUrl from "../assets/logo.png"; export interface InspectorDashboardProps { /** Base URL for the inspector API (default: current origin) */ baseUrl?: string; - /** Initial panel height in pixels (default: 200) */ - initialPanelHeight?: number; - /** Minimum panel height in pixels (default: 100) */ - minPanelHeight?: number; } -const DEFAULT_PANEL_VISIBILITY: PanelVisibility = { - logs: true, - events: true, - agent: true, -}; - -const PANEL_VISIBILITY_STORAGE_KEY = "mcp-dashboard-panel-visibility"; - interface CachedConnectionState { sessions: SessionInfo[]; events: InspectorEvent[]; @@ -53,11 +41,7 @@ interface CachedConnectionState { primitives: McpPrimitives | null; } -export function InspectorDashboard({ - baseUrl = "", - initialPanelHeight = 200, - minPanelHeight = 100, -}: InspectorDashboardProps): React.ReactElement { +export function InspectorDashboard({ baseUrl = "" }: InspectorDashboardProps): React.ReactElement { // Connection state (includes actions and history) const { connections, @@ -138,60 +122,31 @@ export function InspectorDashboard({ resources.length > 0 ? resources : (cachedState?.primitives?.resources ?? []); const displayPrompts = prompts.length > 0 ? prompts : (cachedState?.primitives?.prompts ?? []); - // Left panel state (for MCP primitives when session is active) + // Left panel state (MCP primitives) const [isLeftPanelCollapsed, setIsLeftPanelCollapsed] = useState(false); - // Globals panel state (persisted) - const [isGlobalsPanelVisible, setIsGlobalsPanelVisible] = useState(() => { - if (typeof window !== "undefined") { - const stored = localStorage.getItem("mcp-dashboard-globals-panel-visible"); - return stored !== "false"; // Default to visible - } - return true; - }); - - // Bottom panel state (persisted) - const [isBottomPanelVisible, setIsBottomPanelVisible] = useState(() => { + // Right panel state (persisted) + const [isRightPanelCollapsed, setIsRightPanelCollapsed] = useState(() => { if (typeof window !== "undefined") { - const stored = localStorage.getItem("mcp-dashboard-logs-panel-visible"); - return stored !== "false"; // Default to visible - } - return true; - }); - - // Panel collapse state - const [isPanelCollapsed, setIsPanelCollapsed] = useState(() => { - if (typeof window !== "undefined") { - return localStorage.getItem("mcp-dashboard-logs-panel-collapsed") === "true"; + try { + return localStorage.getItem("mcp-dashboard-right-panel-collapsed") === "true"; + } catch { + return false; + } } return false; }); - // Panel visibility state (which panels are shown) - const [panelVisibility, setPanelVisibility] = useState(() => { + // Globals bar state (persisted) + const [isGlobalsBarCollapsed, setIsGlobalsBarCollapsed] = useState(() => { if (typeof window !== "undefined") { - const stored = localStorage.getItem(PANEL_VISIBILITY_STORAGE_KEY); - if (stored) { - try { - const parsed = JSON.parse(stored) as Partial; - return { - logs: parsed.logs ?? DEFAULT_PANEL_VISIBILITY.logs, - events: parsed.events ?? DEFAULT_PANEL_VISIBILITY.events, - agent: parsed.agent ?? DEFAULT_PANEL_VISIBILITY.agent, - }; - } catch { - // Invalid JSON, use defaults - } + try { + return localStorage.getItem("mcp-dashboard-globals-bar-collapsed") === "true"; + } catch { + return false; } } - return DEFAULT_PANEL_VISIBILITY; - }); - - const { panelHeight, resizeHandleProps, isResizing } = useResizablePanel({ - initialHeight: initialPanelHeight, - minHeight: minPanelHeight, - storageKey: "mcp-dashboard-logs-panel-height", - disabled: isPanelCollapsed, + return false; }); // Left panel (MCP primitives) width resize @@ -207,6 +162,20 @@ export function InspectorDashboard({ disabled: isLeftPanelCollapsed, }); + // Right panel width resize + const { + panelWidth: rightPanelWidth, + resizeHandleProps: rightResizeHandleProps, + isResizing: isRightResizing, + } = useResizablePanelWidth({ + initialWidth: 320, + minWidth: 240, + maxWidth: 520, + storageKey: "mcp-dashboard-right-panel-width", + disabled: isRightPanelCollapsed, + resizeDirection: "right", + }); + // Auto-select first session when available useEffect(() => { const firstSession = displaySessions[0]; @@ -242,33 +211,27 @@ export function InspectorDashboard({ } }, [displaySessions, activeConnectionId, selectedSessionByConnection, clearLogs, clearEvents]); - // Save collapsed state + // Save right panel collapsed state useEffect(() => { if (typeof window !== "undefined") { - localStorage.setItem("mcp-dashboard-logs-panel-collapsed", String(isPanelCollapsed)); - } - }, [isPanelCollapsed]); - - // Save globals panel visibility - useEffect(() => { - if (typeof window !== "undefined") { - localStorage.setItem("mcp-dashboard-globals-panel-visible", String(isGlobalsPanelVisible)); - } - }, [isGlobalsPanelVisible]); - - // Save bottom panel visibility - useEffect(() => { - if (typeof window !== "undefined") { - localStorage.setItem("mcp-dashboard-logs-panel-visible", String(isBottomPanelVisible)); + try { + localStorage.setItem("mcp-dashboard-right-panel-collapsed", String(isRightPanelCollapsed)); + } catch { + // ignore storage access errors + } } - }, [isBottomPanelVisible]); + }, [isRightPanelCollapsed]); - // Save panel visibility state + // Save globals bar collapsed state useEffect(() => { if (typeof window !== "undefined") { - localStorage.setItem(PANEL_VISIBILITY_STORAGE_KEY, JSON.stringify(panelVisibility)); + try { + localStorage.setItem("mcp-dashboard-globals-bar-collapsed", String(isGlobalsBarCollapsed)); + } catch { + // ignore storage access errors + } } - }, [panelVisibility]); + }, [isGlobalsBarCollapsed]); const handleSessionChange = useCallback( (e: React.ChangeEvent) => { @@ -314,23 +277,12 @@ export function InspectorDashboard({ prevConnectionIdRef.current = activeConnectionId; }, [activeConnectionId]); // eslint-disable-line react-hooks/exhaustive-deps - const togglePanel = useCallback(() => { - setIsPanelCollapsed((prev) => !prev); - }, []); - - const toggleGlobalsPanel = useCallback(() => { - setIsGlobalsPanelVisible((prev) => !prev); - }, []); - - const toggleBottomPanel = useCallback(() => { - setIsBottomPanelVisible((prev) => !prev); + const toggleRightPanel = useCallback(() => { + setIsRightPanelCollapsed((prev) => !prev); }, []); - const handleTogglePanel = useCallback((panel: keyof PanelVisibility) => { - setPanelVisibility((prev) => ({ - ...prev, - [panel]: !prev[panel], - })); + const toggleGlobalsBar = useCallback(() => { + setIsGlobalsBarCollapsed((prev) => !prev); }, []); const handleCreateConnection = useCallback( @@ -398,9 +350,6 @@ export function InspectorDashboard({ : "Disconnected" : "Disconnected"; - // Determine if UI session is active (has screencast) - const hasActiveSession = !!selectedSessionId && !!displayImageData; - // Inject keyframe animation for streaming border const keyframeStyles = useMemo( () => ` @@ -438,7 +387,7 @@ export function InspectorDashboard({
MCP Agent Inspector -

MCP Agent Inspector

+

sirius-mcp inspector

{/* Connection Bar */} @@ -493,13 +442,10 @@ export function InspectorDashboard({ setIsLeftPanelCollapsed(!isLeftPanelCollapsed)} - hasActiveSession={hasActiveSession} + isRightPanelVisible={!isRightPanelCollapsed} + onToggleRightPanel={toggleRightPanel} />
@@ -517,34 +463,31 @@ export function InspectorDashboard({ {/* Content Wrapper - horizontal layout */}
- {/* Left Panel - MCP Primitives when session active */} - {hasActiveSession && ( - setIsLeftPanelCollapsed(!isLeftPanelCollapsed)} - position="left" - panelWidth={leftPanelWidth} - resizeHandleProps={leftResizeHandleProps} - isResizing={isLeftResizing} - /> - )} + {/* Left Panel - MCP Primitives (always present) */} + setIsLeftPanelCollapsed(!isLeftPanelCollapsed)} + position="left" + panelWidth={leftPanelWidth} + resizeHandleProps={leftResizeHandleProps} + isResizing={isLeftResizing} + /> - {/* Center Column - main content + bottom panel */} + {/* Center Column - screencast + globals bar */}
- {/* Main Display */}
- {hasActiveSession ? ( - /* Screencast when session is active */ + {isStreaming ? ( + /* Screencast when streaming */
Live browser view + {isGlobalsBarCollapsed && ( + + )}
) : ( - /* MCP Primitives in center when no session */ - + /* Tamagotchi placeholder when no widget */ + )}
- {/* Resize Handle */} -
- - {/* Bottom Panel (Logs + Events + Agent) */} -
- -
+ )}
- {/* Globals Panel - full height on the right */} - + {/* Right Panel - Agent/Events/Logs tabs */} +
); diff --git a/packages/inspector/src/dashboard/react/components/AgentPanel.tsx b/packages/inspector/src/dashboard/react/components/AgentPanel.tsx index cee9485..3883304 100644 --- a/packages/inspector/src/dashboard/react/components/AgentPanel.tsx +++ b/packages/inspector/src/dashboard/react/components/AgentPanel.tsx @@ -22,6 +22,10 @@ export interface AgentPanelProps { onClearEvents: () => void; /** Whether to show the header (default: true) */ showHeader?: boolean; + /** Whether to show the title in the header (default: true) */ + showTitle?: boolean; + /** Whether to show the clear button in the header (default: true) */ + showClearButton?: boolean; } // Agent events only use the "agent" category, but we support filtering for consistency @@ -31,6 +35,8 @@ export function AgentPanel({ events, onClearEvents, showHeader = true, + showTitle = true, + showClearButton = true, }: AgentPanelProps): React.ReactElement { const [categoryFilter, setCategoryFilter] = useState("all"); const containerRef = useRef(null); @@ -58,7 +64,7 @@ export function AgentPanel({
{showHeader && (
- Agent + {showTitle && Agent}
- + {showClearButton && ( + + )}
)} diff --git a/packages/inspector/src/dashboard/react/components/GlobalsPanel.tsx b/packages/inspector/src/dashboard/react/components/GlobalsPanel.tsx index e5cd263..97c93f7 100644 --- a/packages/inspector/src/dashboard/react/components/GlobalsPanel.tsx +++ b/packages/inspector/src/dashboard/react/components/GlobalsPanel.tsx @@ -1,7 +1,7 @@ /** * GlobalsPanel Component * - * Displays formatted environment/globals information in collapsible sections. + * Displays environment/globals information in a horizontal bar. */ import React from "react"; @@ -13,22 +13,10 @@ export interface GlobalsPanelProps { globals: GlobalsState | null; /** Whether the panel is visible */ isVisible: boolean; - /** Panel width in pixels */ - width?: number; -} - -interface SectionProps { - title: string; - children: React.ReactNode; -} - -function Section({ title, children }: SectionProps): React.ReactElement { - return ( -
-
{title}
- {children} -
- ); + /** Whether the panel is collapsed */ + isCollapsed: boolean; + /** Callback to toggle collapsed state */ + onToggleCollapse?: () => void; } interface ItemProps { @@ -39,9 +27,9 @@ interface ItemProps { function Item({ label, value }: ItemProps): React.ReactElement | null { if (value === undefined || value === null) return null; return ( -
- {label} - {String(value)} +
+ {label} + {String(value)}
); } @@ -49,28 +37,35 @@ function Item({ label, value }: ItemProps): React.ReactElement | null { export function GlobalsPanel({ globals, isVisible, - width = 280, + isCollapsed, + onToggleCollapse, }: GlobalsPanelProps): React.ReactElement { + if (!isVisible) { + return
; + } + const panelStyle: React.CSSProperties = { - ...styles.globalsPanel, - width: isVisible ? width : 0, - ...(isVisible ? {} : styles.globalsPanelCollapsed), + ...styles.globalsBar, + ...(isCollapsed ? styles.globalsBarCollapsed : {}), }; if (!globals) { return (
- {isVisible && ( - <> -
- Environment -
-
-
- Loading... -
-
- + {isCollapsed ? ( +
+ +
+ ) : ( +
+ Loading environment… +
)}
); @@ -90,50 +85,36 @@ export function GlobalsPanel({ return (
- {isVisible && ( - <> -
- Environment -
-
-
- - -
- -
- - -
- -
- - {maxHeight !== undefined && } -
- -
- -
- -
- - - -
- - {userLocation && ( -
- {userLocation.city && } - {userLocation.region && } - {userLocation.country && } - {userLocation.timezone && } -
- )} -
- + {isCollapsed ? ( +
+ +
+ ) : ( +
+ + + + + + {maxHeight !== undefined && } + + + + + {userLocation?.city && } + {userLocation?.region && } + {userLocation?.country && } + {userLocation?.timezone && } +
)}
); diff --git a/packages/inspector/src/dashboard/react/components/LogsPanel.tsx b/packages/inspector/src/dashboard/react/components/LogsPanel.tsx index f9e30cd..e05e20d 100644 --- a/packages/inspector/src/dashboard/react/components/LogsPanel.tsx +++ b/packages/inspector/src/dashboard/react/components/LogsPanel.tsx @@ -16,6 +16,10 @@ export interface LogsPanelProps { onClearLogs: () => void; /** Whether to show the header (default: true) */ showHeader?: boolean; + /** Whether to show the title in the header (default: true) */ + showTitle?: boolean; + /** Whether to show the clear button in the header (default: true) */ + showClearButton?: boolean; } /** @@ -63,6 +67,8 @@ export function LogsPanel({ logs, onClearLogs, showHeader = true, + showTitle = true, + showClearButton = true, }: LogsPanelProps): React.ReactElement { const containerRef = useRef(null); @@ -77,12 +83,14 @@ export function LogsPanel({
{showHeader && (
- Logs + {showTitle && Logs}
{logs.length} logs - + {showClearButton && ( + + )}
)} diff --git a/packages/inspector/src/dashboard/react/components/McpPrimitivesPanel.tsx b/packages/inspector/src/dashboard/react/components/McpPrimitivesPanel.tsx index 56cf144..2ed7878 100644 --- a/packages/inspector/src/dashboard/react/components/McpPrimitivesPanel.tsx +++ b/packages/inspector/src/dashboard/react/components/McpPrimitivesPanel.tsx @@ -108,6 +108,7 @@ const localStyles: Record = { }, tabs: { display: "flex", + alignItems: "center", gap: "0.25rem", padding: "0.5rem 0.75rem", backgroundColor: "#0a0a0a", @@ -767,19 +768,6 @@ export function McpPrimitivesPanel({ <>
-
- MCP Primitives - {onToggleCollapse && ( - - )} -
-
{tabs.map((tab) => ( ))} + {onToggleCollapse && ( + + )}
{renderContent()}
@@ -819,15 +816,11 @@ export function McpPrimitivesPanel({ return (
-
- MCP Primitives - {position === "left" && onToggleCollapse && ( - - )} -
- + {position === "center" && ( +
+ MCP Primitives +
+ )}
{tabs.map((tab) => ( ))} + {position === "left" && onToggleCollapse && ( + + )}
{renderContent()}
diff --git a/packages/inspector/src/dashboard/react/components/NoWidgetPlaceholder.tsx b/packages/inspector/src/dashboard/react/components/NoWidgetPlaceholder.tsx new file mode 100644 index 0000000..2e2f0b5 --- /dev/null +++ b/packages/inspector/src/dashboard/react/components/NoWidgetPlaceholder.tsx @@ -0,0 +1,20 @@ +/** + * NoWidgetPlaceholder Component + * + * Crystalline star image with floating animation. + */ + +import React from "react"; +import { styles } from "../styles"; +import starUrl from "../../assets/sirius-star.png"; + +export function NoWidgetPlaceholder(): React.ReactElement { + return ( +
+ Sirius the star +

No active widget yet — ask your agent to test

+
+ ); +} + +export default NoWidgetPlaceholder; diff --git a/packages/inspector/src/dashboard/react/components/RightPanel.tsx b/packages/inspector/src/dashboard/react/components/RightPanel.tsx new file mode 100644 index 0000000..0ee5d02 --- /dev/null +++ b/packages/inspector/src/dashboard/react/components/RightPanel.tsx @@ -0,0 +1,188 @@ +/** + * RightPanel Component + * + * Tabbed sidebar for Agent, Events, and Logs. + */ + +import React, { useEffect, useMemo, useState } from "react"; +import type { LogEntry } from "../hooks/useLogStream"; +import type { InspectorEvent, AgnosticInspectorEvent } from "../../../types"; +import { styles } from "../styles"; +import { LogsPanel } from "./LogsPanel"; +import { EventsPanel } from "./EventsPanel"; +import { AgentPanel } from "./AgentPanel"; + +type RightPanelTab = "agent" | "events" | "logs"; + +const ACTIVE_TAB_STORAGE_KEY = "mcp-dashboard-right-panel-tab"; + +export interface RightPanelProps { + logs: LogEntry[]; + events: InspectorEvent[]; + agentEvents: AgnosticInspectorEvent[]; + onClearLogs?: () => void; + onClearEvents?: () => void; + onClearAgent?: () => void; + isCollapsed: boolean; + onToggleCollapse: () => void; + panelWidth: number; + resizeHandleProps: React.HTMLAttributes; + isResizing: boolean; +} + +export function RightPanel({ + logs, + events, + agentEvents, + onClearLogs, + onClearEvents, + onClearAgent, + isCollapsed, + onToggleCollapse, + panelWidth, + resizeHandleProps, + isResizing, +}: RightPanelProps): React.ReactElement { + const noop = (): void => undefined; + const [activeTab, setActiveTab] = useState(() => { + if (typeof window !== "undefined") { + try { + const stored = window.localStorage.getItem(ACTIVE_TAB_STORAGE_KEY); + if (stored === "agent" || stored === "events" || stored === "logs") { + return stored; + } + } catch { + // ignore storage access errors (e.g. private browsing) + } + } + return "agent"; + }); + + useEffect(() => { + if (typeof window !== "undefined") { + try { + window.localStorage.setItem(ACTIVE_TAB_STORAGE_KEY, activeTab); + } catch { + // ignore storage access errors + } + } + }, [activeTab]); + + const tabs = useMemo( + () => [ + { id: "agent" as const, label: "Agent", count: agentEvents.length }, + { id: "events" as const, label: "Events", count: events.length }, + { id: "logs" as const, label: "Logs", count: logs.length }, + ], + [agentEvents.length, events.length, logs.length] + ); + + const panelStyle: React.CSSProperties = { + ...styles.rightPanel, + width: isCollapsed ? 0 : panelWidth, + ...(isCollapsed ? styles.rightPanelCollapsed : {}), + }; + + if (isCollapsed) { + return
; + } + + const handleClear = (() => { + if (activeTab === "agent") { + return onClearAgent ?? noop; + } + if (activeTab === "events") { + return onClearEvents ?? noop; + } + return onClearLogs ?? noop; + })(); + + const isClearDisabled = + (activeTab === "agent" && !onClearAgent) || + (activeTab === "events" && !onClearEvents) || + (activeTab === "logs" && !onClearLogs); + + return ( + <> +
+
+
+ +
+ {tabs.map((tab) => ( + + ))} +
+
+ +
+
+
+ {activeTab === "logs" && ( + + )} + {activeTab === "events" && ( + + )} + {activeTab === "agent" && ( + + )} +
+
+ + ); +} + +export default RightPanel; diff --git a/packages/inspector/src/dashboard/react/components/Toolbar.tsx b/packages/inspector/src/dashboard/react/components/Toolbar.tsx index 6a56fd5..f12b4cd 100644 --- a/packages/inspector/src/dashboard/react/components/Toolbar.tsx +++ b/packages/inspector/src/dashboard/react/components/Toolbar.tsx @@ -1,31 +1,25 @@ /** * Toolbar Component * - * Icon buttons for toggling dashboard panels (logs, globals, primitives). + * Icon buttons for toggling dashboard panels (primitives, right panel). */ import React from "react"; import { styles } from "../styles"; export interface ToolbarProps { - /** Whether the logs panel is visible */ - isLogsPanelVisible: boolean; - /** Callback to toggle logs panel */ - onToggleLogsPanel: () => void; - /** Whether the globals panel is visible */ - isGlobalsPanelVisible: boolean; - /** Callback to toggle globals panel */ - onToggleGlobalsPanel: () => void; - /** Whether the MCP primitives panel is visible (only shown when session is active) */ - isPrimitivesPanelVisible?: boolean; + /** Whether the MCP primitives panel is visible */ + isPrimitivesPanelVisible: boolean; /** Callback to toggle primitives panel */ - onTogglePrimitivesPanel?: () => void; - /** Whether a session is active (determines if primitives toggle shows) */ - hasActiveSession?: boolean; + onTogglePrimitivesPanel: () => void; + /** Whether the right panel is visible */ + isRightPanelVisible: boolean; + /** Callback to toggle right panel */ + onToggleRightPanel: () => void; } -/** Logs panel icon - bottom panel (like globals icon but rotated) */ -function LogsIcon(): React.ReactElement { +/** Primitives panel icon - left sidebar panel (tools/resources/prompts) */ +function PrimitivesIcon(): React.ReactElement { return ( - + ); } -/** Globals panel icon - right sidebar panel */ -function GlobalsIcon(): React.ReactElement { +/** Right panel icon - tabbed agent/events/logs panel */ +function RightPanelIcon(): React.ReactElement { return ( - - - - ); -} - export function Toolbar({ - isLogsPanelVisible, - onToggleLogsPanel, - isGlobalsPanelVisible, - onToggleGlobalsPanel, isPrimitivesPanelVisible, onTogglePrimitivesPanel, - hasActiveSession, + isRightPanelVisible, + onToggleRightPanel, }: ToolbarProps): React.ReactElement { return (
- {/* Primitives panel toggle - only shown when session is active */} - {hasActiveSession && onTogglePrimitivesPanel && ( - - )} + {/* Primitives panel toggle */}
); diff --git a/packages/inspector/src/dashboard/react/hooks/useResizablePanelWidth.ts b/packages/inspector/src/dashboard/react/hooks/useResizablePanelWidth.ts index c989ce1..a524e91 100644 --- a/packages/inspector/src/dashboard/react/hooks/useResizablePanelWidth.ts +++ b/packages/inspector/src/dashboard/react/hooks/useResizablePanelWidth.ts @@ -14,6 +14,7 @@ export interface UseResizablePanelWidthOptions { maxWidth?: number; storageKey?: string; disabled?: boolean; + resizeDirection?: "left" | "right"; } export interface UseResizablePanelWidthResult { @@ -31,6 +32,7 @@ export function useResizablePanelWidth({ maxWidth = 600, storageKey, disabled = false, + resizeDirection = "left", }: UseResizablePanelWidthOptions): UseResizablePanelWidthResult { const [panelWidth, setPanelWidth] = useState(() => { if (typeof window !== "undefined" && storageKey) { @@ -108,6 +110,7 @@ export function useResizablePanelWidth({ const startX = e.clientX; const startWidth = widthRef.current; + const directionMultiplier = resizeDirection === "right" ? -1 : 1; setIsResizing(true); document.body.style.cursor = "ew-resize"; @@ -118,7 +121,10 @@ export function useResizablePanelWidth({ const deltaX = moveEvent.clientX - startX; const reservedWidth = 400; const effectiveMaxWidth = Math.min(maxWidth, window.innerWidth - reservedWidth); - const newWidth = Math.min(effectiveMaxWidth, Math.max(minWidth, startWidth + deltaX)); + const newWidth = Math.min( + effectiveMaxWidth, + Math.max(minWidth, startWidth + directionMultiplier * deltaX) + ); setPanelWidth(newWidth); }; @@ -145,7 +151,7 @@ export function useResizablePanelWidth({ document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); }, - [disabled, minWidth, maxWidth] + [disabled, minWidth, maxWidth, resizeDirection] ); return { diff --git a/packages/inspector/src/dashboard/react/keyframes.css b/packages/inspector/src/dashboard/react/keyframes.css index a8c0b1e..cbc93f6 100644 --- a/packages/inspector/src/dashboard/react/keyframes.css +++ b/packages/inspector/src/dashboard/react/keyframes.css @@ -1,8 +1,5 @@ /** * Dashboard Keyframes - * - * CSS keyframes animations used by the dashboard. - * These cannot be defined inline and must be in a stylesheet. */ @keyframes snakeBorder { @@ -13,3 +10,13 @@ background-position: 200% 50%; } } + +@keyframes starFloat { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-6px); + } +} diff --git a/packages/inspector/src/dashboard/react/styles.ts b/packages/inspector/src/dashboard/react/styles.ts index 2827249..db329d0 100644 --- a/packages/inspector/src/dashboard/react/styles.ts +++ b/packages/inspector/src/dashboard/react/styles.ts @@ -40,12 +40,12 @@ export const styles: Record = { headerLeft: { display: "flex", alignItems: "center", - gap: "0.75rem", + gap: "0.4rem", }, logo: { - width: "28px", - height: "28px", + width: "20px", + height: "20px", objectFit: "contain", }, @@ -268,6 +268,23 @@ export const styles: Record = { objectFit: "contain", }, + globalsCollapsedToggle: { + position: "absolute", + bottom: "0.5rem", + left: "50%", + transform: "translateX(-50%)", + fontFamily: "inherit", + backgroundColor: "rgba(0, 0, 0, 0.6)", + border: "1px solid #20b2aa", + color: "#20b2aa", + padding: "0.2rem 0.6rem", + borderRadius: "999px", + fontSize: "0.625rem", + cursor: "pointer", + letterSpacing: "0.02em", + transition: "all 0.15s ease", + }, + // Placeholder placeholder: { display: "flex", @@ -395,6 +412,8 @@ export const styles: Record = { logsContainer: { fontFamily: FONT_MONO, flex: 1, + display: "flex", + flexDirection: "column" as const, overflowY: "auto", padding: "0.5rem", fontSize: "0.75rem", @@ -405,9 +424,10 @@ export const styles: Record = { display: "flex", alignItems: "center", justifyContent: "center", - height: "100%", + flex: 1, color: "#4b5563", fontSize: "0.75rem", + fontFamily: FONT_SANS, }, // Log Entry @@ -669,6 +689,8 @@ export const styles: Record = { eventsContainer: { fontFamily: FONT_MONO, flex: 1, + display: "flex", + flexDirection: "column" as const, overflowY: "auto", padding: "0.25rem", fontSize: "0.75rem", @@ -679,9 +701,10 @@ export const styles: Record = { display: "flex", alignItems: "center", justifyContent: "center", - height: "100%", + flex: 1, color: "#4b5563", fontSize: "0.75rem", + fontFamily: FONT_SANS, }, // Filter Select @@ -888,4 +911,214 @@ export const styles: Record = { color: "#6b7280", fontStyle: "italic", }, + + // ========================================================================= + // Right Panel (Agent / Events / Logs) + // ========================================================================= + + rightPanel: { + backgroundColor: "#0d0e0e", + display: "flex", + flexDirection: "column" as const, + overflow: "hidden", + transition: "width 0.25s ease, opacity 0.3s ease", + height: "100%", + flexShrink: 0, + borderLeft: "1px solid #2d2f2f", + }, + + rightPanelCollapsed: { + width: 0, + borderLeft: "none", + opacity: 0, + }, + + rightPanelResizeHandle: { + width: "6px", + cursor: "ew-resize", + backgroundColor: "#2d2f2f", + transition: "background-color 0.15s ease", + flexShrink: 0, + }, + + rightPanelResizeHandleActive: { + backgroundColor: "#20b2aa", + }, + + rightPanelHeader: { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "0.5rem 0.75rem", + backgroundColor: "#0a0a0a", + borderBottom: "1px solid #1a1a1a", + flexShrink: 0, + }, + + rightPanelTabs: { + display: "flex", + gap: "0.25rem", + }, + + rightPanelTab: { + fontFamily: "inherit", + backgroundColor: "transparent", + border: "1px solid #3d4040", + color: "#9ca3af", + padding: "0.3rem 0.6rem", + borderRadius: "4px", + fontSize: "0.6875rem", + cursor: "pointer", + transition: "all 0.15s ease", + display: "flex", + alignItems: "center", + gap: "0.375rem", + }, + + rightPanelTabActive: { + backgroundColor: "rgba(32, 178, 170, 0.15)", + borderColor: "#20b2aa", + color: "#20b2aa", + }, + + rightPanelTabCount: { + backgroundColor: "rgba(255, 255, 255, 0.1)", + padding: "0.1rem 0.35rem", + borderRadius: "3px", + fontSize: "0.5625rem", + }, + + rightPanelTabCountActive: { + backgroundColor: "rgba(32, 178, 170, 0.2)", + }, + + rightPanelActions: { + display: "flex", + alignItems: "center", + gap: "0.375rem", + }, + + rightPanelClearBtn: { + fontFamily: "inherit", + backgroundColor: "transparent", + border: "1px solid #3d4040", + color: "#9ca3af", + padding: "0.25rem 0.5rem", + borderRadius: "4px", + fontSize: "0.625rem", + cursor: "pointer", + transition: "all 0.15s ease", + }, + + rightPanelCollapseBtn: { + fontFamily: "inherit", + background: "transparent", + border: "1px solid #3d4040", + borderRadius: "4px", + padding: "0.25rem 0.375rem", + cursor: "pointer", + color: "#9ca3af", + fontSize: "0.625rem", + transition: "all 0.15s ease", + }, + + rightPanelContent: { + flex: 1, + overflow: "auto", + display: "flex", + flexDirection: "column" as const, + }, + + // ========================================================================= + // No Widget Placeholder (Tamagotchi Star) + // ========================================================================= + + noWidgetWrapper: { + display: "flex", + flexDirection: "column" as const, + alignItems: "center", + justifyContent: "center", + height: "100%", + width: "100%", + gap: "1.5rem", + }, + + noWidgetStar: { + animation: "starFloat 4s ease-in-out infinite", + objectFit: "contain" as CSSProperties["objectFit"], + }, + + noWidgetMessage: { + color: "#6b7280", + fontSize: "0.875rem", + fontFamily: FONT_SANS, + textAlign: "center" as const, + margin: 0, + letterSpacing: "0.01em", + }, + + // ========================================================================= + // Globals Bar (Horizontal, below screencast) + // ========================================================================= + + globalsBar: { + backgroundColor: "#0a0a0a", + borderTop: "1px solid #2d2f2f", + transition: "all 0.2s ease", + flexShrink: 0, + }, + + globalsBarCollapsed: { + borderTop: "1px solid #1a1a1a", + }, + + globalsBarContent: { + display: "flex", + flexWrap: "wrap" as const, + gap: "0.75rem", + padding: "0.5rem 1rem", + alignItems: "center", + }, + + globalsBarCollapsedContent: { + display: "flex", + justifyContent: "center", + padding: "0.2rem 0.5rem", + }, + + globalsBarExpandBtn: { + fontFamily: "inherit", + background: "transparent", + border: "none", + color: "#6b7280", + fontSize: "0.625rem", + cursor: "pointer", + padding: "0.15rem 0.5rem", + transition: "color 0.15s ease", + }, + + globalsBarItem: { + display: "flex", + alignItems: "center", + gap: "0.35rem", + }, + + globalsBarItemLabel: { + color: "#6b7280", + fontSize: "0.625rem", + fontWeight: 500, + textTransform: "uppercase" as const, + letterSpacing: "0.04em", + }, + + globalsBarItemValue: { + color: "#d4d4d4", + fontSize: "0.6875rem", + }, + + globalsBarLoading: { + color: "#6b7280", + fontSize: "0.6875rem", + fontStyle: "italic", + }, };