Problem
During the Logs rescue path, the workbench renders editable Time / Message / Level controls from the saved Logs configuration:
const [controlsArm, controlsCfg] = rescueLogs
? [PANEL_TYPES.logs, { ...saved.cfg }]
: [PANEL_TYPES[resolved.cfg.type], resolved.cfg];
The rescue branch uses a shallow object spread.
The panel configuration contract intentionally preserves unknown fields for forward compatibility, and those unknown fields may be nested objects or arrays. A shallow spread therefore leaves nested values aliased to the live tab.panelCfg.
The core panel module already provides:
which performs the required deep clone for JSON-shaped panel configuration.
Current impact
The problem is currently latent rather than user-visible:
- Logs-owned fields (
time, msg, level) are strings;
- the current Logs controls replace top-level properties rather than mutating nested values;
- no known control presently mutates an unknown nested field in place.
However, the rescue path violates the panel-config ownership invariant:
Controls must receive a detached configuration clone and must never share mutable nested state with the saved tab configuration.
A future object-valued Logs field or extension field could otherwise mutate the saved configuration before the explicit onChange write-back.
Goal
Use the canonical deep-clone helper before passing a saved Logs configuration to rescue controls.
Implementation
src/ui/panels.js
Import:
from:
Replace:
[PANEL_TYPES.logs, { ...saved.cfg }]
with:
[PANEL_TYPES.logs, clonePanelCfg(saved.cfg)]
The normal non-rescue path continues using resolved.cfg, which is already a normalized deep clone returned by resolvePanel().
No change is required to:
- Logs role behavior;
- rescue detection;
- fallback rendering;
- panel type switching;
- dashboard or detached views;
- saved-query serialization.
Why the Table readonly observation is not included
The Table arm's sorting, resizing, and cell-detail interactions do not edit or persist panel configuration.
In the Panel registry, readonly means:
no panel-authoring controls and no panel-config write-back
It does not mean that a rendered result must be inert.
Local Table interactions are intentionally available on read-only result surfaces:
- sorting changes local view state only;
- column widths change local view state only;
- cell-detail inspection does not modify the query or panel configuration.
Therefore the Table arm ignoring readonly is not a correctness bug and should not be changed as part of this issue.
A future request to make a specific preview completely non-interactive would need a separate capability such as:
rather than redefining readonly.
Tests
Add a rescue-path test with a nested unknown field:
const extension = {
nested: {
value: 1,
},
};
tab.panelCfg = {
type: 'logs',
extension,
};
Render a result that triggers Logs rescue, then change a Logs role.
Verify:
- the new
tab.panelCfg preserves extension;
tab.panelCfg.extension !== extension;
tab.panelCfg.extension.nested !== extension.nested;
- the original saved object remains unchanged;
- the selected Logs role is written correctly;
- the tab is marked dirty;
- no SQL query is executed.
Also verify that rendering without a control change does not mutate or replace tab.panelCfg.
Files
Expected changes:
src/ui/panels.js
tests/unit/panels.test.js
No new runtime dependency.
Acceptance criteria
Non-goals
- Disabling Table sorting.
- Disabling column resizing.
- Disabling cell-detail inspection.
- Redefining
readonly.
- Changing the panel configuration schema.
- Adding object-valued Logs fields.
Problem
During the Logs rescue path, the workbench renders editable Time / Message / Level controls from the saved Logs configuration:
The rescue branch uses a shallow object spread.
The panel configuration contract intentionally preserves unknown fields for forward compatibility, and those unknown fields may be nested objects or arrays. A shallow spread therefore leaves nested values aliased to the live
tab.panelCfg.The core panel module already provides:
which performs the required deep clone for JSON-shaped panel configuration.
Current impact
The problem is currently latent rather than user-visible:
time,msg,level) are strings;However, the rescue path violates the panel-config ownership invariant:
A future object-valued Logs field or extension field could otherwise mutate the saved configuration before the explicit
onChangewrite-back.Goal
Use the canonical deep-clone helper before passing a saved Logs configuration to rescue controls.
Implementation
src/ui/panels.jsImport:
clonePanelCfgfrom:
Replace:
with:
The normal non-rescue path continues using
resolved.cfg, which is already a normalized deep clone returned byresolvePanel().No change is required to:
Why the Table
readonlyobservation is not includedThe Table arm's sorting, resizing, and cell-detail interactions do not edit or persist panel configuration.
In the Panel registry,
readonlymeans:It does not mean that a rendered result must be inert.
Local Table interactions are intentionally available on read-only result surfaces:
Therefore the Table arm ignoring
readonlyis not a correctness bug and should not be changed as part of this issue.A future request to make a specific preview completely non-interactive would need a separate capability such as:
interactive: falserather than redefining
readonly.Tests
Add a rescue-path test with a nested unknown field:
Render a result that triggers Logs rescue, then change a Logs role.
Verify:
tab.panelCfgpreservesextension;tab.panelCfg.extension !== extension;tab.panelCfg.extension.nested !== extension.nested;Also verify that rendering without a control change does not mutate or replace
tab.panelCfg.Files
Expected changes:
src/ui/panels.jstests/unit/panels.test.jsNo new runtime dependency.
Acceptance criteria
clonePanelCfg(saved.cfg).Non-goals
readonly.