proteum 2.5.9 → 2.5.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/AGENTS.md +4 -4
  2. package/agents/project/AGENTS.md +131 -91
  3. package/agents/project/CODING_STYLE.md +69 -40
  4. package/agents/project/DOCUMENTATION.md +19 -2
  5. package/agents/project/client/AGENTS.md +0 -1
  6. package/agents/project/diagnostics.md +6 -5
  7. package/agents/project/optimizations.md +1 -7
  8. package/agents/project/server/services/AGENTS.md +1 -3
  9. package/agents/project/tests/AGENTS.md +3 -3
  10. package/cli/commands/docs.ts +223 -0
  11. package/cli/commands/session.ts +36 -5
  12. package/cli/commands/verify.ts +6 -1
  13. package/cli/compiler/client/index.ts +11 -4
  14. package/cli/compiler/common/uiSingletons.ts +76 -0
  15. package/cli/compiler/server/index.ts +34 -12
  16. package/cli/presentation/commands.ts +20 -1
  17. package/cli/runtime/commands.ts +20 -0
  18. package/cli/scaffold/index.ts +3 -0
  19. package/cli/scaffold/templates.ts +62 -6
  20. package/cli/utils/agents.ts +2 -2
  21. package/cli/verification/changed.ts +21 -0
  22. package/client/dev/profiler/index.tsx +761 -455
  23. package/common/dev/mcpPayloads.ts +86 -8
  24. package/common/dev/session.ts +32 -0
  25. package/common/errors/index.tsx +0 -1
  26. package/docAnchors.js +135 -0
  27. package/docs/agent-routing.md +2 -2
  28. package/eslint.js +264 -1
  29. package/package.json +1 -1
  30. package/server/app/container/console/index.ts +0 -17
  31. package/server/services/router/http/index.ts +130 -33
  32. package/tests/agents-utils.test.cjs +0 -4
  33. package/tests/dev-session-login-url.test.cjs +33 -0
  34. package/tests/doc-anchors.test.cjs +115 -0
  35. package/tests/docs-check.test.cjs +138 -0
  36. package/tests/eslint-rules.test.cjs +235 -2
  37. package/tests/mcp.test.cjs +109 -0
  38. package/tests/ui-singletons.test.cjs +104 -0
  39. package/tests/verify-changed.test.cjs +51 -3
  40. package/agents/project/app-root/AGENTS.md +0 -14
  41. package/agents/project/root/AGENTS.md +0 -399
@@ -142,19 +142,36 @@ const profilerStyles = `
142
142
  bottom: calc(10px + env(safe-area-inset-bottom, 0px));
143
143
  display: inline-flex;
144
144
  align-items: center;
145
- gap: 10px;
146
- min-height: 30px;
147
- padding: 0 12px;
145
+ justify-content: center;
146
+ width: 46px;
147
+ height: 46px;
148
+ padding: 0;
148
149
  border: 1px solid var(--profiler-line-strong);
149
- border-radius: 0;
150
+ border-radius: 999px;
150
151
  background: var(--profiler-bg-strong);
151
152
  backdrop-filter: none;
152
153
  color: var(--profiler-brand);
153
- box-shadow: none;
154
- cursor: pointer;
155
- font-size: 11px;
156
- letter-spacing: 0.08em;
157
- text-transform: uppercase;
154
+ box-shadow: 0 8px 26px rgba(19, 32, 51, 0.16);
155
+ cursor: grab;
156
+ line-height: 0;
157
+ touch-action: none;
158
+ user-select: none;
159
+ }
160
+
161
+ .proteum-profiler__handle:hover {
162
+ color: var(--profiler-text);
163
+ background: var(--profiler-surface-hover);
164
+ }
165
+
166
+ .proteum-profiler__handle:active,
167
+ .proteum-profiler__handle--dragging {
168
+ cursor: grabbing;
169
+ }
170
+
171
+ .proteum-profiler__handleIcon {
172
+ width: 25px;
173
+ height: 25px;
174
+ pointer-events: none;
158
175
  }
159
176
 
160
177
  .proteum-profiler__panel {
@@ -514,16 +531,6 @@ const profilerStyles = `
514
531
  color: var(--profiler-muted);
515
532
  }
516
533
 
517
- .proteum-profiler__requestWorkspace {
518
- display: grid;
519
- grid-template-columns: minmax(0, 1.35fr) minmax(320px, 420px);
520
- gap: 0;
521
- align-items: stretch;
522
- min-height: 100%;
523
- height: 100%;
524
- max-height: 100%;
525
- }
526
-
527
534
  .proteum-profiler__splitView {
528
535
  display: grid;
529
536
  grid-template-columns: minmax(0, 1.35fr) minmax(320px, 420px);
@@ -604,6 +611,12 @@ const profilerStyles = `
604
611
  scrollbar-width: thin;
605
612
  }
606
613
 
614
+ .proteum-profiler__sidebarCharts {
615
+ display: grid;
616
+ gap: 0;
617
+ min-width: 0;
618
+ }
619
+
607
620
  .proteum-profiler__titleRow {
608
621
  padding: 8px 10px;
609
622
  background: var(--profiler-title-row-bg);
@@ -679,13 +692,6 @@ const profilerStyles = `
679
692
  background: transparent !important;
680
693
  }
681
694
 
682
- .proteum-profiler__chartGrid {
683
- display: grid;
684
- grid-template-columns: repeat(2, minmax(0, 1fr));
685
- gap: 0;
686
- border-top: 1px solid var(--profiler-line);
687
- }
688
-
689
695
  .proteum-profiler__chartCard {
690
696
  display: grid;
691
697
  align-content: start;
@@ -698,6 +704,10 @@ const profilerStyles = `
698
704
  border-left: none;
699
705
  }
700
706
 
707
+ .proteum-profiler__sidebarCharts > .proteum-profiler__chartCard {
708
+ border-left: none;
709
+ }
710
+
701
711
  .proteum-profiler__chartHeader {
702
712
  display: grid;
703
713
  gap: 4px;
@@ -771,10 +781,6 @@ const profilerStyles = `
771
781
  overflow: auto;
772
782
  }
773
783
 
774
- .proteum-profiler__chartGrid {
775
- grid-template-columns: 1fr;
776
- }
777
-
778
784
  .proteum-profiler__chartCard {
779
785
  border-left: none;
780
786
  }
@@ -787,13 +793,6 @@ const profilerStyles = `
787
793
  min-width: 132px;
788
794
  }
789
795
 
790
- .proteum-profiler__requestWorkspace {
791
- grid-template-columns: 1fr;
792
- min-height: 0;
793
- height: auto;
794
- max-height: none;
795
- }
796
-
797
796
  .proteum-profiler__splitView {
798
797
  grid-template-columns: 1fr;
799
798
  min-height: 0;
@@ -1622,7 +1621,7 @@ const ApiPanel = ({ session }: { session: TProfilerNavigationSession }) => {
1622
1621
  tags: [trace.status, ...(trace.requestId ? [`request:${trace.requestId}`] : [])],
1623
1622
  }));
1624
1623
  const requestItems = [...syncItems, ...asyncItems];
1625
- const [selectedRequestId, setSelectedRequestId] = React.useState<string | undefined>(() => requestItems[0]?.id);
1624
+ const [selectedRequestId, setSelectedRequestId] = React.useState<string | undefined>();
1626
1625
  const endpointDurationChart = buildHorizontalBarChartOptions({
1627
1626
  color: profilerChartTheme.blue,
1628
1627
  entries: buildDurationEntries(
@@ -1652,103 +1651,108 @@ const ApiPanel = ({ session }: { session: TProfilerNavigationSession }) => {
1652
1651
  });
1653
1652
 
1654
1653
  React.useEffect(() => {
1655
- if (requestItems.some((item) => item.id === selectedRequestId)) return;
1656
- setSelectedRequestId(requestItems[0]?.id);
1654
+ if (!selectedRequestId || requestItems.some((item) => item.id === selectedRequestId)) return;
1655
+ setSelectedRequestId(undefined);
1657
1656
  }, [requestItems, selectedRequestId]);
1658
1657
 
1658
+ const toggleRequestSelection = React.useCallback((requestId: string) => {
1659
+ setSelectedRequestId((currentRequestId: string | undefined) => (currentRequestId === requestId ? undefined : requestId));
1660
+ }, []);
1661
+
1659
1662
  const waterfallItems = buildApiWaterfallItems(requestItems);
1660
- const selectedItem = requestItems.find((item) => item.id === selectedRequestId) || requestItems[0];
1663
+ const selectedItem = selectedRequestId ? requestItems.find((item) => item.id === selectedRequestId) : undefined;
1661
1664
 
1662
1665
  return (
1663
- <div className="proteum-profiler__requestWorkspace">
1664
- <div className="proteum-profiler__splitColumn">
1665
- <div className="proteum-profiler__chartGrid">
1666
- <ChartSection
1667
- emptyLabel="No API request timings were captured for this session."
1668
- options={endpointDurationChart}
1669
- subtitle="Rank the most expensive endpoints across synchronous and async requests."
1670
- title="Hot Endpoints"
1671
- />
1672
- <ChartSection
1673
- emptyLabel="No API origin timings were captured for this session."
1674
- options={originWorkloadChart}
1675
- subtitle="Compare time spent in SSR fetchers, batch fetchers, and client async calls."
1676
- title="Origin Mix"
1677
- />
1678
- <ChartSection
1679
- emptyLabel="No API status information was captured for this session."
1680
- options={statusCountChart}
1681
- subtitle="Spot failures or pending requests without scanning every row."
1682
- title="Status Spread"
1683
- />
1684
- </div>
1685
-
1686
- <WaterfallChart
1687
- emptyLabel="No API requests were captured for this session."
1688
- itemLabel="request"
1689
- items={waterfallItems}
1690
- onSelect={setSelectedRequestId}
1691
- />
1692
-
1693
- <div className="proteum-profiler__requestGroups">
1694
- <div className="proteum-profiler__requestGroup">
1695
- <div className="proteum-profiler__requestGroupHeader">
1696
- <div className="proteum-profiler__sectionTitle">Synchronous calls</div>
1697
- <div className="proteum-profiler__requestGroupCount">
1698
- {syncItems.length} item{syncItems.length === 1 ? '' : 's'}
1699
- </div>
1666
+ <ProfilerSplitLayout
1667
+ sidebar={
1668
+ selectedItem ? (
1669
+ <ApiRequestSidebar item={selectedItem} />
1670
+ ) : (
1671
+ <ChartSidebar eyebrow="API charts" title="API overview">
1672
+ <ChartSection
1673
+ emptyLabel="No API request timings were captured for this session."
1674
+ options={endpointDurationChart}
1675
+ subtitle="Rank the most expensive endpoints across synchronous and async requests."
1676
+ title="Hot Endpoints"
1677
+ />
1678
+ <ChartSection
1679
+ emptyLabel="No API origin timings were captured for this session."
1680
+ options={originWorkloadChart}
1681
+ subtitle="Compare time spent in SSR fetchers, batch fetchers, and client async calls."
1682
+ title="Origin Mix"
1683
+ />
1684
+ <ChartSection
1685
+ emptyLabel="No API status information was captured for this session."
1686
+ options={statusCountChart}
1687
+ subtitle="Spot failures or pending requests without scanning every row."
1688
+ title="Status Spread"
1689
+ />
1690
+ <WaterfallChart
1691
+ emptyLabel="No API requests were captured for this session."
1692
+ itemLabel="request"
1693
+ items={waterfallItems}
1694
+ onSelect={(requestId) => setSelectedRequestId(requestId)}
1695
+ />
1696
+ </ChartSidebar>
1697
+ )
1698
+ }
1699
+ >
1700
+ <div className="proteum-profiler__requestGroups">
1701
+ <div className="proteum-profiler__requestGroup">
1702
+ <div className="proteum-profiler__requestGroupHeader">
1703
+ <div className="proteum-profiler__sectionTitle">Synchronous calls</div>
1704
+ <div className="proteum-profiler__requestGroupCount">
1705
+ {syncItems.length} item{syncItems.length === 1 ? '' : 's'}
1700
1706
  </div>
1701
-
1702
- {syncItems.length === 0 ? (
1703
- <div className="proteum-profiler__empty">No synchronous SSR or batched API calls captured.</div>
1704
- ) : (
1705
- <div className="proteum-profiler__list">
1706
- {syncItems.map((item) => (
1707
- <ApiRequestListEntry
1708
- isSelected={item.id === selectedItem?.id}
1709
- item={item}
1710
- key={item.id}
1711
- onSelect={() => setSelectedRequestId(item.id)}
1712
- />
1713
- ))}
1714
- </div>
1715
- )}
1716
1707
  </div>
1717
1708
 
1718
- <div className="proteum-profiler__requestGroup">
1719
- <div className="proteum-profiler__requestGroupHeader">
1720
- <div className="proteum-profiler__sectionTitle">Async requests</div>
1721
- <div className="proteum-profiler__requestGroupCount">
1722
- {asyncItems.length} item{asyncItems.length === 1 ? '' : 's'}
1723
- </div>
1709
+ {syncItems.length === 0 ? (
1710
+ <div className="proteum-profiler__empty">No synchronous SSR or batched API calls captured.</div>
1711
+ ) : (
1712
+ <div className="proteum-profiler__list">
1713
+ {syncItems.map((item) => (
1714
+ <ApiRequestListEntry
1715
+ isSelected={item.id === selectedRequestId}
1716
+ item={item}
1717
+ key={item.id}
1718
+ onSelect={() => toggleRequestSelection(item.id)}
1719
+ />
1720
+ ))}
1724
1721
  </div>
1722
+ )}
1723
+ </div>
1725
1724
 
1726
- {asyncItems.length === 0 ? (
1727
- <div className="proteum-profiler__empty">No async API calls captured.</div>
1728
- ) : (
1729
- <div className="proteum-profiler__list">
1730
- {asyncItems.map((item) => (
1731
- <ApiRequestListEntry
1732
- isSelected={item.id === selectedItem?.id}
1733
- item={item}
1734
- key={item.id}
1735
- onSelect={() => setSelectedRequestId(item.id)}
1736
- />
1737
- ))}
1738
- </div>
1739
- )}
1725
+ <div className="proteum-profiler__requestGroup">
1726
+ <div className="proteum-profiler__requestGroupHeader">
1727
+ <div className="proteum-profiler__sectionTitle">Async requests</div>
1728
+ <div className="proteum-profiler__requestGroupCount">
1729
+ {asyncItems.length} item{asyncItems.length === 1 ? '' : 's'}
1730
+ </div>
1740
1731
  </div>
1732
+
1733
+ {asyncItems.length === 0 ? (
1734
+ <div className="proteum-profiler__empty">No async API calls captured.</div>
1735
+ ) : (
1736
+ <div className="proteum-profiler__list">
1737
+ {asyncItems.map((item) => (
1738
+ <ApiRequestListEntry
1739
+ isSelected={item.id === selectedRequestId}
1740
+ item={item}
1741
+ key={item.id}
1742
+ onSelect={() => toggleRequestSelection(item.id)}
1743
+ />
1744
+ ))}
1745
+ </div>
1746
+ )}
1741
1747
  </div>
1742
1748
  </div>
1743
-
1744
- <ApiRequestSidebar item={selectedItem} />
1745
- </div>
1749
+ </ProfilerSplitLayout>
1746
1750
  );
1747
1751
  };
1748
1752
 
1749
1753
  const SqlPanel = ({ session }: { session: TProfilerNavigationSession }) => {
1750
1754
  const { groups, queryItems } = buildSqlQueryWorkspace(session);
1751
- const [selectedQueryId, setSelectedQueryId] = React.useState<string | undefined>(() => queryItems[0]?.id);
1755
+ const [selectedQueryId, setSelectedQueryId] = React.useState<string | undefined>();
1752
1756
  const callerDurationChart = buildHorizontalBarChartOptions({
1753
1757
  color: profilerChartTheme.teal,
1754
1758
  entries: buildDurationEntries(queryItems, (item) => item.callerLabel, (item) => item.durationMs),
@@ -1781,75 +1785,80 @@ const SqlPanel = ({ session }: { session: TProfilerNavigationSession }) => {
1781
1785
  });
1782
1786
 
1783
1787
  React.useEffect(() => {
1784
- if (queryItems.some((item) => item.id === selectedQueryId)) return;
1785
- setSelectedQueryId(queryItems[0]?.id);
1788
+ if (!selectedQueryId || queryItems.some((item) => item.id === selectedQueryId)) return;
1789
+ setSelectedQueryId(undefined);
1786
1790
  }, [queryItems, selectedQueryId]);
1787
1791
 
1792
+ const toggleQuerySelection = React.useCallback((queryId: string) => {
1793
+ setSelectedQueryId((currentQueryId: string | undefined) => (currentQueryId === queryId ? undefined : queryId));
1794
+ }, []);
1795
+
1788
1796
  const waterfallItems = buildSqlWaterfallItems(queryItems);
1789
- const selectedItem = queryItems.find((item) => item.id === selectedQueryId) || queryItems[0];
1797
+ const selectedItem = selectedQueryId ? queryItems.find((item) => item.id === selectedQueryId) : undefined;
1790
1798
 
1791
1799
  return (
1792
- <div className="proteum-profiler__requestWorkspace">
1793
- <div className="proteum-profiler__splitColumn">
1794
- <div className="proteum-profiler__chartGrid">
1795
- <ChartSection
1796
- emptyLabel="No SQL timings were captured for this session."
1797
- options={callerDurationChart}
1798
- subtitle="Show which callers are driving the most database time."
1799
- title="Hot Callers"
1800
- />
1801
- <ChartSection
1802
- emptyLabel="No SQL operation counts were captured for this session."
1803
- options={operationCountChart}
1804
- subtitle="Highlight whether reads, writes, or raw queries dominate the request."
1805
- title="Operation Mix"
1806
- />
1807
- <ChartSection
1808
- emptyLabel="No caller or operation overlap was captured for this session."
1809
- options={callerOperationHeatmap}
1810
- subtitle="Surface dense caller and operation combinations at a glance."
1811
- title="Caller Heatmap"
1812
- />
1813
- </div>
1814
-
1815
- <WaterfallChart
1816
- emptyLabel="No SQL queries were captured for this session."
1817
- itemLabel="query"
1818
- items={waterfallItems}
1819
- onSelect={setSelectedQueryId}
1820
- />
1821
-
1822
- <div className="proteum-profiler__requestGroups">
1823
- {groups.length === 0 ? (
1824
- <div className="proteum-profiler__empty">No SQL queries were captured for this session.</div>
1825
- ) : (
1826
- groups.map((group) => (
1827
- <div className="proteum-profiler__requestGroup" key={group.id}>
1828
- <div className="proteum-profiler__requestGroupHeader">
1829
- <div className="proteum-profiler__sectionTitle">{group.label}</div>
1830
- <div className="proteum-profiler__requestGroupCount">
1831
- {group.items.length} item{group.items.length === 1 ? '' : 's'}
1832
- </div>
1800
+ <ProfilerSplitLayout
1801
+ sidebar={
1802
+ selectedItem ? (
1803
+ <SqlQuerySidebar item={selectedItem} />
1804
+ ) : (
1805
+ <ChartSidebar eyebrow="SQL charts" title="SQL overview">
1806
+ <ChartSection
1807
+ emptyLabel="No SQL timings were captured for this session."
1808
+ options={callerDurationChart}
1809
+ subtitle="Show which callers are driving the most database time."
1810
+ title="Hot Callers"
1811
+ />
1812
+ <ChartSection
1813
+ emptyLabel="No SQL operation counts were captured for this session."
1814
+ options={operationCountChart}
1815
+ subtitle="Highlight whether reads, writes, or raw queries dominate the request."
1816
+ title="Operation Mix"
1817
+ />
1818
+ <ChartSection
1819
+ emptyLabel="No caller or operation overlap was captured for this session."
1820
+ options={callerOperationHeatmap}
1821
+ subtitle="Surface dense caller and operation combinations at a glance."
1822
+ title="Caller Heatmap"
1823
+ />
1824
+ <WaterfallChart
1825
+ emptyLabel="No SQL queries were captured for this session."
1826
+ itemLabel="query"
1827
+ items={waterfallItems}
1828
+ onSelect={(queryId) => setSelectedQueryId(queryId)}
1829
+ />
1830
+ </ChartSidebar>
1831
+ )
1832
+ }
1833
+ >
1834
+ <div className="proteum-profiler__requestGroups">
1835
+ {groups.length === 0 ? (
1836
+ <div className="proteum-profiler__empty">No SQL queries were captured for this session.</div>
1837
+ ) : (
1838
+ groups.map((group) => (
1839
+ <div className="proteum-profiler__requestGroup" key={group.id}>
1840
+ <div className="proteum-profiler__requestGroupHeader">
1841
+ <div className="proteum-profiler__sectionTitle">{group.label}</div>
1842
+ <div className="proteum-profiler__requestGroupCount">
1843
+ {group.items.length} item{group.items.length === 1 ? '' : 's'}
1833
1844
  </div>
1845
+ </div>
1834
1846
 
1835
- <div className="proteum-profiler__list">
1836
- {group.items.map((item) => (
1837
- <SqlQueryListEntry
1838
- isSelected={item.id === selectedItem?.id}
1839
- item={item}
1840
- key={item.id}
1841
- onSelect={() => setSelectedQueryId(item.id)}
1842
- />
1843
- ))}
1844
- </div>
1847
+ <div className="proteum-profiler__list">
1848
+ {group.items.map((item) => (
1849
+ <SqlQueryListEntry
1850
+ isSelected={item.id === selectedQueryId}
1851
+ item={item}
1852
+ key={item.id}
1853
+ onSelect={() => toggleQuerySelection(item.id)}
1854
+ />
1855
+ ))}
1845
1856
  </div>
1846
- ))
1847
- )}
1848
- </div>
1857
+ </div>
1858
+ ))
1859
+ )}
1849
1860
  </div>
1850
-
1851
- <SqlQuerySidebar item={selectedItem} />
1852
- </div>
1861
+ </ProfilerSplitLayout>
1853
1862
  );
1854
1863
  };
1855
1864
 
@@ -3154,6 +3163,35 @@ const ChartSection = ({
3154
3163
  </div>
3155
3164
  );
3156
3165
 
3166
+ const ChartSidebar = ({
3167
+ children,
3168
+ eyebrow = 'Charts',
3169
+ title = 'Profiler overview',
3170
+ }: {
3171
+ children: React.ReactNode;
3172
+ eyebrow?: string;
3173
+ title?: string;
3174
+ }) => (
3175
+ <aside className="proteum-profiler__sidebar proteum-profiler__sidebar--charts">
3176
+ <div className="proteum-profiler__sidebarScroller">
3177
+ <div className="proteum-profiler__sidebarHeader">
3178
+ <div className="proteum-profiler__sidebarEyebrow">{eyebrow}</div>
3179
+ <div className="proteum-profiler__sidebarTitle">
3180
+ <strong>{title}</strong>
3181
+ </div>
3182
+ </div>
3183
+ <div className="proteum-profiler__sidebarCharts">{children}</div>
3184
+ </div>
3185
+ </aside>
3186
+ );
3187
+
3188
+ const ProfilerSplitLayout = ({ children, sidebar }: { children: React.ReactNode; sidebar: React.ReactNode }) => (
3189
+ <div className="proteum-profiler__splitView">
3190
+ <div className="proteum-profiler__splitColumn">{children}</div>
3191
+ {sidebar}
3192
+ </div>
3193
+ );
3194
+
3157
3195
  const TimelinePanel = ({ session }: { session: TProfilerNavigationSession }) => {
3158
3196
  const selections: TTraceEventInspectorSelection[] = session.traces.flatMap((traceItem) =>
3159
3197
  traceItem.trace
@@ -3165,80 +3203,88 @@ const TimelinePanel = ({ session }: { session: TProfilerNavigationSession }) =>
3165
3203
  }))
3166
3204
  : [],
3167
3205
  );
3168
- const [selectedEventKey, setSelectedEventKey] = React.useState<string | undefined>(() => selections[0]?.key);
3206
+ const [selectedEventKey, setSelectedEventKey] = React.useState<string | undefined>();
3169
3207
 
3170
3208
  React.useEffect(() => {
3171
- if (selections.some((selection) => selection.key === selectedEventKey)) return;
3172
- setSelectedEventKey(selections[0]?.key);
3209
+ if (!selectedEventKey || selections.some((selection) => selection.key === selectedEventKey)) return;
3210
+ setSelectedEventKey(undefined);
3173
3211
  }, [selectedEventKey, selections]);
3174
3212
 
3213
+ const toggleEventSelection = React.useCallback((eventKey: string) => {
3214
+ setSelectedEventKey((currentEventKey: string | undefined) => (currentEventKey === eventKey ? undefined : eventKey));
3215
+ }, []);
3216
+
3175
3217
  const waterfallItems = buildTimelineWaterfallItems(session);
3176
- const selected = selections.find((selection) => selection.key === selectedEventKey) || selections[0];
3218
+ const selected = selectedEventKey ? selections.find((selection) => selection.key === selectedEventKey) : undefined;
3177
3219
 
3178
3220
  return (
3179
- <div className="proteum-profiler__splitView">
3180
- <div className="proteum-profiler__splitColumn">
3181
- <WaterfallChart
3182
- emptyLabel="No timeline events were captured for this session."
3183
- itemLabel="event"
3184
- items={waterfallItems}
3185
- onSelect={setSelectedEventKey}
3186
- />
3187
-
3188
- <div className="proteum-profiler__section">
3189
- <div className="proteum-profiler__titleRow">
3190
- <div className="proteum-profiler__sectionTitle">Navigation steps</div>
3191
- </div>
3192
- <div className="proteum-profiler__list">
3193
- {session.steps.map((step) => (
3194
- <div className="proteum-profiler__row" key={step.id}>
3195
- <div className="proteum-profiler__rowHeader">
3196
- <strong>{step.label}</strong>
3197
- <span className="proteum-profiler__mono proteum-profiler__muted">{formatDuration(step.durationMs)}</span>
3198
- </div>
3199
- <div className="proteum-profiler__tags">
3200
- <span className="proteum-profiler__tag">{step.status}</span>
3201
- {Object.entries(step.details || {}).map(([key, value]) => (
3202
- <span className="proteum-profiler__tag" key={`${step.id}:${key}`}>
3203
- {key}:{String(value)}
3204
- </span>
3205
- ))}
3206
- {step.errorMessage ? <span className="proteum-profiler__tag">{truncate(step.errorMessage, 72)}</span> : null}
3207
- </div>
3208
- </div>
3209
- ))}
3210
- </div>
3211
- </div>
3212
-
3213
- {session.traces.map((traceItem) =>
3214
- traceItem.trace ? (
3215
- <TraceRows
3216
- key={traceItem.id}
3217
- onSelect={setSelectedEventKey}
3218
- selectedEventKey={selectedEventKey}
3219
- trace={traceItem.trace}
3221
+ <ProfilerSplitLayout
3222
+ sidebar={
3223
+ selected ? (
3224
+ <TraceEventSidebar event={selected.event} label={selected.label} trace={selected.trace} />
3225
+ ) : (
3226
+ <ChartSidebar eyebrow="Timeline charts" title="Timeline overview">
3227
+ <WaterfallChart
3228
+ emptyLabel="No timeline events were captured for this session."
3229
+ itemLabel="event"
3230
+ items={waterfallItems}
3231
+ onSelect={(eventKey) => setSelectedEventKey(eventKey)}
3220
3232
  />
3221
- ) : (
3222
- <div className="proteum-profiler__row" key={traceItem.id}>
3223
- <div className="proteum-profiler__rowHeader">
3224
- <strong>{formatSessionTraceDisplay(traceItem)}</strong>
3225
- <span className="proteum-profiler__mono proteum-profiler__muted">{traceItem.status}</span>
3233
+ </ChartSidebar>
3234
+ )
3235
+ }
3236
+ >
3237
+ <div className="proteum-profiler__section">
3238
+ <div className="proteum-profiler__titleRow">
3239
+ <div className="proteum-profiler__sectionTitle">Navigation steps</div>
3240
+ </div>
3241
+ <div className="proteum-profiler__list">
3242
+ {session.steps.map((step) => (
3243
+ <div className="proteum-profiler__row" key={step.id}>
3244
+ <div className="proteum-profiler__rowHeader">
3245
+ <strong>{step.label}</strong>
3246
+ <span className="proteum-profiler__mono proteum-profiler__muted">{formatDuration(step.durationMs)}</span>
3226
3247
  </div>
3227
- <div className="proteum-profiler__mono">
3228
- {formatProfilerRequestReference({
3229
- fallbackLabel: traceItem.label,
3230
- method: traceItem.method,
3231
- path: traceItem.path,
3232
- requestData: getTraceRequestData(traceItem.trace),
3233
- })}
3248
+ <div className="proteum-profiler__tags">
3249
+ <span className="proteum-profiler__tag">{step.status}</span>
3250
+ {Object.entries(step.details || {}).map(([key, value]) => (
3251
+ <span className="proteum-profiler__tag" key={`${step.id}:${key}`}>
3252
+ {key}:{String(value)}
3253
+ </span>
3254
+ ))}
3255
+ {step.errorMessage ? <span className="proteum-profiler__tag">{truncate(step.errorMessage, 72)}</span> : null}
3234
3256
  </div>
3235
3257
  </div>
3236
- ),
3237
- )}
3258
+ ))}
3259
+ </div>
3238
3260
  </div>
3239
3261
 
3240
- <TraceEventSidebar event={selected?.event} label={selected?.label || 'Trace event'} trace={selected?.trace} />
3241
- </div>
3262
+ {session.traces.map((traceItem) =>
3263
+ traceItem.trace ? (
3264
+ <TraceRows
3265
+ key={traceItem.id}
3266
+ onSelect={toggleEventSelection}
3267
+ selectedEventKey={selectedEventKey}
3268
+ trace={traceItem.trace}
3269
+ />
3270
+ ) : (
3271
+ <div className="proteum-profiler__row" key={traceItem.id}>
3272
+ <div className="proteum-profiler__rowHeader">
3273
+ <strong>{formatSessionTraceDisplay(traceItem)}</strong>
3274
+ <span className="proteum-profiler__mono proteum-profiler__muted">{traceItem.status}</span>
3275
+ </div>
3276
+ <div className="proteum-profiler__mono">
3277
+ {formatProfilerRequestReference({
3278
+ fallbackLabel: traceItem.label,
3279
+ method: traceItem.method,
3280
+ path: traceItem.path,
3281
+ requestData: getTraceRequestData(traceItem.trace),
3282
+ })}
3283
+ </div>
3284
+ </div>
3285
+ ),
3286
+ )}
3287
+ </ProfilerSplitLayout>
3242
3288
  );
3243
3289
  };
3244
3290
 
@@ -3297,55 +3343,61 @@ const AuthPanel = ({ session }: { session: TProfilerNavigationSession }) => {
3297
3343
  trace: section.trace,
3298
3344
  })),
3299
3345
  );
3300
- const [selectedEventKey, setSelectedEventKey] = React.useState<string | undefined>(() => selections[0]?.key);
3346
+ const [selectedEventKey, setSelectedEventKey] = React.useState<string | undefined>();
3301
3347
 
3302
3348
  React.useEffect(() => {
3303
- if (selections.some((selection) => selection.key === selectedEventKey)) return;
3304
- setSelectedEventKey(selections[0]?.key);
3349
+ if (!selectedEventKey || selections.some((selection) => selection.key === selectedEventKey)) return;
3350
+ setSelectedEventKey(undefined);
3305
3351
  }, [selectedEventKey, selections]);
3306
3352
 
3353
+ const toggleEventSelection = React.useCallback((eventKey: string) => {
3354
+ setSelectedEventKey((currentEventKey: string | undefined) => (currentEventKey === eventKey ? undefined : eventKey));
3355
+ }, []);
3356
+
3307
3357
  if (authSections.length === 0) return <div className="proteum-profiler__empty">No auth activity was captured for this session.</div>;
3308
3358
 
3309
- const selected = selections.find((selection) => selection.key === selectedEventKey) || selections[0];
3359
+ const selected = selectedEventKey ? selections.find((selection) => selection.key === selectedEventKey) : undefined;
3310
3360
 
3311
3361
  return (
3312
- <div className="proteum-profiler__splitView">
3313
- <div className="proteum-profiler__splitColumn">
3314
- <div className="proteum-profiler__chartGrid">
3315
- <ChartSection
3316
- emptyLabel="No auth events were captured for this session."
3317
- options={authEventTypeChart}
3318
- subtitle="See which auth phases are actually active for the selected navigation."
3319
- title="Auth Flow"
3320
- />
3321
- <ChartSection
3322
- emptyLabel="No rule checks were captured for this session."
3323
- options={authRuleChart}
3324
- subtitle="Highlight the rules that are firing most often in the current auth flow."
3325
- title="Rule Pressure"
3326
- />
3327
- <ChartSection
3328
- emptyLabel="No auth outcome events were captured for this session."
3329
- options={authResultChart}
3330
- subtitle="Summarize allow, deny, and routing outcomes without reading every trace row."
3331
- title="Outcomes"
3332
- />
3333
- </div>
3334
-
3335
- {authSections.map((section) => (
3336
- <AuthTraceSection
3337
- authEvents={section.authEvents}
3338
- key={section.id}
3339
- label={section.label}
3340
- onSelect={setSelectedEventKey}
3341
- selectedEventKey={selectedEventKey}
3342
- trace={section.trace}
3343
- />
3344
- ))}
3345
- </div>
3346
-
3347
- <TraceEventSidebar event={selected?.event} label={selected?.label || 'Auth event'} trace={selected?.trace} />
3348
- </div>
3362
+ <ProfilerSplitLayout
3363
+ sidebar={
3364
+ selected ? (
3365
+ <TraceEventSidebar event={selected.event} label={selected.label} trace={selected.trace} />
3366
+ ) : (
3367
+ <ChartSidebar eyebrow="Auth charts" title="Auth overview">
3368
+ <ChartSection
3369
+ emptyLabel="No auth events were captured for this session."
3370
+ options={authEventTypeChart}
3371
+ subtitle="See which auth phases are actually active for the selected navigation."
3372
+ title="Auth Flow"
3373
+ />
3374
+ <ChartSection
3375
+ emptyLabel="No rule checks were captured for this session."
3376
+ options={authRuleChart}
3377
+ subtitle="Highlight the rules that are firing most often in the current auth flow."
3378
+ title="Rule Pressure"
3379
+ />
3380
+ <ChartSection
3381
+ emptyLabel="No auth outcome events were captured for this session."
3382
+ options={authResultChart}
3383
+ subtitle="Summarize allow, deny, and routing outcomes without reading every trace row."
3384
+ title="Outcomes"
3385
+ />
3386
+ </ChartSidebar>
3387
+ )
3388
+ }
3389
+ >
3390
+ {authSections.map((section) => (
3391
+ <AuthTraceSection
3392
+ authEvents={section.authEvents}
3393
+ key={section.id}
3394
+ label={section.label}
3395
+ onSelect={toggleEventSelection}
3396
+ selectedEventKey={selectedEventKey}
3397
+ trace={section.trace}
3398
+ />
3399
+ ))}
3400
+ </ProfilerSplitLayout>
3349
3401
  );
3350
3402
  };
3351
3403
 
@@ -3458,8 +3510,9 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
3458
3510
  });
3459
3511
 
3460
3512
  return (
3461
- <>
3462
- <div className="proteum-profiler__chartGrid">
3513
+ <ProfilerSplitLayout
3514
+ sidebar={
3515
+ <ChartSidebar eyebrow="Summary charts" title="Session overview">
3463
3516
  <ChartSection
3464
3517
  emptyLabel="No recent session durations were captured yet."
3465
3518
  options={durationTrendChart}
@@ -3484,8 +3537,9 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
3484
3537
  subtitle="Quick view of SSR, navigation, and request status patterns."
3485
3538
  title="Status Spread"
3486
3539
  />
3487
- </div>
3488
-
3540
+ </ChartSidebar>
3541
+ }
3542
+ >
3489
3543
  <div className="proteum-profiler__metrics">
3490
3544
  <SummaryRow label="Session" value={session.label} />
3491
3545
  <SummaryRow label="Status" value={summary.statusLabel} />
@@ -3504,7 +3558,7 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
3504
3558
  <SummaryRow label="Errors" value={String(summary.errorCount)} />
3505
3559
  <SummaryRow label="Request" value={session.requestId || 'client-only'} />
3506
3560
  </div>
3507
- </>
3561
+ </ProfilerSplitLayout>
3508
3562
  );
3509
3563
  }
3510
3564
 
@@ -3552,7 +3606,42 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
3552
3606
  const memoryChart = perf.memory ? buildPerfMemoryChartOptions(perf.memory.rows) : undefined;
3553
3607
 
3554
3608
  return (
3555
- <div className="proteum-profiler__section">
3609
+ <ProfilerSplitLayout
3610
+ sidebar={
3611
+ <ChartSidebar eyebrow="Performance charts" title="Performance overview">
3612
+ <WaterfallChart
3613
+ emptyLabel="No request stages were captured."
3614
+ itemLabel="stage"
3615
+ items={waterfallItems}
3616
+ />
3617
+ <ChartSection
3618
+ emptyLabel="No hot-path latency data matched this window."
3619
+ options={topLatencyChart}
3620
+ subtitle={`Compare average and p95 latency across the hottest ${perf.groupBy}s.`}
3621
+ title={`Hot ${perf.groupBy}s`}
3622
+ />
3623
+ <ChartSection
3624
+ emptyLabel="No breakdown data matched this window."
3625
+ options={topBreakdownChart}
3626
+ subtitle="See whether self time, SQL, external calls, or render work dominates the response."
3627
+ title="Time Breakdown"
3628
+ />
3629
+ <ChartSection
3630
+ emptyLabel="No compare data matched these windows."
3631
+ options={compareChart}
3632
+ subtitle={`Track p95 regression pressure between ${perf.baseline} and ${perf.target}.`}
3633
+ title="Regression Delta"
3634
+ />
3635
+ <ChartSection
3636
+ emptyLabel="No memory drift data matched this window."
3637
+ options={memoryChart}
3638
+ subtitle="Compare average heap growth, peak heap growth, and average RSS drift per group."
3639
+ title="Memory Drift"
3640
+ />
3641
+ </ChartSidebar>
3642
+ }
3643
+ >
3644
+ <div className="proteum-profiler__section">
3556
3645
  <div className="proteum-profiler__sectionHeader">
3557
3646
  <div className="proteum-profiler__sectionTitle">Performance</div>
3558
3647
  <div className="proteum-profiler__actions">
@@ -3646,11 +3735,6 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
3646
3735
 
3647
3736
  {currentRequest ? (
3648
3737
  <>
3649
- <WaterfallChart
3650
- emptyLabel="No request stages were captured."
3651
- itemLabel="stage"
3652
- items={waterfallItems}
3653
- />
3654
3738
  <div className="proteum-profiler__metrics">
3655
3739
  <SummaryRow label="Route" value={currentRequest.routeLabel} />
3656
3740
  <SummaryRow label="Controller" value={currentRequest.controllerLabel} />
@@ -3669,33 +3753,6 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
3669
3753
  <div className="proteum-profiler__empty">No traced request is attached to this session yet.</div>
3670
3754
  )}
3671
3755
 
3672
- <div className="proteum-profiler__chartGrid">
3673
- <ChartSection
3674
- emptyLabel="No hot-path latency data matched this window."
3675
- options={topLatencyChart}
3676
- subtitle={`Compare average and p95 latency across the hottest ${perf.groupBy}s.`}
3677
- title={`Hot ${perf.groupBy}s`}
3678
- />
3679
- <ChartSection
3680
- emptyLabel="No breakdown data matched this window."
3681
- options={topBreakdownChart}
3682
- subtitle="See whether self time, SQL, external calls, or render work dominates the response."
3683
- title="Time Breakdown"
3684
- />
3685
- <ChartSection
3686
- emptyLabel="No compare data matched these windows."
3687
- options={compareChart}
3688
- subtitle={`Track p95 regression pressure between ${perf.baseline} and ${perf.target}.`}
3689
- title="Regression Delta"
3690
- />
3691
- <ChartSection
3692
- emptyLabel="No memory drift data matched this window."
3693
- options={memoryChart}
3694
- subtitle="Compare average heap growth, peak heap growth, and average RSS drift per group."
3695
- title="Memory Drift"
3696
- />
3697
- </div>
3698
-
3699
3756
  <SimpleSection empty="No hot calls captured for this request." rows={callRows} title="Current Request Calls" />
3700
3757
  <SimpleSection empty="No hot SQL captured for this request." rows={sqlRows} title="Current Request SQL" />
3701
3758
  <SimpleSection empty="No perf rollups matched this window." rows={topRows} title={`Hot ${perf.groupBy}s`} />
@@ -3703,7 +3760,8 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
3703
3760
  <SimpleSection empty="No memory drift data matched this window." rows={memoryRows} title="Memory" />
3704
3761
  </>
3705
3762
  )}
3706
- </div>
3763
+ </div>
3764
+ </ProfilerSplitLayout>
3707
3765
  );
3708
3766
  }
3709
3767
 
@@ -3761,8 +3819,9 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
3761
3819
  });
3762
3820
 
3763
3821
  return (
3764
- <>
3765
- <div className="proteum-profiler__chartGrid">
3822
+ <ProfilerSplitLayout
3823
+ sidebar={
3824
+ <ChartSidebar eyebrow="Routing charts" title="Routing overview">
3766
3825
  <ChartSection
3767
3826
  emptyLabel="No routing events were captured yet."
3768
3827
  options={routingFlowChart}
@@ -3781,8 +3840,9 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
3781
3840
  subtitle="Highlight skip reasons, matched routes, and controller routing outcomes."
3782
3841
  title="Decisions"
3783
3842
  />
3784
- </div>
3785
-
3843
+ </ChartSidebar>
3844
+ }
3845
+ >
3786
3846
  <SimpleSection
3787
3847
  empty="No routing data captured yet."
3788
3848
  rows={routingEvents.map((event) => ({
@@ -3795,7 +3855,7 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
3795
3855
  showTitle={false}
3796
3856
  title="Routing"
3797
3857
  />
3798
- </>
3858
+ </ProfilerSplitLayout>
3799
3859
  );
3800
3860
  }
3801
3861
 
@@ -3828,8 +3888,9 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
3828
3888
  });
3829
3889
 
3830
3890
  return (
3831
- <>
3832
- <div className="proteum-profiler__chartGrid">
3891
+ <ProfilerSplitLayout
3892
+ sidebar={
3893
+ <ChartSidebar eyebrow="Controller charts" title="Controller overview">
3833
3894
  <ChartSection
3834
3895
  emptyLabel="No controller events were captured yet."
3835
3896
  options={controllerFlowChart}
@@ -3848,8 +3909,9 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
3848
3909
  subtitle="Highlight which detail fields are most common across controller events."
3849
3910
  title="Detail Keys"
3850
3911
  />
3851
- </div>
3852
-
3912
+ </ChartSidebar>
3913
+ }
3914
+ >
3853
3915
  <SimpleSection
3854
3916
  empty="No controller data captured yet."
3855
3917
  rows={controllerEvents.map((event) => ({
@@ -3862,7 +3924,7 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
3862
3924
  showTitle={false}
3863
3925
  title="Controller"
3864
3926
  />
3865
- </>
3927
+ </ProfilerSplitLayout>
3866
3928
  );
3867
3929
  }
3868
3930
 
@@ -3906,8 +3968,9 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
3906
3968
  });
3907
3969
 
3908
3970
  return (
3909
- <>
3910
- <div className="proteum-profiler__chartGrid">
3971
+ <ProfilerSplitLayout
3972
+ sidebar={
3973
+ <ChartSidebar eyebrow="SSR charts" title="SSR overview">
3911
3974
  <ChartSection
3912
3975
  emptyLabel="No SSR timing and payload data was captured yet."
3913
3976
  options={ssrScatterChart}
@@ -3926,8 +3989,9 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
3926
3989
  subtitle="Track render time across the last few SSR sessions."
3927
3990
  title="Render Trend"
3928
3991
  />
3929
- </div>
3930
-
3992
+ </ChartSidebar>
3993
+ }
3994
+ >
3931
3995
  <SimpleSection
3932
3996
  empty="No SSR data captured for this session."
3933
3997
  rows={ssrEvents.map((event) => ({
@@ -3940,7 +4004,7 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
3940
4004
  showTitle={false}
3941
4005
  title="SSR"
3942
4006
  />
3943
- </>
4007
+ </ProfilerSplitLayout>
3944
4008
  );
3945
4009
  }
3946
4010
 
@@ -4024,7 +4088,31 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
4024
4088
  })) || [];
4025
4089
 
4026
4090
  return (
4027
- <div className="proteum-profiler__section">
4091
+ <ProfilerSplitLayout
4092
+ sidebar={
4093
+ <ChartSidebar eyebrow="Diagnose charts" title="Diagnose overview">
4094
+ <ChartSection
4095
+ emptyLabel="No suspect scores were returned for this diagnose run."
4096
+ options={suspectScoreChart}
4097
+ subtitle="Rank the files Proteum currently believes are most likely involved."
4098
+ title="Suspects"
4099
+ />
4100
+ <ChartSection
4101
+ emptyLabel="No owner matches were returned for this diagnose run."
4102
+ options={ownerScoreChart}
4103
+ subtitle="Compare owner candidates and their confidence scores."
4104
+ title="Owner Matches"
4105
+ />
4106
+ <ChartSection
4107
+ emptyLabel="No diagnose severity data was returned for this run."
4108
+ options={diagnoseSeverityChart}
4109
+ subtitle="Compare doctor diagnostics against contract diagnostics in one view."
4110
+ title="Severity"
4111
+ />
4112
+ </ChartSidebar>
4113
+ }
4114
+ >
4115
+ <div className="proteum-profiler__section">
4028
4116
  <div className="proteum-profiler__sectionHeader">
4029
4117
  <div className="proteum-profiler__sectionTitle">Diagnose</div>
4030
4118
  <div className="proteum-profiler__actions">
@@ -4059,33 +4147,14 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
4059
4147
  value={diagnose.lastLoadedAt ? formatTimestamp(diagnose.lastLoadedAt) : 'Not loaded'}
4060
4148
  />
4061
4149
  </div>
4062
- <div className="proteum-profiler__chartGrid">
4063
- <ChartSection
4064
- emptyLabel="No suspect scores were returned for this diagnose run."
4065
- options={suspectScoreChart}
4066
- subtitle="Rank the files Proteum currently believes are most likely involved."
4067
- title="Suspects"
4068
- />
4069
- <ChartSection
4070
- emptyLabel="No owner matches were returned for this diagnose run."
4071
- options={ownerScoreChart}
4072
- subtitle="Compare owner candidates and their confidence scores."
4073
- title="Owner Matches"
4074
- />
4075
- <ChartSection
4076
- emptyLabel="No diagnose severity data was returned for this run."
4077
- options={diagnoseSeverityChart}
4078
- subtitle="Compare doctor diagnostics against contract diagnostics in one view."
4079
- title="Severity"
4080
- />
4081
- </div>
4082
4150
  <SimpleSection empty="No likely suspect files were found." rows={suspectRows} title="Suspects" />
4083
4151
  <SimpleSection empty="No owner matches were found." rows={ownerRows} title="Owner Matches" />
4084
4152
  <SimpleSection empty="No contract diagnostics were found." rows={contractRows} title="Contracts" />
4085
4153
  <SimpleSection empty="No recent server logs were captured." rows={logRows} title="Server Logs" />
4086
4154
  </>
4087
4155
  )}
4088
- </div>
4156
+ </div>
4157
+ </ProfilerSplitLayout>
4089
4158
  );
4090
4159
  }
4091
4160
 
@@ -4167,7 +4236,31 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
4167
4236
  : undefined;
4168
4237
 
4169
4238
  return (
4170
- <div className="proteum-profiler__section">
4239
+ <ProfilerSplitLayout
4240
+ sidebar={
4241
+ <ChartSidebar eyebrow="Explain charts" title="Manifest overview">
4242
+ <ChartSection
4243
+ emptyLabel="No manifest structure data is available."
4244
+ options={structureChart}
4245
+ subtitle="Summarize the main object counts in the current Proteum manifest."
4246
+ title="Structure"
4247
+ />
4248
+ <ChartSection
4249
+ emptyLabel="No manifest scope data is available."
4250
+ options={manifestScopeChart}
4251
+ subtitle="Show how much of the current manifest comes from app code vs framework code."
4252
+ title="Scope Split"
4253
+ />
4254
+ <ChartSection
4255
+ emptyLabel="No manifest env readiness data is available."
4256
+ options={envReadinessChart}
4257
+ subtitle="Check required variable coverage before digging through the raw manifest."
4258
+ title="Env Ready"
4259
+ />
4260
+ </ChartSidebar>
4261
+ }
4262
+ >
4263
+ <div className="proteum-profiler__section">
4171
4264
  <div className="proteum-profiler__sectionHeader">
4172
4265
  <div className="proteum-profiler__sectionTitle">Explain</div>
4173
4266
  <div className="proteum-profiler__actions">
@@ -4192,27 +4285,6 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
4192
4285
  <div className="proteum-profiler__empty">No explain manifest is available.</div>
4193
4286
  ) : (
4194
4287
  <>
4195
- <div className="proteum-profiler__chartGrid">
4196
- <ChartSection
4197
- emptyLabel="No manifest structure data is available."
4198
- options={structureChart}
4199
- subtitle="Summarize the main object counts in the current Proteum manifest."
4200
- title="Structure"
4201
- />
4202
- <ChartSection
4203
- emptyLabel="No manifest scope data is available."
4204
- options={manifestScopeChart}
4205
- subtitle="Show how much of the current manifest comes from app code vs framework code."
4206
- title="Scope Split"
4207
- />
4208
- <ChartSection
4209
- emptyLabel="No manifest env readiness data is available."
4210
- options={envReadinessChart}
4211
- subtitle="Check required variable coverage before digging through the raw manifest."
4212
- title="Env Ready"
4213
- />
4214
- </div>
4215
-
4216
4288
  <div className="proteum-profiler__row">
4217
4289
  <div className="proteum-profiler__rowHeader">
4218
4290
  <strong>Manifest snapshot</strong>
@@ -4227,7 +4299,8 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
4227
4299
  <TextBlocks blocks={blocks} />
4228
4300
  </>
4229
4301
  )}
4230
- </div>
4302
+ </div>
4303
+ </ProfilerSplitLayout>
4231
4304
  );
4232
4305
  }
4233
4306
 
@@ -4289,7 +4362,31 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
4289
4362
  const doctorBlocks = state.explain.manifest ? buildDoctorBlocks(state.explain.manifest) : [];
4290
4363
 
4291
4364
  return (
4292
- <div className="proteum-profiler__section">
4365
+ <ProfilerSplitLayout
4366
+ sidebar={
4367
+ <ChartSidebar eyebrow="Doctor charts" title="Doctor overview">
4368
+ <ChartSection
4369
+ emptyLabel="No doctor severity data is available."
4370
+ options={doctorSeverityChart}
4371
+ subtitle="Compare doctor and contract diagnostics across errors and warnings."
4372
+ title="Severity"
4373
+ />
4374
+ <ChartSection
4375
+ emptyLabel="No diagnostic codes are available."
4376
+ options={doctorCodeChart}
4377
+ subtitle="See which diagnostic families are dominating the current manifest."
4378
+ title="Codes"
4379
+ />
4380
+ <ChartSection
4381
+ emptyLabel="No diagnostic file hotspots are available."
4382
+ options={doctorFileChart}
4383
+ subtitle="Highlight the files attracting the most diagnostics."
4384
+ title="Files"
4385
+ />
4386
+ </ChartSidebar>
4387
+ }
4388
+ >
4389
+ <div className="proteum-profiler__section">
4293
4390
  <div className="proteum-profiler__sectionHeader">
4294
4391
  <div className="proteum-profiler__sectionTitle">Doctor</div>
4295
4392
  <div className="proteum-profiler__actions">
@@ -4331,26 +4428,6 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
4331
4428
  value={doctor.lastLoadedAt ? formatTimestamp(doctor.lastLoadedAt) : 'Not loaded'}
4332
4429
  />
4333
4430
  </div>
4334
- <div className="proteum-profiler__chartGrid">
4335
- <ChartSection
4336
- emptyLabel="No doctor severity data is available."
4337
- options={doctorSeverityChart}
4338
- subtitle="Compare doctor and contract diagnostics across errors and warnings."
4339
- title="Severity"
4340
- />
4341
- <ChartSection
4342
- emptyLabel="No diagnostic codes are available."
4343
- options={doctorCodeChart}
4344
- subtitle="See which diagnostic families are dominating the current manifest."
4345
- title="Codes"
4346
- />
4347
- <ChartSection
4348
- emptyLabel="No diagnostic file hotspots are available."
4349
- options={doctorFileChart}
4350
- subtitle="Highlight the files attracting the most diagnostics."
4351
- title="Files"
4352
- />
4353
- </div>
4354
4431
  {doctorBlocks.length > 0 ? (
4355
4432
  <TextBlocks blocks={doctorBlocks} />
4356
4433
  ) : (
@@ -4359,7 +4436,8 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
4359
4436
  <SimpleSection empty="No contract diagnostics were found." rows={contractRows} title="Contracts" />
4360
4437
  </>
4361
4438
  )}
4362
- </div>
4439
+ </div>
4440
+ </ProfilerSplitLayout>
4363
4441
  );
4364
4442
  }
4365
4443
 
@@ -4394,7 +4472,31 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
4394
4472
  });
4395
4473
 
4396
4474
  return (
4397
- <div className="proteum-profiler__section">
4475
+ <ProfilerSplitLayout
4476
+ sidebar={
4477
+ <ChartSidebar eyebrow="Command charts" title="Command overview">
4478
+ <ChartSection
4479
+ emptyLabel="No command scope data is available."
4480
+ options={commandScopeChart}
4481
+ subtitle="Show how the registered dev commands split between app and framework scopes."
4482
+ title="Scope"
4483
+ />
4484
+ <ChartSection
4485
+ emptyLabel="No command execution durations have been captured yet."
4486
+ options={commandDurationChart}
4487
+ subtitle="Highlight the commands with the slowest latest execution."
4488
+ title="Latest Duration"
4489
+ />
4490
+ <ChartSection
4491
+ emptyLabel="No command execution statuses have been captured yet."
4492
+ options={commandStatusChart}
4493
+ subtitle="Separate commands that have never run from completed or failed commands."
4494
+ title="Status"
4495
+ />
4496
+ </ChartSidebar>
4497
+ }
4498
+ >
4499
+ <div className="proteum-profiler__section">
4398
4500
  <div className="proteum-profiler__sectionHeader">
4399
4501
  <div className="proteum-profiler__sectionTitle">Available commands</div>
4400
4502
  <div className="proteum-profiler__actions">
@@ -4431,27 +4533,6 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
4431
4533
  <div className="proteum-profiler__empty">No commands are registered for this app.</div>
4432
4534
  ) : (
4433
4535
  <>
4434
- <div className="proteum-profiler__chartGrid">
4435
- <ChartSection
4436
- emptyLabel="No command scope data is available."
4437
- options={commandScopeChart}
4438
- subtitle="Show how the registered dev commands split between app and framework scopes."
4439
- title="Scope"
4440
- />
4441
- <ChartSection
4442
- emptyLabel="No command execution durations have been captured yet."
4443
- options={commandDurationChart}
4444
- subtitle="Highlight the commands with the slowest latest execution."
4445
- title="Latest Duration"
4446
- />
4447
- <ChartSection
4448
- emptyLabel="No command execution statuses have been captured yet."
4449
- options={commandStatusChart}
4450
- subtitle="Separate commands that have never run from completed or failed commands."
4451
- title="Status"
4452
- />
4453
- </div>
4454
-
4455
4536
  <div className="proteum-profiler__list">
4456
4537
  {commandsState.commands.map((command: TDevCommandDefinition) => {
4457
4538
  const execution = commandsState.executions[command.path] as TDevCommandExecution | undefined;
@@ -4513,7 +4594,8 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
4513
4594
  </div>
4514
4595
  </>
4515
4596
  )}
4516
- </div>
4597
+ </div>
4598
+ </ProfilerSplitLayout>
4517
4599
  );
4518
4600
  }
4519
4601
 
@@ -4545,7 +4627,31 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
4545
4627
  });
4546
4628
 
4547
4629
  return (
4548
- <div className="proteum-profiler__section">
4630
+ <ProfilerSplitLayout
4631
+ sidebar={
4632
+ <ChartSidebar eyebrow="Cron charts" title="Cron overview">
4633
+ <ChartSection
4634
+ emptyLabel="No cron run counts are available."
4635
+ options={cronRunCountChart}
4636
+ subtitle="Highlight which tasks have been exercised the most in the current dev session."
4637
+ title="Runs"
4638
+ />
4639
+ <ChartSection
4640
+ emptyLabel="No cron duration data is available."
4641
+ options={cronDurationChart}
4642
+ subtitle="Compare the latest duration of tasks that have been executed."
4643
+ title="Duration"
4644
+ />
4645
+ <ChartSection
4646
+ emptyLabel="No cron status data is available."
4647
+ options={cronStatusChart}
4648
+ subtitle="Separate currently running, completed, failed, and never-run tasks."
4649
+ title="Status"
4650
+ />
4651
+ </ChartSidebar>
4652
+ }
4653
+ >
4654
+ <div className="proteum-profiler__section">
4549
4655
  <div className="proteum-profiler__sectionHeader">
4550
4656
  <div className="proteum-profiler__sectionTitle">Registered tasks</div>
4551
4657
  <div className="proteum-profiler__actions">
@@ -4583,27 +4689,6 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
4583
4689
  <div className="proteum-profiler__empty">No cron tasks are registered for this app.</div>
4584
4690
  ) : (
4585
4691
  <>
4586
- <div className="proteum-profiler__chartGrid">
4587
- <ChartSection
4588
- emptyLabel="No cron run counts are available."
4589
- options={cronRunCountChart}
4590
- subtitle="Highlight which tasks have been exercised the most in the current dev session."
4591
- title="Runs"
4592
- />
4593
- <ChartSection
4594
- emptyLabel="No cron duration data is available."
4595
- options={cronDurationChart}
4596
- subtitle="Compare the latest duration of tasks that have been executed."
4597
- title="Duration"
4598
- />
4599
- <ChartSection
4600
- emptyLabel="No cron status data is available."
4601
- options={cronStatusChart}
4602
- subtitle="Separate currently running, completed, failed, and never-run tasks."
4603
- title="Status"
4604
- />
4605
- </div>
4606
-
4607
4692
  <div className="proteum-profiler__list">
4608
4693
  {cron.tasks.map((task) => (
4609
4694
  <div className="proteum-profiler__row" key={task.name}>
@@ -4657,7 +4742,8 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
4657
4742
  </div>
4658
4743
  </>
4659
4744
  )}
4660
- </div>
4745
+ </div>
4746
+ </ProfilerSplitLayout>
4661
4747
  );
4662
4748
  }
4663
4749
 
@@ -4693,8 +4779,9 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
4693
4779
  });
4694
4780
 
4695
4781
  return (
4696
- <>
4697
- <div className="proteum-profiler__chartGrid">
4782
+ <ProfilerSplitLayout
4783
+ sidebar={
4784
+ <ChartSidebar eyebrow="Error charts" title="Error overview">
4698
4785
  <ChartSection
4699
4786
  emptyLabel="No errors were captured for this session."
4700
4787
  options={errorSourceChart}
@@ -4707,17 +4794,145 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
4707
4794
  subtitle="Highlight the error families that are repeating in the selected session."
4708
4795
  title="Groups"
4709
4796
  />
4710
- </div>
4711
-
4797
+ </ChartSidebar>
4798
+ }
4799
+ >
4712
4800
  <SimpleSection empty="No errors captured." rows={errorRows} showTitle={false} title="Errors" />
4713
- </>
4801
+ </ProfilerSplitLayout>
4714
4802
  );
4715
4803
  };
4716
4804
 
4717
- const splitScrollPanels = new Set<TProfilerPanel>(['timeline', 'auth', 'api', 'sql']);
4805
+ const splitScrollPanels = new Set<TProfilerPanel>(Object.keys(panelLabels) as TProfilerPanel[]);
4806
+ const profilerHandleStorageKey = 'proteum.dev.profiler.handle-position';
4807
+ const profilerHandleSize = 46;
4808
+ const profilerHandleViewportMargin = 10;
4809
+
4810
+ type TProfilerHandlePosition = {
4811
+ x: number;
4812
+ y: number;
4813
+ };
4814
+
4815
+ type TProfilerHandleDragState = {
4816
+ element: HTMLButtonElement;
4817
+ moved: boolean;
4818
+ origin: TProfilerHandlePosition;
4819
+ pointerId: number;
4820
+ size: number;
4821
+ startX: number;
4822
+ startY: number;
4823
+ };
4824
+
4825
+ const clampProfilerHandlePosition = (position: TProfilerHandlePosition, size = profilerHandleSize): TProfilerHandlePosition => {
4826
+ if (typeof window === 'undefined') return position;
4827
+
4828
+ const maxX = Math.max(profilerHandleViewportMargin, window.innerWidth - size - profilerHandleViewportMargin);
4829
+ const maxY = Math.max(profilerHandleViewportMargin, window.innerHeight - size - profilerHandleViewportMargin);
4830
+
4831
+ return {
4832
+ x: Math.min(Math.max(position.x, profilerHandleViewportMargin), maxX),
4833
+ y: Math.min(Math.max(position.y, profilerHandleViewportMargin), maxY),
4834
+ };
4835
+ };
4836
+
4837
+ const isProfilerHandlePosition = (value: unknown): value is TProfilerHandlePosition => {
4838
+ if (!value || typeof value !== 'object') return false;
4839
+
4840
+ const position = value as Partial<TProfilerHandlePosition>;
4841
+ return typeof position.x === 'number' && Number.isFinite(position.x) && typeof position.y === 'number' && Number.isFinite(position.y);
4842
+ };
4843
+
4844
+ const readProfilerHandlePosition = (): TProfilerHandlePosition | undefined => {
4845
+ if (typeof window === 'undefined') return undefined;
4846
+
4847
+ try {
4848
+ const value = window.sessionStorage.getItem(profilerHandleStorageKey);
4849
+ if (!value) return undefined;
4850
+
4851
+ const position = JSON.parse(value) as unknown;
4852
+ return isProfilerHandlePosition(position) ? clampProfilerHandlePosition(position) : undefined;
4853
+ } catch (error) {
4854
+ return undefined;
4855
+ }
4856
+ };
4857
+
4858
+ const writeProfilerHandlePosition = (position: TProfilerHandlePosition) => {
4859
+ if (typeof window === 'undefined') return;
4860
+
4861
+ try {
4862
+ window.sessionStorage.setItem(profilerHandleStorageKey, JSON.stringify(position));
4863
+ } catch (error) {}
4864
+ };
4865
+
4866
+ const getProfilerHandleElementPosition = (element: HTMLElement): TProfilerHandlePosition => {
4867
+ const rect = element.getBoundingClientRect();
4868
+ return { x: rect.left, y: rect.top };
4869
+ };
4870
+
4871
+ type TProfilerHandleMoveEvent = {
4872
+ clientX: number;
4873
+ clientY: number;
4874
+ pointerId?: number;
4875
+ preventDefault?: () => void;
4876
+ };
4877
+
4878
+ type TProfilerHandleEndEvent = {
4879
+ pointerId?: number;
4880
+ };
4881
+
4882
+ const AtomIcon = () => (
4883
+ <svg aria-hidden="true" className="proteum-profiler__handleIcon" fill="none" viewBox="0 0 24 24">
4884
+ <circle cx="12" cy="12" fill="currentColor" r="1.9" />
4885
+ <ellipse cx="12" cy="12" rx="9" ry="3.7" stroke="currentColor" strokeWidth="1.7" />
4886
+ <ellipse cx="12" cy="12" rx="9" ry="3.7" stroke="currentColor" strokeWidth="1.7" transform="rotate(60 12 12)" />
4887
+ <ellipse cx="12" cy="12" rx="9" ry="3.7" stroke="currentColor" strokeWidth="1.7" transform="rotate(120 12 12)" />
4888
+ </svg>
4889
+ );
4718
4890
 
4719
4891
  export default function DevProfiler() {
4720
4892
  const [state, setState] = React.useState(() => profilerRuntime.getState());
4893
+ const [handlePosition, setHandlePosition] = React.useState<TProfilerHandlePosition | undefined>(() => readProfilerHandlePosition());
4894
+ const [isHandleDragging, setIsHandleDragging] = React.useState(false);
4895
+ const handleDragStateRef = React.useRef<TProfilerHandleDragState | undefined>(undefined);
4896
+ const ignoreHandleClickRef = React.useRef(false);
4897
+
4898
+ const dragHandle = React.useCallback((event: TProfilerHandleMoveEvent) => {
4899
+ const dragState = handleDragStateRef.current;
4900
+ if (!dragState || (event.pointerId !== undefined && dragState.pointerId !== event.pointerId)) return;
4901
+
4902
+ const deltaX = event.clientX - dragState.startX;
4903
+ const deltaY = event.clientY - dragState.startY;
4904
+ if (!dragState.moved && Math.hypot(deltaX, deltaY) > 3) dragState.moved = true;
4905
+
4906
+ const nextPosition = clampProfilerHandlePosition(
4907
+ {
4908
+ x: dragState.origin.x + deltaX,
4909
+ y: dragState.origin.y + deltaY,
4910
+ },
4911
+ dragState.size,
4912
+ );
4913
+
4914
+ setHandlePosition(nextPosition);
4915
+ writeProfilerHandlePosition(nextPosition);
4916
+ event.preventDefault?.();
4917
+ }, []);
4918
+
4919
+ const finishHandleDrag = React.useCallback((event: TProfilerHandleEndEvent) => {
4920
+ const dragState = handleDragStateRef.current;
4921
+ if (!dragState || (event.pointerId !== undefined && dragState.pointerId !== event.pointerId)) return;
4922
+
4923
+ if (dragState.moved) {
4924
+ ignoreHandleClickRef.current = true;
4925
+ window.setTimeout(() => {
4926
+ ignoreHandleClickRef.current = false;
4927
+ }, 250);
4928
+ }
4929
+
4930
+ handleDragStateRef.current = undefined;
4931
+ setIsHandleDragging(false);
4932
+ if (dragState.element.hasPointerCapture(dragState.pointerId)) {
4933
+ dragState.element.releasePointerCapture(dragState.pointerId);
4934
+ }
4935
+ }, []);
4721
4936
 
4722
4937
  React.useEffect(() => profilerRuntime.subscribe(() => setState(profilerRuntime.getState())), []);
4723
4938
  React.useEffect(() => {
@@ -4736,6 +4951,81 @@ export default function DevProfiler() {
4736
4951
  return () => window.removeEventListener('keydown', onKeyDown);
4737
4952
  }, []);
4738
4953
 
4954
+ React.useEffect(() => {
4955
+ const onResize = () => {
4956
+ setHandlePosition((position: TProfilerHandlePosition | undefined) => {
4957
+ if (!position) return position;
4958
+
4959
+ const nextPosition = clampProfilerHandlePosition(position);
4960
+ writeProfilerHandlePosition(nextPosition);
4961
+ return nextPosition;
4962
+ });
4963
+ };
4964
+
4965
+ window.addEventListener('resize', onResize);
4966
+ return () => window.removeEventListener('resize', onResize);
4967
+ }, []);
4968
+
4969
+ React.useEffect(() => {
4970
+ if (!isHandleDragging) return;
4971
+
4972
+ const onPointerMove = (event: PointerEvent) => dragHandle(event);
4973
+ const onPointerEnd = (event: PointerEvent) => finishHandleDrag(event);
4974
+ const onMouseMove = (event: MouseEvent) => dragHandle(event);
4975
+ const onMouseEnd = () => finishHandleDrag({});
4976
+ const eventOptions: AddEventListenerOptions = { passive: false };
4977
+
4978
+ window.addEventListener('pointermove', onPointerMove, eventOptions);
4979
+ window.addEventListener('pointerup', onPointerEnd);
4980
+ window.addEventListener('pointercancel', onPointerEnd);
4981
+ window.addEventListener('mousemove', onMouseMove, eventOptions);
4982
+ window.addEventListener('mouseup', onMouseEnd);
4983
+
4984
+ return () => {
4985
+ window.removeEventListener('pointermove', onPointerMove, eventOptions);
4986
+ window.removeEventListener('pointerup', onPointerEnd);
4987
+ window.removeEventListener('pointercancel', onPointerEnd);
4988
+ window.removeEventListener('mousemove', onMouseMove, eventOptions);
4989
+ window.removeEventListener('mouseup', onMouseEnd);
4990
+ };
4991
+ }, [dragHandle, finishHandleDrag, isHandleDragging]);
4992
+
4993
+ const beginHandleDrag = React.useCallback(
4994
+ (event: React.PointerEvent<HTMLButtonElement>) => {
4995
+ if (event.button !== 0) return;
4996
+
4997
+ const element = event.currentTarget;
4998
+ const rect = element.getBoundingClientRect();
4999
+ const origin = handlePosition || getProfilerHandleElementPosition(element);
5000
+
5001
+ handleDragStateRef.current = {
5002
+ element,
5003
+ moved: false,
5004
+ origin,
5005
+ pointerId: event.pointerId,
5006
+ size: Math.max(rect.width, rect.height, profilerHandleSize),
5007
+ startX: event.clientX,
5008
+ startY: event.clientY,
5009
+ };
5010
+
5011
+ setIsHandleDragging(true);
5012
+ element.setPointerCapture(event.pointerId);
5013
+ event.preventDefault();
5014
+ },
5015
+ [handlePosition],
5016
+ );
5017
+
5018
+ const openProfilerFromHandle = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
5019
+ if (ignoreHandleClickRef.current) {
5020
+ ignoreHandleClickRef.current = false;
5021
+ event.preventDefault();
5022
+ event.stopPropagation();
5023
+ return;
5024
+ }
5025
+
5026
+ profilerRuntime.setUiState('minimized');
5027
+ }, []);
5028
+
4739
5029
  if (!window.dev) return null;
4740
5030
 
4741
5031
  const session = getSelectedSession(state.sessions, state.selectedSessionId, state.currentSessionId);
@@ -4763,11 +5053,27 @@ export default function DevProfiler() {
4763
5053
 
4764
5054
  {state.uiState === 'pinned-handle' ? (
4765
5055
  <button
4766
- className="proteum-profiler__handle"
4767
- onClick={() => profilerRuntime.setUiState('minimized')}
5056
+ aria-label="Open Proteum profiler"
5057
+ className={isHandleDragging ? 'proteum-profiler__handle proteum-profiler__handle--dragging' : 'proteum-profiler__handle'}
5058
+ onClick={openProfilerFromHandle}
5059
+ onPointerCancel={finishHandleDrag}
5060
+ onPointerDown={beginHandleDrag}
5061
+ onPointerMove={dragHandle}
5062
+ onPointerUp={finishHandleDrag}
5063
+ style={
5064
+ handlePosition
5065
+ ? {
5066
+ bottom: 'auto',
5067
+ left: handlePosition.x,
5068
+ right: 'auto',
5069
+ top: handlePosition.y,
5070
+ }
5071
+ : undefined
5072
+ }
5073
+ title="Open Proteum profiler"
4768
5074
  type="button"
4769
5075
  >
4770
- Proteum Profiler
5076
+ <AtomIcon />
4771
5077
  </button>
4772
5078
  ) : (
4773
5079
  <>