From 3eda824868e4641723dcb8d286b4193b13c2a7fa Mon Sep 17 00:00:00 2001 From: Alex Denford SBL Date: Fri, 27 Mar 2026 14:57:47 -0700 Subject: [PATCH 01/25] wip --- src/panels/minecraft-diagnostics.ts | 6 + src/session.ts | 2 + src/stats/stats-provider.ts | 29 +++ webview-ui/src/diagnostics_panel/App.tsx | 114 +++++++++--- .../diagnostics_panel/DiagnosticsSchema.ts | 29 +++ .../src/diagnostics_panel/DynamicTab.tsx | 166 ++++++++++++++++++ webview-ui/src/diagnostics_panel/index.tsx | 2 +- .../diagnostics_panel/prefabs/TabPrefab.ts | 2 - 8 files changed, 319 insertions(+), 31 deletions(-) create mode 100644 webview-ui/src/diagnostics_panel/DiagnosticsSchema.ts create mode 100644 webview-ui/src/diagnostics_panel/DynamicTab.tsx diff --git a/src/panels/minecraft-diagnostics.ts b/src/panels/minecraft-diagnostics.ts index b535c058..bce18d0d 100644 --- a/src/panels/minecraft-diagnostics.ts +++ b/src/panels/minecraft-diagnostics.ts @@ -96,6 +96,12 @@ export class MinecraftDiagnosticsPanel { onNotification: (message: string) => { window.showInformationMessage(message); }, + onSchemaReceived: (schema) => { + this._panel.webview.postMessage({ + type: 'diagnostics-schema', + schema, + }); + }, }; this._statsTracker.addStatListener(this._statsCallback); diff --git a/src/session.ts b/src/session.ts index 3b2059a1..3a6ce906 100644 --- a/src/session.ts +++ b/src/session.ts @@ -894,6 +894,8 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { this.handleProtocolEvent(eventMessage as ProtocolCapabilities); } else if (eventMessage.type === 'StatEvent2') { this._statsProvider.setStats(eventMessage as StatMessageModel); + } else if (eventMessage.type === 'SchemaEvent') { + this._statsProvider.setSchema(eventMessage.descriptors); } else if (eventMessage.type === 'ProfilerCapture') { this.handleProfilerCapture(eventMessage as ProfilerCapture); } diff --git a/src/stats/stats-provider.ts b/src/stats/stats-provider.ts index 48c53f80..0b8c664d 100644 --- a/src/stats/stats-provider.ts +++ b/src/stats/stats-provider.ts @@ -26,12 +26,35 @@ export interface StatMessageModel { stats: StatDataModel[]; } +// Mirrors ScriptDiagnosticsDescriptor from C++. Sent once on connect via SchemaEvent. +export interface DiagnosticsTabDescriptor { + name: string; + stat_group_id: string; + data_source: 'server' | 'client' | 'server_script'; + display_type: 'line_chart' | 'stacked_line_chart' | 'stacked_bar_chart' | 'table' | 'multi_column_table' | 'dynamic_properties_table'; + title?: string; + y_label?: string; + tick_range?: number; + value_scalar?: number; + target_value?: number; + key_label?: string; + value_labels?: string[]; + statistic_id?: string; + statistic_ids?: string[]; +} + +export interface SchemaMessageModel { + type: 'SchemaEvent'; + descriptors: DiagnosticsTabDescriptor[]; +} + export interface StatsListener { onStatUpdated?: (stat: StatData) => void; onSpeedUpdated?: (speed: number) => void; onPauseUpdated?: (paused: boolean) => void; onStopped?: () => void; onNotification?: (message: string) => void; + onSchemaReceived?: (schema: DiagnosticsTabDescriptor[]) => void; } export class StatsProvider { @@ -47,6 +70,12 @@ export class StatsProvider { } } + public setSchema(schema: DiagnosticsTabDescriptor[]): void { + this._statListeners.forEach((listener: StatsListener) => { + listener.onSchemaReceived?.(schema); + }); + } + public start(): void { throw new Error('Method not implemented.'); } diff --git a/webview-ui/src/diagnostics_panel/App.tsx b/webview-ui/src/diagnostics_panel/App.tsx index 51f6d232..d41e66fc 100644 --- a/webview-ui/src/diagnostics_panel/App.tsx +++ b/webview-ui/src/diagnostics_panel/App.tsx @@ -1,6 +1,6 @@ // Copyright (C) Microsoft Corporation. All rights reserved. -import { VSCodePanelTab, VSCodePanelView, VSCodePanels } from '@vscode/webview-ui-toolkit/react'; +import { VSCodeDropdown, VSCodeOption, VSCodePanelTab, VSCodePanelView, VSCodePanels } from '@vscode/webview-ui-toolkit/react'; import { StatGroupSelectionBox } from './controls/StatGroupSelectionBox'; import { useCallback, useEffect, useState } from 'react'; import { StatisticType, YAxisStyle, YAxisType, createStatResolver } from './StatisticResolver'; @@ -14,6 +14,8 @@ import { Icons } from './Icons'; import './App.css'; import tabPrefabs from './prefabs'; import { TabPrefabDataSource } from './prefabs/TabPrefab'; +import { DiagnosticsTabDescriptor } from './DiagnosticsSchema'; +import DynamicTab from './DynamicTab'; declare global { interface Window { @@ -53,6 +55,10 @@ function App() { const [currentTab, setCurrentTab] = useState(); const [paused, setPaused] = useState(true); const [speed, setSpeed] = useState(''); + // Dynamic schema received from the game on connect. Empty = use static prefab fallback. + const [schema, setSchema] = useState([]); + // Index of the selected tab in the dynamic dropdown + const [selectedSchemaIndex, setSelectedSchemaIndex] = useState(0); const handlePluginSelection = useCallback((pluginSelectionId: string) => { setSelectedPlugin(() => pluginSelectionId); @@ -69,6 +75,11 @@ function App() { } }, []); + const handleSchemaTabChange = useCallback((e: Event | React.FormEvent): void => { + const target = e.target as HTMLSelectElement; + setSelectedSchemaIndex(target.selectedIndex); + }, []); + useEffect(() => { const handleMessage = (event: MessageEvent) => { const message = event.data; @@ -76,6 +87,9 @@ function App() { setSpeed(`${message.speed}hz`); } else if (message.type === 'pause-updated') { setPaused(message.paused); + } else if (message.type === 'diagnostics-schema') { + setSchema(message.schema as DiagnosticsTabDescriptor[]); + setSelectedSchemaIndex(0); } }; window.addEventListener('message', handleMessage); @@ -84,6 +98,9 @@ function App() { }; }, []); + const usingDynamicSchema = schema.length > 0; + const activeDescriptor = usingDynamicSchema ? schema[selectedSchemaIndex] : undefined; + return (
{window.initialParams.showReplayControls && ( @@ -98,34 +115,75 @@ function App() { svgIcons={Icons} /> )} - handlePanelChange(event as VSCodePanelsChangeEvent)}> - {tabPrefabs.map((tabPrefab, index) => ( - {tabPrefab.name} - ))} - {tabPrefabs.map((tabPrefab, index) => ( - - {tabPrefab.dataSource === TabPrefabDataSource.Client ? ( - - ) : ( -
- )} - {tabPrefab.dataSource === TabPrefabDataSource.ServerScript ? ( - +
+ + + {schema.map((descriptor, index) => ( + {descriptor.name} + ))} + +
+ + {activeDescriptor && ( +
+ {activeDescriptor.data_source === 'client' && ( + + )} + {activeDescriptor.data_source === 'server_script' && ( + + )} + - ) : ( -
- )} - {tabPrefab.content({ selectedClient, selectedPlugin })} - - ))} - +
+ )} +
+ ) : ( + // Static fallback: no schema received (older game version) — render hardcoded prefab tabs + handlePanelChange(event as VSCodePanelsChangeEvent)}> + {tabPrefabs.map((tabPrefab, index) => ( + {tabPrefab.name} + ))} + {tabPrefabs.map((tabPrefab, index) => ( + + {tabPrefab.dataSource === TabPrefabDataSource.Client ? ( + + ) : ( +
+ )} + {tabPrefab.dataSource === TabPrefabDataSource.ServerScript ? ( + + ) : ( +
+ )} + {tabPrefab.content({ selectedClient, selectedPlugin })} + + ))} + + )}
); } diff --git a/webview-ui/src/diagnostics_panel/DiagnosticsSchema.ts b/webview-ui/src/diagnostics_panel/DiagnosticsSchema.ts new file mode 100644 index 00000000..4988c2dd --- /dev/null +++ b/webview-ui/src/diagnostics_panel/DiagnosticsSchema.ts @@ -0,0 +1,29 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. +// TypeScript mirror of ScriptDiagnosticsDescriptor (C++) and its Cereal-serialized wire format. + +export type DiagnosticsDataSource = 'server' | 'client' | 'server_script'; + +export type DiagnosticsDisplayType = + | 'line_chart' + | 'stacked_line_chart' + | 'stacked_bar_chart' + | 'table' + | 'multi_column_table' + | 'dynamic_properties_table'; + +// One-to-one with ScriptDiagnosticsDescriptor fields (snake_case matches cereal wire names). +export interface DiagnosticsTabDescriptor { + name: string; + stat_group_id: string; + data_source: DiagnosticsDataSource; + display_type: DiagnosticsDisplayType; + title?: string; + y_label?: string; + tick_range?: number; + value_scalar?: number; + target_value?: number; + key_label?: string; + value_labels?: string[]; + statistic_id?: string; + statistic_ids?: string[]; +} diff --git a/webview-ui/src/diagnostics_panel/DynamicTab.tsx b/webview-ui/src/diagnostics_panel/DynamicTab.tsx new file mode 100644 index 00000000..f39ba038 --- /dev/null +++ b/webview-ui/src/diagnostics_panel/DynamicTab.tsx @@ -0,0 +1,166 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. + +// Generic tab renderer driven by a DiagnosticsTabDescriptor from the C++ schema registry. +// Derives the appropriate StatisticProvider and StatisticResolver from the descriptor fields +// and renders the matching control — no new tab files needed for new diagnostics. + +import { useMemo } from 'react'; +import { DiagnosticsTabDescriptor } from './DiagnosticsSchema'; +import { + SimpleStatisticProvider, + MultipleStatisticProvider, + StatisticProvider, +} from './StatisticProvider'; +import { StatisticType, YAxisType, createStatResolver, StatisticResolver } from './StatisticResolver'; +import MinecraftStatisticLineChart from './controls/MinecraftStatisticLineChart'; +import MinecraftStatisticStackedLineChart from './controls/MinecraftStatisticStackedLineChart'; +import MinecraftStatisticStackedBarChart from './controls/MinecraftStatisticStackedBarChart'; +import MinecraftStatisticTable from './controls/MinecraftStatisticTable'; +import MinecraftMultiColumnStatisticTable from './controls/MinecraftMultiColumnStatisticTable'; +import { MinecraftDynamicPropertiesTable } from './controls/MinecraftDynamicPropertiesTable'; + +type DynamicTabProps = { + descriptor: DiagnosticsTabDescriptor; + selectedClient: string; + selectedPlugin: string; +}; + +// Build a StatisticProvider from descriptor fields. +// Client-source tabs use a regex to match the per-player stat group prefix. +function buildProvider( + descriptor: DiagnosticsTabDescriptor, + selectedClient: string, + _selectedPlugin: string +): StatisticProvider { + const { data_source, display_type, stat_group_id, statistic_id, statistic_ids } = descriptor; + + // DynamicPropertiesTable has its own fixed provider shape + if (display_type === 'dynamic_properties_table') { + return new SimpleStatisticProvider({ + statisticId: 'consolidated_data', + statisticParentId: new RegExp(`${stat_group_id}.*`), + }); + } + + // LineChart uses SimpleStatisticProvider (single stat series) + if (display_type === 'line_chart') { + const effectiveStatId = statistic_id ?? stat_group_id; + const parentId: string | RegExp = + data_source === 'client' + ? new RegExp(`.*${selectedClient}_${stat_group_id}`) + : stat_group_id; + return new SimpleStatisticProvider({ + statisticId: effectiveStatId, + statisticParentId: parentId, + }); + } + + // All other chart/table types use MultipleStatisticProvider + const parentId: string | RegExp = + data_source === 'client' + ? new RegExp(`.*${selectedClient}_${stat_group_id}`) + : stat_group_id; + + return new MultipleStatisticProvider({ + statisticParentId: parentId, + statisticIds: statistic_ids, + }); +} + +function buildResolver(descriptor: DiagnosticsTabDescriptor): StatisticResolver { + return createStatResolver({ + type: StatisticType.Absolute, + yAxisType: YAxisType.Absolute, + tickRange: descriptor.tick_range ?? 20 * 10, + valueScalar: descriptor.value_scalar, + }); +} + +export default function DynamicTab({ descriptor, selectedClient, selectedPlugin }: DynamicTabProps) { + const title = descriptor.title ?? descriptor.name; + const yLabel = descriptor.y_label ?? ''; + const tickRange = descriptor.tick_range ?? 20 * 10; + + // Memoize provider and resolver so controls' useEffect deps remain stable across renders. + const provider = useMemo( + () => buildProvider(descriptor, selectedClient, selectedPlugin), + // re-create only when the identity of the tab or the selected client/plugin changes + [descriptor.stat_group_id, descriptor.data_source, descriptor.display_type, + descriptor.statistic_id, descriptor.statistic_ids?.join(','), selectedClient, selectedPlugin] + ); + + const resolver = useMemo( + () => buildResolver(descriptor), + [descriptor.tick_range, descriptor.value_scalar] + ); + + switch (descriptor.display_type) { + case 'line_chart': + return ( + + ); + + case 'stacked_line_chart': + return ( + + ); + + case 'stacked_bar_chart': + return ( + + ); + + case 'table': + return ( + + ); + + case 'multi_column_table': + return ( + + ); + + case 'dynamic_properties_table': + return ( + + ); + + default: + return
Unsupported display type: {descriptor.display_type}
; + } +} diff --git a/webview-ui/src/diagnostics_panel/index.tsx b/webview-ui/src/diagnostics_panel/index.tsx index 9551ae90..9b52539a 100644 --- a/webview-ui/src/diagnostics_panel/index.tsx +++ b/webview-ui/src/diagnostics_panel/index.tsx @@ -5,7 +5,7 @@ import { createRoot } from 'react-dom/client'; import App from './App'; const container = document.getElementById('root'); -const root = createRoot(container!); // createRoot(container!) if you use TypeScript +const root = createRoot(container!); root.render( diff --git a/webview-ui/src/diagnostics_panel/prefabs/TabPrefab.ts b/webview-ui/src/diagnostics_panel/prefabs/TabPrefab.ts index ca70a627..12fab43d 100644 --- a/webview-ui/src/diagnostics_panel/prefabs/TabPrefab.ts +++ b/webview-ui/src/diagnostics_panel/prefabs/TabPrefab.ts @@ -1,5 +1,3 @@ -import { StatisticPrefab } from './StatisticPrefab'; - export type TabPrefabParams = { selectedClient: string; selectedPlugin: string; From ee74ea5a3e1b8dbe35567a170292e2e382f4a443 Mon Sep 17 00:00:00 2001 From: Alex Denford SBL Date: Fri, 10 Apr 2026 14:33:17 -0700 Subject: [PATCH 02/25] remove descriptor changes --- src/stats/stats-provider.ts | 31 +---- webview-ui/package-lock.json | 12 +- webview-ui/src/diagnostics_panel/App.tsx | 114 +++++------------- webview-ui/src/diagnostics_panel/index.tsx | 2 +- .../diagnostics_panel/prefabs/TabPrefab.ts | 2 + 5 files changed, 43 insertions(+), 118 deletions(-) diff --git a/src/stats/stats-provider.ts b/src/stats/stats-provider.ts index 0b8c664d..ddace2ff 100644 --- a/src/stats/stats-provider.ts +++ b/src/stats/stats-provider.ts @@ -26,35 +26,12 @@ export interface StatMessageModel { stats: StatDataModel[]; } -// Mirrors ScriptDiagnosticsDescriptor from C++. Sent once on connect via SchemaEvent. -export interface DiagnosticsTabDescriptor { - name: string; - stat_group_id: string; - data_source: 'server' | 'client' | 'server_script'; - display_type: 'line_chart' | 'stacked_line_chart' | 'stacked_bar_chart' | 'table' | 'multi_column_table' | 'dynamic_properties_table'; - title?: string; - y_label?: string; - tick_range?: number; - value_scalar?: number; - target_value?: number; - key_label?: string; - value_labels?: string[]; - statistic_id?: string; - statistic_ids?: string[]; -} - -export interface SchemaMessageModel { - type: 'SchemaEvent'; - descriptors: DiagnosticsTabDescriptor[]; -} - export interface StatsListener { onStatUpdated?: (stat: StatData) => void; onSpeedUpdated?: (speed: number) => void; onPauseUpdated?: (paused: boolean) => void; onStopped?: () => void; onNotification?: (message: string) => void; - onSchemaReceived?: (schema: DiagnosticsTabDescriptor[]) => void; } export class StatsProvider { @@ -69,13 +46,7 @@ export class StatsProvider { this._fireStatUpdated(stat, stats.tick); } } - - public setSchema(schema: DiagnosticsTabDescriptor[]): void { - this._statListeners.forEach((listener: StatsListener) => { - listener.onSchemaReceived?.(schema); - }); - } - + public start(): void { throw new Error('Method not implemented.'); } diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json index 9891a45a..d0dce43b 100644 --- a/webview-ui/package-lock.json +++ b/webview-ui/package-lock.json @@ -69,6 +69,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -1526,6 +1527,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.0.tgz", "integrity": "sha512-ClIbNe36lawluuvq3+YYhnIN2CELi+6q8NpnM7PYp4hBn/TatfboPgVSm2rwKRfnV2M+Ty9GWDFI64KEe+kysA==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.20.0" } @@ -1630,6 +1632,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001629", "electron-to-chromium": "^1.4.796", @@ -2005,6 +2008,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "peer": true, "engines": { "node": ">=12" } @@ -2432,6 +2436,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -2762,6 +2767,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", "dev": true, + "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -3708,6 +3714,7 @@ "version": "22.13.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.0.tgz", "integrity": "sha512-ClIbNe36lawluuvq3+YYhnIN2CELi+6q8NpnM7PYp4hBn/TatfboPgVSm2rwKRfnV2M+Ty9GWDFI64KEe+kysA==", + "peer": true, "requires": { "undici-types": "~6.20.0" } @@ -3789,6 +3796,7 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", "dev": true, + "peer": true, "requires": { "caniuse-lite": "^1.0.30001629", "electron-to-chromium": "^1.4.796", @@ -4053,7 +4061,8 @@ "d3-selection": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==" + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "peer": true }, "d3-shape": { "version": "3.2.0", @@ -4347,6 +4356,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "requires": { "loose-envify": "^1.1.0" } diff --git a/webview-ui/src/diagnostics_panel/App.tsx b/webview-ui/src/diagnostics_panel/App.tsx index d41e66fc..51f6d232 100644 --- a/webview-ui/src/diagnostics_panel/App.tsx +++ b/webview-ui/src/diagnostics_panel/App.tsx @@ -1,6 +1,6 @@ // Copyright (C) Microsoft Corporation. All rights reserved. -import { VSCodeDropdown, VSCodeOption, VSCodePanelTab, VSCodePanelView, VSCodePanels } from '@vscode/webview-ui-toolkit/react'; +import { VSCodePanelTab, VSCodePanelView, VSCodePanels } from '@vscode/webview-ui-toolkit/react'; import { StatGroupSelectionBox } from './controls/StatGroupSelectionBox'; import { useCallback, useEffect, useState } from 'react'; import { StatisticType, YAxisStyle, YAxisType, createStatResolver } from './StatisticResolver'; @@ -14,8 +14,6 @@ import { Icons } from './Icons'; import './App.css'; import tabPrefabs from './prefabs'; import { TabPrefabDataSource } from './prefabs/TabPrefab'; -import { DiagnosticsTabDescriptor } from './DiagnosticsSchema'; -import DynamicTab from './DynamicTab'; declare global { interface Window { @@ -55,10 +53,6 @@ function App() { const [currentTab, setCurrentTab] = useState(); const [paused, setPaused] = useState(true); const [speed, setSpeed] = useState(''); - // Dynamic schema received from the game on connect. Empty = use static prefab fallback. - const [schema, setSchema] = useState([]); - // Index of the selected tab in the dynamic dropdown - const [selectedSchemaIndex, setSelectedSchemaIndex] = useState(0); const handlePluginSelection = useCallback((pluginSelectionId: string) => { setSelectedPlugin(() => pluginSelectionId); @@ -75,11 +69,6 @@ function App() { } }, []); - const handleSchemaTabChange = useCallback((e: Event | React.FormEvent): void => { - const target = e.target as HTMLSelectElement; - setSelectedSchemaIndex(target.selectedIndex); - }, []); - useEffect(() => { const handleMessage = (event: MessageEvent) => { const message = event.data; @@ -87,9 +76,6 @@ function App() { setSpeed(`${message.speed}hz`); } else if (message.type === 'pause-updated') { setPaused(message.paused); - } else if (message.type === 'diagnostics-schema') { - setSchema(message.schema as DiagnosticsTabDescriptor[]); - setSelectedSchemaIndex(0); } }; window.addEventListener('message', handleMessage); @@ -98,9 +84,6 @@ function App() { }; }, []); - const usingDynamicSchema = schema.length > 0; - const activeDescriptor = usingDynamicSchema ? schema[selectedSchemaIndex] : undefined; - return (
{window.initialParams.showReplayControls && ( @@ -115,75 +98,34 @@ function App() { svgIcons={Icons} /> )} - - {usingDynamicSchema ? ( - // Dynamic mode: schema received from game — dropdown selector + single DynamicTab -
-
- - - {schema.map((descriptor, index) => ( - {descriptor.name} - ))} - -
- - {activeDescriptor && ( -
- {activeDescriptor.data_source === 'client' && ( - - )} - {activeDescriptor.data_source === 'server_script' && ( - - )} - handlePanelChange(event as VSCodePanelsChangeEvent)}> + {tabPrefabs.map((tabPrefab, index) => ( + {tabPrefab.name} + ))} + {tabPrefabs.map((tabPrefab, index) => ( + + {tabPrefab.dataSource === TabPrefabDataSource.Client ? ( + -
- )} -
- ) : ( - // Static fallback: no schema received (older game version) — render hardcoded prefab tabs - handlePanelChange(event as VSCodePanelsChangeEvent)}> - {tabPrefabs.map((tabPrefab, index) => ( - {tabPrefab.name} - ))} - {tabPrefabs.map((tabPrefab, index) => ( - - {tabPrefab.dataSource === TabPrefabDataSource.Client ? ( - - ) : ( -
- )} - {tabPrefab.dataSource === TabPrefabDataSource.ServerScript ? ( - - ) : ( -
- )} - {tabPrefab.content({ selectedClient, selectedPlugin })} - - ))} - - )} + ) : ( +
+ )} + {tabPrefab.dataSource === TabPrefabDataSource.ServerScript ? ( + + ) : ( +
+ )} + {tabPrefab.content({ selectedClient, selectedPlugin })} + + ))} +
); } diff --git a/webview-ui/src/diagnostics_panel/index.tsx b/webview-ui/src/diagnostics_panel/index.tsx index 9b52539a..9551ae90 100644 --- a/webview-ui/src/diagnostics_panel/index.tsx +++ b/webview-ui/src/diagnostics_panel/index.tsx @@ -5,7 +5,7 @@ import { createRoot } from 'react-dom/client'; import App from './App'; const container = document.getElementById('root'); -const root = createRoot(container!); +const root = createRoot(container!); // createRoot(container!) if you use TypeScript root.render( diff --git a/webview-ui/src/diagnostics_panel/prefabs/TabPrefab.ts b/webview-ui/src/diagnostics_panel/prefabs/TabPrefab.ts index 12fab43d..ca70a627 100644 --- a/webview-ui/src/diagnostics_panel/prefabs/TabPrefab.ts +++ b/webview-ui/src/diagnostics_panel/prefabs/TabPrefab.ts @@ -1,3 +1,5 @@ +import { StatisticPrefab } from './StatisticPrefab'; + export type TabPrefabParams = { selectedClient: string; selectedPlugin: string; From 870c27c16ac58a3e7754e1ee451ae51ede1e56e8 Mon Sep 17 00:00:00 2001 From: Alex Denford SBL Date: Tue, 12 May 2026 11:51:37 -0700 Subject: [PATCH 03/25] fixes --- CONTRIBUTING.md | 11 +- package-lock.json | 82 ++++-------- src/breakpoints-legacy.ts | 5 +- src/debuggee-message-sender.ts | 3 +- src/panels/minecraft-diagnostics.ts | 6 - src/protocol-events.ts | 195 ++++++++++++++++++++++++++++ src/session.ts | 135 ++++++++++--------- 7 files changed, 299 insertions(+), 138 deletions(-) create mode 100644 src/protocol-events.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 441ef5da..470bffc5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,8 +13,17 @@ Some examples of this are: To see the current list of supported types, see [CI.yml](./.github/workflows/ci.yml#L13)'s 'Lint PR Title' task. -# Tests +# Build +To build and use this extension locally, run the following commands: + ``` +cd webview-ui +npm install +``` +Go to the Run and Debug panel (Ctrl-Shift-D) and select "Run Extension". This was automatically build and run the extension. +You can now use the extension as normal (in the new VS Code instance) by attaching to Minecraft to debug. + +# Tests Create tests using vitest. Follow the convention `sourcefilename.test.ts` when adding new files. diff --git a/package-lock.json b/package-lock.json index 9183e6ce..716e8130 100644 --- a/package-lock.json +++ b/package-lock.json @@ -867,7 +867,6 @@ "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -883,7 +882,6 @@ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6.0.0" } @@ -894,7 +892,6 @@ "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6.0.0" } @@ -905,7 +902,6 @@ "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -924,7 +920,6 @@ "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -1035,6 +1030,7 @@ "integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^5.0.0", "@octokit/graphql": "^8.0.0", @@ -2186,6 +2182,7 @@ "integrity": "sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "4.33.0", "@typescript-eslint/types": "4.33.0", @@ -2677,7 +2674,6 @@ "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.11.6", "@webassemblyjs/helper-wasm-bytecode": "1.11.6" @@ -2688,24 +2684,21 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.11.6", @@ -2713,7 +2706,6 @@ "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.11.6", "@webassemblyjs/helper-api-error": "1.11.6", @@ -2725,8 +2717,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.12.1", @@ -2734,7 +2725,6 @@ "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-buffer": "1.12.1", @@ -2748,7 +2738,6 @@ "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -2759,7 +2748,6 @@ "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -2769,8 +2757,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.12.1", @@ -2778,7 +2765,6 @@ "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-buffer": "1.12.1", @@ -2796,7 +2782,6 @@ "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", @@ -2811,7 +2796,6 @@ "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-buffer": "1.12.1", @@ -2825,7 +2809,6 @@ "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-api-error": "1.11.6", @@ -2841,7 +2824,6 @@ "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.12.1", "@xtuc/long": "4.2.2" @@ -2852,16 +2834,14 @@ "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", "dev": true, - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true, - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/acorn": { "version": "8.13.0", @@ -2920,6 +2900,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2937,7 +2918,6 @@ "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "ajv": "^6.9.1" } @@ -3279,8 +3259,7 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/cac": { "version": "6.7.14", @@ -3352,8 +3331,7 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "CC-BY-4.0", - "peer": true + "license": "CC-BY-4.0" }, "node_modules/chai": { "version": "5.1.2", @@ -3490,7 +3468,6 @@ "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6.0" } @@ -4210,8 +4187,7 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.41.tgz", "integrity": "sha512-dfdv/2xNjX0P8Vzme4cfzHqnPm5xsZXwsolTYr0eyW18IUmNyG08vL+fttvinTfhKfIKdRoqkDIC9e9iWQCNYQ==", "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -4547,6 +4523,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -4715,6 +4692,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.4.3", @@ -5600,8 +5578,7 @@ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true, - "license": "BSD-2-Clause", - "peer": true + "license": "BSD-2-Clause" }, "node_modules/globals": { "version": "13.24.0", @@ -6218,7 +6195,6 @@ "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -6234,7 +6210,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -6245,7 +6220,6 @@ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -6500,7 +6474,6 @@ "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6.11.5" } @@ -6623,7 +6596,6 @@ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -6685,6 +6657,7 @@ "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==", "dev": true, "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -7005,8 +6978,7 @@ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/normalize-package-data": { "version": "6.0.2", @@ -10861,7 +10833,6 @@ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "^5.1.0" } @@ -11239,7 +11210,6 @@ "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", @@ -11259,6 +11229,7 @@ "integrity": "sha512-Cb0Pm3Ye15u8k/B+7EnusMUSIIucAIEBD3QDRmmclv53KVyqmg1Lb3XPx0AMNxfJZEI+ZT+M+IXDyTrudK6Rew==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^13.0.0-beta.1", "@semantic-release/error": "^4.0.0", @@ -11828,7 +11799,6 @@ "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "randombytes": "^2.1.0" } @@ -12126,7 +12096,6 @@ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -12138,7 +12107,6 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -12611,7 +12579,6 @@ "integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -12631,7 +12598,6 @@ "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.20", "jest-worker": "^27.4.5", @@ -12666,8 +12632,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/text-table": { "version": "0.2.0", @@ -13001,6 +12966,7 @@ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13130,7 +13096,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.0" @@ -13200,6 +13165,7 @@ "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -13779,7 +13745,6 @@ "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -13794,7 +13759,6 @@ "integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", @@ -13842,7 +13806,6 @@ "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10.13.0" } @@ -13853,7 +13816,6 @@ "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "acorn": "^8" } diff --git a/src/breakpoints-legacy.ts b/src/breakpoints-legacy.ts index 0756fe07..8d368385 100644 --- a/src/breakpoints-legacy.ts +++ b/src/breakpoints-legacy.ts @@ -4,6 +4,7 @@ import { DebugProtocol } from '@vscode/debugprotocol'; import { IBreakpointsHandler } from './ibreakpoints-handler'; import { SourceMaps } from './source-maps'; import { IDebuggeeMessageSender } from './debuggee-message-sender'; +import { BreakpointsMessage, OutgoingEventType } from './protocol-events'; import * as path from 'path'; // respond to a setBreakPointsRequest from session, deprecated. @@ -67,8 +68,8 @@ export class BreakpointsLegacy implements IBreakpointsHandler { // send full set of breakpoints for each generated file, a message per file for (const [generatedRemoteLocalPath, generatedBreakpoints] of generatedBreakpointsMap) { - const envelope = { - type: 'breakpoints', + const envelope: BreakpointsMessage = { + type: OutgoingEventType.breakpoints, breakpoints: { path: generatedRemoteLocalPath, breakpoints: generatedBreakpoints.length ? generatedBreakpoints : undefined, diff --git a/src/debuggee-message-sender.ts b/src/debuggee-message-sender.ts index 73ea598b..03707083 100644 --- a/src/debuggee-message-sender.ts +++ b/src/debuggee-message-sender.ts @@ -1,9 +1,10 @@ // Copyright (C) Microsoft Corporation. All rights reserved. import { DebugProtocol } from '@vscode/debugprotocol'; +import { OutgoingDebuggeeMessage } from './protocol-events'; // Interface for sending debugger messages and requests to MC export interface IDebuggeeMessageSender { - sendDebuggeeMessage(envelope: unknown): void; + sendDebuggeeMessage(envelope: OutgoingDebuggeeMessage): void; sendDebugeeRequestAsync(response: DebugProtocol.Response, args: unknown): Promise; } diff --git a/src/panels/minecraft-diagnostics.ts b/src/panels/minecraft-diagnostics.ts index bce18d0d..b535c058 100644 --- a/src/panels/minecraft-diagnostics.ts +++ b/src/panels/minecraft-diagnostics.ts @@ -96,12 +96,6 @@ export class MinecraftDiagnosticsPanel { onNotification: (message: string) => { window.showInformationMessage(message); }, - onSchemaReceived: (schema) => { - this._panel.webview.postMessage({ - type: 'diagnostics-schema', - schema, - }); - }, }; this._statsTracker.addStatListener(this._statsCallback); diff --git a/src/protocol-events.ts b/src/protocol-events.ts new file mode 100644 index 00000000..a3d61646 --- /dev/null +++ b/src/protocol-events.ts @@ -0,0 +1,195 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. + +import { LogLevel } from '@vscode/debugadapter/lib/logger'; +import { DebugProtocol } from '@vscode/debugprotocol'; +import { StatMessageModel } from './stats/stats-provider'; + +// protocol version history +// 1 - initial version +// 2 - add targetModuleUuid to protocol event +// 3 - add array of plugins and target module ids to incoming protocol event +// 4 - mc can require a passcode to connect +// 5 - debugger can take mc script profiler captures +// 6 - breakpoints as request, MC can reject +// 7 - New serialization tech (use Cereal) + +export enum ProtocolVersion { + _Unknown = 0, + Initial = 1, + SupportTargetModuleUuid = 2, + SupportTargetSelection = 3, + SupportPasscode = 4, + SupportProfilerCaptures = 5, + SupportBreakpointsAsRequest = 6, + SupportCerealSerialization = 7, +} + +export const DEBUGGER_PROTOCOL_VERSION = ProtocolVersion.SupportCerealSerialization; + +// ------------------------------------------------------------------------- +// Interfaces for event message payloads (received from the debugee) +// ------------------------------------------------------------------------- +export const IncomingEventType = { + stopped: 'StoppedEvent', + thread: 'ThreadEvent', + print: 'PrintEvent', + notification: 'NotificationEvent', + protocol: 'ProtocolEvent', + stat2: 'StatEvent2', + schema: 'SchemaEvent', + profilerCapture: 'ProfilerCapture', +} as const; + +export interface PluginDetails { + name: string; + module_uuid: string; +} + +export interface ProtocolCapabilities { + type: typeof IncomingEventType.protocol; + version: number; + plugins: PluginDetails[]; + require_passcode?: boolean; +} + +export interface ProfilerCapture { + type: typeof IncomingEventType.profilerCapture; + capture_base_path: string; + capture_data: string; +} + +export interface StoppedEventMessage { + type: typeof IncomingEventType.stopped; + reason: string; + thread: number; +} + +export interface ThreadEventMessage { + type: typeof IncomingEventType.thread; + reason: string; + thread: number; +} + +export interface PrintEventMessage { + type: typeof IncomingEventType.print; + message: string; + logLevel: LogLevel; +} + +export interface NotificationEventMessage { + type: typeof IncomingEventType.notification; + message: string; + logLevel: LogLevel; +} + +export type IncomingDebuggeeMessage = + | PluginDetails + | ProtocolCapabilities + | ProfilerCapture + | StoppedEventMessage + | ThreadEventMessage + | PrintEventMessage + | NotificationEventMessage; + + + +// ------------------------------------------------------------------------- +// Interfaces for outbound message payloads (sent to the debugee) +// ------------------------------------------------------------------------- +export const OutgoingEventType = { + protocol: 'protocol', + minecraftCommand: 'minecraftCommand', + startProfiler: 'startProfiler', + stopProfiler: 'stopProfiler', + stopOnException: 'stopOnException', + resume: 'resume', + request: 'request', + breakpoints: 'breakpoints', +} as const; + +export interface ProtocolResponse { + type: typeof OutgoingEventType.protocol; + version: number; + target_module_uuid?: string; + passcode?: string; +} + +export interface MinecraftCommandLegacyMessage { + type: typeof OutgoingEventType.minecraftCommand; + command: string; + dimension_type: string; +} + +export interface MinecraftCommandMessage { + type: typeof OutgoingEventType.minecraftCommand; + command: { command: string; dimension_type: string }; +} + +export interface StartProfilerMessage { + type: typeof OutgoingEventType.startProfiler; + profiler: { target_module_uuid?: string }; +} + +export interface StopProfilerMessage { + type: typeof OutgoingEventType.stopProfiler; + profiler: { captures_path: string; target_module_uuid?: string }; +} + +export interface StopOnExceptionMessage { + type: typeof OutgoingEventType.stopOnException; + stopOnException: boolean; +} + +export interface ResumeMessage { + type: typeof OutgoingEventType.resume; +} + +export interface RequestMessage { + type: typeof OutgoingEventType.request; + request: { request_seq: number; command: string; args: unknown }; +} + +export interface BreakpointsMessage { + type: typeof OutgoingEventType.breakpoints; + breakpoints: { path: string; breakpoints: DebugProtocol.SourceBreakpoint[] | undefined }; +} + +export type OutgoingDebuggeeMessage = + | ProtocolResponse + | MinecraftCommandLegacyMessage + | MinecraftCommandMessage + | StartProfilerMessage + | StopProfilerMessage + | StopOnExceptionMessage + | ResumeMessage + | RequestMessage + | BreakpointsMessage; + + + +// Re-export for callers that need these alongside event types +export type { StatMessageModel }; + +// ------------------------------------------------------------------------- +// Registry that maps event type name strings to handler callbacks +// ------------------------------------------------------------------------- + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type DebuggeeEventHandler = (event: any) => void; + +export class DebuggeeEventRegistry { + private readonly _handlers = new Map(); + + public register(eventType: string, handler: DebuggeeEventHandler): void { + this._handlers.set(eventType, handler); + } + + public dispatch(eventMessage: { type: string }): boolean { + const handler = this._handlers.get(eventMessage.type); + if (handler) { + handler(eventMessage); + return true; + } + return false; + } +} diff --git a/src/session.ts b/src/session.ts index 3a6ce906..cd94e474 100644 --- a/src/session.ts +++ b/src/session.ts @@ -42,6 +42,22 @@ import { IBreakpointsHandler } from './ibreakpoints-handler'; import { injectSourceMapIntoProfilerCapture } from './profiler-utils'; import { isUUID } from './utils'; import { MessageStreamParser } from './message-stream-parser'; +import { + DebuggeeEventRegistry, + DEBUGGER_PROTOCOL_VERSION, + IncomingEventType, + NotificationEventMessage, + OutgoingDebuggeeMessage, + OutgoingEventType, + PluginDetails, + PrintEventMessage, + ProfilerCapture, + ProtocolCapabilities, + ProtocolVersion, + RequestMessage, + StoppedEventMessage, + ThreadEventMessage, +} from './protocol-events'; import { SourceMaps } from './source-maps'; import { StatMessageModel, StatsProvider } from './stats/stats-provider'; @@ -55,24 +71,6 @@ export interface ModuleMapping { [moduleName: string]: string; } -interface PluginDetails { - name: string; - module_uuid: string; -} - -interface ProtocolCapabilities { - type: string; - version: number; - plugins: PluginDetails[]; - require_passcode?: boolean; -} - -interface ProfilerCapture { - type: string; - capture_base_path: string; - capture_data: string; -} - // Interface for specific launch arguments. // See package.json for schema. interface IAttachRequestArguments extends DebugProtocol.AttachRequestArguments { @@ -108,23 +106,6 @@ interface DebuggerStackFrame { column: number; } -// protocol version history -// 1 - initial version -// 2 - add targetModuleUuid to protocol event -// 3 - add array of plugins and target module ids to incoming protocol event -// 4 - mc can require a passcode to connect -// 5 - debugger can take mc script profiler captures -// 6 - breakpoints as request, MC can reject -enum ProtocolVersion { - _Unknown = 0, - Initial = 1, - SupportTargetModuleUuid = 2, - SupportTargetSelection = 3, - SupportPasscode = 4, - SupportProfilerCaptures = 5, - SupportBreakpointsAsRequest = 6, -} - // capabilites based on protocol version export interface MinecraftCapabilities { supportsCommands: boolean; @@ -135,10 +116,9 @@ export interface MinecraftCapabilities { // The Debug Adapter for 'minecraft-js' // export class Session extends DebugSession implements IDebuggeeMessageSender { - private readonly _debuggerProtocolVersion = ProtocolVersion.SupportBreakpointsAsRequest; + private readonly _connectionRetryAttempts = 3; private readonly _connectionRetryWaitMs = 500; - private _debugeeServer?: Server; // when listening for incoming connections private _connectionSocket?: Socket; private _connected = false; @@ -162,6 +142,7 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { supportsBreakpointsAsRequest: false, }; private _passcode?: string; + private _eventRegistry: DebuggeeEventRegistry; // external communication private _homeViewProvider: HomeViewProvider; @@ -178,12 +159,45 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { this.setDebuggerLinesStartAt1(true); this.setDebuggerColumnsStartAt1(true); + this._eventRegistry = new DebuggeeEventRegistry(); + this.registerServerEvents(); + this._eventEmitter.on('run-minecraft-command', this.onRunMinecraftCommand.bind(this)); this._eventEmitter.on('start-profiler', this.onStartProfiler.bind(this)); this._eventEmitter.on('stop-profiler', this.onStopProfiler.bind(this)); this._eventEmitter.on('request-debugger-status', this.onRequestDebuggerStatus.bind(this)); } + // Use this to register new events that are handled from the debugee (Minecraft) + // For example you want to send new arbitary data to the debugger that doesn't fit into the existing event types (such as a live stat) + // then you can create a new event type in protocol-events.ts, have Minecraft send that event with the new data, and then register a handler + // for that event here to handle the incoming data and do something with it (e.g. update the home view, send a notification, etc). + private registerServerEvents() { + this._eventRegistry.register(IncomingEventType.stopped, (msg: StoppedEventMessage) => { + this.trackThreadChanges(msg.reason, msg.thread); + this.sendEvent(new StoppedEvent(msg.reason, msg.thread)); + }); + this._eventRegistry.register(IncomingEventType.thread, (msg: ThreadEventMessage) => { + this.trackThreadChanges(msg.reason, msg.thread); + this.sendEvent(new ThreadEvent(msg.reason, msg.thread)); + }); + this._eventRegistry.register(IncomingEventType.print, (msg: PrintEventMessage) => { + this.handlePrintEvent(msg.message, msg.logLevel); + }); + this._eventRegistry.register(IncomingEventType.notification, (msg: NotificationEventMessage) => { + this.showNotification(msg.message, msg.logLevel); + }); + this._eventRegistry.register(IncomingEventType.protocol, (msg: ProtocolCapabilities) => { + this.handleProtocolEvent(msg); + }); + this._eventRegistry.register(IncomingEventType.stat2, (msg: StatMessageModel) => { + this._statsProvider.setStats(msg); + }); + this._eventRegistry.register(IncomingEventType.profilerCapture, (msg: ProfilerCapture) => { + this.handleProfilerCapture(msg); + }); + } + public dispose(): void { this._eventEmitter.removeAllListeners('run-minecraft-command'); this._eventEmitter.removeAllListeners('start-profiler'); @@ -203,13 +217,13 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { private onRunMinecraftCommand(command: string): void { if (this._clientProtocolVersion < ProtocolVersion.SupportProfilerCaptures) { this.sendDebuggeeMessage({ - type: 'minecraftCommand', + type: OutgoingEventType.minecraftCommand, command: command, dimension_type: 'overworld', }); } else { this.sendDebuggeeMessage({ - type: 'minecraftCommand', + type: OutgoingEventType.minecraftCommand, command: { command: command, dimension_type: 'overworld', @@ -220,7 +234,7 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { private onStartProfiler(): void { this.sendDebuggeeMessage({ - type: 'startProfiler', + type: OutgoingEventType.startProfiler, profiler: { target_module_uuid: this._targetModuleUuid, }, @@ -229,7 +243,7 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { private onStopProfiler(capturesBasePath: string): void { this.sendDebuggeeMessage({ - type: 'stopProfiler', + type: OutgoingEventType.stopProfiler, profiler: { captures_path: capturesBasePath, target_module_uuid: this._targetModuleUuid, @@ -430,7 +444,7 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { args: DebugProtocol.SetExceptionBreakpointsArguments ): void { this.sendDebuggeeMessage({ - type: 'stopOnException', + type: OutgoingEventType.stopOnException, stopOnException: args.filters.length > 0, // there's only 1 type for now so no need to look at which one it is }); @@ -439,7 +453,7 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { protected configurationDoneRequest(response: DebugProtocol.ConfigurationDoneResponse): void { this.sendDebuggeeMessage({ - type: 'resume', + type: OutgoingEventType.resume, }); this.sendResponse(response); @@ -730,7 +744,7 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { // respond with protocol version and chosen debugee target this.sendDebuggeeMessage({ - type: 'protocol', + type: OutgoingEventType.protocol, version: protocolVersion, target_module_uuid: targetModuleUuid, passcode: passcode, @@ -838,9 +852,9 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { this.sendDebuggeeMessage(this.makeRequestPayload(requestSeq, response.command, args)); } - private makeRequestPayload(requestSeq: number, responseCommand: string, args: any) { - const envelope = { - type: 'request', + private makeRequestPayload(requestSeq: number, responseCommand: string, args: unknown): RequestMessage { + const envelope: RequestMessage = { + type: OutgoingEventType.request, request: { request_seq: requestSeq, command: responseCommand, @@ -850,7 +864,7 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { return envelope; } - public sendDebuggeeMessage(envelope: unknown): void { + public sendDebuggeeMessage(envelope: OutgoingDebuggeeMessage): void { if (!this._connectionSocket) { return; } @@ -876,29 +890,14 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { } else if (envelope.type === 'response') { this.handleDebugeeResponse(envelope); } + else { + this.log(`Debugee message error: Unknown message type: ${envelope?.type ?? "NO TYPE"}`, LogLevel.Error); + } } // Debugee (MC) has sent an event. private handleDebugeeEvent(eventMessage: any) { - if (eventMessage.type === 'StoppedEvent') { - this.trackThreadChanges(eventMessage.reason, eventMessage.thread); - this.sendEvent(new StoppedEvent(eventMessage.reason, eventMessage.thread)); - } else if (eventMessage.type === 'ThreadEvent') { - this.trackThreadChanges(eventMessage.reason, eventMessage.thread); - this.sendEvent(new ThreadEvent(eventMessage.reason, eventMessage.thread)); - } else if (eventMessage.type === 'PrintEvent') { - this.handlePrintEvent(eventMessage.message, eventMessage.logLevel); - } else if (eventMessage.type === 'NotificationEvent') { - this.showNotification(eventMessage.message, eventMessage.logLevel); - } else if (eventMessage.type === 'ProtocolEvent') { - this.handleProtocolEvent(eventMessage as ProtocolCapabilities); - } else if (eventMessage.type === 'StatEvent2') { - this._statsProvider.setStats(eventMessage as StatMessageModel); - } else if (eventMessage.type === 'SchemaEvent') { - this._statsProvider.setSchema(eventMessage.descriptors); - } else if (eventMessage.type === 'ProfilerCapture') { - this.handleProfilerCapture(eventMessage as ProfilerCapture); - } + this._eventRegistry.dispatch(eventMessage); } private async handlePrintEvent(message: string, logLevel: LogLevel) { @@ -967,7 +966,7 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { // handle protocol capabilities here... // can fail connection on errors // - if (this._debuggerProtocolVersion < protocolCapabilities.version) { + if (DEBUGGER_PROTOCOL_VERSION < protocolCapabilities.version) { this.terminateSession( `protocol unsupported. Upgrade Debugger Extension. Protocol Version: ${protocolCapabilities.version} is not supported by the current version of the Debugger.`, LogLevel.Error From 4553d18ea01538948013c77c1c97e0e701368ac2 Mon Sep 17 00:00:00 2001 From: Alex Denford SBL Date: Fri, 22 May 2026 16:34:32 -0700 Subject: [PATCH 04/25] merge --- src/protocol-events.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/protocol-events.ts b/src/protocol-events.ts index a3d61646..092f11bb 100644 --- a/src/protocol-events.ts +++ b/src/protocol-events.ts @@ -11,7 +11,8 @@ import { StatMessageModel } from './stats/stats-provider'; // 4 - mc can require a passcode to connect // 5 - debugger can take mc script profiler captures // 6 - breakpoints as request, MC can reject -// 7 - New serialization tech (use Cereal) +// 7 - support for debugger requests, MC can reject or respond with args +// 8 - New serialization tech (use Cereal) export enum ProtocolVersion { _Unknown = 0, @@ -21,7 +22,8 @@ export enum ProtocolVersion { SupportPasscode = 4, SupportProfilerCaptures = 5, SupportBreakpointsAsRequest = 6, - SupportCerealSerialization = 7, + SupportDebuggerRequests = 7, + SupportCerealSerialization = 8, } export const DEBUGGER_PROTOCOL_VERSION = ProtocolVersion.SupportCerealSerialization; From 50c52ba3ac737a255d8e6e511593ebece94fa70c Mon Sep 17 00:00:00 2001 From: Alex Denford SBL Date: Tue, 26 May 2026 16:12:19 -0700 Subject: [PATCH 05/25] fixes/merge --- src/protocol-events.ts | 24 ++++++++++++++++--- src/requests/debugger-request-schema.ts | 21 +--------------- src/requests/request-manager.ts | 3 ++- src/session.ts | 3 ++- .../utilities/useDebuggerRequests.ts | 2 +- 5 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/protocol-events.ts b/src/protocol-events.ts index 092f11bb..a2f316aa 100644 --- a/src/protocol-events.ts +++ b/src/protocol-events.ts @@ -84,6 +84,14 @@ export interface NotificationEventMessage { logLevel: LogLevel; } +export interface DebuggeeResponseEnvelope { + type: 'debuggee-response'; + request_seq: number; + args?: unknown; + success?: boolean; + response_message?: string; +} + export type IncomingDebuggeeMessage = | PluginDetails | ProtocolCapabilities @@ -91,8 +99,8 @@ export type IncomingDebuggeeMessage = | StoppedEventMessage | ThreadEventMessage | PrintEventMessage - | NotificationEventMessage; - + | NotificationEventMessage + | DebuggeeResponseEnvelope; // ------------------------------------------------------------------------- @@ -156,6 +164,15 @@ export interface BreakpointsMessage { breakpoints: { path: string; breakpoints: DebugProtocol.SourceBreakpoint[] | undefined }; } +export interface DebuggerRequestEnvelope { + type: 'debugger-request'; + request: { + request_seq: number; + request: string; + args?: unknown; + }; +} + export type OutgoingDebuggeeMessage = | ProtocolResponse | MinecraftCommandLegacyMessage @@ -165,7 +182,8 @@ export type OutgoingDebuggeeMessage = | StopOnExceptionMessage | ResumeMessage | RequestMessage - | BreakpointsMessage; + | BreakpointsMessage + | DebuggerRequestEnvelope; diff --git a/src/requests/debugger-request-schema.ts b/src/requests/debugger-request-schema.ts index 5b0a73ed..81aa67b8 100644 --- a/src/requests/debugger-request-schema.ts +++ b/src/requests/debugger-request-schema.ts @@ -4,23 +4,4 @@ export interface DebuggerRequestArguments { request: string; args?: unknown; -} - -// Sent from the debug session to the debuggee (MC) -export interface DebuggerRequestEnvelope { - type: 'debugger-request'; - request: { - request_seq: number; - request: string; - args?: unknown; - }; -} - -// Received from the debuggee (MC) in response to a DebuggerRequestEnvelope -export interface DebuggeeResponseEnvelope { - type: 'debuggee-response'; - request_seq: number; - args?: unknown; - success?: boolean; - response_message?: string; -} +} \ No newline at end of file diff --git a/src/requests/request-manager.ts b/src/requests/request-manager.ts index 78264210..b33d6eb9 100644 --- a/src/requests/request-manager.ts +++ b/src/requests/request-manager.ts @@ -1,8 +1,9 @@ // Copyright (C) Microsoft Corporation. All rights reserved. import { DebugProtocol } from '@vscode/debugprotocol'; -import { DebuggerRequestArguments, DebuggerRequestEnvelope, DebuggeeResponseEnvelope } from './debugger-request-schema'; +import { DebuggerRequestArguments} from './debugger-request-schema'; import { IDebuggeeMessageSender } from '../debuggee-message-sender'; +import { DebuggeeResponseEnvelope, DebuggerRequestEnvelope } from '../protocol-events'; interface PendingDebuggerRequest { resolve: (value: DebuggeeResponseEnvelope) => void; diff --git a/src/session.ts b/src/session.ts index 451532b3..08bd39ce 100644 --- a/src/session.ts +++ b/src/session.ts @@ -57,11 +57,12 @@ import { RequestMessage, StoppedEventMessage, ThreadEventMessage, + DebuggeeResponseEnvelope } from './protocol-events'; import { SourceMaps } from './source-maps'; import { StatMessageModel, StatsProvider } from './stats/stats-provider'; import { RequestManager } from './requests/request-manager'; -import { DebuggeeResponseEnvelope, DebuggerRequestArguments } from './requests/debugger-request-schema'; +import { DebuggerRequestArguments } from './requests/debugger-request-schema'; interface PendingResponse { onSuccess?: (result: any) => void; diff --git a/webview-ui/src/diagnostics_panel/utilities/useDebuggerRequests.ts b/webview-ui/src/diagnostics_panel/utilities/useDebuggerRequests.ts index 6efdada8..ba212287 100644 --- a/webview-ui/src/diagnostics_panel/utilities/useDebuggerRequests.ts +++ b/webview-ui/src/diagnostics_panel/utilities/useDebuggerRequests.ts @@ -1,7 +1,7 @@ // Copyright (C) Microsoft Corporation. All rights reserved. import { useEffect, useState } from 'react'; -import type { DebuggeeResponseEnvelope } from '../../../../src/requests/debugger-request-schema'; +import type { DebuggeeResponseEnvelope } from '../../../../src/protocol-events'; import { vscode } from './vscode'; // Result payload posted back from the extension host after a debugger request completes. From 863d46dfbf3e9a5bf63b5027063bd509bc625113 Mon Sep 17 00:00:00 2001 From: Alex Denford SBL Date: Wed, 27 May 2026 13:32:48 -0700 Subject: [PATCH 06/25] fixes --- src/protocol-events.ts | 6 ++++-- src/requests/request-manager.ts | 4 ++-- src/session.ts | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/protocol-events.ts b/src/protocol-events.ts index a2f316aa..7b878ca7 100644 --- a/src/protocol-events.ts +++ b/src/protocol-events.ts @@ -40,6 +40,7 @@ export const IncomingEventType = { stat2: 'StatEvent2', schema: 'SchemaEvent', profilerCapture: 'ProfilerCapture', + debuggeeResponse: 'debuggee-response', } as const; export interface PluginDetails { @@ -85,7 +86,7 @@ export interface NotificationEventMessage { } export interface DebuggeeResponseEnvelope { - type: 'debuggee-response'; + type: typeof IncomingEventType.debuggeeResponse; request_seq: number; args?: unknown; success?: boolean; @@ -115,6 +116,7 @@ export const OutgoingEventType = { resume: 'resume', request: 'request', breakpoints: 'breakpoints', + debuggerRequest: 'debugger-request' } as const; export interface ProtocolResponse { @@ -165,7 +167,7 @@ export interface BreakpointsMessage { } export interface DebuggerRequestEnvelope { - type: 'debugger-request'; + type: typeof OutgoingEventType.debuggerRequest; request: { request_seq: number; request: string; diff --git a/src/requests/request-manager.ts b/src/requests/request-manager.ts index b33d6eb9..53225427 100644 --- a/src/requests/request-manager.ts +++ b/src/requests/request-manager.ts @@ -3,7 +3,7 @@ import { DebugProtocol } from '@vscode/debugprotocol'; import { DebuggerRequestArguments} from './debugger-request-schema'; import { IDebuggeeMessageSender } from '../debuggee-message-sender'; -import { DebuggeeResponseEnvelope, DebuggerRequestEnvelope } from '../protocol-events'; +import { DebuggeeResponseEnvelope, DebuggerRequestEnvelope, OutgoingEventType } from '../protocol-events'; interface PendingDebuggerRequest { resolve: (value: DebuggeeResponseEnvelope) => void; @@ -47,7 +47,7 @@ export class RequestManager { // Create an envelope to hold the request, and send it to the debuggee const envelope: DebuggerRequestEnvelope = { - type: 'debugger-request', + type: OutgoingEventType.debuggerRequest, request: { request_seq: seq, request, diff --git a/src/session.ts b/src/session.ts index 08bd39ce..d23c8669 100644 --- a/src/session.ts +++ b/src/session.ts @@ -927,7 +927,7 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { private receiveDebugeeMessage(envelope: any) { if (envelope.type === 'event') { this.handleDebugeeEvent(envelope.event); - } else if (envelope.type === 'debuggee-response') { + } else if (envelope.type === IncomingEventType.debuggeeResponse) { if (!this._minecraftCapabilities.supportsDebuggerRequests) { this.log( 'Received debuggee-response from a Minecraft instance that should not support it.', From e462c4794c12d203f8992a8815f9bfa80fd9237d Mon Sep 17 00:00:00 2001 From: Alex Denford SBL Date: Wed, 27 May 2026 14:23:28 -0700 Subject: [PATCH 07/25] Update session.ts --- src/session.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/session.ts b/src/session.ts index d23c8669..d7ec2cd9 100644 --- a/src/session.ts +++ b/src/session.ts @@ -936,7 +936,7 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { return; } - this._requestManager?.handleDebuggeeResponse(envelope as DebuggeeResponseEnvelope); + this._requestManager?.handleDebuggeeResponse(envelope.event as DebuggeeResponseEnvelope); } else if (envelope.type === 'response') { this.handleDebugeeResponse(envelope); } From fa1d26eaed29d96508a8f6818d76d1a9526909c3 Mon Sep 17 00:00:00 2001 From: Alex Denford SBL Date: Wed, 27 May 2026 14:56:10 -0700 Subject: [PATCH 08/25] remove --- .../diagnostics_panel/DiagnosticsSchema.ts | 29 --- .../src/diagnostics_panel/DynamicTab.tsx | 166 ------------------ 2 files changed, 195 deletions(-) delete mode 100644 webview-ui/src/diagnostics_panel/DiagnosticsSchema.ts delete mode 100644 webview-ui/src/diagnostics_panel/DynamicTab.tsx diff --git a/webview-ui/src/diagnostics_panel/DiagnosticsSchema.ts b/webview-ui/src/diagnostics_panel/DiagnosticsSchema.ts deleted file mode 100644 index 4988c2dd..00000000 --- a/webview-ui/src/diagnostics_panel/DiagnosticsSchema.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (C) Microsoft Corporation. All rights reserved. -// TypeScript mirror of ScriptDiagnosticsDescriptor (C++) and its Cereal-serialized wire format. - -export type DiagnosticsDataSource = 'server' | 'client' | 'server_script'; - -export type DiagnosticsDisplayType = - | 'line_chart' - | 'stacked_line_chart' - | 'stacked_bar_chart' - | 'table' - | 'multi_column_table' - | 'dynamic_properties_table'; - -// One-to-one with ScriptDiagnosticsDescriptor fields (snake_case matches cereal wire names). -export interface DiagnosticsTabDescriptor { - name: string; - stat_group_id: string; - data_source: DiagnosticsDataSource; - display_type: DiagnosticsDisplayType; - title?: string; - y_label?: string; - tick_range?: number; - value_scalar?: number; - target_value?: number; - key_label?: string; - value_labels?: string[]; - statistic_id?: string; - statistic_ids?: string[]; -} diff --git a/webview-ui/src/diagnostics_panel/DynamicTab.tsx b/webview-ui/src/diagnostics_panel/DynamicTab.tsx deleted file mode 100644 index f39ba038..00000000 --- a/webview-ui/src/diagnostics_panel/DynamicTab.tsx +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright (C) Microsoft Corporation. All rights reserved. - -// Generic tab renderer driven by a DiagnosticsTabDescriptor from the C++ schema registry. -// Derives the appropriate StatisticProvider and StatisticResolver from the descriptor fields -// and renders the matching control — no new tab files needed for new diagnostics. - -import { useMemo } from 'react'; -import { DiagnosticsTabDescriptor } from './DiagnosticsSchema'; -import { - SimpleStatisticProvider, - MultipleStatisticProvider, - StatisticProvider, -} from './StatisticProvider'; -import { StatisticType, YAxisType, createStatResolver, StatisticResolver } from './StatisticResolver'; -import MinecraftStatisticLineChart from './controls/MinecraftStatisticLineChart'; -import MinecraftStatisticStackedLineChart from './controls/MinecraftStatisticStackedLineChart'; -import MinecraftStatisticStackedBarChart from './controls/MinecraftStatisticStackedBarChart'; -import MinecraftStatisticTable from './controls/MinecraftStatisticTable'; -import MinecraftMultiColumnStatisticTable from './controls/MinecraftMultiColumnStatisticTable'; -import { MinecraftDynamicPropertiesTable } from './controls/MinecraftDynamicPropertiesTable'; - -type DynamicTabProps = { - descriptor: DiagnosticsTabDescriptor; - selectedClient: string; - selectedPlugin: string; -}; - -// Build a StatisticProvider from descriptor fields. -// Client-source tabs use a regex to match the per-player stat group prefix. -function buildProvider( - descriptor: DiagnosticsTabDescriptor, - selectedClient: string, - _selectedPlugin: string -): StatisticProvider { - const { data_source, display_type, stat_group_id, statistic_id, statistic_ids } = descriptor; - - // DynamicPropertiesTable has its own fixed provider shape - if (display_type === 'dynamic_properties_table') { - return new SimpleStatisticProvider({ - statisticId: 'consolidated_data', - statisticParentId: new RegExp(`${stat_group_id}.*`), - }); - } - - // LineChart uses SimpleStatisticProvider (single stat series) - if (display_type === 'line_chart') { - const effectiveStatId = statistic_id ?? stat_group_id; - const parentId: string | RegExp = - data_source === 'client' - ? new RegExp(`.*${selectedClient}_${stat_group_id}`) - : stat_group_id; - return new SimpleStatisticProvider({ - statisticId: effectiveStatId, - statisticParentId: parentId, - }); - } - - // All other chart/table types use MultipleStatisticProvider - const parentId: string | RegExp = - data_source === 'client' - ? new RegExp(`.*${selectedClient}_${stat_group_id}`) - : stat_group_id; - - return new MultipleStatisticProvider({ - statisticParentId: parentId, - statisticIds: statistic_ids, - }); -} - -function buildResolver(descriptor: DiagnosticsTabDescriptor): StatisticResolver { - return createStatResolver({ - type: StatisticType.Absolute, - yAxisType: YAxisType.Absolute, - tickRange: descriptor.tick_range ?? 20 * 10, - valueScalar: descriptor.value_scalar, - }); -} - -export default function DynamicTab({ descriptor, selectedClient, selectedPlugin }: DynamicTabProps) { - const title = descriptor.title ?? descriptor.name; - const yLabel = descriptor.y_label ?? ''; - const tickRange = descriptor.tick_range ?? 20 * 10; - - // Memoize provider and resolver so controls' useEffect deps remain stable across renders. - const provider = useMemo( - () => buildProvider(descriptor, selectedClient, selectedPlugin), - // re-create only when the identity of the tab or the selected client/plugin changes - [descriptor.stat_group_id, descriptor.data_source, descriptor.display_type, - descriptor.statistic_id, descriptor.statistic_ids?.join(','), selectedClient, selectedPlugin] - ); - - const resolver = useMemo( - () => buildResolver(descriptor), - [descriptor.tick_range, descriptor.value_scalar] - ); - - switch (descriptor.display_type) { - case 'line_chart': - return ( - - ); - - case 'stacked_line_chart': - return ( - - ); - - case 'stacked_bar_chart': - return ( - - ); - - case 'table': - return ( - - ); - - case 'multi_column_table': - return ( - - ); - - case 'dynamic_properties_table': - return ( - - ); - - default: - return
Unsupported display type: {descriptor.display_type}
; - } -} From 4614baa85e63418017a92a319f0f0b9c7400bcd9 Mon Sep 17 00:00:00 2001 From: Alex Denford SBL Date: Wed, 27 May 2026 17:06:39 -0700 Subject: [PATCH 09/25] feedback --- CONTRIBUTING.md | 2 +- package-lock.json | 80 +++++++++++++++------- src/breakpoints-legacy.ts | 2 +- src/protocol-events.ts | 99 +++++++++++++++------------- src/requests/request-manager.test.ts | 9 +-- src/requests/request-manager.ts | 2 +- src/session.ts | 32 ++++----- src/stats/stats-provider.ts | 2 +- 8 files changed, 136 insertions(+), 92 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 470bffc5..3ac1ed5c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,7 +20,7 @@ To build and use this extension locally, run the following commands: cd webview-ui npm install ``` -Go to the Run and Debug panel (Ctrl-Shift-D) and select "Run Extension". This was automatically build and run the extension. +Go to the Run and Debug panel (Ctrl-Shift-D) and select "Run Extension". This will automatically build and run the extension. You can now use the extension as normal (in the new VS Code instance) by attaching to Minecraft to debug. # Tests diff --git a/package-lock.json b/package-lock.json index 68c8ae70..d3527aeb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -918,6 +918,7 @@ "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -933,6 +934,7 @@ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6.0.0" } @@ -943,6 +945,7 @@ "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6.0.0" } @@ -953,6 +956,7 @@ "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -971,6 +975,7 @@ "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -1362,7 +1367,6 @@ "integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^5.0.0", "@octokit/graphql": "^8.0.0", @@ -2909,7 +2913,6 @@ "integrity": "sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "4.33.0", "@typescript-eslint/types": "4.33.0", @@ -3493,6 +3496,7 @@ "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" @@ -3503,21 +3507,24 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", @@ -3525,6 +3532,7 @@ "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", @@ -3536,7 +3544,8 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", @@ -3544,6 +3553,7 @@ "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -3557,6 +3567,7 @@ "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -3567,6 +3578,7 @@ "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -3576,7 +3588,8 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", @@ -3584,6 +3597,7 @@ "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -3601,6 +3615,7 @@ "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", @@ -3615,6 +3630,7 @@ "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -3628,6 +3644,7 @@ "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", @@ -3643,6 +3660,7 @@ "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" @@ -3653,14 +3671,16 @@ "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", "dev": true, - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/acorn": { "version": "8.15.0", @@ -3733,7 +3753,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -4162,7 +4181,8 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/cac": { "version": "6.7.14", @@ -4234,7 +4254,8 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "CC-BY-4.0" + "license": "CC-BY-4.0", + "peer": true }, "node_modules/chai": { "version": "5.1.2", @@ -4371,6 +4392,7 @@ "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6.0" } @@ -5143,7 +5165,8 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", "dev": true, - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -5479,7 +5502,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -5648,7 +5670,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.4.3", @@ -6524,7 +6545,8 @@ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true, - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/globals": { "version": "13.24.0", @@ -7202,6 +7224,7 @@ "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -7217,6 +7240,7 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -7227,6 +7251,7 @@ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -7494,6 +7519,7 @@ "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6.11.5" }, @@ -7620,6 +7646,7 @@ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -7681,7 +7708,6 @@ "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==", "dev": true, "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -8002,7 +8028,8 @@ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/node-sarif-builder": { "version": "3.4.0", @@ -11892,6 +11919,7 @@ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "safe-buffer": "^5.1.0" } @@ -12282,6 +12310,7 @@ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -12434,7 +12463,6 @@ "integrity": "sha512-Cb0Pm3Ye15u8k/B+7EnusMUSIIucAIEBD3QDRmmclv53KVyqmg1Lb3XPx0AMNxfJZEI+ZT+M+IXDyTrudK6Rew==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^13.0.0-beta.1", "@semantic-release/error": "^4.0.0", @@ -12869,6 +12897,7 @@ "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "randombytes": "^2.1.0" } @@ -13176,6 +13205,7 @@ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -13187,6 +13217,7 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -13690,6 +13721,7 @@ "integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -13709,6 +13741,7 @@ "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", @@ -13743,7 +13776,8 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/text-table": { "version": "0.2.0", @@ -14093,7 +14127,6 @@ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14223,6 +14256,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" @@ -14305,7 +14339,6 @@ "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -14885,6 +14918,7 @@ "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -14899,6 +14933,7 @@ "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -14948,6 +14983,7 @@ "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.13.0" } diff --git a/src/breakpoints-legacy.ts b/src/breakpoints-legacy.ts index 8d368385..b0a9ba57 100644 --- a/src/breakpoints-legacy.ts +++ b/src/breakpoints-legacy.ts @@ -69,7 +69,7 @@ export class BreakpointsLegacy implements IBreakpointsHandler { // send full set of breakpoints for each generated file, a message per file for (const [generatedRemoteLocalPath, generatedBreakpoints] of generatedBreakpointsMap) { const envelope: BreakpointsMessage = { - type: OutgoingEventType.breakpoints, + type: OutgoingEventType.Breakpoints, breakpoints: { path: generatedRemoteLocalPath, breakpoints: generatedBreakpoints.length ? generatedBreakpoints : undefined, diff --git a/src/protocol-events.ts b/src/protocol-events.ts index 7b878ca7..2b44972c 100644 --- a/src/protocol-events.ts +++ b/src/protocol-events.ts @@ -31,17 +31,17 @@ export const DEBUGGER_PROTOCOL_VERSION = ProtocolVersion.SupportCerealSerializat // ------------------------------------------------------------------------- // Interfaces for event message payloads (received from the debugee) // ------------------------------------------------------------------------- -export const IncomingEventType = { - stopped: 'StoppedEvent', - thread: 'ThreadEvent', - print: 'PrintEvent', - notification: 'NotificationEvent', - protocol: 'ProtocolEvent', - stat2: 'StatEvent2', - schema: 'SchemaEvent', - profilerCapture: 'ProfilerCapture', - debuggeeResponse: 'debuggee-response', -} as const; +export enum IncomingEventType { + Stopped = 'StoppedEvent', + Thread = 'ThreadEvent', + Print = 'PrintEvent', + Notification = 'NotificationEvent', + Protocol = 'ProtocolEvent', + Stat2 = 'StatEvent2', + Schema = 'SchemaEvent', + ProfilerCapture = 'ProfilerCapture', + DebuggeeResponse = 'debuggee-response', +} export interface PluginDetails { name: string; @@ -49,125 +49,129 @@ export interface PluginDetails { } export interface ProtocolCapabilities { - type: typeof IncomingEventType.protocol; + type: IncomingEventType.Protocol; version: number; plugins: PluginDetails[]; require_passcode?: boolean; } export interface ProfilerCapture { - type: typeof IncomingEventType.profilerCapture; + type: IncomingEventType.ProfilerCapture; capture_base_path: string; capture_data: string; } export interface StoppedEventMessage { - type: typeof IncomingEventType.stopped; + type: IncomingEventType.Stopped; reason: string; thread: number; } export interface ThreadEventMessage { - type: typeof IncomingEventType.thread; + type: IncomingEventType.Thread; reason: string; thread: number; } export interface PrintEventMessage { - type: typeof IncomingEventType.print; + type: IncomingEventType.Print; message: string; logLevel: LogLevel; } export interface NotificationEventMessage { - type: typeof IncomingEventType.notification; + type: IncomingEventType.Notification; message: string; logLevel: LogLevel; } export interface DebuggeeResponseEnvelope { - type: typeof IncomingEventType.debuggeeResponse; + type: IncomingEventType.DebuggeeResponse; request_seq: number; args?: unknown; success?: boolean; response_message?: string; } +type StatEventMessage = StatMessageModel & { + type: IncomingEventType.Stat2; +}; + export type IncomingDebuggeeMessage = - | PluginDetails | ProtocolCapabilities | ProfilerCapture | StoppedEventMessage | ThreadEventMessage | PrintEventMessage | NotificationEventMessage + | StatEventMessage | DebuggeeResponseEnvelope; // ------------------------------------------------------------------------- // Interfaces for outbound message payloads (sent to the debugee) // ------------------------------------------------------------------------- -export const OutgoingEventType = { - protocol: 'protocol', - minecraftCommand: 'minecraftCommand', - startProfiler: 'startProfiler', - stopProfiler: 'stopProfiler', - stopOnException: 'stopOnException', - resume: 'resume', - request: 'request', - breakpoints: 'breakpoints', - debuggerRequest: 'debugger-request' -} as const; +export enum OutgoingEventType { + Protocol = 'protocol', + MinecraftCommand = 'minecraftCommand', + StartProfiler = 'startProfiler', + StopProfiler = 'stopProfiler', + StopOnException = 'stopOnException', + Resume = 'resume', + Request = 'request', + Breakpoints = 'breakpoints', + DebuggerRequest = 'debugger-request' +} export interface ProtocolResponse { - type: typeof OutgoingEventType.protocol; + type: OutgoingEventType.Protocol; version: number; target_module_uuid?: string; passcode?: string; } export interface MinecraftCommandLegacyMessage { - type: typeof OutgoingEventType.minecraftCommand; + type: OutgoingEventType.MinecraftCommand; command: string; dimension_type: string; } export interface MinecraftCommandMessage { - type: typeof OutgoingEventType.minecraftCommand; + type: OutgoingEventType.MinecraftCommand; command: { command: string; dimension_type: string }; } export interface StartProfilerMessage { - type: typeof OutgoingEventType.startProfiler; + type: OutgoingEventType.StartProfiler; profiler: { target_module_uuid?: string }; } export interface StopProfilerMessage { - type: typeof OutgoingEventType.stopProfiler; + type: OutgoingEventType.StopProfiler; profiler: { captures_path: string; target_module_uuid?: string }; } export interface StopOnExceptionMessage { - type: typeof OutgoingEventType.stopOnException; + type: OutgoingEventType.StopOnException; stopOnException: boolean; } export interface ResumeMessage { - type: typeof OutgoingEventType.resume; + type: OutgoingEventType.Resume; } export interface RequestMessage { - type: typeof OutgoingEventType.request; + type: OutgoingEventType.Request; request: { request_seq: number; command: string; args: unknown }; } export interface BreakpointsMessage { - type: typeof OutgoingEventType.breakpoints; + type: OutgoingEventType.Breakpoints; breakpoints: { path: string; breakpoints: DebugProtocol.SourceBreakpoint[] | undefined }; } export interface DebuggerRequestEnvelope { - type: typeof OutgoingEventType.debuggerRequest; + type: OutgoingEventType.DebuggerRequest; request: { request_seq: number; request: string; @@ -196,17 +200,20 @@ export type { StatMessageModel }; // Registry that maps event type name strings to handler callbacks // ------------------------------------------------------------------------- -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type DebuggeeEventHandler = (event: any) => void; +type DebuggeeEventHandler = ( + event: Extract, +) => void; export class DebuggeeEventRegistry { - private readonly _handlers = new Map(); + private readonly _handlers = new Map void>(); - public register(eventType: string, handler: DebuggeeEventHandler): void { - this._handlers.set(eventType, handler); + public register(eventType: T, handler: DebuggeeEventHandler): void { + this._handlers.set(eventType, event => { + handler(event as Extract); + }); } - public dispatch(eventMessage: { type: string }): boolean { + public dispatch(eventMessage: IncomingDebuggeeMessage): boolean { const handler = this._handlers.get(eventMessage.type); if (handler) { handler(eventMessage); diff --git a/src/requests/request-manager.test.ts b/src/requests/request-manager.test.ts index 0bfd83c0..fcc81fd3 100644 --- a/src/requests/request-manager.test.ts +++ b/src/requests/request-manager.test.ts @@ -4,6 +4,7 @@ import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest'; import type { DebugProtocol } from '@vscode/debugprotocol'; import { RequestManager } from './request-manager'; import type { IDebuggeeMessageSender } from '../debuggee-message-sender'; +import { IncomingEventType } from '../protocol-events'; describe('RequestManager', () => { let manager: RequestManager; @@ -27,7 +28,7 @@ describe('RequestManager', () => { }); const handled = manager.handleDebuggeeResponse({ - type: 'debuggee-response', + type: IncomingEventType.DebuggeeResponse, request_seq: 42, success: true, args: { ok: true }, @@ -44,7 +45,7 @@ describe('RequestManager', () => { }); expect(handled).toBe(true); await expect(promise).resolves.toEqual({ - type: 'debuggee-response', + type: IncomingEventType.DebuggeeResponse, request_seq: 42, success: true, args: { ok: true }, @@ -78,7 +79,7 @@ describe('RequestManager', () => { }); manager.handleDebuggeeResponse({ - type: 'debuggee-response', + type: IncomingEventType.DebuggeeResponse, request_seq: 7, success: false, response_message: 'Denied', @@ -91,7 +92,7 @@ describe('RequestManager', () => { manager = new RequestManager(mockSender); const handled = manager.handleDebuggeeResponse({ - type: 'debuggee-response', + type: IncomingEventType.DebuggeeResponse, request_seq: -1, success: true, }); diff --git a/src/requests/request-manager.ts b/src/requests/request-manager.ts index 53225427..cc59dd5e 100644 --- a/src/requests/request-manager.ts +++ b/src/requests/request-manager.ts @@ -47,7 +47,7 @@ export class RequestManager { // Create an envelope to hold the request, and send it to the debuggee const envelope: DebuggerRequestEnvelope = { - type: OutgoingEventType.debuggerRequest, + type: OutgoingEventType.DebuggerRequest, request: { request_seq: seq, request, diff --git a/src/session.ts b/src/session.ts index d7ec2cd9..8337f1e5 100644 --- a/src/session.ts +++ b/src/session.ts @@ -178,27 +178,27 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { // then you can create a new event type in protocol-events.ts, have Minecraft send that event with the new data, and then register a handler // for that event here to handle the incoming data and do something with it (e.g. update the home view, send a notification, etc). private registerServerEvents() { - this._eventRegistry.register(IncomingEventType.stopped, (msg: StoppedEventMessage) => { + this._eventRegistry.register(IncomingEventType.Stopped, (msg: StoppedEventMessage) => { this.trackThreadChanges(msg.reason, msg.thread); this.sendEvent(new StoppedEvent(msg.reason, msg.thread)); }); - this._eventRegistry.register(IncomingEventType.thread, (msg: ThreadEventMessage) => { + this._eventRegistry.register(IncomingEventType.Thread, (msg: ThreadEventMessage) => { this.trackThreadChanges(msg.reason, msg.thread); this.sendEvent(new ThreadEvent(msg.reason, msg.thread)); }); - this._eventRegistry.register(IncomingEventType.print, (msg: PrintEventMessage) => { + this._eventRegistry.register(IncomingEventType.Print, (msg: PrintEventMessage) => { this.handlePrintEvent(msg.message, msg.logLevel); }); - this._eventRegistry.register(IncomingEventType.notification, (msg: NotificationEventMessage) => { + this._eventRegistry.register(IncomingEventType.Notification, (msg: NotificationEventMessage) => { this.showNotification(msg.message, msg.logLevel); }); - this._eventRegistry.register(IncomingEventType.protocol, (msg: ProtocolCapabilities) => { + this._eventRegistry.register(IncomingEventType.Protocol, (msg: ProtocolCapabilities) => { this.handleProtocolEvent(msg); }); - this._eventRegistry.register(IncomingEventType.stat2, (msg: StatMessageModel) => { + this._eventRegistry.register(IncomingEventType.Stat2, (msg: StatMessageModel) => { this._statsProvider.setStats(msg); }); - this._eventRegistry.register(IncomingEventType.profilerCapture, (msg: ProfilerCapture) => { + this._eventRegistry.register(IncomingEventType.ProfilerCapture, (msg: ProfilerCapture) => { this.handleProfilerCapture(msg); }); } @@ -222,13 +222,13 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { private onRunMinecraftCommand(command: string): void { if (this._clientProtocolVersion < ProtocolVersion.SupportProfilerCaptures) { this.sendDebuggeeMessage({ - type: OutgoingEventType.minecraftCommand, + type: OutgoingEventType.MinecraftCommand, command: command, dimension_type: 'overworld', }); } else { this.sendDebuggeeMessage({ - type: OutgoingEventType.minecraftCommand, + type: OutgoingEventType.MinecraftCommand, command: { command: command, dimension_type: 'overworld', @@ -239,7 +239,7 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { private onStartProfiler(): void { this.sendDebuggeeMessage({ - type: OutgoingEventType.startProfiler, + type: OutgoingEventType.StartProfiler, profiler: { target_module_uuid: this._targetModuleUuid, }, @@ -248,7 +248,7 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { private onStopProfiler(capturesBasePath: string): void { this.sendDebuggeeMessage({ - type: OutgoingEventType.stopProfiler, + type: OutgoingEventType.StopProfiler, profiler: { captures_path: capturesBasePath, target_module_uuid: this._targetModuleUuid, @@ -449,7 +449,7 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { args: DebugProtocol.SetExceptionBreakpointsArguments, ): void { this.sendDebuggeeMessage({ - type: OutgoingEventType.stopOnException, + type: OutgoingEventType.StopOnException, stopOnException: args.filters.length > 0, // there's only 1 type for now so no need to look at which one it is }); @@ -458,7 +458,7 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { protected configurationDoneRequest(response: DebugProtocol.ConfigurationDoneResponse): void { this.sendDebuggeeMessage({ - type: OutgoingEventType.resume, + type: OutgoingEventType.Resume, }); this.sendResponse(response); @@ -775,7 +775,7 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { // respond with protocol version and chosen debugee target this.sendDebuggeeMessage({ - type: OutgoingEventType.protocol, + type: OutgoingEventType.Protocol, version: protocolVersion, target_module_uuid: targetModuleUuid, passcode: passcode, @@ -894,7 +894,7 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { private makeRequestPayload(requestSeq: number, responseCommand: string, args: unknown): RequestMessage { const envelope: RequestMessage = { - type: OutgoingEventType.request, + type: OutgoingEventType.Request, request: { request_seq: requestSeq, command: responseCommand, @@ -927,7 +927,7 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { private receiveDebugeeMessage(envelope: any) { if (envelope.type === 'event') { this.handleDebugeeEvent(envelope.event); - } else if (envelope.type === IncomingEventType.debuggeeResponse) { + } else if (envelope.type === IncomingEventType.DebuggeeResponse) { if (!this._minecraftCapabilities.supportsDebuggerRequests) { this.log( 'Received debuggee-response from a Minecraft instance that should not support it.', diff --git a/src/stats/stats-provider.ts b/src/stats/stats-provider.ts index ddace2ff..48c53f80 100644 --- a/src/stats/stats-provider.ts +++ b/src/stats/stats-provider.ts @@ -46,7 +46,7 @@ export class StatsProvider { this._fireStatUpdated(stat, stats.tick); } } - + public start(): void { throw new Error('Method not implemented.'); } From 24f294eff4b6fe9792ab384e63c0db41c31a78f0 Mon Sep 17 00:00:00 2001 From: Alex Denford SBL Date: Wed, 27 May 2026 17:09:05 -0700 Subject: [PATCH 10/25] Update protocol-events.ts --- src/protocol-events.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/protocol-events.ts b/src/protocol-events.ts index 2b44972c..54fd7f50 100644 --- a/src/protocol-events.ts +++ b/src/protocol-events.ts @@ -93,7 +93,7 @@ export interface DebuggeeResponseEnvelope { response_message?: string; } -type StatEventMessage = StatMessageModel & { +export type StatEventMessage = StatMessageModel & { type: IncomingEventType.Stat2; }; @@ -193,9 +193,6 @@ export type OutgoingDebuggeeMessage = -// Re-export for callers that need these alongside event types -export type { StatMessageModel }; - // ------------------------------------------------------------------------- // Registry that maps event type name strings to handler callbacks // ------------------------------------------------------------------------- From 4f7416d9543c267cc34cd95e6d4a2a62f619ca46 Mon Sep 17 00:00:00 2001 From: Alex Denford SBL Date: Wed, 27 May 2026 17:12:00 -0700 Subject: [PATCH 11/25] Update package-lock.json --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index d3527aeb..17d04ca8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15326,4 +15326,4 @@ } } } -} +} \ No newline at end of file From a9ea44a7c3a27e567db43392296b665bfa262073 Mon Sep 17 00:00:00 2001 From: Alex Denford SBL Date: Wed, 27 May 2026 17:13:21 -0700 Subject: [PATCH 12/25] Update package-lock.json --- webview-ui/package-lock.json | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json index d0dce43b..9891a45a 100644 --- a/webview-ui/package-lock.json +++ b/webview-ui/package-lock.json @@ -69,7 +69,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", "dev": true, - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -1527,7 +1526,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.0.tgz", "integrity": "sha512-ClIbNe36lawluuvq3+YYhnIN2CELi+6q8NpnM7PYp4hBn/TatfboPgVSm2rwKRfnV2M+Ty9GWDFI64KEe+kysA==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.20.0" } @@ -1632,7 +1630,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001629", "electron-to-chromium": "^1.4.796", @@ -2008,7 +2005,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "peer": true, "engines": { "node": ">=12" } @@ -2436,7 +2432,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -2767,7 +2762,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", "dev": true, - "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -3714,7 +3708,6 @@ "version": "22.13.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.0.tgz", "integrity": "sha512-ClIbNe36lawluuvq3+YYhnIN2CELi+6q8NpnM7PYp4hBn/TatfboPgVSm2rwKRfnV2M+Ty9GWDFI64KEe+kysA==", - "peer": true, "requires": { "undici-types": "~6.20.0" } @@ -3796,7 +3789,6 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", "dev": true, - "peer": true, "requires": { "caniuse-lite": "^1.0.30001629", "electron-to-chromium": "^1.4.796", @@ -4061,8 +4053,7 @@ "d3-selection": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "peer": true + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==" }, "d3-shape": { "version": "3.2.0", @@ -4356,7 +4347,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "peer": true, "requires": { "loose-envify": "^1.1.0" } From 97d25d6cc12ce7061215534a589b8ba5c79fb333 Mon Sep 17 00:00:00 2001 From: Alex Denford SBL Date: Wed, 27 May 2026 17:14:23 -0700 Subject: [PATCH 13/25] Update package-lock.json --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 17d04ca8..d3527aeb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15326,4 +15326,4 @@ } } } -} \ No newline at end of file +} From 0cefdc57fb72754c2ad99b8d5750cfbcfa358c74 Mon Sep 17 00:00:00 2001 From: Alex Denford SBL Date: Fri, 29 May 2026 16:05:18 -0700 Subject: [PATCH 14/25] tweaks --- src/breakpoints-legacy.ts | 6 ++---- src/protocol-events.ts | 23 +++++++++++--------- src/requests/request-manager.ts | 8 +++---- src/session.ts | 38 ++++++++++----------------------- 4 files changed, 29 insertions(+), 46 deletions(-) diff --git a/src/breakpoints-legacy.ts b/src/breakpoints-legacy.ts index b0a9ba57..59fb1ef0 100644 --- a/src/breakpoints-legacy.ts +++ b/src/breakpoints-legacy.ts @@ -70,10 +70,8 @@ export class BreakpointsLegacy implements IBreakpointsHandler { for (const [generatedRemoteLocalPath, generatedBreakpoints] of generatedBreakpointsMap) { const envelope: BreakpointsMessage = { type: OutgoingEventType.Breakpoints, - breakpoints: { - path: generatedRemoteLocalPath, - breakpoints: generatedBreakpoints.length ? generatedBreakpoints : undefined, - }, + path: generatedRemoteLocalPath, + breakpoints: generatedBreakpoints.length ? generatedBreakpoints : undefined, }; this._messageSender.sendDebuggeeMessage(envelope); } diff --git a/src/protocol-events.ts b/src/protocol-events.ts index 54fd7f50..d5db206e 100644 --- a/src/protocol-events.ts +++ b/src/protocol-events.ts @@ -138,17 +138,19 @@ export interface MinecraftCommandLegacyMessage { export interface MinecraftCommandMessage { type: OutgoingEventType.MinecraftCommand; - command: { command: string; dimension_type: string }; + command: string; + dimension_type: string; } export interface StartProfilerMessage { type: OutgoingEventType.StartProfiler; - profiler: { target_module_uuid?: string }; + target_module_uuid?: string; } export interface StopProfilerMessage { type: OutgoingEventType.StopProfiler; - profiler: { captures_path: string; target_module_uuid?: string }; + captures_path: string; + target_module_uuid?: string; } export interface StopOnExceptionMessage { @@ -162,21 +164,22 @@ export interface ResumeMessage { export interface RequestMessage { type: OutgoingEventType.Request; - request: { request_seq: number; command: string; args: unknown }; + request_seq: number; + command: string; + args: unknown; } export interface BreakpointsMessage { type: OutgoingEventType.Breakpoints; - breakpoints: { path: string; breakpoints: DebugProtocol.SourceBreakpoint[] | undefined }; + path: string; + breakpoints: DebugProtocol.SourceBreakpoint[] | undefined; } export interface DebuggerRequestEnvelope { type: OutgoingEventType.DebuggerRequest; - request: { - request_seq: number; - request: string; - args?: unknown; - }; + request_seq: number; + request: string; + args?: unknown; } export type OutgoingDebuggeeMessage = diff --git a/src/requests/request-manager.ts b/src/requests/request-manager.ts index cc59dd5e..9716a37f 100644 --- a/src/requests/request-manager.ts +++ b/src/requests/request-manager.ts @@ -48,11 +48,9 @@ export class RequestManager { // Create an envelope to hold the request, and send it to the debuggee const envelope: DebuggerRequestEnvelope = { type: OutgoingEventType.DebuggerRequest, - request: { - request_seq: seq, - request, - args, - }, + request_seq: seq, + request, + args, }; this._sender.sendDebuggeeMessage(envelope); diff --git a/src/session.ts b/src/session.ts index 8337f1e5..faffe178 100644 --- a/src/session.ts +++ b/src/session.ts @@ -220,39 +220,25 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { // ------------------------------------------------------------------------ private onRunMinecraftCommand(command: string): void { - if (this._clientProtocolVersion < ProtocolVersion.SupportProfilerCaptures) { - this.sendDebuggeeMessage({ - type: OutgoingEventType.MinecraftCommand, - command: command, - dimension_type: 'overworld', - }); - } else { - this.sendDebuggeeMessage({ - type: OutgoingEventType.MinecraftCommand, - command: { - command: command, - dimension_type: 'overworld', - }, - }); - } + this.sendDebuggeeMessage({ + type: OutgoingEventType.MinecraftCommand, + command: command, + dimension_type: 'overworld', + }); } private onStartProfiler(): void { this.sendDebuggeeMessage({ type: OutgoingEventType.StartProfiler, - profiler: { - target_module_uuid: this._targetModuleUuid, - }, + target_module_uuid: this._targetModuleUuid, }); } private onStopProfiler(capturesBasePath: string): void { this.sendDebuggeeMessage({ type: OutgoingEventType.StopProfiler, - profiler: { - captures_path: capturesBasePath, - target_module_uuid: this._targetModuleUuid, - }, + captures_path: capturesBasePath, + target_module_uuid: this._targetModuleUuid, }); } @@ -895,11 +881,9 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { private makeRequestPayload(requestSeq: number, responseCommand: string, args: unknown): RequestMessage { const envelope: RequestMessage = { type: OutgoingEventType.Request, - request: { - request_seq: requestSeq, - command: responseCommand, - args, - }, + request_seq: requestSeq, + command: responseCommand, + args, }; return envelope; } From 7c8efc86ec148ac1ad920982f173e37609a2308d Mon Sep 17 00:00:00 2001 From: Alex Denford SBL Date: Fri, 29 May 2026 17:32:33 -0700 Subject: [PATCH 15/25] legacy support --- src/protocol-events.ts | 39 +++++++++++- src/requests/request-manager.test.ts | 10 +-- src/requests/request-manager.ts | 28 ++++++--- src/session.ts | 92 +++++++++++++++++++++------- 4 files changed, 129 insertions(+), 40 deletions(-) diff --git a/src/protocol-events.ts b/src/protocol-events.ts index d5db206e..8ca3fefa 100644 --- a/src/protocol-events.ts +++ b/src/protocol-events.ts @@ -132,21 +132,30 @@ export interface ProtocolResponse { export interface MinecraftCommandLegacyMessage { type: OutgoingEventType.MinecraftCommand; - command: string; - dimension_type: string; + command: { command: string; dimension_type: string }; } export interface MinecraftCommandMessage { type: OutgoingEventType.MinecraftCommand; - command: string; + command: string; dimension_type: string; } +export interface StartProfilerLegacyMessage { + type: OutgoingEventType.StartProfiler; + profiler: { target_module_uuid?: string }; +} + export interface StartProfilerMessage { type: OutgoingEventType.StartProfiler; target_module_uuid?: string; } +export interface StopProfilerLegacyMessage { + type: OutgoingEventType.StopProfiler; + profiler: { captures_path: string; target_module_uuid?: string }; +} + export interface StopProfilerMessage { type: OutgoingEventType.StopProfiler; captures_path: string; @@ -162,6 +171,11 @@ export interface ResumeMessage { type: OutgoingEventType.Resume; } +export interface RequestLegacyMessage { + type: OutgoingEventType.Request; + request: { request_seq: number; command: string; args: unknown }; +} + export interface RequestMessage { type: OutgoingEventType.Request; request_seq: number; @@ -169,12 +183,26 @@ export interface RequestMessage { args: unknown; } +export interface BreakpointsLegacyMessage { + type: OutgoingEventType.Breakpoints; + breakpoints: { path: string; breakpoints: DebugProtocol.SourceBreakpoint[] | undefined }; +} + export interface BreakpointsMessage { type: OutgoingEventType.Breakpoints; path: string; breakpoints: DebugProtocol.SourceBreakpoint[] | undefined; } +export interface DebuggerRequestLegacyEnvelope { + type: OutgoingEventType.DebuggerRequest; + request: { + request_seq: number; + request: string; + args?: unknown; + }; +} + export interface DebuggerRequestEnvelope { type: OutgoingEventType.DebuggerRequest; request_seq: number; @@ -186,12 +214,17 @@ export type OutgoingDebuggeeMessage = | ProtocolResponse | MinecraftCommandLegacyMessage | MinecraftCommandMessage + | StartProfilerLegacyMessage | StartProfilerMessage + | StopProfilerLegacyMessage | StopProfilerMessage | StopOnExceptionMessage | ResumeMessage + | RequestLegacyMessage | RequestMessage + | BreakpointsLegacyMessage | BreakpointsMessage + | DebuggerRequestLegacyEnvelope | DebuggerRequestEnvelope; diff --git a/src/requests/request-manager.test.ts b/src/requests/request-manager.test.ts index fcc81fd3..5a6736c2 100644 --- a/src/requests/request-manager.test.ts +++ b/src/requests/request-manager.test.ts @@ -22,7 +22,7 @@ describe('RequestManager', () => { manager = new RequestManager(mockSender); const response = { request_seq: 42 } as DebugProtocol.Response; - const promise = manager.sendDebuggerRequest(response, { + const promise = manager.sendDebuggerRequest(0, response, { request: 'test-debugger-request', args: { foo: 'bar' }, }); @@ -57,7 +57,7 @@ describe('RequestManager', () => { manager = new RequestManager(mockSender); const response = { request_seq: 123 } as DebugProtocol.Response; - const promise = manager.sendDebuggerRequest(response, { + const promise = manager.sendDebuggerRequest(0, response, { request: 'test-debugger-request', }); const rejection = expect(promise).rejects.toThrow( @@ -74,7 +74,7 @@ describe('RequestManager', () => { it('should reject request on failed response', async () => { manager = new RequestManager(mockSender); const response = { request_seq: 7 } as DebugProtocol.Response; - const promise = manager.sendDebuggerRequest(response, { + const promise = manager.sendDebuggerRequest(0, response, { request: 'test-debugger-request', }); @@ -107,8 +107,8 @@ describe('RequestManager', () => { const responseA = { request_seq: 1 } as DebugProtocol.Response; const responseB = { request_seq: 2 } as DebugProtocol.Response; - const promiseA = manager.sendDebuggerRequest(responseA, { request: 'A' }); - const promiseB = manager.sendDebuggerRequest(responseB, { request: 'B' }); + const promiseA = manager.sendDebuggerRequest(0, responseA, { request: 'A' }); + const promiseB = manager.sendDebuggerRequest(0, responseB, { request: 'B' }); manager.rejectPendingRequests('Disconnected'); diff --git a/src/requests/request-manager.ts b/src/requests/request-manager.ts index 9716a37f..6482fa78 100644 --- a/src/requests/request-manager.ts +++ b/src/requests/request-manager.ts @@ -3,7 +3,7 @@ import { DebugProtocol } from '@vscode/debugprotocol'; import { DebuggerRequestArguments} from './debugger-request-schema'; import { IDebuggeeMessageSender } from '../debuggee-message-sender'; -import { DebuggeeResponseEnvelope, DebuggerRequestEnvelope, OutgoingEventType } from '../protocol-events'; +import { DebuggeeResponseEnvelope, DebuggerRequestEnvelope, OutgoingEventType, ProtocolVersion } from '../protocol-events'; interface PendingDebuggerRequest { resolve: (value: DebuggeeResponseEnvelope) => void; @@ -21,6 +21,7 @@ export class RequestManager { } public sendDebuggerRequest( + clientProtocolVersion: number, response: DebugProtocol.Response, debuggerRequestArgs: DebuggerRequestArguments, timeoutMs: number = this._defaultDebuggerRequestTimeoutMs, @@ -46,14 +47,23 @@ export class RequestManager { }); // Create an envelope to hold the request, and send it to the debuggee - const envelope: DebuggerRequestEnvelope = { - type: OutgoingEventType.DebuggerRequest, - request_seq: seq, - request, - args, - }; - - this._sender.sendDebuggeeMessage(envelope); + if (clientProtocolVersion >= ProtocolVersion.SupportCerealSerialization) { + this._sender.sendDebuggeeMessage({ + type: OutgoingEventType.DebuggerRequest, + request_seq: seq, + request, + args, + }); + } else { + this._sender.sendDebuggeeMessage({ + type: OutgoingEventType.DebuggerRequest, + request: { + request_seq: seq, + request, + args, + }, + }); + } }); } diff --git a/src/session.ts b/src/session.ts index faffe178..43fd4048 100644 --- a/src/session.ts +++ b/src/session.ts @@ -57,7 +57,8 @@ import { RequestMessage, StoppedEventMessage, ThreadEventMessage, - DebuggeeResponseEnvelope + DebuggeeResponseEnvelope, + RequestLegacyMessage } from './protocol-events'; import { SourceMaps } from './source-maps'; import { StatMessageModel, StatsProvider } from './stats/stats-provider'; @@ -220,26 +221,57 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { // ------------------------------------------------------------------------ private onRunMinecraftCommand(command: string): void { - this.sendDebuggeeMessage({ - type: OutgoingEventType.MinecraftCommand, - command: command, - dimension_type: 'overworld', - }); + if (this._clientProtocolVersion < ProtocolVersion.SupportProfilerCaptures || + this._clientProtocolVersion >= ProtocolVersion.SupportCerealSerialization) { + this.sendDebuggeeMessage({ + type: OutgoingEventType.MinecraftCommand, + command: command, + dimension_type: 'overworld', + }); + } else { + this.sendDebuggeeMessage({ + type: OutgoingEventType.MinecraftCommand, + command: { + command: command, + dimension_type: 'overworld', + }, + }); + } } private onStartProfiler(): void { - this.sendDebuggeeMessage({ - type: OutgoingEventType.StartProfiler, - target_module_uuid: this._targetModuleUuid, - }); + if (this._clientProtocolVersion >= ProtocolVersion.SupportCerealSerialization) { + this.sendDebuggeeMessage({ + type: OutgoingEventType.StartProfiler, + target_module_uuid: this._targetModuleUuid, + }); + } + else { + this.sendDebuggeeMessage({ + type: OutgoingEventType.StartProfiler, + profiler: { + target_module_uuid: this._targetModuleUuid, + }, + }); + } } private onStopProfiler(capturesBasePath: string): void { - this.sendDebuggeeMessage({ - type: OutgoingEventType.StopProfiler, - captures_path: capturesBasePath, - target_module_uuid: this._targetModuleUuid, - }); + if (this._clientProtocolVersion >= ProtocolVersion.SupportCerealSerialization) { + this.sendDebuggeeMessage({ + type: OutgoingEventType.StopProfiler, + captures_path: capturesBasePath, + target_module_uuid: this._targetModuleUuid, + }); + } else { + this.sendDebuggeeMessage({ + type: OutgoingEventType.StopProfiler, + profiler: { + captures_path: capturesBasePath, + target_module_uuid: this._targetModuleUuid, + }, + }); + } } private writeProfilerCaptureToFile( @@ -659,6 +691,7 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { try { const result = await this._requestManager?.sendDebuggerRequest( + this._clientProtocolVersion, response, args as DebuggerRequestArguments, ); @@ -878,14 +911,27 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { this.sendDebuggeeMessage(this.makeRequestPayload(requestSeq, response.command, args)); } - private makeRequestPayload(requestSeq: number, responseCommand: string, args: unknown): RequestMessage { - const envelope: RequestMessage = { - type: OutgoingEventType.Request, - request_seq: requestSeq, - command: responseCommand, - args, - }; - return envelope; + private makeRequestPayload(requestSeq: number, responseCommand: string, args: unknown): RequestMessage | RequestLegacyMessage { + if (this._clientProtocolVersion >= ProtocolVersion.SupportCerealSerialization) { + const envelope: RequestMessage = { + type: OutgoingEventType.Request, + request_seq: requestSeq, + command: responseCommand, + args, + }; + return envelope; + } + else { + const envelope: RequestLegacyMessage = { + type: OutgoingEventType.Request, + request: { + request_seq: requestSeq, + command: responseCommand, + args, + }, + }; + return envelope; + } } public sendDebuggeeMessage(envelope: OutgoingDebuggeeMessage): void { From 5a2baed8a5b41e6b6bbc429e9c69cbd6908bf972 Mon Sep 17 00:00:00 2001 From: Alex Denford SBL Date: Fri, 29 May 2026 17:42:10 -0700 Subject: [PATCH 16/25] Update request-manager.ts --- src/requests/request-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/requests/request-manager.ts b/src/requests/request-manager.ts index 6482fa78..9d72ddc6 100644 --- a/src/requests/request-manager.ts +++ b/src/requests/request-manager.ts @@ -3,7 +3,7 @@ import { DebugProtocol } from '@vscode/debugprotocol'; import { DebuggerRequestArguments} from './debugger-request-schema'; import { IDebuggeeMessageSender } from '../debuggee-message-sender'; -import { DebuggeeResponseEnvelope, DebuggerRequestEnvelope, OutgoingEventType, ProtocolVersion } from '../protocol-events'; +import { DebuggeeResponseEnvelope, OutgoingEventType, ProtocolVersion } from '../protocol-events'; interface PendingDebuggerRequest { resolve: (value: DebuggeeResponseEnvelope) => void; From 1b70c43663be0025af6670160f4d0d162523dbd5 Mon Sep 17 00:00:00 2001 From: Alex Denford SBL Date: Wed, 3 Jun 2026 16:01:40 -0700 Subject: [PATCH 17/25] diagnostics --- src/stats/stats-provider.ts | 29 +++++++++++++++++++ webview-ui/src/diagnostics_panel/App.tsx | 16 ++++++++++ .../diagnostics_panel/DiagnosticsSchema.ts | 29 +++++++++++++++++++ webview-ui/src/diagnostics_panel/index.tsx | 2 +- .../diagnostics_panel/prefabs/TabPrefab.ts | 2 -- 5 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 webview-ui/src/diagnostics_panel/DiagnosticsSchema.ts diff --git a/src/stats/stats-provider.ts b/src/stats/stats-provider.ts index 48c53f80..0b8c664d 100644 --- a/src/stats/stats-provider.ts +++ b/src/stats/stats-provider.ts @@ -26,12 +26,35 @@ export interface StatMessageModel { stats: StatDataModel[]; } +// Mirrors ScriptDiagnosticsDescriptor from C++. Sent once on connect via SchemaEvent. +export interface DiagnosticsTabDescriptor { + name: string; + stat_group_id: string; + data_source: 'server' | 'client' | 'server_script'; + display_type: 'line_chart' | 'stacked_line_chart' | 'stacked_bar_chart' | 'table' | 'multi_column_table' | 'dynamic_properties_table'; + title?: string; + y_label?: string; + tick_range?: number; + value_scalar?: number; + target_value?: number; + key_label?: string; + value_labels?: string[]; + statistic_id?: string; + statistic_ids?: string[]; +} + +export interface SchemaMessageModel { + type: 'SchemaEvent'; + descriptors: DiagnosticsTabDescriptor[]; +} + export interface StatsListener { onStatUpdated?: (stat: StatData) => void; onSpeedUpdated?: (speed: number) => void; onPauseUpdated?: (paused: boolean) => void; onStopped?: () => void; onNotification?: (message: string) => void; + onSchemaReceived?: (schema: DiagnosticsTabDescriptor[]) => void; } export class StatsProvider { @@ -47,6 +70,12 @@ export class StatsProvider { } } + public setSchema(schema: DiagnosticsTabDescriptor[]): void { + this._statListeners.forEach((listener: StatsListener) => { + listener.onSchemaReceived?.(schema); + }); + } + public start(): void { throw new Error('Method not implemented.'); } diff --git a/webview-ui/src/diagnostics_panel/App.tsx b/webview-ui/src/diagnostics_panel/App.tsx index 548125c7..2cb5e032 100644 --- a/webview-ui/src/diagnostics_panel/App.tsx +++ b/webview-ui/src/diagnostics_panel/App.tsx @@ -9,6 +9,7 @@ import tabPrefabs from './prefabs'; import { TabPrefab, TabPrefabDataSource, TabPrefabParams } from './prefabs/TabPrefab'; import { handleDebuggerRequestResult } from './utilities/useDebuggerRequests'; import { vscode } from './utilities/vscode'; +import { DiagnosticsTabDescriptor } from './DiagnosticsSchema'; // Wraps each tab's content() as a proper React component so that any hooks // inside the content function are correctly isolated and not called conditionally @@ -58,6 +59,10 @@ function App() { const [currentTab, setCurrentTab] = useState('tab-0'); const [paused, setPaused] = useState(true); const [speed, setSpeed] = useState(''); + // Dynamic schema received from the game on connect. Empty = use static prefab fallback. + const [schema, setSchema] = useState([]); + // Index of the selected tab in the dynamic dropdown + const [selectedSchemaIndex, setSelectedSchemaIndex] = useState(0); const handlePluginSelection = useCallback((pluginSelectionId: string) => { setSelectedPlugin(() => pluginSelectionId); @@ -67,6 +72,11 @@ function App() { setSelectedClient(() => clientSelectionId); }, []); + const handleSchemaTabChange = useCallback((e: Event | React.FormEvent): void => { + const target = e.target as HTMLSelectElement; + setSelectedSchemaIndex(target.selectedIndex); + }, []); + useEffect(() => { const handleMessage = (event: MessageEvent) => { const message = event.data; @@ -76,6 +86,9 @@ function App() { setPaused(message.paused); } else if (message.type === 'debugger-request-result') { handleDebuggerRequestResult(message); + } else if (message.type === 'diagnostics-schema') { + setSchema(message.schema as DiagnosticsTabDescriptor[]); + setSelectedSchemaIndex(0); } }; window.addEventListener('message', handleMessage); @@ -84,6 +97,9 @@ function App() { }; }, [handleDebuggerRequestResult]); + const usingDynamicSchema = schema.length > 0; + const activeDescriptor = usingDynamicSchema ? schema[selectedSchemaIndex] : undefined; + return (
{window.initialParams.showReplayControls && ( diff --git a/webview-ui/src/diagnostics_panel/DiagnosticsSchema.ts b/webview-ui/src/diagnostics_panel/DiagnosticsSchema.ts new file mode 100644 index 00000000..45811aa7 --- /dev/null +++ b/webview-ui/src/diagnostics_panel/DiagnosticsSchema.ts @@ -0,0 +1,29 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. +// TypeScript mirror of ScriptDiagnosticsDescriptor (C++) and its Cereal based wire format. + +export type DiagnosticsDataSource = 'server' | 'client' | 'server_script'; + +export type DiagnosticsDisplayType = + | 'line_chart' + | 'stacked_line_chart' + | 'stacked_bar_chart' + | 'table' + | 'multi_column_table' + | 'dynamic_properties_table'; + +// One-to-one with ScriptDiagnosticsDescriptor fields (snake_case matches cereal wire names). +export interface DiagnosticsTabDescriptor { + name: string; + stat_group_id: string; + data_source: DiagnosticsDataSource; + display_type: DiagnosticsDisplayType; + title?: string; + y_label?: string; + tick_range?: number; + value_scalar?: number; + target_value?: number; + key_label?: string; + value_labels?: string[]; + statistic_id?: string; + statistic_ids?: string[]; +} diff --git a/webview-ui/src/diagnostics_panel/index.tsx b/webview-ui/src/diagnostics_panel/index.tsx index 9551ae90..9b52539a 100644 --- a/webview-ui/src/diagnostics_panel/index.tsx +++ b/webview-ui/src/diagnostics_panel/index.tsx @@ -5,7 +5,7 @@ import { createRoot } from 'react-dom/client'; import App from './App'; const container = document.getElementById('root'); -const root = createRoot(container!); // createRoot(container!) if you use TypeScript +const root = createRoot(container!); root.render( diff --git a/webview-ui/src/diagnostics_panel/prefabs/TabPrefab.ts b/webview-ui/src/diagnostics_panel/prefabs/TabPrefab.ts index 298d1356..e8cd3c3a 100644 --- a/webview-ui/src/diagnostics_panel/prefabs/TabPrefab.ts +++ b/webview-ui/src/diagnostics_panel/prefabs/TabPrefab.ts @@ -1,5 +1,3 @@ -import { StatisticPrefab } from './StatisticPrefab'; - export type TabPrefabParams = { selectedClient: string; selectedPlugin: string; From 0e36a1b0ce0dc2f78aa549d232dd76692eded742 Mon Sep 17 00:00:00 2001 From: Alex Denford SBL Date: Mon, 15 Jun 2026 16:22:47 -0700 Subject: [PATCH 18/25] iteration --- src/panels/minecraft-diagnostics.ts | 9 +- src/protocol-events.ts | 17 +- src/session.ts | 8 +- src/stats/stats-provider.ts | 27 ++- webview-ui/src/diagnostics_panel/App.tsx | 102 +++++++---- .../src/diagnostics_panel/DynamicTab.tsx | 166 ++++++++++++++++++ 6 files changed, 283 insertions(+), 46 deletions(-) create mode 100644 webview-ui/src/diagnostics_panel/DynamicTab.tsx diff --git a/src/panels/minecraft-diagnostics.ts b/src/panels/minecraft-diagnostics.ts index 07667059..6fd43f7f 100644 --- a/src/panels/minecraft-diagnostics.ts +++ b/src/panels/minecraft-diagnostics.ts @@ -5,7 +5,7 @@ import { EventEmitter } from 'stream'; import { getUri } from '../utilities/getUri'; import { getNonce } from '../utilities/getNonce'; import { DebuggerRequestHandler } from '../requests/debugger-request-handler'; -import { StatData, StatsListener, StatsProvider } from '../stats/stats-provider'; +import { DiagnosticsTabDescriptor, StatData, StatsListener, StatsProvider } from '../stats/stats-provider'; export class MinecraftDiagnosticsPanel { private static activeDiagnosticsPanels: MinecraftDiagnosticsPanel[] = []; @@ -116,6 +116,13 @@ export class MinecraftDiagnosticsPanel { onNotification: (message: string) => { window.showInformationMessage(message); }, + onSchemaReceived: (schema: DiagnosticsTabDescriptor[]) => { + const message = { + type: 'diagnostics-schema', + schema: schema, + }; + this._panel.webview.postMessage(message); + } }; this._statsTracker.addStatListener(this._statsCallback); diff --git a/src/protocol-events.ts b/src/protocol-events.ts index 8ca3fefa..8a1d1133 100644 --- a/src/protocol-events.ts +++ b/src/protocol-events.ts @@ -2,7 +2,7 @@ import { LogLevel } from '@vscode/debugadapter/lib/logger'; import { DebugProtocol } from '@vscode/debugprotocol'; -import { StatMessageModel } from './stats/stats-provider'; +import { DiagnosticsTabDescriptor, StatMessageModel } from './stats/stats-provider'; // protocol version history // 1 - initial version @@ -13,6 +13,7 @@ import { StatMessageModel } from './stats/stats-provider'; // 6 - breakpoints as request, MC can reject // 7 - support for debugger requests, MC can reject or respond with args // 8 - New serialization tech (use Cereal) +// 9 - Added support for MC C++/native driven stat descriptors/schemas for UI display export enum ProtocolVersion { _Unknown = 0, @@ -24,9 +25,10 @@ export enum ProtocolVersion { SupportBreakpointsAsRequest = 6, SupportDebuggerRequests = 7, SupportCerealSerialization = 8, + SupportNativeDescriptors = 9, } -export const DEBUGGER_PROTOCOL_VERSION = ProtocolVersion.SupportCerealSerialization; +export const DEBUGGER_PROTOCOL_VERSION = ProtocolVersion.SupportNativeDescriptors; // ------------------------------------------------------------------------- // Interfaces for event message payloads (received from the debugee) @@ -38,9 +40,9 @@ export enum IncomingEventType { Notification = 'NotificationEvent', Protocol = 'ProtocolEvent', Stat2 = 'StatEvent2', - Schema = 'SchemaEvent', ProfilerCapture = 'ProfilerCapture', DebuggeeResponse = 'debuggee-response', + DiagnosticsDescriptor = 'SchemaEvent', } export interface PluginDetails { @@ -97,6 +99,11 @@ export type StatEventMessage = StatMessageModel & { type: IncomingEventType.Stat2; }; +export interface DiagnosticsDescriptorMessage { + type: IncomingEventType.DiagnosticsDescriptor; + descriptors: DiagnosticsTabDescriptor[]; +} + export type IncomingDebuggeeMessage = | ProtocolCapabilities | ProfilerCapture @@ -105,7 +112,8 @@ export type IncomingDebuggeeMessage = | PrintEventMessage | NotificationEventMessage | StatEventMessage - | DebuggeeResponseEnvelope; + | DebuggeeResponseEnvelope + | DiagnosticsDescriptorMessage; // ------------------------------------------------------------------------- @@ -252,6 +260,7 @@ export class DebuggeeEventRegistry { handler(eventMessage); return true; } + console.warn(`No handler found for incoming event type: ${eventMessage.type}`); return false; } } diff --git a/src/session.ts b/src/session.ts index dcc56834..096a5a71 100644 --- a/src/session.ts +++ b/src/session.ts @@ -58,7 +58,8 @@ import { StoppedEventMessage, ThreadEventMessage, DebuggeeResponseEnvelope, - RequestLegacyMessage + RequestLegacyMessage, + DiagnosticsDescriptorMessage, } from './protocol-events'; import { SourceMaps } from './source-maps'; import { StatMessageModel, StatsProvider } from './stats/stats-provider'; @@ -202,6 +203,9 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { this._eventRegistry.register(IncomingEventType.ProfilerCapture, (msg: ProfilerCapture) => { this.handleProfilerCapture(msg); }); + this._eventRegistry.register(IncomingEventType.DiagnosticsDescriptor, (msg: DiagnosticsDescriptorMessage) => { + this._statsProvider.setSchema(msg.descriptors); + }); } public dispose(): void { @@ -957,7 +961,7 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { const newline = Buffer.from('\n'); const buffer = Buffer.concat([lengthBuffer, jsonBuffer, newline]); - this._connectionSocket.write(buffer); + this._connectionSocket.write(buffer);55 } private receiveDebugeeMessage(envelope: any) { diff --git a/src/stats/stats-provider.ts b/src/stats/stats-provider.ts index 0b8c664d..613f862d 100644 --- a/src/stats/stats-provider.ts +++ b/src/stats/stats-provider.ts @@ -26,12 +26,22 @@ export interface StatMessageModel { stats: StatDataModel[]; } +export type DiagnosticsDataSource = 'server' | 'client' | 'server_script'; + +export type DiagnosticsDisplayType = + | 'line_chart' + | 'stacked_line_chart' + | 'stacked_bar_chart' + | 'table' + | 'multi_column_table' + | 'dynamic_properties_table'; + // Mirrors ScriptDiagnosticsDescriptor from C++. Sent once on connect via SchemaEvent. export interface DiagnosticsTabDescriptor { name: string; stat_group_id: string; - data_source: 'server' | 'client' | 'server_script'; - display_type: 'line_chart' | 'stacked_line_chart' | 'stacked_bar_chart' | 'table' | 'multi_column_table' | 'dynamic_properties_table'; + data_source: DiagnosticsDataSource; + display_type: DiagnosticsDisplayType; title?: string; y_label?: string; tick_range?: number; @@ -43,11 +53,6 @@ export interface DiagnosticsTabDescriptor { statistic_ids?: string[]; } -export interface SchemaMessageModel { - type: 'SchemaEvent'; - descriptors: DiagnosticsTabDescriptor[]; -} - export interface StatsListener { onStatUpdated?: (stat: StatData) => void; onSpeedUpdated?: (speed: number) => void; @@ -59,6 +64,7 @@ export interface StatsListener { export class StatsProvider { protected _statListeners: StatsListener[]; + private _cachedSchema: DiagnosticsTabDescriptor[] | undefined; constructor(public readonly name: string, public readonly uniqueId: string) { this._statListeners = []; @@ -71,6 +77,7 @@ export class StatsProvider { } public setSchema(schema: DiagnosticsTabDescriptor[]): void { + this._cachedSchema = schema; this._statListeners.forEach((listener: StatsListener) => { listener.onSchemaReceived?.(schema); }); @@ -103,6 +110,12 @@ export class StatsProvider { public addStatListener(listener: StatsListener): void { this._statListeners.push(listener); + + // If we have a cached schema from diagnostics previously sent when the page was no active, + // send it to the new listener immediately + if (this._cachedSchema !== undefined) { + listener.onSchemaReceived?.(this._cachedSchema); + } } public removeStatListener(listener: StatsListener): void { diff --git a/webview-ui/src/diagnostics_panel/App.tsx b/webview-ui/src/diagnostics_panel/App.tsx index 2cb5e032..7dacefb4 100644 --- a/webview-ui/src/diagnostics_panel/App.tsx +++ b/webview-ui/src/diagnostics_panel/App.tsx @@ -10,6 +10,7 @@ import { TabPrefab, TabPrefabDataSource, TabPrefabParams } from './prefabs/TabPr import { handleDebuggerRequestResult } from './utilities/useDebuggerRequests'; import { vscode } from './utilities/vscode'; import { DiagnosticsTabDescriptor } from './DiagnosticsSchema'; +import DynamicTab from './DynamicTab'; // Wraps each tab's content() as a proper React component so that any hooks // inside the content function are correctly isolated and not called conditionally @@ -18,6 +19,11 @@ function TabView({ tabPrefab, params }: { tabPrefab: TabPrefab; params: TabPrefa return <>{tabPrefab.content(params)}; } +// A tab entry is either a hardcoded prefab or a dynamic descriptor received from the game. +type MergedTab = + | { kind: 'prefab'; name: string; tab: TabPrefab } + | { kind: 'dynamic'; name: string; descriptor: DiagnosticsTabDescriptor }; + declare global { interface Window { initialParams: any; @@ -59,10 +65,8 @@ function App() { const [currentTab, setCurrentTab] = useState('tab-0'); const [paused, setPaused] = useState(true); const [speed, setSpeed] = useState(''); - // Dynamic schema received from the game on connect. Empty = use static prefab fallback. + // Dynamic schema received from the game on connect. Merged into the prefab tab list. const [schema, setSchema] = useState([]); - // Index of the selected tab in the dynamic dropdown - const [selectedSchemaIndex, setSelectedSchemaIndex] = useState(0); const handlePluginSelection = useCallback((pluginSelectionId: string) => { setSelectedPlugin(() => pluginSelectionId); @@ -72,11 +76,6 @@ function App() { setSelectedClient(() => clientSelectionId); }, []); - const handleSchemaTabChange = useCallback((e: Event | React.FormEvent): void => { - const target = e.target as HTMLSelectElement; - setSelectedSchemaIndex(target.selectedIndex); - }, []); - useEffect(() => { const handleMessage = (event: MessageEvent) => { const message = event.data; @@ -88,7 +87,6 @@ function App() { handleDebuggerRequestResult(message); } else if (message.type === 'diagnostics-schema') { setSchema(message.schema as DiagnosticsTabDescriptor[]); - setSelectedSchemaIndex(0); } }; window.addEventListener('message', handleMessage); @@ -97,8 +95,21 @@ function App() { }; }, [handleDebuggerRequestResult]); - const usingDynamicSchema = schema.length > 0; - const activeDescriptor = usingDynamicSchema ? schema[selectedSchemaIndex] : undefined; + // Merge schema tabs into the prefab list. Schema tabs whose name matches a prefab replace it; + // new names are appended. Falls back to all prefabs when no schema has arrived yet. + const mergedTabs: MergedTab[] = sortedTabPrefabs.map(tab => ({ + kind: 'prefab' as const, + name: tab.name, + tab, + })); + for (const descriptor of schema) { + const existingIndex = mergedTabs.findIndex(t => t.name === descriptor.name); + if (existingIndex !== -1) { + mergedTabs[existingIndex] = { kind: 'dynamic' as const, name: descriptor.name, descriptor }; + } else { + mergedTabs.push({ kind: 'dynamic' as const, name: descriptor.name, descriptor }); + } + } return (
@@ -116,18 +127,18 @@ function App() { )}
- {sortedTabPrefabs.map((tabPrefab, index) => ( + {mergedTabs.map((tab, index) => ( ))}
- {sortedTabPrefabs.map((tabPrefab, index) => ( + {mergedTabs.map((tab, index) => (
- {tabPrefab.dataSource === TabPrefabDataSource.Client ? ( - - ) : ( -
- )} - {tabPrefab.dataSource === TabPrefabDataSource.ServerScript ? ( - + {tab.kind === 'prefab' ? ( + <> + {tab.tab.dataSource === TabPrefabDataSource.Client ? ( + + ) : ( +
+ )} + {tab.tab.dataSource === TabPrefabDataSource.ServerScript ? ( + + ) : ( +
+ )} + + ) : ( -
+ <> + {tab.descriptor.data_source === 'client' && ( + + )} + {tab.descriptor.data_source === 'server_script' && ( + + )} + + )} -
))}
diff --git a/webview-ui/src/diagnostics_panel/DynamicTab.tsx b/webview-ui/src/diagnostics_panel/DynamicTab.tsx new file mode 100644 index 00000000..3f3bfec2 --- /dev/null +++ b/webview-ui/src/diagnostics_panel/DynamicTab.tsx @@ -0,0 +1,166 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. + +// Generic tab renderer driven by a DiagnosticsTabDescriptor from the C++ schema registry. +// Derives the appropriate StatisticProvider and StatisticResolver from the descriptor fields +// and renders the matching control — no new tab files needed for new diagnostics. + +import { useMemo } from 'react'; +import { DiagnosticsTabDescriptor } from './DiagnosticsSchema'; +import { + SimpleStatisticProvider, + MultipleStatisticProvider, + StatisticProvider, +} from './StatisticProvider'; +import { StatisticType, YAxisType, createStatResolver, StatisticResolver } from './StatisticResolver'; +import MinecraftStatisticLineChart from './controls/MinecraftStatisticLineChart'; +import MinecraftStatisticStackedLineChart from './controls/MinecraftStatisticStackedLineChart'; +import MinecraftStatisticStackedBarChart from './controls/MinecraftStatisticStackedBarChart'; +import MinecraftStatisticTable from './controls/MinecraftStatisticTable'; +import MinecraftMultiColumnStatisticTable from './controls/MinecraftMultiColumnStatisticTable'; +import { MinecraftDynamicPropertiesTable } from './controls/MinecraftDynamicPropertiesTable'; + +type DynamicTabProps = { + descriptor: DiagnosticsTabDescriptor; + selectedClient: string; + selectedPlugin: string; +}; + +// Build a StatisticProvider from descriptor fields. +// Client-source tabs use a regex to match the per-player stat group prefix. +function buildProvider( + descriptor: DiagnosticsTabDescriptor, + selectedClient: string, + _selectedPlugin: string +): StatisticProvider { + const { data_source, display_type, stat_group_id, statistic_id, statistic_ids } = descriptor; + + // DynamicPropertiesTable has its own fixed provider shape + if (display_type === 'dynamic_properties_table') { + return new SimpleStatisticProvider({ + statisticId: 'consolidated_data', + statisticParentId: new RegExp(`${stat_group_id}.*`), + }); + } + + // LineChart uses SimpleStatisticProvider (single stat series) + if (display_type === 'line_chart') { + const effectiveStatId = statistic_id ?? stat_group_id; + const parentId: string | RegExp = + data_source === 'client' + ? new RegExp(`.*${selectedClient}_${stat_group_id}`) + : stat_group_id; + return new SimpleStatisticProvider({ + statisticId: effectiveStatId, + statisticParentId: parentId, + }); + } + + // All other chart/table types use MultipleStatisticProvider + const parentId: string | RegExp = + data_source === 'client' + ? new RegExp(`.*${selectedClient}_${stat_group_id}`) + : stat_group_id; + + return new MultipleStatisticProvider({ + statisticParentId: parentId, + statisticIds: statistic_ids, + }); +} + +function buildResolver(descriptor: DiagnosticsTabDescriptor): StatisticResolver { + return createStatResolver({ + type: StatisticType.Absolute, + yAxisType: YAxisType.Absolute, + tickRange: descriptor.tick_range ?? 20 * 10, + valueScalar: descriptor.value_scalar, + }); +} + +export default function DynamicTab({ descriptor, selectedClient, selectedPlugin }: DynamicTabProps) { + const title = descriptor.title ?? descriptor.name; + const yLabel = descriptor.y_label ?? ''; + const tickRange = descriptor.tick_range ?? 20 * 10; + + // Memoize provider and resolver so controls' useEffect deps remain stable across renders. + const provider = useMemo( + () => buildProvider(descriptor, selectedClient, selectedPlugin), + // re-create only when the identity of the tab or the selected client/plugin changes + [descriptor.stat_group_id, descriptor.data_source, descriptor.display_type, + descriptor.statistic_id, descriptor.statistic_ids?.join(','), selectedClient, selectedPlugin] + ); + + const resolver = useMemo( + () => buildResolver(descriptor), + [descriptor.tick_range, descriptor.value_scalar] + ); + + switch (descriptor.display_type) { + case 'line_chart': + return ( + + ); + + case 'stacked_line_chart': + return ( + + ); + + case 'stacked_bar_chart': + return ( + + ); + + case 'table': + return ( + + ); + + case 'multi_column_table': + return ( + + ); + + case 'dynamic_properties_table': + return ( + + ); + + default: + return
Unsupported display type: {descriptor.display_type}
; + } +} \ No newline at end of file From 3409c83e1fa135d0c39a6fc3d9f44a32b381a26a Mon Sep 17 00:00:00 2001 From: Alex Denford SBL Date: Tue, 16 Jun 2026 13:22:48 -0700 Subject: [PATCH 19/25] remove the now c++ driven schema, sort native ones --- webview-ui/src/diagnostics_panel/App.tsx | 1 + .../src/diagnostics_panel/prefabs/index.tsx | 4 -- .../prefabs/tabs/ClientMemory.tsx | 55 ---------------- .../prefabs/tabs/ClientTiming.tsx | 64 ------------------- 4 files changed, 1 insertion(+), 123 deletions(-) delete mode 100644 webview-ui/src/diagnostics_panel/prefabs/tabs/ClientMemory.tsx delete mode 100644 webview-ui/src/diagnostics_panel/prefabs/tabs/ClientTiming.tsx diff --git a/webview-ui/src/diagnostics_panel/App.tsx b/webview-ui/src/diagnostics_panel/App.tsx index 7dacefb4..5c546cc9 100644 --- a/webview-ui/src/diagnostics_panel/App.tsx +++ b/webview-ui/src/diagnostics_panel/App.tsx @@ -110,6 +110,7 @@ function App() { mergedTabs.push({ kind: 'dynamic' as const, name: descriptor.name, descriptor }); } } + mergedTabs.sort((a, b) => a.name.localeCompare(b.name)); return (
diff --git a/webview-ui/src/diagnostics_panel/prefabs/index.tsx b/webview-ui/src/diagnostics_panel/prefabs/index.tsx index 77812102..8905a916 100644 --- a/webview-ui/src/diagnostics_panel/prefabs/index.tsx +++ b/webview-ui/src/diagnostics_panel/prefabs/index.tsx @@ -9,8 +9,6 @@ import dynamicPropertyTab from './tabs/DynamicProperties'; import serverScriptHandleCountsTab from './tabs/ServerScriptHandleCounts'; import serverScriptSubscriberCountsTab from './tabs/ServerScriptSubscriberCounts'; -import clientTimingTab from './tabs/ClientTiming'; -import clientMemoryTab from './tabs/ClientMemory'; import clientEntitySystemTab from './tabs/ClientEntitySystems'; import clientProfilerScopesTab from './tabs/ClientCPUProfiler'; @@ -24,8 +22,6 @@ export default [ serverBandwidthTab, serverScriptHandleCountsTab, serverScriptSubscriberCountsTab, - clientTimingTab, - clientMemoryTab, clientEntitySystemTab, clientProfilerScopesTab, dynamicPropertyTab, diff --git a/webview-ui/src/diagnostics_panel/prefabs/tabs/ClientMemory.tsx b/webview-ui/src/diagnostics_panel/prefabs/tabs/ClientMemory.tsx deleted file mode 100644 index 74e59929..00000000 --- a/webview-ui/src/diagnostics_panel/prefabs/tabs/ClientMemory.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import MinecraftStatisticStackedLineChart from '../../controls/MinecraftStatisticStackedLineChart'; -import { MultipleStatisticProvider } from '../../StatisticProvider'; -import { StatisticType, YAxisType, createStatResolver } from '../../StatisticResolver'; -import { TabPrefab, TabPrefabDataSource, TabPrefabParams } from '../TabPrefab'; -import MinecraftStatisticTable from '../../controls/MinecraftStatisticTable'; - -const statsTab: TabPrefab = { - name: 'Client - Memory', - dataSource: TabPrefabDataSource.Client, - content: ({ selectedClient }: TabPrefabParams) => { - return ( -
-
- {/* Added wrapper div with flex: 1 */} - -
-
- {/* Added wrapper div with flex: 1 */} - -
-
- ); - }, -}; - -export default statsTab; diff --git a/webview-ui/src/diagnostics_panel/prefabs/tabs/ClientTiming.tsx b/webview-ui/src/diagnostics_panel/prefabs/tabs/ClientTiming.tsx deleted file mode 100644 index f0538977..00000000 --- a/webview-ui/src/diagnostics_panel/prefabs/tabs/ClientTiming.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import MinecraftStatisticStackedLineChart from '../../controls/MinecraftStatisticStackedLineChart'; -import { MultipleStatisticProvider, SimpleStatisticProvider } from '../../StatisticProvider'; -import { StatisticType, YAxisType, createStatResolver } from '../../StatisticResolver'; -import { TabPrefab, TabPrefabDataSource, TabPrefabParams } from '../TabPrefab'; -import MinecraftStatisticLineChart from '../../controls/MinecraftStatisticLineChart'; - -const statsTab: TabPrefab = { - name: 'Client - Timings', - dataSource: TabPrefabDataSource.Client, - content: ({ selectedClient }: TabPrefabParams) => { - return ( -
- - -
- ); - }, -}; - -export default statsTab; From 0e61340ed59613f6c9fa153a2471b07163836de9 Mon Sep 17 00:00:00 2001 From: Alex Denford SBL Date: Tue, 16 Jun 2026 16:29:00 -0700 Subject: [PATCH 20/25] Scaling, fixing highlight, launch settings --- .vscode/launch.json | 4 +- .../controls/LineChart/LineChart.tsx | 40 ++++++++++++------- .../prefabs/tabs/ServerTiming.tsx | 2 +- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 06213438..eb724931 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,6 +5,7 @@ { "version": "0.2.0", "configurations": [ + { "name": "Run Extension", "type": "extensionHost", @@ -28,7 +29,8 @@ "outFiles": [ "${workspaceFolder}/out/test/**/*.js" ], - "preLaunchTask": "${defaultBuildTask}" + "preLaunchTask": "${defaultBuildTask}", + "debugWebviews": true } ] } diff --git a/webview-ui/src/diagnostics_panel/controls/LineChart/LineChart.tsx b/webview-ui/src/diagnostics_panel/controls/LineChart/LineChart.tsx index 91586a7a..08b60ff8 100644 --- a/webview-ui/src/diagnostics_panel/controls/LineChart/LineChart.tsx +++ b/webview-ui/src/diagnostics_panel/controls/LineChart/LineChart.tsx @@ -86,13 +86,11 @@ export function LineChart({ x: { //domain: [0, _maxDataPoints - 1], grid: true, - tickFormat: function (d: d3.NumberValue, i: number) { + tickFormat: function (d: d3.NumberValue, _i: number) { const tickDifference = latestTime - d.valueOf(); - if (tickDifference < 20) { return 'now'; } - // Assume 20 ticks per second return Math.floor(tickDifference / 20) + 's'; }, @@ -117,32 +115,40 @@ export function LineChart({ : undefined, Plot.lineY(data, { x: 'time', y: 'value' }), enableFilledChart ? Plot.ruleY([0]) : undefined, - Plot.crosshairX(data, { x: 'time', y: 'value' }), + Plot.tip( + data, + Plot.pointerX({ + x: 'time', + y: 'value', + format: { + x: (d: number) => { + const diff = latestTime - d; + return diff < 20 ? 'now' : `${Math.floor(diff / 20)}s ago`; + }, + y: (d: number) => formatYAxisTick(d), + }, + }) + ), ], }); }; const generateDifferenceChart = (): PlotResult => { - return Plot.differenceY(data, { - x: 'time', - y: 'value', - //positiveFill: "red", - //negativeFill: "blue", - tip: true, - }).plot({ + const xFormat = (d: number) => { + const diff = latestTime - d; + return diff < 20 ? 'now' : `${Math.floor(diff / 20)}s ago`; + }; + return Plot.plot({ className: 'difference-chart', title: title, marginLeft: 50, // Y Axis labels were getting cut off x: { grid: true, - tickFormat: function (d: d3.NumberValue, i: number) { + tickFormat: function (d: d3.NumberValue) { const tickDifference = latestTime - d.valueOf(); - if (tickDifference < 20) { return 'now'; } - - // Assume 20 ticks per second return Math.floor(tickDifference / 20) + 's'; }, label: xLabel, @@ -154,6 +160,10 @@ export function LineChart({ label: yLabel, tickFormat: formatYAxisTick, }, + marks: [ + Plot.differenceY(data, { x: 'time', y: 'value' }), + Plot.tip(data, Plot.pointerX({ x: 'time', y: 'value', format: { x: xFormat, y: (d: number) => formatYAxisTick(d) } })), + ], }); }; diff --git a/webview-ui/src/diagnostics_panel/prefabs/tabs/ServerTiming.tsx b/webview-ui/src/diagnostics_panel/prefabs/tabs/ServerTiming.tsx index 42aa416c..fe76fbf0 100644 --- a/webview-ui/src/diagnostics_panel/prefabs/tabs/ServerTiming.tsx +++ b/webview-ui/src/diagnostics_panel/prefabs/tabs/ServerTiming.tsx @@ -25,7 +25,7 @@ const serverTickTimings: StatisticPrefab = { type: StatisticType.Absolute, tickRange: 20 * 10 /* About 10 seconds */, yAxisType: YAxisType.Absolute, - valueScalar: 1 / 1000, // Microseconds to milliseconds + valueScalar: 1, // milliseconds })} yLabel="Server Tick Time (ms)" targetValue={50} // 50ms is the target for server time, 20hz From 73dfe722a756fa2b97a060398a89b3d6ca228984 Mon Sep 17 00:00:00 2001 From: Alex Denford SBL Date: Tue, 16 Jun 2026 17:12:39 -0700 Subject: [PATCH 21/25] Re-add client memory & timing for backwards compatabilikty --- .../src/diagnostics_panel/prefabs/index.tsx | 4 ++ .../prefabs/tabs/ClientMemory.tsx | 55 ++++++++++++++++ .../prefabs/tabs/ClientTiming.tsx | 64 +++++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 webview-ui/src/diagnostics_panel/prefabs/tabs/ClientMemory.tsx create mode 100644 webview-ui/src/diagnostics_panel/prefabs/tabs/ClientTiming.tsx diff --git a/webview-ui/src/diagnostics_panel/prefabs/index.tsx b/webview-ui/src/diagnostics_panel/prefabs/index.tsx index 8905a916..77812102 100644 --- a/webview-ui/src/diagnostics_panel/prefabs/index.tsx +++ b/webview-ui/src/diagnostics_panel/prefabs/index.tsx @@ -9,6 +9,8 @@ import dynamicPropertyTab from './tabs/DynamicProperties'; import serverScriptHandleCountsTab from './tabs/ServerScriptHandleCounts'; import serverScriptSubscriberCountsTab from './tabs/ServerScriptSubscriberCounts'; +import clientTimingTab from './tabs/ClientTiming'; +import clientMemoryTab from './tabs/ClientMemory'; import clientEntitySystemTab from './tabs/ClientEntitySystems'; import clientProfilerScopesTab from './tabs/ClientCPUProfiler'; @@ -22,6 +24,8 @@ export default [ serverBandwidthTab, serverScriptHandleCountsTab, serverScriptSubscriberCountsTab, + clientTimingTab, + clientMemoryTab, clientEntitySystemTab, clientProfilerScopesTab, dynamicPropertyTab, diff --git a/webview-ui/src/diagnostics_panel/prefabs/tabs/ClientMemory.tsx b/webview-ui/src/diagnostics_panel/prefabs/tabs/ClientMemory.tsx new file mode 100644 index 00000000..74e59929 --- /dev/null +++ b/webview-ui/src/diagnostics_panel/prefabs/tabs/ClientMemory.tsx @@ -0,0 +1,55 @@ +import MinecraftStatisticStackedLineChart from '../../controls/MinecraftStatisticStackedLineChart'; +import { MultipleStatisticProvider } from '../../StatisticProvider'; +import { StatisticType, YAxisType, createStatResolver } from '../../StatisticResolver'; +import { TabPrefab, TabPrefabDataSource, TabPrefabParams } from '../TabPrefab'; +import MinecraftStatisticTable from '../../controls/MinecraftStatisticTable'; + +const statsTab: TabPrefab = { + name: 'Client - Memory', + dataSource: TabPrefabDataSource.Client, + content: ({ selectedClient }: TabPrefabParams) => { + return ( +
+
+ {/* Added wrapper div with flex: 1 */} + +
+
+ {/* Added wrapper div with flex: 1 */} + +
+
+ ); + }, +}; + +export default statsTab; diff --git a/webview-ui/src/diagnostics_panel/prefabs/tabs/ClientTiming.tsx b/webview-ui/src/diagnostics_panel/prefabs/tabs/ClientTiming.tsx new file mode 100644 index 00000000..f0538977 --- /dev/null +++ b/webview-ui/src/diagnostics_panel/prefabs/tabs/ClientTiming.tsx @@ -0,0 +1,64 @@ +import MinecraftStatisticStackedLineChart from '../../controls/MinecraftStatisticStackedLineChart'; +import { MultipleStatisticProvider, SimpleStatisticProvider } from '../../StatisticProvider'; +import { StatisticType, YAxisType, createStatResolver } from '../../StatisticResolver'; +import { TabPrefab, TabPrefabDataSource, TabPrefabParams } from '../TabPrefab'; +import MinecraftStatisticLineChart from '../../controls/MinecraftStatisticLineChart'; + +const statsTab: TabPrefab = { + name: 'Client - Timings', + dataSource: TabPrefabDataSource.Client, + content: ({ selectedClient }: TabPrefabParams) => { + return ( +
+ + +
+ ); + }, +}; + +export default statsTab; From c90e2feedd3e48bbfc0fce06d89f91e07835ef90 Mon Sep 17 00:00:00 2001 From: Alex Denford SBL Date: Wed, 17 Jun 2026 13:52:10 -0700 Subject: [PATCH 22/25] linter --- src/session.ts | 2 +- .../controls/LineChart/LineChart.tsx | 57 ++++++++++++------- .../controls/MinecraftStatisticLineChart.tsx | 2 +- .../prefabs/tabs/ServerMemory.tsx | 16 +++--- .../prefabs/tabs/ServerTiming.tsx | 12 ++-- 5 files changed, 52 insertions(+), 37 deletions(-) diff --git a/src/session.ts b/src/session.ts index 096a5a71..9812c0f2 100644 --- a/src/session.ts +++ b/src/session.ts @@ -961,7 +961,7 @@ export class Session extends DebugSession implements IDebuggeeMessageSender { const newline = Buffer.from('\n'); const buffer = Buffer.concat([lengthBuffer, jsonBuffer, newline]); - this._connectionSocket.write(buffer);55 + this._connectionSocket.write(buffer); } private receiveDebugeeMessage(envelope: any) { diff --git a/webview-ui/src/diagnostics_panel/controls/LineChart/LineChart.tsx b/webview-ui/src/diagnostics_panel/controls/LineChart/LineChart.tsx index 08b60ff8..d945a387 100644 --- a/webview-ui/src/diagnostics_panel/controls/LineChart/LineChart.tsx +++ b/webview-ui/src/diagnostics_panel/controls/LineChart/LineChart.tsx @@ -21,13 +21,27 @@ type PlotResult = ((SVGSVGElement | HTMLElement) & Plot.Plot) | undefined; function formatYAxisTick(d: d3.NumberValue): string { const n = d.valueOf(); - if (n === 0) return '0'; + if (n === 0) { + return '0'; + } const abs = Math.abs(n); - if (abs >= 1000) return `${Math.round(n)}`; - if (abs >= 1) return `${parseFloat(n.toPrecision(4))}`; + if (abs >= 1000) { + return `${Math.round(n)}`; + } + if (abs >= 1) { + return `${parseFloat(n.toPrecision(4))}`; + } return `${parseFloat(n.toPrecision(3))}`; } +function formatRelativeTime(latestTime: number, tick: number): string { + const diff = latestTime - tick; + if (diff < 20) { + return 'now'; + } + return `${Math.floor(diff / 20)}s ago`; +} + //chart component export function LineChart({ title, @@ -36,7 +50,7 @@ export function LineChart({ statisticOptions, statisticDataProvider, yAxisStyle, -}: LineChartProps) { +}: LineChartProps): JSX.Element { // state const [data, setData] = useState([]); @@ -107,26 +121,22 @@ export function LineChart({ // TODO: Clean up with factory? enableFilledChart ? Plot.areaY(data, { - x: 'time', - y: d => Math.max(yDomain[0], d.value), - fillOpacity: 0.3, - y1: yDomain[0], - }) + x: 'time', + y: d => Math.max(yDomain[0], d.value), + fillOpacity: 0.3, + y1: yDomain[0], + }) : undefined, Plot.lineY(data, { x: 'time', y: 'value' }), enableFilledChart ? Plot.ruleY([0]) : undefined, + Plot.crosshairX(data, { x: 'time', y: 'value' }), Plot.tip( data, Plot.pointerX({ x: 'time', y: 'value', - format: { - x: (d: number) => { - const diff = latestTime - d; - return diff < 20 ? 'now' : `${Math.floor(diff / 20)}s ago`; - }, - y: (d: number) => formatYAxisTick(d), - }, + title: (d: TrackedStat) => + `${xLabel}: ${formatRelativeTime(latestTime, d.time)}\n${yLabel}: ${formatYAxisTick(d.value)}`, }) ), ], @@ -134,10 +144,6 @@ export function LineChart({ }; const generateDifferenceChart = (): PlotResult => { - const xFormat = (d: number) => { - const diff = latestTime - d; - return diff < 20 ? 'now' : `${Math.floor(diff / 20)}s ago`; - }; return Plot.plot({ className: 'difference-chart', title: title, @@ -162,7 +168,16 @@ export function LineChart({ }, marks: [ Plot.differenceY(data, { x: 'time', y: 'value' }), - Plot.tip(data, Plot.pointerX({ x: 'time', y: 'value', format: { x: xFormat, y: (d: number) => formatYAxisTick(d) } })), + Plot.crosshairX(data, { x: 'time', y: 'value' }), + Plot.tip( + data, + Plot.pointerX({ + x: 'time', + y: 'value', + title: (d: TrackedStat) => + `${xLabel}: ${formatRelativeTime(latestTime, d.time)}\n${yLabel}: ${formatYAxisTick(d.value)}`, + }) + ), ], }); }; diff --git a/webview-ui/src/diagnostics_panel/controls/MinecraftStatisticLineChart.tsx b/webview-ui/src/diagnostics_panel/controls/MinecraftStatisticLineChart.tsx index e46c22ec..f2407252 100644 --- a/webview-ui/src/diagnostics_panel/controls/MinecraftStatisticLineChart.tsx +++ b/webview-ui/src/diagnostics_panel/controls/MinecraftStatisticLineChart.tsx @@ -22,7 +22,7 @@ export default function MinecraftStatisticLineChart({ xLabel = 'Time', yAxisStyle, statisticDataProvider, -}: MinecraftStatisticLineChartProps) { +}: MinecraftStatisticLineChartProps): JSX.Element { const [_statisticOptions, _setStatisticOptions] = useState(statisticOptions); const yAxisResolverChanged = useCallback((selectedType: YAxisType): void => { diff --git a/webview-ui/src/diagnostics_panel/prefabs/tabs/ServerMemory.tsx b/webview-ui/src/diagnostics_panel/prefabs/tabs/ServerMemory.tsx index 29bca47d..668c13cd 100644 --- a/webview-ui/src/diagnostics_panel/prefabs/tabs/ServerMemory.tsx +++ b/webview-ui/src/diagnostics_panel/prefabs/tabs/ServerMemory.tsx @@ -5,7 +5,7 @@ import { StatisticType, YAxisType } from '../../StatisticResolver'; import { TabPrefab, TabPrefabDataSource } from '../TabPrefab'; import { generateRowsFromStatsPrefabs } from '../utilities'; -const appMemoryUsage: StatisticPrefab = { +const AppMemoryUsage: StatisticPrefab = { name: 'App Memory Usage', reactNode: ( { return generateRowsFromStatsPrefabs([ - [appMemoryUsage, appMemoryFree], - [javaScriptMemoryAllocated, javaScriptMemoryFree], + [AppMemoryUsage, AppMemoryFree], + [JavaScriptMemoryAllocated, JavaScriptMemoryUsed], ]); }, }; -export default statsTab; +export default StatsTab; diff --git a/webview-ui/src/diagnostics_panel/prefabs/tabs/ServerTiming.tsx b/webview-ui/src/diagnostics_panel/prefabs/tabs/ServerTiming.tsx index fe76fbf0..db5af90b 100644 --- a/webview-ui/src/diagnostics_panel/prefabs/tabs/ServerTiming.tsx +++ b/webview-ui/src/diagnostics_panel/prefabs/tabs/ServerTiming.tsx @@ -5,7 +5,7 @@ import { StatisticType, YAxisType, createStatResolver } from '../../StatisticRes import { TabPrefab, TabPrefabDataSource } from '../TabPrefab'; import { generateRowsFromStatsPrefabs } from '../utilities'; -const serverTickTimings: StatisticPrefab = { +const ServerTickTimings: StatisticPrefab = { name: 'Server Tick Timings', reactNode: ( { - return generateRowsFromStatsPrefabs([[serverTickTimings], [commandsRan]]); + return generateRowsFromStatsPrefabs([[ServerTickTimings], [CommandsRan]]); }, }; -export default statsTab; +export default StatsTab; From f9010a5fbcfcc8b55e8429ef2bbad8a3f0aba90b Mon Sep 17 00:00:00 2001 From: Alex Denford SBL Date: Wed, 17 Jun 2026 15:12:49 -0700 Subject: [PATCH 23/25] better tip display options --- .../diagnostics_panel/StatisticResolver.tsx | 2 +- .../controls/LineChart/LineChart.tsx | 37 +++++++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/webview-ui/src/diagnostics_panel/StatisticResolver.tsx b/webview-ui/src/diagnostics_panel/StatisticResolver.tsx index 369bff13..4b48161a 100644 --- a/webview-ui/src/diagnostics_panel/StatisticResolver.tsx +++ b/webview-ui/src/diagnostics_panel/StatisticResolver.tsx @@ -79,7 +79,7 @@ function DifferenceStatResolver( let result = [...previousValues]; for (let i = 0; i < msg.values.length; i++) { - let value = msg.values[i]; + const value = msg.values[i]; let newValue = 0; let absoluteValue = value; if (result.length !== 0) { diff --git a/webview-ui/src/diagnostics_panel/controls/LineChart/LineChart.tsx b/webview-ui/src/diagnostics_panel/controls/LineChart/LineChart.tsx index d945a387..c4a125d3 100644 --- a/webview-ui/src/diagnostics_panel/controls/LineChart/LineChart.tsx +++ b/webview-ui/src/diagnostics_panel/controls/LineChart/LineChart.tsx @@ -8,6 +8,9 @@ import * as Plot from '@observablehq/plot'; import { StatisticProvider, StatisticUpdatedMessage } from '../../StatisticProvider'; import { removeAllStyleElements } from '../../../util/CSPUtilities'; +// Set to true to generate fake data with sine wave pattern for testing without a data source +const GenerateTestData = true; + type LineChartProps = { title: string; yLabel: string; @@ -42,6 +45,34 @@ function formatRelativeTime(latestTime: number, tick: number): string { return `${Math.floor(diff / 20)}s ago`; } +const SharedTipDisplayOptions = { + fillOpacity: 0.5, + fill: 'black', + stroke: 'white', + style: { + color: '#ffffff', + background: '#333333', + }, +}; + +function createInitialChartData(pointCount = 40): TrackedStat[] { + if(!GenerateTestData) { + return []; + } + const seed: TrackedStat[] = []; + for (let i = 0; i < pointCount; i++) { + const time = i + 1; + const value = 35 + Math.sin(i / 4) * 10 + i * 0.2; + seed.push({ + time, + value, + absoluteValue: value, + category: 'sample', + }); + } + return seed; +} + //chart component export function LineChart({ title, @@ -52,7 +83,7 @@ export function LineChart({ yAxisStyle, }: LineChartProps): JSX.Element { // state - const [data, setData] = useState([]); + const [data, setData] = useState(() => createInitialChartData()); // refs const containerRef = useRef(null); @@ -129,7 +160,6 @@ export function LineChart({ : undefined, Plot.lineY(data, { x: 'time', y: 'value' }), enableFilledChart ? Plot.ruleY([0]) : undefined, - Plot.crosshairX(data, { x: 'time', y: 'value' }), Plot.tip( data, Plot.pointerX({ @@ -137,6 +167,7 @@ export function LineChart({ y: 'value', title: (d: TrackedStat) => `${xLabel}: ${formatRelativeTime(latestTime, d.time)}\n${yLabel}: ${formatYAxisTick(d.value)}`, + ...SharedTipDisplayOptions, }) ), ], @@ -168,7 +199,6 @@ export function LineChart({ }, marks: [ Plot.differenceY(data, { x: 'time', y: 'value' }), - Plot.crosshairX(data, { x: 'time', y: 'value' }), Plot.tip( data, Plot.pointerX({ @@ -176,6 +206,7 @@ export function LineChart({ y: 'value', title: (d: TrackedStat) => `${xLabel}: ${formatRelativeTime(latestTime, d.time)}\n${yLabel}: ${formatYAxisTick(d.value)}`, + ...SharedTipDisplayOptions, }) ), ], From 6e1bac46bb33d646b8977a7069d092a9f012cee7 Mon Sep 17 00:00:00 2001 From: Alex Denford SBL Date: Wed, 17 Jun 2026 15:12:56 -0700 Subject: [PATCH 24/25] Update LineChart.tsx --- .../src/diagnostics_panel/controls/LineChart/LineChart.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webview-ui/src/diagnostics_panel/controls/LineChart/LineChart.tsx b/webview-ui/src/diagnostics_panel/controls/LineChart/LineChart.tsx index c4a125d3..6d4e13e1 100644 --- a/webview-ui/src/diagnostics_panel/controls/LineChart/LineChart.tsx +++ b/webview-ui/src/diagnostics_panel/controls/LineChart/LineChart.tsx @@ -9,7 +9,7 @@ import { StatisticProvider, StatisticUpdatedMessage } from '../../StatisticProvi import { removeAllStyleElements } from '../../../util/CSPUtilities'; // Set to true to generate fake data with sine wave pattern for testing without a data source -const GenerateTestData = true; +const GenerateTestData = false; type LineChartProps = { title: string; From 90ddf8df0a0b5020f63ffaba2d3c3581fe09df0d Mon Sep 17 00:00:00 2001 From: Alex Denford SBL Date: Fri, 26 Jun 2026 12:43:29 -0700 Subject: [PATCH 25/25] change to use memo, use vs code style --- webview-ui/src/diagnostics_panel/App.tsx | 34 +++++++++++-------- .../controls/LineChart/LineChart.tsx | 6 ++-- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/webview-ui/src/diagnostics_panel/App.tsx b/webview-ui/src/diagnostics_panel/App.tsx index 5c546cc9..127bed99 100644 --- a/webview-ui/src/diagnostics_panel/App.tsx +++ b/webview-ui/src/diagnostics_panel/App.tsx @@ -1,7 +1,7 @@ // Copyright (C) Microsoft Corporation. All rights reserved. import { StatGroupSelectionBox } from './controls/StatGroupSelectionBox'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import ReplayControls from './controls/ReplayControls'; import { Icons } from './Icons'; import './App.css'; @@ -19,6 +19,8 @@ function TabView({ tabPrefab, params }: { tabPrefab: TabPrefab; params: TabPrefa return <>{tabPrefab.content(params)}; } +const sortedTabPrefabs = [...tabPrefabs].sort((a, b) => a.name.localeCompare(b.name)); + // A tab entry is either a hardcoded prefab or a dynamic descriptor received from the game. type MergedTab = | { kind: 'prefab'; name: string; tab: TabPrefab } @@ -59,7 +61,6 @@ const CLIENT_SELECTION_HELP_TOOLTIP = 'Please enable "Creator > Script Diagnostics Settings > Enable Client Diagnostics" from within the game settings.'; function App() { - const sortedTabPrefabs = [...tabPrefabs].sort((a, b) => a.name.localeCompare(b.name)); const [selectedPlugin, setSelectedPlugin] = useState(''); const [selectedClient, setSelectedClient] = useState(''); const [currentTab, setCurrentTab] = useState('tab-0'); @@ -97,20 +98,23 @@ function App() { // Merge schema tabs into the prefab list. Schema tabs whose name matches a prefab replace it; // new names are appended. Falls back to all prefabs when no schema has arrived yet. - const mergedTabs: MergedTab[] = sortedTabPrefabs.map(tab => ({ - kind: 'prefab' as const, - name: tab.name, - tab, - })); - for (const descriptor of schema) { - const existingIndex = mergedTabs.findIndex(t => t.name === descriptor.name); - if (existingIndex !== -1) { - mergedTabs[existingIndex] = { kind: 'dynamic' as const, name: descriptor.name, descriptor }; - } else { - mergedTabs.push({ kind: 'dynamic' as const, name: descriptor.name, descriptor }); + const mergedTabs: MergedTab[] = useMemo(() => { + const merged: MergedTab[] = sortedTabPrefabs.map(tab => ({ + kind: 'prefab' as const, + name: tab.name, + tab, + })); + for (const descriptor of schema) { + const existingIndex = merged.findIndex(t => t.name === descriptor.name); + if (existingIndex !== -1) { + merged[existingIndex] = { kind: 'dynamic' as const, name: descriptor.name, descriptor }; + } else { + merged.push({ kind: 'dynamic' as const, name: descriptor.name, descriptor }); + } } - } - mergedTabs.sort((a, b) => a.name.localeCompare(b.name)); + merged.sort((a, b) => a.name.localeCompare(b.name)); + return merged; + }, [schema]); return (
diff --git a/webview-ui/src/diagnostics_panel/controls/LineChart/LineChart.tsx b/webview-ui/src/diagnostics_panel/controls/LineChart/LineChart.tsx index 6d4e13e1..594d7051 100644 --- a/webview-ui/src/diagnostics_panel/controls/LineChart/LineChart.tsx +++ b/webview-ui/src/diagnostics_panel/controls/LineChart/LineChart.tsx @@ -50,13 +50,13 @@ const SharedTipDisplayOptions = { fill: 'black', stroke: 'white', style: { - color: '#ffffff', - background: '#333333', + color: 'var(--vscode-editorHoverWidget-foreground)', + background: 'var(--vscode-editorHoverWidget-background)', }, }; function createInitialChartData(pointCount = 40): TrackedStat[] { - if(!GenerateTestData) { +if(!GenerateTestData) { return []; } const seed: TrackedStat[] = [];