Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
52 changes: 44 additions & 8 deletions npm_modules/cli/debugger/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ the CLI package.
- `debugger-render.js`: header, target list, tree, preview overlay, inspector, and export rendering.
- `debugger-runtime.js`: target discovery, snapshots, runtime log streaming, heap, and copy/export helpers.
- `debugger-performance.js`: renderer trace and Hermes CPU profile controls.
- `debugger-providers.js`: provider discovery plus read-only Storage and SQL inspector rendering.
- `debugger-settings.js`: published debug-setting discovery, rendering, validation, and mutation requests.
- `debugger-actions.js`: UI actions, command prompt handling, auto-refresh, and externally driven debugger actions.
- `debugger-session.js`: `sessionStorage` restore/persist for reload-friendly debugger state.
- `debugger-bootstrap.js`: DOM event wiring and boot sequence.
Expand All @@ -36,7 +38,10 @@ Important routes:
- `/api/snapshot`: fetches the selected target's view tree and preview data.
- `/api/runtime-logs` and `/api/runtime-logs/stream`: read and stream target logs.
- `/api/debugger/state`, `/api/debugger/events`, and `/api/debugger/actions`: keep the browser UI and external agents in sync.
- `/api/input`: validates and forwards bounded input requests to the selected Valdi target.
- `/api/performance/trace/*`: start, stop, capture, and export native renderer traces.
- `/api/debugger/providers` and `/api/debugger/providers/request`: discover and query target-owned debugger providers.
- `/api/debugger/settings`: discover and update target-published debug settings.
- `/api/performance/profile/*`: list Hermes contexts and capture CPU profiles.
- `/api/devtools/target`: matches the inspected Chromium page to the exact configured preview origin and path.
- `/api/devtools/snapshot`, `/api/devtools/highlight`, and `/api/devtools/evaluate`: proxy the explicit web debugger bridge contract through loopback CDP.
Expand All @@ -48,14 +53,44 @@ One-shot captures are limited to 15 seconds so the debugger handler can retain
the result before the native recorder's independent safety timeout, and
exported JSON can be opened in Perfetto. Native bounds report dropped event
counts, and retained timeout/retry results expire after one minute.
Hermes CPU profiling uses the existing inspector transport. Target input
forwarding and data/network provider tabs should likewise land
with their runtime-side contracts and end-to-end tests rather than as inactive
browser-only surfaces.
Web-renderer inspection depends on the target page explicitly exposing
Hermes CPU profiling uses the existing inspector transport.

The native and synthetic previews forward capability, query, tap, focus, text,
key, and scroll requests through the selected target's bounded input contract.
Web-renderer inspection uses the first-party bridge exposed as
`window.__VALDI_WEB_DEBUGGER__` with `getSnapshot()`, `highlightNode()`, and
`clearHighlight()`. The renderer-side adapter is intentionally outside this
CLI/DevTools core.
`clearHighlight()`. The DevTools panel proxies inspection through the exact
configured loopback Chromium target.

The Data section discovers target-owned providers through a generic custom
message contract. This slice includes read-only Storage and SQL presentation,
but it does not register either backend. A later thin registration layer can
adapt a bounded Storage snapshot callback or an existing SQL API without
changing this contract. Until then, the UI reports both surfaces as unavailable
instead of synthesizing sample data. Network and key-value integrations are not
part of this slice and remain unavailable unless a future runtime provider is
registered.

Runtime adapters cross the provider boundary with an already serialized JSON
object document, not an arbitrary object graph. They should call
`createDebuggerProviderOwner(module, 'stable/adapter/module/key')`, register
their provider through that owner, and return
`createDebuggerProviderResult(JSON.stringify(snapshot))` from `handleRequest`.
Core caps action documents at 48 KiB of UTF-8, validates their depth,
collection sizes, strings, and total value count, and then enforces 128 KiB on
the complete serialized custom-response body including metadata. Owners bind
to the creating module object's hot-reload callback automatically, including
webpack modules where `module.path` is absent. Adapters call
`owner.dispose()` only when stopping before a reload. A newer registration for
an existing provider ID permanently invalidates the older registration. This
is the integration point for the later thin Storage/SQL registration layer.
On native runtimes the owner observes `module.path`; the explicit stable key is
replacement identity only. Web runtimes fall back to observing that stable key
because webpack does not provide `module.path`.

Published settings are application-owned controls registered only in debug
runtimes. Values are limited to declared toggle, select, text, and number
settings; the server does not expose arbitrary runtime property mutation.

Detailed debugger snapshots explicitly opt in to component ViewModel and state
serialization. That data can be sensitive, is bounded by a per-field and
Expand Down Expand Up @@ -110,7 +145,8 @@ If you add a new static asset type, update the server MIME map and watcher.
Session state is persisted in `sessionStorage` under
`valdi.debugger.session.v1`, so normal debugger refreshes should preserve the
active section, selected target/node, filters, expanded tree nodes, and capture
settings.
settings, plus the active provider and published-settings group. Provider and
settings payloads are cleared when the selected target changes.

## Validation

Expand Down
31 changes: 29 additions & 2 deletions npm_modules/cli/debugger/debugger-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ function setActiveSection(section) {
if (normalized === UI_SECTION && state.autoRefresh && state.source === 'daemon' && isDebuggerAttached()) {
window.setTimeout(() => refreshLiveSnapshot(), 0);
}
maybeRefreshDebuggerTools(normalized);
}

function shouldAutoRefresh() {
Expand Down Expand Up @@ -154,6 +155,7 @@ function detachDebuggerView() {
state.manualDetach = true;
state.rootSnapshotImage = null;
state.rootSnapshotRequestId++;
resetDebuggerToolsForTarget();
stopRuntimeLogStream();
addLog('warn', 'proxy', 'Detached debugger view from live daemon data.');
render();
Expand All @@ -173,6 +175,7 @@ async function setTargetPort(port, nextTarget) {
state.followLatestTarget = true;
state.rootSnapshotImage = null;
state.rootSnapshotRequestId++;
resetDebuggerToolsForTarget();
stopRuntimeLogStream();
render();
}
Expand All @@ -182,7 +185,7 @@ function clearDebuggerLogs() {
renderLogs();
}

function applyDebuggerAction(action, params = {}) {
function applyDebuggerAction(action, params = {}, result) {
if (action === 'selectNode') {
const nodeId = actionString(params, 'id', 'nodeId');
if (nodeId) selectNode(nodeId);
Expand Down Expand Up @@ -291,6 +294,24 @@ function applyDebuggerAction(action, params = {}) {
return;
}

if (action === 'refreshDebuggerProviders') {
if (result) applyDebuggerProvidersResult(result, params);
else void refreshDebuggerProviders();
return;
}

if (action === 'refreshDebugSettings') {
if (result) applyDebugSettingsResult(result, params);
else void refreshDebugSettings();
return;
}

if (action === 'setDebugSetting' || action === 'resetDebugSetting') {
if (result) applyDebugSettingsResult(result, params);
else void refreshDebugSettings({ silent: true });
return;
}

addLog('warn', 'debugger', `Unknown debugger action: ${action}.`);
}

Expand All @@ -303,10 +324,16 @@ function runCommand(rawCommand) {
addLog(
'info',
'console',
'Commands: help, section <ui|performance|logs>, path, issues, select <id>, connect, refresh, reload, status, snapshot, heap, trace, profile, auto on, auto off, clear.',
'Commands: help, section <ui|performance|data|settings|logs>, data, settings, path, issues, select <id>, connect, refresh, reload, status, snapshot, heap, trace, profile, auto on, auto off, clear.',
);
} else if (command.startsWith('section ')) {
void requestDebuggerAction('setActiveSection', { section: command.slice('section '.length).trim() });
} else if (command === 'data') {
void requestDebuggerAction('setActiveSection', { section: 'data' });
void requestDebuggerAction('refreshDebuggerProviders', getSelectedTargetParams());
} else if (command === 'settings') {
void requestDebuggerAction('setActiveSection', { section: 'settings' });
void requestDebuggerAction('refreshDebugSettings', getSelectedTargetParams());
} else if (command === 'path') {
const path = getPathToNode(state.selectedNodeId)
.map(node => `${node.tag}#${getNodeId(node)}`)
Expand Down
19 changes: 16 additions & 3 deletions npm_modules/cli/debugger/debugger-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,19 +142,25 @@ function applyDebuggerActionPayload(payload) {
const revision = Number(payload?.state?.revision) || 0;
if (revision && revision <= state.lastDebuggerRevision) return;
if (payload?.action) {
applyDebuggerAction(payload.action, payload.params || {});
applyDebuggerAction(payload.action, payload.params || {}, payload.result);
}
if (revision) state.lastDebuggerRevision = revision;
}

async function requestDebuggerAction(action, params = {}) {
const shouldApplyLocally = !state.debuggerEventsConnected;
const runtimeActions = new Set([
'refreshDebuggerProviders',
'refreshDebugSettings',
'setDebugSetting',
'resetDebugSetting',
]);
const shouldApplyLocally = !state.debuggerEventsConnected && !runtimeActions.has(action);
if (shouldApplyLocally) {
applyDebuggerAction(action, params);
}

try {
await apiPost(
const response = await apiPost(
'/api/debugger/actions',
{},
{
Expand All @@ -164,11 +170,18 @@ async function requestDebuggerAction(action, params = {}) {
},
{ timeoutMs: 5000 },
);
const revision = Number(response?.state?.revision) || 0;
if (response?.result && (!revision || revision > state.lastDebuggerRevision)) {
applyDebuggerAction(action, params, response.result);
if (revision) state.lastDebuggerRevision = revision;
}
return response;
} catch (error) {
if (!shouldApplyLocally) {
applyDebuggerAction(action, params);
}
addLog('warn', 'debugger', `Debugger action bus unavailable; applied ${action} locally. ${error.message}`);
return null;
}
}

Expand Down
66 changes: 66 additions & 0 deletions npm_modules/cli/debugger/debugger-bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,23 @@ document.addEventListener('click', event => {
const issue = event.target.closest('[data-issue-node]');
if (issue && issue.dataset.issueNode) void requestDebuggerAction('selectNode', { id: issue.dataset.issueNode });

const providerTab = event.target.closest('[data-provider-tab]');
if (providerTab) setActiveProviderTab(providerTab.dataset.providerTab);

const resetSetting = event.target.closest('[data-reset-setting]');
if (resetSetting && !resetSetting.disabled) {
void changeDebugSetting('reset', resetSetting.dataset.resetSetting);
}

if (event.target.closest('#sqlPreviousButton')) {
state.providers.sqlOffset = Math.max(0, state.providers.sqlOffset - state.providers.sqlLimit);
void loadSqlTable();
}
if (event.target.closest('#sqlNextButton')) {
state.providers.sqlOffset += state.providers.sqlLimit;
void loadSqlTable();
}

const target = event.target.closest('.target');
if (target) void requestDebuggerAction('selectTarget', { id: target.dataset.targetId });
});
Expand Down Expand Up @@ -62,9 +79,17 @@ async function selectTarget(id) {
}
state.followLatestTarget = false;
if (target.clientId && target.contextId) {
resetDebuggerToolsForTarget();
state.snapshot.target = {
...state.snapshot.target,
...target,
state: 'attached',
};
render();
loadRealSnapshot(target);
return;
}
resetDebuggerToolsForTarget();
state.snapshot.target = {
...state.snapshot.target,
id: target.id,
Expand Down Expand Up @@ -176,6 +201,47 @@ elements.profileStartButton.addEventListener('click', () => void requestDebugger
elements.profileStopButton.addEventListener('click', () => void requestDebuggerAction('stopCpuProfile'));
elements.profileCaptureButton.addEventListener('click', () => void requestDebuggerAction('captureCpuProfile'));
elements.profileExportButton.addEventListener('click', exportCpuProfile);
elements.providerRefreshButton.addEventListener('click', () => {
void requestDebuggerAction('refreshDebuggerProviders', getSelectedTargetParams());
});
elements.settingsRefreshButton.addEventListener('click', () => {
void requestDebuggerAction('refreshDebugSettings', getSelectedTargetParams());
});
elements.settingsGroupSelect.addEventListener('change', () => {
state.settings.selectedGroupId = elements.settingsGroupSelect.value || null;
renderDebugSettings();
});

document.addEventListener('change', event => {
if (event.target.id === 'sqlDatabaseSelect') {
state.providers.selectedDatabaseId = event.target.value;
const database = selectedSQLDatabase();
state.providers.selectedTable = Array.isArray(database?.tables) ? database.tables[0]?.name || null : null;
state.providers.sqlOffset = 0;
state.providers.sqlTable = null;
void loadSqlTable();
return;
}
if (event.target.id === 'sqlTableSelect') {
state.providers.selectedTable = event.target.value;
state.providers.sqlOffset = 0;
state.providers.sqlTable = null;
void loadSqlTable();
return;
}
const setting = event.target.closest('[data-setting-id]');
if (setting) {
const parsed = readDebugSettingInput(setting);
if (parsed.error) {
setting.setCustomValidity?.(parsed.error);
setting.reportValidity?.();
addLog('warn', 'settings', parsed.error);
return;
}
setting.setCustomValidity?.('');
void changeDebugSetting('set', setting.dataset.settingId, parsed.value);
}
});

document.getElementById('copyPathButton').addEventListener('click', async () => {
const path = getPathToNode(state.selectedNodeId)
Expand Down
Loading
Loading