@dxos/plugin-devtools 0.12.0-next-73daef452f1464b62efdbca75fb0067c1066f264 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/PLUGIN.mdl +144 -57
  2. package/dist/lib/Devtools.mjs +9 -1
  3. package/dist/lib/Devtools.mjs.map +1 -1
  4. package/dist/lib/capabilities.browser.mjs +45 -0
  5. package/dist/lib/capabilities.browser.mjs.map +1 -0
  6. package/dist/lib/capabilities.mjs +9 -7
  7. package/dist/lib/capabilities.mjs.map +1 -1
  8. package/dist/lib/capabilities.node.mjs +7 -3
  9. package/dist/lib/capabilities.node.mjs.map +1 -1
  10. package/dist/lib/capabilities.tauri.mjs +45 -0
  11. package/dist/lib/capabilities.tauri.mjs.map +1 -0
  12. package/dist/lib/capabilities.workerd.mjs +2 -2
  13. package/dist/lib/capabilities.workerd.mjs.map +1 -1
  14. package/dist/lib/{chunk-CliPanel.mjs → chunk-CliArticle.mjs} +28 -24
  15. package/dist/lib/chunk-CliArticle.mjs.map +1 -0
  16. package/dist/lib/chunk-DebugGraph.mjs +12 -8
  17. package/dist/lib/chunk-DebugGraph.mjs.map +1 -1
  18. package/dist/lib/chunk-DevtoolsCards.mjs +108 -0
  19. package/dist/lib/chunk-DevtoolsCards.mjs.map +1 -0
  20. package/dist/lib/chunk-DevtoolsOverviewContainer.mjs +59 -22
  21. package/dist/lib/chunk-DevtoolsOverviewContainer.mjs.map +1 -1
  22. package/dist/lib/chunk-GithubPanel.mjs +13 -10
  23. package/dist/lib/chunk-GithubPanel.mjs.map +1 -1
  24. package/dist/lib/chunk-PLUGIN.mjs +6 -0
  25. package/dist/lib/chunk-PLUGIN.mjs.map +1 -0
  26. package/dist/lib/{chunk-RegistryPanel.mjs → chunk-RegistryArticle.mjs} +26 -25
  27. package/dist/lib/chunk-RegistryArticle.mjs.map +1 -0
  28. package/dist/lib/chunk-ToolsExplorerContainer.mjs +7 -3
  29. package/dist/lib/chunk-ToolsExplorerContainer.mjs.map +1 -1
  30. package/dist/lib/chunk-app-graph-builder.mjs +323 -312
  31. package/dist/lib/chunk-app-graph-builder.mjs.map +1 -1
  32. package/dist/lib/chunk-react-context.mjs +1 -1
  33. package/dist/lib/chunk-react-context.mjs.map +1 -1
  34. package/dist/lib/chunk-react-surface.mjs +244 -88
  35. package/dist/lib/chunk-react-surface.mjs.map +1 -1
  36. package/dist/lib/containers.mjs +3 -3
  37. package/dist/lib/containers.mjs.map +1 -1
  38. package/dist/lib/index.mjs +0 -1
  39. package/dist/lib/meta.mjs +2 -2
  40. package/dist/lib/meta.mjs.map +1 -1
  41. package/dist/lib/translations.mjs +55 -49
  42. package/dist/lib/translations.mjs.map +1 -1
  43. package/dist/types/src/capabilities/DevtoolsCards.d.ts +28 -0
  44. package/dist/types/src/capabilities/DevtoolsCards.d.ts.map +1 -0
  45. package/dist/types/src/capabilities/DevtoolsSurfaces.d.ts +19 -6
  46. package/dist/types/src/capabilities/DevtoolsSurfaces.d.ts.map +1 -1
  47. package/dist/types/src/capabilities/app-graph-builder.d.ts +19 -0
  48. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
  49. package/dist/types/src/capabilities/app-graph-builder.test.d.ts +2 -0
  50. package/dist/types/src/capabilities/app-graph-builder.test.d.ts.map +1 -0
  51. package/dist/types/src/capabilities/gen/browser.d.ts +8 -0
  52. package/dist/types/src/capabilities/gen/browser.d.ts.map +1 -0
  53. package/dist/types/src/capabilities/gen/tauri.d.ts +8 -0
  54. package/dist/types/src/capabilities/gen/tauri.d.ts.map +1 -0
  55. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  56. package/dist/types/src/containers/CliArticle/CliArticle.d.ts +8 -0
  57. package/dist/types/src/containers/CliArticle/CliArticle.d.ts.map +1 -0
  58. package/dist/types/src/containers/{CliPanel/CliPanel.stories.d.ts → CliArticle/CliArticle.stories.d.ts} +33 -5
  59. package/dist/types/src/containers/CliArticle/CliArticle.stories.d.ts.map +1 -0
  60. package/dist/types/src/containers/CliArticle/index.d.ts +2 -0
  61. package/dist/types/src/containers/CliArticle/index.d.ts.map +1 -0
  62. package/dist/types/src/containers/{CliPanel → CliArticle}/useCliApp.d.ts +1 -1
  63. package/dist/types/src/containers/CliArticle/useCliApp.d.ts.map +1 -0
  64. package/dist/types/src/containers/DebugGraph/DebugGraph.d.ts +2 -1
  65. package/dist/types/src/containers/DebugGraph/DebugGraph.d.ts.map +1 -1
  66. package/dist/types/src/containers/DebugGraph/index.d.ts +1 -1
  67. package/dist/types/src/containers/DebugGraph/index.d.ts.map +1 -1
  68. package/dist/types/src/containers/DevtoolsOverviewContainer/DevtoolsOverviewContainer.d.ts.map +1 -1
  69. package/dist/types/src/containers/DevtoolsOverviewContainer/index.d.ts +1 -1
  70. package/dist/types/src/containers/DevtoolsOverviewContainer/index.d.ts.map +1 -1
  71. package/dist/types/src/containers/GithubPanel/GithubComponent.d.ts.map +1 -1
  72. package/dist/types/src/containers/GithubPanel/GithubComponent.stories.d.ts +30 -2
  73. package/dist/types/src/containers/GithubPanel/GithubComponent.stories.d.ts.map +1 -1
  74. package/dist/types/src/containers/GithubPanel/index.d.ts +1 -1
  75. package/dist/types/src/containers/GithubPanel/index.d.ts.map +1 -1
  76. package/dist/types/src/containers/RegistryArticle/RegistryArticle.d.ts +8 -0
  77. package/dist/types/src/containers/RegistryArticle/RegistryArticle.d.ts.map +1 -0
  78. package/dist/types/src/containers/RegistryArticle/index.d.ts +2 -0
  79. package/dist/types/src/containers/RegistryArticle/index.d.ts.map +1 -0
  80. package/dist/types/src/containers/ToolsExplorerContainer/ToolsExplorerContainer.d.ts +3 -1
  81. package/dist/types/src/containers/ToolsExplorerContainer/ToolsExplorerContainer.d.ts.map +1 -1
  82. package/dist/types/src/containers/ToolsExplorerContainer/index.d.ts +1 -1
  83. package/dist/types/src/containers/ToolsExplorerContainer/index.d.ts.map +1 -1
  84. package/dist/types/src/containers/index.d.ts +2 -2
  85. package/dist/types/src/containers/index.d.ts.map +1 -1
  86. package/dist/types/src/index.d.ts +1 -2
  87. package/dist/types/src/index.d.ts.map +1 -1
  88. package/dist/types/src/translations.d.ts +30 -2
  89. package/dist/types/src/translations.d.ts.map +1 -1
  90. package/dist/types/src/types/Devtools.d.ts +6 -0
  91. package/dist/types/src/types/Devtools.d.ts.map +1 -1
  92. package/dist/types/src/types/index.d.ts +2 -2
  93. package/dist/types/src/types/index.d.ts.map +1 -1
  94. package/dist/types/tsconfig.tsbuildinfo +1 -1
  95. package/dx.config.ts +2 -2
  96. package/package.json +43 -39
  97. package/src/capabilities/DevtoolsCards.tsx +140 -0
  98. package/src/capabilities/DevtoolsSurfaces.tsx +32 -38
  99. package/src/capabilities/app-graph-builder.test.ts +75 -0
  100. package/src/capabilities/app-graph-builder.ts +324 -313
  101. package/src/capabilities/gen/browser.ts +61 -0
  102. package/src/capabilities/gen/node.ts +4 -4
  103. package/src/capabilities/gen/tauri.ts +61 -0
  104. package/src/capabilities/gen/workerd.ts +2 -2
  105. package/src/capabilities/index.ts +7 -7
  106. package/src/capabilities/react-context.tsx +1 -1
  107. package/src/capabilities/react-surface.ts +222 -79
  108. package/src/containers/{CliPanel/CliPanel.stories.tsx → CliArticle/CliArticle.stories.tsx} +9 -10
  109. package/src/containers/{CliPanel/CliPanel.tsx → CliArticle/CliArticle.tsx} +7 -7
  110. package/src/containers/CliArticle/index.ts +5 -0
  111. package/src/containers/{CliPanel → CliArticle}/useCliApp.ts +17 -12
  112. package/src/containers/DebugGraph/DebugGraph.tsx +7 -6
  113. package/src/containers/DebugGraph/index.ts +1 -1
  114. package/src/containers/DevtoolsOverviewContainer/DevtoolsOverviewContainer.tsx +72 -32
  115. package/src/containers/DevtoolsOverviewContainer/index.ts +1 -1
  116. package/src/containers/GithubPanel/GithubComponent.stories.tsx +2 -2
  117. package/src/containers/GithubPanel/GithubComponent.tsx +14 -11
  118. package/src/containers/GithubPanel/GithubPanel.tsx +2 -2
  119. package/src/containers/GithubPanel/index.ts +1 -1
  120. package/src/containers/{RegistryPanel/RegistryPanel.tsx → RegistryArticle/RegistryArticle.tsx} +11 -10
  121. package/src/containers/RegistryArticle/index.ts +5 -0
  122. package/src/containers/ToolsExplorerContainer/ToolsExplorerContainer.tsx +10 -3
  123. package/src/containers/ToolsExplorerContainer/index.ts +1 -1
  124. package/src/containers/index.ts +6 -6
  125. package/src/index.ts +1 -2
  126. package/src/meta.ts +1 -1
  127. package/src/plugin.test.ts +2 -2
  128. package/src/translations.ts +7 -3
  129. package/src/types/Devtools.ts +10 -0
  130. package/src/types/index.ts +2 -2
  131. package/dist/lib/chunk-CliPanel.mjs.map +0 -1
  132. package/dist/lib/chunk-RegistryPanel.mjs.map +0 -1
  133. package/dist/types/src/containers/CliPanel/CliPanel.d.ts +0 -6
  134. package/dist/types/src/containers/CliPanel/CliPanel.d.ts.map +0 -1
  135. package/dist/types/src/containers/CliPanel/CliPanel.stories.d.ts.map +0 -1
  136. package/dist/types/src/containers/CliPanel/index.d.ts +0 -2
  137. package/dist/types/src/containers/CliPanel/index.d.ts.map +0 -1
  138. package/dist/types/src/containers/CliPanel/useCliApp.d.ts.map +0 -1
  139. package/dist/types/src/containers/RegistryPanel/RegistryPanel.d.ts +0 -6
  140. package/dist/types/src/containers/RegistryPanel/RegistryPanel.d.ts.map +0 -1
  141. package/dist/types/src/containers/RegistryPanel/index.d.ts +0 -2
  142. package/dist/types/src/containers/RegistryPanel/index.d.ts.map +0 -1
  143. package/src/containers/CliPanel/index.ts +0 -5
  144. package/src/containers/RegistryPanel/index.ts +0 -5
@@ -9,18 +9,18 @@ import { userEvent } from 'storybook/test';
9
9
  import { withPluginManager } from '@dxos/app-framework/testing';
10
10
  import { ClientPlugin, initializeIdentity } from '@dxos/plugin-client/testing';
11
11
  import * as SpacePlugin from '@dxos/plugin-space/SpacePlugin';
12
- import { corePlugins } from '@dxos/plugin-testing';
12
+ import * as CorePlugins from '@dxos/plugin-testing/CorePlugins';
13
13
  import * as StorybookPlugin from '@dxos/plugin-testing/StorybookPlugin';
14
14
  import { runCommand, waitForTerminal } from '@dxos/react-ui-terminal/testing';
15
15
  import { withLayout, withTheme } from '@dxos/react-ui/testing';
16
16
 
17
17
  import { translations } from '#translations';
18
18
 
19
- import { CliPanel } from './CliPanel';
19
+ import { CliArticle } from './CliArticle.tsx';
20
20
 
21
21
  const meta = {
22
- title: 'plugins/plugin-devtools/containers/CliPanel',
23
- component: CliPanel,
22
+ title: 'plugins/plugin-devtools/containers/CliArticle',
23
+ component: CliArticle,
24
24
  decorators: [
25
25
  withLayout({ layout: 'fullscreen' }),
26
26
  withTheme(),
@@ -28,7 +28,7 @@ const meta = {
28
28
  // No setup event: the panel fires `CommandsRequested` itself, which is what pulls the
29
29
  // plugins' command modules in — the same path the app takes.
30
30
  plugins: [
31
- ...corePlugins(),
31
+ ...CorePlugins.make(),
32
32
  // The identity brings a default space, which is what the commands resolve against.
33
33
  ClientPlugin.make({ onClientInitialized: ({ client }) => Effect.asVoid(initializeIdentity(client)) }),
34
34
  StorybookPlugin.make({}),
@@ -42,7 +42,7 @@ const meta = {
42
42
  controls: { disable: true },
43
43
  translations,
44
44
  },
45
- } satisfies Meta<typeof CliPanel>;
45
+ } satisfies Meta<typeof CliArticle>;
46
46
 
47
47
  export default meta;
48
48
 
@@ -56,20 +56,19 @@ export const Default: Story = {};
56
56
  */
57
57
  export const Spec: Story = {
58
58
  play: async ({ canvasElement }) => {
59
- const keyboard = (text: string) => userEvent.keyboard(text);
60
59
  await waitForTerminal(canvasElement, 'dx>');
61
60
 
62
61
  // `space list` reaches the client and reports its space.
63
- await runCommand(canvasElement, 'space list', keyboard);
62
+ await runCommand(canvasElement, 'space list', userEvent.keyboard);
64
63
  await waitForTerminal(canvasElement, 'SPACE_READY');
65
64
 
66
65
  // `database query` reaches ECHO through the layer the command provides for itself.
67
- await runCommand(canvasElement, 'database query', keyboard);
66
+ await runCommand(canvasElement, 'database query', userEvent.keyboard);
68
67
  await waitForTerminal(canvasElement, 'org.dxos.type.spaceProperties');
69
68
 
70
69
  // A command from another plugin, whose service arrives via that plugin's contributed layer
71
70
  // rather than anything this panel names.
72
- await runCommand(canvasElement, 'config view', keyboard);
71
+ await runCommand(canvasElement, 'config view', userEvent.keyboard);
73
72
  await waitForTerminal(canvasElement, 'runtime');
74
73
  },
75
74
  };
@@ -6,10 +6,10 @@ import React from 'react';
6
6
 
7
7
  import { Placeholder } from '@dxos/devtools';
8
8
  import { useClient } from '@dxos/react-client';
9
- import { Panel } from '@dxos/react-ui';
10
9
  import { Terminal } from '@dxos/react-ui-terminal';
10
+ import * as Panel from '@dxos/react-ui/Panel';
11
11
 
12
- import { useCliApp } from './useCliApp';
12
+ import { useCliApp } from './useCliApp.ts';
13
13
 
14
14
  const BOLD = '\x1b[1m';
15
15
  const DIM = '\x1b[2m';
@@ -24,22 +24,22 @@ const BANNER = `${BOLD}DXOS CLI${RESET}\n${DIM}The dx commands, running against
24
24
  * services are built once per client, so state the handlers hold survives between commands the way
25
25
  * it does in a long-lived shell.
26
26
  */
27
- export const CliPanel = () => {
27
+ export const CliArticle = ({ role }: { role?: string }) => {
28
28
  const client = useClient();
29
29
  const cli = useCliApp(client);
30
30
 
31
31
  // No ScrollArea: xterm owns its own viewport and scrollback.
32
32
  return (
33
- <Panel.Root>
34
- <Panel.Content>
33
+ <Panel.Root role={role}>
34
+ <Panel.Body>
35
35
  {cli ? (
36
36
  <Terminal command={cli.command} layer={cli.layer} name='dx' banner={BANNER} />
37
37
  ) : (
38
38
  <Placeholder label='No commands' />
39
39
  )}
40
- </Panel.Content>
40
+ </Panel.Body>
41
41
  </Panel.Root>
42
42
  );
43
43
  };
44
44
 
45
- CliPanel.displayName = 'CliPanel';
45
+ CliArticle.displayName = 'CliArticle';
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export { CliArticle, CliArticle as default } from './CliArticle.tsx';
@@ -2,21 +2,22 @@
2
2
  // Copyright 2026 DXOS.org
3
3
  //
4
4
 
5
+ import * as Command from 'effect/cli/Command';
6
+ import * as Options from 'effect/cli/Flag';
5
7
  import * as Effect from 'effect/Effect';
6
8
  import * as Layer from 'effect/Layer';
7
- import * as Command from 'effect/unstable/cli/Command';
8
- import * as Options from 'effect/unstable/cli/Flag';
9
9
  import { useEffect, useMemo } from 'react';
10
10
 
11
11
  import * as ActivationEvents from '@dxos/app-framework/ActivationEvents';
12
12
  import * as Capabilities from '@dxos/app-framework/Capabilities';
13
13
  import * as Capability from '@dxos/app-framework/Capability';
14
+ import * as Hooks from '@dxos/app-framework/Hooks';
14
15
  import * as Plugin from '@dxos/app-framework/Plugin';
15
- import { useCapabilities, usePluginManager } from '@dxos/app-framework/ui';
16
+ import * as PluginManagerProvider from '@dxos/app-framework/PluginManagerProvider';
16
17
  import { CommandConfig, type CommandServices } from '@dxos/cli-util';
17
- import { type Client, ClientService, ConfigService } from '@dxos/client';
18
+ import { type Client, fromClient, fromConfig } from '@dxos/client';
18
19
  import * as Operation from '@dxos/compute/Operation';
19
- import { EffectEx } from '@dxos/effect';
20
+ import * as EffectEx from '@dxos/effect/EffectEx';
20
21
 
21
22
  const OPERATIONS_UNAVAILABLE = 'Operations are not available in the devtools terminal.';
22
23
 
@@ -46,9 +47,9 @@ const operationLayer = Layer.succeed(Operation.Service, {
46
47
  * Returns `undefined` until something contributes commands — there is no shell to offer yet.
47
48
  */
48
49
  export const useCliApp = (client: Client) => {
49
- const manager = usePluginManager();
50
- const commands = useCapabilities(Capabilities.Command);
51
- const contributed = useCapabilities(Capabilities.Layer);
50
+ const manager = PluginManagerProvider.usePluginManager();
51
+ const commands = Hooks.useCapabilities(Capabilities.Command);
52
+ const contributed = Hooks.useCapabilities(Capabilities.Layer);
52
53
 
53
54
  useEffect(() => {
54
55
  EffectEx.runDetached(manager.activate(ActivationEvents.CommandsRequested));
@@ -66,8 +67,12 @@ export const useCliApp = (client: Client) => {
66
67
  // Shared so they parse anywhere in the line — a plain command config is only read when `dx`
67
68
  // itself is the leaf.
68
69
  Command.withSharedFlags({
69
- json: Options.boolean('json').pipe(Options.withDescription('JSON output.')),
70
- verbose: Options.boolean('verbose').pipe(Options.withAlias('v'), Options.withDescription('Verbose output.')),
70
+ json: Options.Boolean('json').pipe(Options.withDefault(false), Options.withDescription('JSON output.')),
71
+ verbose: Options.Boolean('verbose').pipe(
72
+ Options.withDefault(false),
73
+ Options.withAlias('v'),
74
+ Options.withDescription('Verbose output.'),
75
+ ),
71
76
  }),
72
77
  Command.withSubcommands([first, ...rest]),
73
78
  // After the subcommands so the config reaches them: `provide` wraps the handler it is applied
@@ -86,8 +91,8 @@ export const useCliApp = (client: Client) => {
86
91
  // Annotated so a service this host owes is a build error naming it, rather than a
87
92
  // "Service not found" the first time someone runs the command that needed it.
88
93
  const hostLayer: Layer.Layer<CommandServices, never, never> = Layer.mergeAll(
89
- ClientService.fromClient(client),
90
- ConfigService.fromConfig(client.config),
94
+ fromClient(client),
95
+ fromConfig(client.config),
91
96
  Layer.succeed(Plugin.Service, manager),
92
97
  Layer.succeed(Capability.Service, manager.capabilities),
93
98
  operationLayer,
@@ -6,20 +6,21 @@ import React from 'react';
6
6
 
7
7
  import * as AppGraph from '@dxos/app-graph/AppGraph';
8
8
  import { Tree } from '@dxos/devtools';
9
- import { Panel, ScrollArea } from '@dxos/react-ui';
9
+ import * as Panel from '@dxos/react-ui/Panel';
10
+ import * as ScrollArea from '@dxos/react-ui/ScrollArea';
10
11
 
11
- export type DebugGraphProps = { graph: AppGraph.Graph; root: string };
12
+ export type DebugGraphProps = { role?: string; graph: AppGraph.Graph; root: string };
12
13
 
13
- export const DebugGraph = ({ graph, root }: DebugGraphProps) => {
14
+ export const DebugGraph = ({ role, graph, root }: DebugGraphProps) => {
14
15
  return (
15
- <Panel.Root>
16
- <Panel.Content asChild>
16
+ <Panel.Root role={role}>
17
+ <Panel.Body asChild>
17
18
  <ScrollArea.Root orientation='all'>
18
19
  <ScrollArea.Viewport>
19
20
  <Tree data={AppGraph.toJSON(graph, root)} />
20
21
  </ScrollArea.Viewport>
21
22
  </ScrollArea.Root>
22
- </Panel.Content>
23
+ </Panel.Body>
23
24
  </Panel.Root>
24
25
  );
25
26
  };
@@ -2,4 +2,4 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- export { DebugGraph as default } from './DebugGraph';
5
+ export { DebugGraph as default } from './DebugGraph.tsx';
@@ -2,49 +2,89 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React, { useCallback, useMemo } from 'react';
5
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
6
6
 
7
- import { Surface } from '@dxos/app-framework/ui';
8
- import { AppSurface } from '@dxos/app-toolkit/ui';
9
- import { StatsPanel, useStats } from '@dxos/devtools';
7
+ import * as Surface from '@dxos/app-framework/Surface';
8
+ import * as AppSurface from '@dxos/app-toolkit/AppSurface';
9
+ import { StatsPanel, type SurfaceProfilerStats, useStats } from '@dxos/devtools';
10
10
 
11
+ import { type DevtoolsCardData, isDebugSurface } from '../../capabilities/DevtoolsCards.tsx';
12
+
13
+ /**
14
+ * The stats stack: one poll of the client's stats, fanned out to every card contributed to the role.
15
+ * The profiler and dispatch metrics are sampled on each refresh rather than subscribed to: this
16
+ * container is itself a profiled surface, so a live subscription would record an entry for every
17
+ * re-render the notification caused and loop at frame rate, flushing every other surface out of the
18
+ * profiler's window — which is what made the Surfaces card's count wander with no interaction.
19
+ */
11
20
  export const DevtoolsOverviewContainer = () => {
12
21
  const [stats, refreshStats] = useStats();
13
- const surfaceProfilerStats = Surface.useProfilerStats();
14
- const surfaceMetrics = Surface.useMetrics();
22
+ const getProfilerStats = Surface.useProfilerSnapshot();
15
23
  const clearSurfaceProfiler = Surface.useProfilerClear();
24
+ // Resampled as surfaces mount and unmount: with a restored layout this container mounts at boot,
25
+ // before most of the app, so a single sample on mount would list only what existed then.
26
+ const mounted = Surface.useMounted();
27
+ const [surfaceProfilerStats, setSurfaceProfilerStats] = useState<SurfaceProfilerStats[]>([]);
28
+
29
+ // One row per surface mounted right now (the profiler records renders, not mounts), with its
30
+ // cumulative render timings and dispatch metrics joined on `surface/<id>/<role>` where they exist.
31
+ // The debug tooling's own surfaces are left out so the panel does not measure itself.
32
+ const sampleProfiler = useCallback(() => {
33
+ const timings = new Map(getProfilerStats().map((stat) => [stat.id, stat]));
34
+ const metrics = new Map(Surface.getMetrics().map((metric) => [metric.id, metric]));
35
+ setSurfaceProfilerStats(
36
+ mounted
37
+ .filter((surface) => !isDebugSurface(surface))
38
+ .map(({ id, role }) => {
39
+ // Mirrors the profiler's own id, which prints an anonymous surface as `undefined`.
40
+ const profilerId = `surface/${id}/${role}`;
41
+ const timing = timings.get(profilerId) ?? {
42
+ id: profilerId,
43
+ mountCount: 0,
44
+ updateCount: 0,
45
+ totalRenders: 0,
46
+ avgActualDuration: 0,
47
+ maxActualDuration: 0,
48
+ avgBaseDuration: 0,
49
+ lastActualDuration: 0,
50
+ lastCommitTime: 0,
51
+ };
52
+ const metric = metrics.get(profilerId);
53
+ return metric
54
+ ? {
55
+ ...timing,
56
+ candidates: metric.candidates,
57
+ truncated: metric.truncated,
58
+ errors: metric.errors,
59
+ dataUnstable: metric.dataUnstable,
60
+ }
61
+ : timing;
62
+ })
63
+ .sort((a, b) => b.maxActualDuration - a.maxActualDuration || a.id.localeCompare(b.id)),
64
+ );
65
+ }, [getProfilerStats, mounted]);
66
+ useEffect(sampleProfiler, [sampleProfiler]);
67
+
68
+ const handleRefresh = useCallback(() => {
69
+ refreshStats();
70
+ sampleProfiler();
71
+ }, [refreshStats, sampleProfiler]);
16
72
 
17
- // The panel joins profiler stats with dispatch metrics, so reset must clear both.
73
+ // The profiler card joins profiler stats with dispatch metrics, so reset must clear both.
18
74
  const handleClearSurfaceProfiler = useCallback(() => {
19
75
  clearSurfaceProfiler?.();
20
76
  Surface.clearMetrics();
21
- }, [clearSurfaceProfiler]);
22
-
23
- // Join dispatch metrics onto the render-timing stats (both keyed by `surface/<id>/<role>`).
24
- const enrichedStats = useMemo(() => {
25
- const byId = new Map(surfaceMetrics.map((metric) => [metric.id, metric]));
26
- return surfaceProfilerStats.map((stat) => {
27
- const metric = byId.get(stat.id);
28
- return metric
29
- ? {
30
- ...stat,
31
- candidates: metric.candidates,
32
- truncated: metric.truncated,
33
- errors: metric.errors,
34
- dataUnstable: metric.dataUnstable,
35
- }
36
- : stat;
37
- });
38
- }, [surfaceProfilerStats, surfaceMetrics]);
77
+ sampleProfiler();
78
+ }, [clearSurfaceProfiler, sampleProfiler]);
79
+
80
+ const data = useMemo<DevtoolsCardData>(
81
+ () => ({ stats, surfaceProfilerStats, onClearSurfaceProfiler: handleClearSurfaceProfiler }),
82
+ [stats, surfaceProfilerStats, handleClearSurfaceProfiler],
83
+ );
39
84
 
40
85
  return (
41
- <StatsPanel
42
- stats={stats}
43
- surfaceProfilerStats={enrichedStats}
44
- onRefresh={refreshStats}
45
- onClearSurfaceProfiler={handleClearSurfaceProfiler}
46
- >
47
- <Surface.Surface type={AppSurface.DevtoolsOverview} />
86
+ <StatsPanel onRefresh={handleRefresh}>
87
+ <Surface.Surface type={AppSurface.DevtoolsOverview} data={data} />
48
88
  </StatsPanel>
49
89
  );
50
90
  };
@@ -2,4 +2,4 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- export { DevtoolsOverviewContainer as default } from './DevtoolsOverviewContainer';
5
+ export { DevtoolsOverviewContainer as default } from './DevtoolsOverviewContainer.tsx';
@@ -9,11 +9,11 @@ import { withLayout, withTheme } from '@dxos/react-ui/testing';
9
9
 
10
10
  import { translations } from '#translations';
11
11
 
12
- import { GithubComponent } from './GithubComponent';
12
+ import { GithubComponent } from './GithubComponent.tsx';
13
13
 
14
14
  const DefaultStory = () => (
15
15
  <GithubComponent.Root>
16
- <div className='grid grid-rows-[auto_minmax(0,1fr)_auto] overflow-hidden h-full w-full'>
16
+ <div className='grid grid-rows-[auto_minmax(0,1fr)_auto] overflow-hidden dx-fill'>
17
17
  <GithubComponent.Header />
18
18
  <GithubComponent.Content />
19
19
  <GithubComponent.StatusBar />
@@ -4,7 +4,10 @@
4
4
 
5
5
  import React, { type ReactNode, createContext, useContext, useEffect, useState } from 'react';
6
6
 
7
- import { Flex, IconButton, ScrollArea, useTranslation } from '@dxos/react-ui';
7
+ import * as Button from '@dxos/react-ui/Button';
8
+ import * as Hooks from '@dxos/react-ui/Hooks';
9
+ import * as Layout from '@dxos/react-ui/Layout';
10
+ import * as ScrollArea from '@dxos/react-ui/ScrollArea';
8
11
 
9
12
  import { meta } from '#meta';
10
13
 
@@ -90,10 +93,10 @@ const Root = ({ repo = DEFAULT_REPO, limit = DEFAULT_LIMIT, children }: GithubCo
90
93
  };
91
94
 
92
95
  const Header = () => {
93
- const { t } = useTranslation(meta.profile.key);
96
+ const { t } = Hooks.useTranslation(meta.profile.key);
94
97
  const { repo, pulls, unavailable } = useComponentContext();
95
98
  return (
96
- <header className='flex items-center justify-between gap-1 px-4 py-3 dx-modal-surface border-b border-subdued-separator'>
99
+ <header className='flex items-center justify-between gap-1 px-4 py-3 dx-modal-surface border-b border-separator-subtle'>
97
100
  <a
98
101
  href={`https://github.com/${repo}`}
99
102
  target='_blank'
@@ -102,7 +105,7 @@ const Header = () => {
102
105
  >
103
106
  @{repo}
104
107
  </a>
105
- <div className='text-xs text-description'>
108
+ <div className='text-xs text-fg-muted'>
106
109
  {unavailable
107
110
  ? t('github-unavailable.message')
108
111
  : pulls.length > 0
@@ -136,18 +139,18 @@ const formatRelative = (iso: string): string => {
136
139
 
137
140
  const PullRow = ({ pull }: { pull: GithubPullRequest }) => (
138
141
  <li>
139
- <Flex asChild align='start' gap='sm' classNames='px-trim-sm py-trim-xs rounded-sm hover:bg-hover-surface'>
142
+ <Layout.Flex asChild align='start' gap='sm' classNames='px-trim-sm py-trim-xs rounded-sm hover:bg-hover-surface'>
140
143
  <a href={pull.html_url} target='_blank' rel='noopener noreferrer'>
141
144
  <img src={pull.user.avatar_url} alt='' className='size-6 rounded-full shrink-0' />
142
- <Flex column classNames='min-w-0 flex-1'>
145
+ <Layout.Flex column classNames='min-w-0 flex-1'>
143
146
  {/* `leading-6` gives the title the avatar's line box, so the two align on their own. */}
144
147
  <span className='text-sm leading-6 truncate'>{pull.title}</span>
145
- <span className='text-xs text-description truncate'>
148
+ <span className='text-xs text-fg-muted truncate'>
146
149
  #{pull.number} · {pull.user.login} · {pull.merged_at ? formatRelative(pull.merged_at) : ''}
147
150
  </span>
148
- </Flex>
151
+ </Layout.Flex>
149
152
  </a>
150
- </Flex>
153
+ </Layout.Flex>
151
154
  </li>
152
155
  );
153
156
 
@@ -167,10 +170,10 @@ const Content = () => {
167
170
  };
168
171
 
169
172
  const StatusBar = () => {
170
- const { t } = useTranslation(meta.profile.key);
173
+ const { t } = Hooks.useTranslation(meta.profile.key);
171
174
  const { repo } = useComponentContext();
172
175
  return (
173
- <IconButton
176
+ <Button.Root
174
177
  icon='ph--github-logo--regular'
175
178
  label={t('view-on-github.button')}
176
179
  variant='primary'
@@ -4,11 +4,11 @@
4
4
 
5
5
  import React from 'react';
6
6
 
7
- import { GithubComponent } from './GithubComponent';
7
+ import { GithubComponent } from './GithubComponent.tsx';
8
8
 
9
9
  export const GithubPanel = () => (
10
10
  <GithubComponent.Root>
11
- <div className='h-full grid grid-rows-[auto_minmax(0,1fr)_auto] overflow-hidden h-full w-full'>
11
+ <div className='dx-fill grid grid-rows-[auto_minmax(0,1fr)_auto] overflow-hidden'>
12
12
  <GithubComponent.Header />
13
13
  <GithubComponent.Content />
14
14
  <GithubComponent.StatusBar />
@@ -2,4 +2,4 @@
2
2
  // Copyright 2026 DXOS.org
3
3
  //
4
4
 
5
- export { GithubPanel as default } from './GithubPanel';
5
+ export { GithubPanel as default } from './GithubPanel.tsx';
@@ -8,8 +8,9 @@ import * as Operation from '@dxos/compute/Operation';
8
8
  import { JsonView, Placeholder, Searchbar } from '@dxos/devtools';
9
9
  import { Entity, Format, Obj, Type } from '@dxos/echo';
10
10
  import { useClient } from '@dxos/react-client';
11
- import { Panel, Toolbar } from '@dxos/react-ui';
12
11
  import { DynamicTable, type TableFeatures } from '@dxos/react-ui-table';
12
+ import * as Panel from '@dxos/react-ui/Panel';
13
+ import * as Toolbar from '@dxos/react-ui/Toolbar';
13
14
  import { mx } from '@dxos/ui-theme';
14
15
 
15
16
  type RegistryRow = {
@@ -84,7 +85,7 @@ const toDetailJson = (entity: Entity.Unknown): object => {
84
85
  return { id: getEntityId(entity), typename: Entity.getTypename(entity) };
85
86
  };
86
87
 
87
- export const RegistryPanel = () => {
88
+ export const RegistryArticle = ({ role }: { role?: string }) => {
88
89
  const client = useClient();
89
90
  const [entities, setEntities] = useState<Entity.Unknown[]>([]);
90
91
  const [filter, setFilter] = useState('');
@@ -130,24 +131,24 @@ export const RegistryPanel = () => {
130
131
  const features: Partial<TableFeatures> = useMemo(() => ({ selection: { enabled: true, mode: 'single' } }), []);
131
132
 
132
133
  return (
133
- <Panel.Root>
134
- <Panel.Toolbar asChild>
134
+ <Panel.Root role={role}>
135
+ <Panel.Header>
135
136
  <Toolbar.Root>
136
137
  <Searchbar placeholder='Filter...' onChange={setFilter} />
137
138
  </Toolbar.Root>
138
- </Panel.Toolbar>
139
- <Panel.Content>
139
+ </Panel.Header>
140
+ <Panel.Body>
140
141
  <div className={mx('h-full grid grid-cols-[2fr_1fr] overflow-hidden')}>
141
- <div className={mx('flex flex-col min-h-0 overflow-hidden')}>
142
+ <div className={mx('flex flex-col dx-grow overflow-hidden')}>
142
143
  <DynamicTable properties={properties} rows={rows} features={features} onRowClick={handleRowClicked} />
143
144
  </div>
144
- <div className={mx('min-h-0 h-full overflow-auto border-s border-separator text-sm')}>
145
+ <div className={mx('h-full overflow-auto border-s border-separator text-sm')}>
145
146
  {detailJson ? <JsonView data={detailJson} /> : <Placeholder label='Details' />}
146
147
  </div>
147
148
  </div>
148
- </Panel.Content>
149
+ </Panel.Body>
149
150
  </Panel.Root>
150
151
  );
151
152
  };
152
153
 
153
- RegistryPanel.displayName = 'RegistryPanel';
154
+ RegistryArticle.displayName = 'RegistryArticle';
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export { RegistryArticle, RegistryArticle as default } from './RegistryArticle.tsx';
@@ -7,13 +7,20 @@ import React from 'react';
7
7
  import { EdgeServiceName } from '@dxos/config';
8
8
  import { useEdgeServiceEndpoint } from '@dxos/react-client';
9
9
  import { ToolsExplorer } from '@dxos/react-ui-introspect';
10
+ import * as Panel from '@dxos/react-ui/Panel';
10
11
 
11
12
  /**
12
13
  * Binds the tools explorer to the introspect endpoint from config; the explorer renders its
13
- * unconfigured state when `runtime.services.edgeServices` has no `introspect` entry.
14
+ * unconfigured state when neither `runtime.services.edge.url` nor an `introspect` override is set.
14
15
  */
15
- export const ToolsExplorerContainer = () => {
16
- return <ToolsExplorer serverUrl={useEdgeServiceEndpoint(EdgeServiceName.Introspect)} />;
16
+ export const ToolsExplorerContainer = ({ role }: { role?: string }) => {
17
+ return (
18
+ <Panel.Root role={role}>
19
+ <Panel.Body>
20
+ <ToolsExplorer serverUrl={useEdgeServiceEndpoint(EdgeServiceName.Introspect)} />
21
+ </Panel.Body>
22
+ </Panel.Root>
23
+ );
17
24
  };
18
25
 
19
26
  ToolsExplorerContainer.displayName = 'ToolsExplorerContainer';
@@ -2,4 +2,4 @@
2
2
  // Copyright 2026 DXOS.org
3
3
  //
4
4
 
5
- export { ToolsExplorerContainer, ToolsExplorerContainer as default } from './ToolsExplorerContainer';
5
+ export { ToolsExplorerContainer, ToolsExplorerContainer as default } from './ToolsExplorerContainer.tsx';
@@ -4,9 +4,9 @@
4
4
 
5
5
  import { type ComponentType, lazy } from 'react';
6
6
 
7
- export const CliPanel: ComponentType<any> = lazy(() => import('./CliPanel'));
8
- export const DebugGraph: ComponentType<any> = lazy(() => import('./DebugGraph'));
9
- export const DevtoolsOverviewContainer: ComponentType<any> = lazy(() => import('./DevtoolsOverviewContainer'));
10
- export const GithubPanel: ComponentType<any> = lazy(() => import('./GithubPanel'));
11
- export const RegistryPanel: ComponentType<any> = lazy(() => import('./RegistryPanel'));
12
- export const ToolsExplorerContainer: ComponentType<any> = lazy(() => import('./ToolsExplorerContainer'));
7
+ export const CliArticle: ComponentType<any> = lazy(() => import('./CliArticle/index.ts'));
8
+ export const DebugGraph: ComponentType<any> = lazy(() => import('./DebugGraph/index.ts'));
9
+ export const DevtoolsOverviewContainer: ComponentType<any> = lazy(() => import('./DevtoolsOverviewContainer/index.ts'));
10
+ export const GithubPanel: ComponentType<any> = lazy(() => import('./GithubPanel/index.ts'));
11
+ export const RegistryArticle: ComponentType<any> = lazy(() => import('./RegistryArticle/index.ts'));
12
+ export const ToolsExplorerContainer: ComponentType<any> = lazy(() => import('./ToolsExplorerContainer/index.ts'));
package/src/index.ts CHANGED
@@ -2,6 +2,5 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- export * as DevtoolsPlugin from './DevtoolsPlugin';
6
- export * from '#meta';
5
+ export * as DevtoolsPlugin from './DevtoolsPlugin.ts';
7
6
  export * from '#types';
package/src/meta.ts CHANGED
@@ -4,6 +4,6 @@
4
4
 
5
5
  import * as Plugin from '@dxos/app-framework/Plugin';
6
6
 
7
- import config from '../dx.config';
7
+ import config from '../dx.config.ts';
8
8
 
9
9
  export const meta = Plugin.getMetaFromConfig(config);
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { describe, test } from 'vitest';
6
6
 
7
- import { createComposerTestApp } from '@dxos/plugin-testing/harness';
7
+ import * as Harness from '@dxos/plugin-testing/Harness';
8
8
 
9
9
  import { meta } from '#meta';
10
10
  import { DevtoolsPlugin } from '#plugin';
@@ -15,7 +15,7 @@ describe('DevtoolsPlugin', () => {
15
15
  // Boot imports the app-graph-builder body (the harness fires the plugin's start event), which
16
16
  // can exceed the default 15s under vite-node transform load.
17
17
  test('modules activate on the expected events', { timeout: 60_000 }, async ({ expect }) => {
18
- await using harness = await createComposerTestApp({
18
+ await using harness = await Harness.createComposerTestApp({
19
19
  plugins: [DevtoolsPlugin()],
20
20
  });
21
21
 
@@ -2,13 +2,16 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { type Resource } from '@dxos/react-ui';
6
5
  import { translations as introspectTranslations } from '@dxos/react-ui-introspect/translations';
6
+ import { translations as tableTranslations } from '@dxos/react-ui-table/translations';
7
+ import type * as Theme from '@dxos/react-ui/Theme';
7
8
 
8
9
  import { meta } from '#meta';
9
10
 
10
11
  export const translations = [
11
12
  ...introspectTranslations,
13
+ // The panels' tables (credentials, feeds, …) read their column menus from react-ui-table.
14
+ ...tableTranslations,
12
15
  {
13
16
  'en-US': {
14
17
  [meta.profile.key]: {
@@ -25,7 +28,7 @@ export const translations = [
25
28
  'config.label': 'Config',
26
29
  'storage.label': 'Storage',
27
30
  'sqlite.label': 'SQLite',
28
- 'logging.label': 'Logging',
31
+ 'logging.label': 'Logs',
29
32
  'diagnostics.label': 'Diagnostics',
30
33
  'tracing.label': 'Tracing',
31
34
  'halo.label': 'HALO',
@@ -38,6 +41,7 @@ export const translations = [
38
41
  'space.label': 'Space',
39
42
  'feeds.label': 'Feeds',
40
43
  'objects.label': 'Objects',
44
+ 'queries.label': 'Queries',
41
45
  'schema.label': 'Schema',
42
46
  'registry.label': 'Registry',
43
47
  'automerge.label': 'Automerge',
@@ -64,4 +68,4 @@ export const translations = [
64
68
  },
65
69
  },
66
70
  },
67
- ] as const satisfies Resource[];
71
+ ] as const satisfies Theme.Resource[];