diff --git a/ui/components/devProfiler.tsx b/ui/components/devProfiler.tsx
new file mode 100644
index 000000000..e0c53fcab
--- /dev/null
+++ b/ui/components/devProfiler.tsx
@@ -0,0 +1,422 @@
+'use client'
+
+import { useGetDevPprofQuery } from '@/lib/store'
+import { isDevelopmentMode } from '@/lib/utils/port'
+import { Activity, ChevronDown, ChevronUp, Cpu, HardDrive, X } from 'lucide-react'
+import React, { useCallback, useMemo, useState } from 'react'
+import {
+ Area,
+ AreaChart,
+ CartesianGrid,
+ ResponsiveContainer,
+ Tooltip,
+ XAxis,
+ YAxis,
+} from 'recharts'
+
+// Format bytes to human-readable string
+function formatBytes (bytes: number): string {
+ if (bytes === 0) return '0 B'
+ const k = 1024
+ const sizes = ['B', 'KB', 'MB', 'GB']
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
+ return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`
+}
+
+// Format nanoseconds to human-readable string
+function formatNs (ns: number): string {
+ if (ns < 1000) return `${ns}ns`
+ if (ns < 1000000) return `${(ns / 1000).toFixed(1)}µs`
+ if (ns < 1000000000) return `${(ns / 1000000).toFixed(1)}ms`
+ return `${(ns / 1000000000).toFixed(2)}s`
+}
+
+// Format timestamp to HH:MM:SS
+function formatTime (timestamp: string): string {
+ const date = new Date(timestamp)
+ return date.toLocaleTimeString('en-US', {
+ hour12: false,
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ })
+}
+
+// Truncate function name for display
+function truncateFunction (fn: string): string {
+ const parts = fn.split('/')
+ const last = parts[parts.length - 1]
+ if (last.length > 40) {
+ return '...' + last.slice(-37)
+ }
+ return last
+}
+
+export function DevProfiler (): React.ReactNode {
+ const [isVisible, setIsVisible] = useState(true)
+ const [isExpanded, setIsExpanded] = useState(true)
+ const [isDismissed, setIsDismissed] = useState(false)
+
+ // Only fetch in development mode and when not dismissed
+ const shouldFetch = isDevelopmentMode() && !isDismissed
+
+ const { data, isLoading, error } = useGetDevPprofQuery(undefined, {
+ pollingInterval: shouldFetch ? 10000 : 0, // Poll every 10 seconds
+ skip: !shouldFetch,
+ })
+
+ // Memoize chart data transformation
+ const memoryChartData = useMemo(() => {
+ if (!data?.history) return []
+ return data.history.map((point) => ({
+ time: formatTime(point.timestamp),
+ alloc: point.alloc / (1024 * 1024), // Convert to MB
+ heapInuse: point.heap_inuse / (1024 * 1024),
+ }))
+ }, [data?.history])
+
+ const cpuChartData = useMemo(() => {
+ if (!data?.history) return []
+ return data.history.map((point) => ({
+ time: formatTime(point.timestamp),
+ cpuPercent: point.cpu_percent,
+ goroutines: point.goroutines,
+ }))
+ }, [data?.history])
+
+ const handleDismiss = useCallback(() => {
+ setIsDismissed(true)
+ }, [])
+
+ const handleToggleExpand = useCallback(() => {
+ setIsExpanded((prev) => !prev)
+ }, [])
+
+ const handleToggleVisible = useCallback(() => {
+ setIsVisible((prev) => !prev)
+ }, [])
+
+ // Don't render in production mode or if dismissed
+ if (!isDevelopmentMode() || isDismissed) {
+ return null
+ }
+
+ // Minimized state - just show a small button
+ if (!isVisible) {
+ return (
+
+ )
+ }
+
+ return (
+
+ {/* Header */}
+
+
+ Dev Profiler
+ {isLoading && (
+
+ )}
+
+
+
+
+
+
+
+
+ {Boolean(error) && (
+
+ Failed to load profiling data
+
+ )}
+
+ {isExpanded && data && (
+
+ {/* Current Stats */}
+
+
+ CPU Usage
+
+ {data.cpu.usage_percent.toFixed(1)}%
+
+
+
+ Heap Alloc
+
+ {formatBytes(data.memory.alloc)}
+
+
+
+ Heap In-Use
+
+ {formatBytes(data.memory.heap_inuse)}
+
+
+
+ System
+
+ {formatBytes(data.memory.sys)}
+
+
+
+ Goroutines
+
+ {data.runtime.num_goroutine}
+
+
+
+ GC Pause
+
+ {formatNs(data.runtime.gc_pause_ns)}
+
+
+
+
+ {/* CPU Chart */}
+
+
+
+ CPU Usage (last 5 min)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `${Number(v).toFixed(0)}%`}
+ width={35}
+ domain={[0, 'auto']}
+ />
+
+
+
+
+
+
+
+
+
+
+ CPU %
+
+
+
+ Goroutines
+
+
+
+
+ {/* Memory Chart */}
+
+
+
+ Memory (last 5 min)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `${Number(v).toFixed(0)}MB`}
+ width={45}
+ />
+
+
+
+
+
+
+
+
+
+ Alloc
+
+
+
+ Heap In-Use
+
+
+
+
+ {/* Top Allocations */}
+
+
+
+ Top Allocations
+
+
+ {(data.top_allocations ?? []).map((alloc, i) => (
+
+
+
+ {truncateFunction(alloc.function)}
+
+
+ {alloc.file}:{alloc.line}
+
+
+
+
+ {formatBytes(alloc.bytes)}
+
+
+ {alloc.count.toLocaleString()} allocs
+
+
+
+ ))}
+
+
+
+ {/* Footer with info */}
+
+ CPUs: {data.runtime.num_cpu} | GOMAXPROCS: {data.runtime.gomaxprocs} |
+ GC: {data.runtime.num_gc} | Objects: {data.memory.heap_objects.toLocaleString()}
+
+
+ )}
+
+ {/* Collapsed state */}
+ {!isExpanded && data && (
+
+
+ CPU: {data.cpu.usage_percent.toFixed(1)}%
+
+
+ Heap: {formatBytes(data.memory.heap_inuse)}
+
+
+ Goroutines: {data.runtime.num_goroutine}
+
+
+ )}
+
+ )
+}
diff --git a/ui/components/sidebar.tsx b/ui/components/sidebar.tsx
index af8717413..367f2f7e1 100644
--- a/ui/components/sidebar.tsx
+++ b/ui/components/sidebar.tsx
@@ -205,7 +205,7 @@ const SidebarItemView = ({
return (
, SwitchProps>(
- ({ className, size = "default", ...props }, ref) => (
+ ({ className, size = "md", ...props }, ref) => (
,
>
({
+ // Get dev pprof data - polls every 10 seconds
+ getDevPprof: builder.query({
+ query: () => ({
+ url: '/dev/pprof',
+ }),
+ }),
+ }),
+})
+
+export const {
+ useGetDevPprofQuery,
+ useLazyGetDevPprofQuery,
+} = devApi
+
diff --git a/ui/lib/store/apis/index.ts b/ui/lib/store/apis/index.ts
index 99fc3eb56..3946f3318 100644
--- a/ui/lib/store/apis/index.ts
+++ b/ui/lib/store/apis/index.ts
@@ -3,6 +3,7 @@ export { baseApi, clearAuthStorage, getErrorMessage, setAuthToken } from "./base
// API slices and hooks
export * from "./configApi";
+export * from "./devApi";
export * from "./governanceApi";
export * from "./logsApi";
export * from "./mcpApi";