Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3eda824
wip
AlexDenfordSkyboxLabs Mar 27, 2026
ee74ea5
remove descriptor changes
AlexDenfordSkyboxLabs Apr 10, 2026
870c27c
fixes
AlexDenfordSkyboxLabs May 12, 2026
75672f2
Merge branch 'main' into sbl/alex.denford/debugger-investigation
AlexDenfordSkyboxLabs May 22, 2026
4553d18
merge
AlexDenfordSkyboxLabs May 22, 2026
50c52ba
fixes/merge
AlexDenfordSkyboxLabs May 26, 2026
863d46d
fixes
AlexDenfordSkyboxLabs May 27, 2026
e462c47
Update session.ts
AlexDenfordSkyboxLabs May 27, 2026
fa1d26e
remove
AlexDenfordSkyboxLabs May 27, 2026
4614baa
feedback
AlexDenfordSkyboxLabs May 28, 2026
24f294e
Update protocol-events.ts
AlexDenfordSkyboxLabs May 28, 2026
4f7416d
Update package-lock.json
AlexDenfordSkyboxLabs May 28, 2026
a9ea44a
Update package-lock.json
AlexDenfordSkyboxLabs May 28, 2026
97d25d6
Update package-lock.json
AlexDenfordSkyboxLabs May 28, 2026
0cefdc5
tweaks
AlexDenfordSkyboxLabs May 29, 2026
7c8efc8
legacy support
AlexDenfordSkyboxLabs May 30, 2026
5a2baed
Update request-manager.ts
AlexDenfordSkyboxLabs May 30, 2026
38b9c3c
Merge branch 'main' into sbl/alex.denford/debugger-native-descriptors
AlexDenfordSkyboxLabs Jun 3, 2026
1b70c43
diagnostics
AlexDenfordSkyboxLabs Jun 3, 2026
0e36a1b
iteration
AlexDenfordSkyboxLabs Jun 15, 2026
3409c83
remove the now c++ driven schema, sort native ones
AlexDenfordSkyboxLabs Jun 16, 2026
0e61340
Scaling, fixing highlight, launch settings
AlexDenfordSkyboxLabs Jun 16, 2026
73dfe72
Re-add client memory & timing for backwards compatabilikty
AlexDenfordSkyboxLabs Jun 17, 2026
c90e2fe
linter
AlexDenfordSkyboxLabs Jun 17, 2026
f9010a5
better tip display options
AlexDenfordSkyboxLabs Jun 17, 2026
6e1bac4
Update LineChart.tsx
AlexDenfordSkyboxLabs Jun 17, 2026
90ddf8d
change to use memo, use vs code style
AlexDenfordSkyboxLabs Jun 26, 2026
4637541
Merge branch 'main' into sbl/alex.denford/debugger-native-descriptors
AlexDenfordSkyboxLabs Jun 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
{
"version": "0.2.0",
"configurations": [

{
"name": "Run Extension",
"type": "extensionHost",
Expand All @@ -28,7 +29,8 @@
"outFiles": [
"${workspaceFolder}/out/test/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}"
"preLaunchTask": "${defaultBuildTask}",
"debugWebviews": true
}
]
}
9 changes: 8 additions & 1 deletion src/panels/minecraft-diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -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);
Expand Down
17 changes: 13 additions & 4 deletions src/protocol-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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)
Expand All @@ -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 {
Expand Down Expand Up @@ -97,6 +99,11 @@ export type StatEventMessage = StatMessageModel & {
type: IncomingEventType.Stat2;
};

export interface DiagnosticsDescriptorMessage {
type: IncomingEventType.DiagnosticsDescriptor;
descriptors: DiagnosticsTabDescriptor[];
}

export type IncomingDebuggeeMessage =
| ProtocolCapabilities
| ProfilerCapture
Expand All @@ -105,7 +112,8 @@ export type IncomingDebuggeeMessage =
| PrintEventMessage
| NotificationEventMessage
| StatEventMessage
| DebuggeeResponseEnvelope;
| DebuggeeResponseEnvelope
| DiagnosticsDescriptorMessage;


// -------------------------------------------------------------------------
Expand Down Expand Up @@ -252,6 +260,7 @@ export class DebuggeeEventRegistry {
handler(eventMessage);
return true;
}
console.warn(`No handler found for incoming event type: ${eventMessage.type}`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking that this extension needs a custom output, to show all here.

Image

Not for these changes.

return false;
}
}
6 changes: 5 additions & 1 deletion src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {
Expand Down
42 changes: 42 additions & 0 deletions src/stats/stats-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,45 @@ 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: 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[];
}

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 {
protected _statListeners: StatsListener[];
private _cachedSchema: DiagnosticsTabDescriptor[] | undefined;

constructor(public readonly name: string, public readonly uniqueId: string) {
this._statListeners = [];
Expand All @@ -47,6 +76,13 @@ export class StatsProvider {
}
}

public setSchema(schema: DiagnosticsTabDescriptor[]): void {
this._cachedSchema = schema;
this._statListeners.forEach((listener: StatsListener) => {
listener.onSchemaReceived?.(schema);
});
}

public start(): void {
throw new Error('Method not implemented.');
}
Expand Down Expand Up @@ -74,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 {
Expand Down
105 changes: 82 additions & 23 deletions webview-ui/src/diagnostics_panel/App.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
// 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';
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';
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
Expand All @@ -17,6 +19,13 @@ 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 }
| { kind: 'dynamic'; name: string; descriptor: DiagnosticsTabDescriptor };

declare global {
interface Window {
initialParams: any;
Expand Down Expand Up @@ -52,12 +61,13 @@ 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<string>('');
const [selectedClient, setSelectedClient] = useState<string>('');
const [currentTab, setCurrentTab] = useState<string>('tab-0');
const [paused, setPaused] = useState<boolean>(true);
const [speed, setSpeed] = useState<string>('');
// Dynamic schema received from the game on connect. Merged into the prefab tab list.
const [schema, setSchema] = useState<DiagnosticsTabDescriptor[]>([]);

const handlePluginSelection = useCallback((pluginSelectionId: string) => {
setSelectedPlugin(() => pluginSelectionId);
Expand All @@ -76,6 +86,8 @@ 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[]);
}
};
window.addEventListener('message', handleMessage);
Expand All @@ -84,6 +96,26 @@ function App() {
};
}, [handleDebuggerRequestResult]);

// 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[] = 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 });
}
}
merged.sort((a, b) => a.name.localeCompare(b.name));
return merged;
}, [schema]);

Comment on lines +99 to +118

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be under a hook, in this case useMemo.
That will avoid unnecessary re-renders when consuming mergedTabs.
React will detect changes on that only if the dependencies to the hook change.

return (
<main>
{window.initialParams.showReplayControls && (
Expand All @@ -100,18 +132,18 @@ function App() {
)}
<div className="vertical-tabs-container">
<div className="vertical-tab-list">
{sortedTabPrefabs.map((tabPrefab, index) => (
{mergedTabs.map((tab, index) => (
<button
key={`tab-${index}`}
className={`vertical-tab-item${currentTab === `tab-${index}` ? ' active' : ''}`}
onClick={() => setCurrentTab(`tab-${index}`)}
>
{tabPrefab.name}
{tab.name}
</button>
))}
</div>
<div className="vertical-tab-content">
{sortedTabPrefabs.map((tabPrefab, index) => (
{mergedTabs.map((tab, index) => (
<div
key={`view-${index}`}
style={{
Expand All @@ -120,26 +152,53 @@ function App() {
flex: 1,
}}
>
{tabPrefab.dataSource === TabPrefabDataSource.Client ? (
<StatGroupSelectionBox
labelName="Client"
statParentId="client_stats"
onChange={handleClientSelection}
helpTooltip={CLIENT_SELECTION_HELP_TOOLTIP}
/>
) : (
<div />
)}
{tabPrefab.dataSource === TabPrefabDataSource.ServerScript ? (
<StatGroupSelectionBox
labelName="Script Plugin"
statParentId="handle_counts"
onChange={handlePluginSelection}
/>
{tab.kind === 'prefab' ? (
<>
{tab.tab.dataSource === TabPrefabDataSource.Client ? (
<StatGroupSelectionBox
labelName="Client"
statParentId="client_stats"
onChange={handleClientSelection}
helpTooltip={CLIENT_SELECTION_HELP_TOOLTIP}
/>
) : (
<div />
)}
{tab.tab.dataSource === TabPrefabDataSource.ServerScript ? (
<StatGroupSelectionBox
labelName="Script Plugin"
statParentId="handle_counts"
onChange={handlePluginSelection}
/>
) : (
<div />
)}
<TabView tabPrefab={tab.tab} params={{ selectedClient, selectedPlugin, onRunCommand }} />
</>
) : (
<div />
<>
{tab.descriptor.data_source === 'client' && (
<StatGroupSelectionBox
labelName="Client"
statParentId="client_stats"
onChange={handleClientSelection}
helpTooltip={CLIENT_SELECTION_HELP_TOOLTIP}
/>
)}
{tab.descriptor.data_source === 'server_script' && (
<StatGroupSelectionBox
labelName="Script Plugin"
statParentId="handle_counts"
onChange={handlePluginSelection}
/>
)}
<DynamicTab
descriptor={tab.descriptor}
selectedClient={selectedClient}
selectedPlugin={selectedPlugin}
/>
</>
)}
<TabView tabPrefab={tabPrefab} params={{ selectedClient, selectedPlugin, onRunCommand }} />
</div>
))}
</div>
Expand Down
Loading