You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The Data Pane Expand action opens the current result in a separate browser tab, with an in-app overlay fallback.
Today that detached view is intentionally a frozen snapshot:
it can switch between result representations;
it can sort, resize columns, inspect cells, and copy data;
it does not retain the source query as an independently runnable surface;
it has no {name:Type} filter controls;
changing a parameter in the workbench does not update it.
That is useful for inspecting a result, but it is not sufficient as a full-screen analytical panel. A user who expands a filtered query should be able to change its filters and inspect the refreshed result without returning to the SQL editor.
The detached view must also track the Panels work: after #166, the result switcher is Table | JSON | Panel, not a detached copy of the old Chart-only behavior.
Goal
Turn the detached Data view into a self-contained, read-only query surface:
Query · system.text_log
Warnings emitted by ClickHouse and authentication services
┌──────────────────────────────────────────────────────────────────────┐
│ Table | JSON | Panel 12 rows Copy │
├──────────────────────────────────────────────────────────────────────┤
│ level: [Warning ▾] from: [-7d] to: [2026-07-10] Refresh │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ current result │
│ │
└──────────────────────────────────────────────────────────────────────┘
Requirements:
Preserve the full result-view switcher: Table | JSON | Panel.
Show the query's parameter controls when its source SQL declares {name:Type} fields.
Re-run only the detached view when a filter is committed.
Use the same persisted filter values, optional-filter activation, and recent-value history as the SQL Browser and dashboards.
Keep the main workbench result, active view, sorting, and panel state unchanged.
Continue supporting both a real child tab and the existing overlay fallback.
Product semantics
Open behavior
Opening the detached view must be immediate:
render the current result snapshot first;
do not issue another request merely because Expand was clicked;
capture the exact source SQL that produced the result;
capture the saved-query title and description associated with that run;
capture the originating tab/session identity;
clone the source panel config into detached-local state.
The detached view remains bound to that captured source. Later edits or runs in the SQL editor do not replace its SQL or result.
Panel config changes made later in the workbench do not live-sync into an already-open detached view.
Do not maintain a separate detached-only list of panel types. The detached surface must dispatch through the same panel registry as the workbench and dashboard.
Filter controls
Analyze the captured source SQL through the shared parameter pipeline.
Render the same field families already supported elsewhere:
text;
declared Enum;
schema-inferred Enum suggestions;
relative time;
recent values;
optional-filter activation;
validation and type-conflict states.
Only fields referenced by this one source are shown.
When the source has no parameters, omit the filter row entirely—no empty toolbar or spacing.
Shared filter state
Treat the detached view as a full-screen preview of the same query/panel used by the SQL Browser and dashboards.
Its filter controls read and write the existing shared stores:
Use the same persistence paths and keys as the other two surfaces:
asb:varValues;
asb:filterActive;
asb:varRecent;
asb:varRecentDisabled.
A value entered in any of the three surfaces must be the value offered by the other surfaces. The detached view must not introduce a second value map, detached-only activation state, or detached-only recent-value history.
On every committed edit:
update app.state.varValues[name];
update app.state.filterActive[name] using the existing optional-filter rules;
persist both stores immediately through the same helpers used by the SQL Browser/dashboard controls;
repaint the detached field from the shared state.
Changing a shared value in the detached view does not automatically re-run the SQL Browser or dashboard. It re-runs only this detached query. Other surfaces use the updated value on their next explicit run, refresh, or relevant control repaint.
When the detached surface and another surface share the same application instance, their controls should reflect the shared in-memory value. Separate browser application tabs share through browser storage. If live cross-tab control repaint is already supported by the common storage layer, preserve it; introducing a new detached-only synchronization mechanism is unnecessary.
Recent values
A successful detached run records bound values through the exact same app.recordBoundParams(...) path as SQL Browser runs and dashboard tile runs.
Record only after the winning request completes successfully:
never on validation failure;
never on request error;
never for an aborted or stale superseded response;
never for parameters confined to inactive optional blocks;
respect varRecentDisabled;
preserve the existing MRU/deduplication/cap behavior.
The detached view reads suggestions from the same recent-value store and writes successful bound values back to that store. This is shared browser behavior, not detached-view-local history.
Commit and refresh behavior
A committed filter change re-runs only the detached query:
Enter and blur commit immediately;
dropdown, enum, and relative-time selections commit immediately;
free-text typing uses the existing dashboard-style debounce;
a visible Refresh button re-runs with the current local values;
switching Table/JSON/Panel never re-runs.
Before a request:
prepare the source using one wall-clock snapshot;
apply optional-block activation;
validate and serialize typed parameters;
block the request when required values are missing or invalid;
show field-level validation in the filter row.
A blocked detached run keeps the previous result visible and displays a concise status such as:
Enter a value for: from
Result replacement
A successful detached run replaces only the detached result and updates:
rows and columns;
elapsed time;
bytes/rows metadata;
capped state;
the Table, JSON, and Panel renderers.
The main tab's tab.result, resultView, resultSort, panel config, and history are untouched.
Copy always copies the detached view's current result, not the original expand-time snapshot.
Request concurrency and cancellation
Each detached Data view owns:
AbortControllergenerationrunning
A newer filter run supersedes an older one.
Abort the previous detached request when practical.
Ignore any response whose generation is stale.
Disable Refresh while that detached view's request is active.
Closing the tab/overlay aborts its request and destroys any live panel/chart renderer.
A detached request must not toggle the workbench's global state.running.
Multiple detached views may exist independently.
Source-query identity
expandDataPane(app, result) currently receives only a rendered result snapshot. Interactive filters require the exact query template and execution context.
Record execution metadata on normal row-returning results:
result.source={
sql,// exact authored single-statement template used for this run
tabId,// originating tab
rowLimit,// cap used by the run
title,// query title captured at run time
description,// query description captured at run time, or ''}
Rules:
For a normal whole-tab single-statement run, sql is that statement's authored template.
For Run selection, sql is the selected statement text captured at run time.
Optional-block markers remain in source.sql; the parameter pipeline derives the execution view on every detached refresh.
Explicit raw FORMAT results, EXPLAIN results, script summaries, errors without a structured result, and empty results remain ineligible for Data Pane Expand.
Do not reconstruct source SQL later from the active editor; it may have changed or another tab may be active.
Do not resolve title or description from the current Library at expand time; the saved entry may have been renamed, edited, deleted, or another tab may be active.
For a saved query, capture its saved name and description.
For an unsaved query, use the current tab name as the title.
If the title is blank or still the default Untitled, derive a stable fallback with the existing query-name helper.
Missing descriptions normalize to an empty string.
The panel config is cloned separately at expand time because it is presentation state, not query execution metadata.
Execution seam
Do not call the normal workbench run() action from the detached view. It mutates the active tab, global running state, history, result view, and editor-coupled state.
Add a dedicated app/controller seam, for example:
app.runDetachedData({
source,
values,// snapshot of shared app.state.varValues for this wave
active,// snapshot of shared effective activation for this wave
signal,
queryId,})// -> { columns, rows, progress, capped, error }
It should reuse the same lower-level behavior as a normal structured SELECT:
fresh-token check;
parameter analysis/preparation;
optional-block execution view;
native ClickHouse param_* bindings;
server-side result cap;
streaming/parser logic;
total error shape.
It must not:
write tab.result;
write query history;
change state.resultView;
change state.resultSort;
change state.running;
trigger schema refresh.
After a successful winning response, the detached controller must call the existing shared recent-value recorder with that prepared source's boundParams. The lower-level request seam should return or expose those immutable bound-parameter snapshots rather than re-reading mutable global values after the request.
Session behavior
The detached rerun must preserve the originating tab's ClickHouse session when that source depends on session state such as temporary tables or SET.
Do not read app.activeTab() at refresh time. Resolve the captured source.tabId.
Because ClickHouse serializes requests within one session, prevent detached and workbench requests from racing on the same session:
reuse the existing session-lock retry policy, or
introduce a small per-session request queue shared by workbench and detached reads.
A detached query must never silently drop the originating session and produce a different result.
If the source tab no longer exists, its captured session identifier may still be used for the lifetime of the detached view; otherwise show a clear error rather than switching to another tab's session.
UI layout
Detached title and description
Replace the generic Data heading with the captured query identity.
Title
Render the title as the primary heading:
Query · system.text_log
Requirements:
use the captured result.source.title;
preserve the user's capitalization and punctuation;
render as plain text, never HTML;
single line on desktop;
ellipsize visually when necessary;
keep the full title in the title attribute;
use an actual heading element;
set the child browser tab's document.title to the same value.
Description
Render the captured description directly below the title:
Warnings emitted by ClickHouse and authentication services
Requirements:
show it only when non-empty after trimming;
preserve meaningful internal whitespace while collapsing accidental line breaks for the header;
render as plain text, never Markdown or HTML in this surface;
use a smaller, muted style;
allow up to two lines;
clamp overflow with an ellipsis;
expose the full description through the title attribute;
do not reserve vertical space when no description exists.
Header structure
The detached title bar should be structured as:
[ title ] [ close, overlay only ]
[ description ]
The view switcher, result statistics, filters, Refresh, and Copy remain in the result toolbars below it. Do not place the query description into the same compact row as those controls.
Top toolbar
Below the title/description header, keep the compact first result row:
Table | JSON | Panel rows / elapsed / capped Copy
The browser tab itself supplies closing chrome. The overlay fallback retains its close button in the detached title bar.
Filter row
Add a second row below the view toolbar:
level: [Warning] from: [-7d] to: [2026-07-10] Refresh
Behavior:
sticky with the top toolbar while result content scrolls;
wraps cleanly on narrower widths;
field labels and controls use the shared parameter-control styles;
Refresh remains aligned to the end when space permits;
no horizontal page overflow;
hidden completely when there are no fields.
The result body consumes the remaining height.
Accessibility
The view switcher uses the existing accessible tab/button semantics.
The filter row has an accessible label such as Query filters.
Refresh exposes its running/disabled state.
Validation is available through visible text and accessible descriptions.
Keyboard focus remains in the edited field across local repaint where possible.
Shared-control extraction
Do not copy the dashboard's private buildFilterBar() or the workbench's variable-strip field construction into results.js.
Extract a reusable parameter-control builder that supports:
Surface-specific layout wrappers may remain separate, but type selection, validation state, optional behavior, enum/relative/recent controls, and commit wiring must have one implementation.
If extracting all three consumers in one PR is too disruptive, at minimum extract the field-level builder first and keep only the row composition surface-specific.
Files
Likely changes:
src/ui/results.js
detached controller;
Table/JSON/Panel switcher;
filter row;
local rerun lifecycle.
src/ui/app.js
capture result source metadata;
add isolated detached-run seam.
src/ui/param-controls.js or equivalent
shared parameter field builder.
src/ui/dashboard.js
consume the shared field builder instead of private duplicated construction.
workbench variable-strip code
consume the shared field builder where practical.
src/core/param-pipeline.js
no new semantics expected; use the existing analyze/prepare API.
src/styles.css
detached filter row and sticky/full-height layout.
tests for results, app/controller, parameter controls, and detached fallback.
Tests
Source capture
Whole-tab single statement captures its exact authored source.
Saved-query runs capture the saved title and description used for that run.
Unsaved runs capture the tab title and derive a fallback when it is blank or Untitled.
Renaming or deleting the saved query after the run does not change an already-open detached view.
Title and description render as text and cannot inject markup.
Run selection captures the selected SQL, not the whole editor.
Changing editor text after the run does not change detached refresh SQL.
Switching active tabs does not change detached source/session identity.
Ineligible result kinds do not expose Expand.
View switcher
Detached view renders Table, JSON, and Panel.
Switching views performs zero network requests.
View state is local and does not change the workbench result view.
Panel rendering uses the shared registry and a cloned read-only config.
Destroy hooks run on switch and close.
Filters
No parameter declarations: no filter row.
Required and optional parameters render correctly.
Enum, inferred Enum, relative-time, and recent-value controls work.
Missing/invalid values block the request and preserve the previous result.
Optional blank values remove their optional SQL blocks.
Enter/blur commit immediately; text typing debounces.
Refresh runs with the current shared persisted values.
Edits update and persist the same value/activation stores used by SQL Browser and dashboards.
A successful detached run records recent bound values through the shared recorder.
Failed, aborted, validation-blocked, or stale runs do not record recents.
Detached edits do not automatically run or replace results in the other surfaces.
Requests
Opening Expand performs no extra query.
A filter commit runs exactly one detached request.
A newer run wins over an older response.
Closing aborts an in-flight request.
Detached running state does not modify global state.running.
Main result/history/sort/view remain unchanged.
Current detached result is used by Copy.
Row cap and capped indicator are preserved across reruns.
Originating ClickHouse session is used when present.
Shared varValues and filterActive are persisted on commit.
Successful winning responses record their immutable boundParams.
Errors, aborts, stale responses, inactive optional parameters, and disabled-history mode do not record recents.
Detached-document behavior
Run the same cases for:
real child-tab document;
popup-blocked overlay fallback.
Verify that all nodes are created in the correct document realm and that cell drawers, controls, panel renderers, and event listeners are cleaned up.
Acceptance criteria
Expanded Data opens immediately with the existing result and issues no extra request.
The detached header shows the captured query title instead of the generic Data label.
A non-empty captured description appears below the title with two-line clamping; an empty description consumes no space.
The browser tab title matches the captured query title.
It exposes a local Table | JSON | Panel switcher.
Parameterized source SQL produces a full filter row using the shared field controls.
Filter commits and Refresh re-run only the detached query.
Detached filter values and optional activation are local and do not mutate global workbench/dashboard values.
The exact expand-time source SQL and originating session are used even after editor/tab changes.
Missing or invalid values block only the detached refresh and leave the previous result visible.
Problem
The Data Pane Expand action opens the current result in a separate browser tab, with an in-app overlay fallback.
Today that detached view is intentionally a frozen snapshot:
{name:Type}filter controls;That is useful for inspecting a result, but it is not sufficient as a full-screen analytical panel. A user who expands a filtered query should be able to change its filters and inspect the refreshed result without returning to the SQL editor.
The detached view must also track the Panels work: after #166, the result switcher is Table | JSON | Panel, not a detached copy of the old Chart-only behavior.
Goal
Turn the detached Data view into a self-contained, read-only query surface:
Requirements:
{name:Type}fields.Product semantics
Open behavior
Opening the detached view must be immediate:
The detached view remains bound to that captured source. Later edits or runs in the SQL editor do not replace its SQL or result.
View switcher
The switcher is local to the detached view:
Do not maintain a separate detached-only list of panel types. The detached surface must dispatch through the same panel registry as the workbench and dashboard.
Filter controls
Analyze the captured source SQL through the shared parameter pipeline.
Render the same field families already supported elsewhere:
Only fields referenced by this one source are shown.
When the source has no parameters, omit the filter row entirely—no empty toolbar or spacing.
Shared filter state
Treat the detached view as a full-screen preview of the same query/panel used by the SQL Browser and dashboards.
Its filter controls read and write the existing shared stores:
Use the same persistence paths and keys as the other two surfaces:
asb:varValues;asb:filterActive;asb:varRecent;asb:varRecentDisabled.A value entered in any of the three surfaces must be the value offered by the other surfaces. The detached view must not introduce a second value map, detached-only activation state, or detached-only recent-value history.
On every committed edit:
app.state.varValues[name];app.state.filterActive[name]using the existing optional-filter rules;Changing a shared value in the detached view does not automatically re-run the SQL Browser or dashboard. It re-runs only this detached query. Other surfaces use the updated value on their next explicit run, refresh, or relevant control repaint.
When the detached surface and another surface share the same application instance, their controls should reflect the shared in-memory value. Separate browser application tabs share through browser storage. If live cross-tab control repaint is already supported by the common storage layer, preserve it; introducing a new detached-only synchronization mechanism is unnecessary.
Recent values
A successful detached run records bound values through the exact same
app.recordBoundParams(...)path as SQL Browser runs and dashboard tile runs.Record only after the winning request completes successfully:
varRecentDisabled;The detached view reads suggestions from the same recent-value store and writes successful bound values back to that store. This is shared browser behavior, not detached-view-local history.
Commit and refresh behavior
A committed filter change re-runs only the detached query:
Before a request:
A blocked detached run keeps the previous result visible and displays a concise status such as:
Result replacement
A successful detached run replaces only the detached result and updates:
The main tab's
tab.result,resultView,resultSort, panel config, and history are untouched.Copy always copies the detached view's current result, not the original expand-time snapshot.
Request concurrency and cancellation
Each detached Data view owns:
state.running.Multiple detached views may exist independently.
Source-query identity
expandDataPane(app, result)currently receives only a rendered result snapshot. Interactive filters require the exact query template and execution context.Record execution metadata on normal row-returning results:
Rules:
sqlis that statement's authored template.sqlis the selected statement text captured at run time.source.sql; the parameter pipeline derives the execution view on every detached refresh.FORMATresults, EXPLAIN results, script summaries, errors without a structured result, and empty results remain ineligible for Data Pane Expand.nameanddescription.Untitled, derive a stable fallback with the existing query-name helper.The panel config is cloned separately at expand time because it is presentation state, not query execution metadata.
Execution seam
Do not call the normal workbench
run()action from the detached view. It mutates the active tab, global running state, history, result view, and editor-coupled state.Add a dedicated app/controller seam, for example:
It should reuse the same lower-level behavior as a normal structured SELECT:
param_*bindings;It must not:
tab.result;state.resultView;state.resultSort;state.running;After a successful winning response, the detached controller must call the existing shared recent-value recorder with that prepared source's
boundParams. The lower-level request seam should return or expose those immutable bound-parameter snapshots rather than re-reading mutable global values after the request.Session behavior
The detached rerun must preserve the originating tab's ClickHouse session when that source depends on session state such as temporary tables or
SET.Do not read
app.activeTab()at refresh time. Resolve the capturedsource.tabId.Because ClickHouse serializes requests within one session, prevent detached and workbench requests from racing on the same session:
A detached query must never silently drop the originating session and produce a different result.
If the source tab no longer exists, its captured session identifier may still be used for the lifetime of the detached view; otherwise show a clear error rather than switching to another tab's session.
UI layout
Detached title and description
Replace the generic
Dataheading with the captured query identity.Title
Render the title as the primary heading:
Requirements:
result.source.title;titleattribute;document.titleto the same value.Description
Render the captured description directly below the title:
Requirements:
titleattribute;Header structure
The detached title bar should be structured as:
The view switcher, result statistics, filters, Refresh, and Copy remain in the result toolbars below it. Do not place the query description into the same compact row as those controls.
Top toolbar
Below the title/description header, keep the compact first result row:
The browser tab itself supplies closing chrome. The overlay fallback retains its close button in the detached title bar.
Filter row
Add a second row below the view toolbar:
Behavior:
The result body consumes the remaining height.
Accessibility
Query filters.Shared-control extraction
Do not copy the dashboard's private
buildFilterBar()or the workbench's variable-strip field construction intoresults.js.Extract a reusable parameter-control builder that supports:
Expected consumers:
Surface-specific layout wrappers may remain separate, but type selection, validation state, optional behavior, enum/relative/recent controls, and commit wiring must have one implementation.
If extracting all three consumers in one PR is too disruptive, at minimum extract the field-level builder first and keep only the row composition surface-specific.
Files
Likely changes:
src/ui/results.jssrc/ui/app.jssrc/ui/param-controls.jsor equivalentsrc/ui/dashboard.jssrc/core/param-pipeline.jssrc/styles.cssTests
Source capture
Untitled.View switcher
Filters
Requests
state.running.varValuesandfilterActiveare persisted on commit.boundParams.Detached-document behavior
Run the same cases for:
Verify that all nodes are created in the correct document realm and that cell drawers, controls, panel renderers, and event listeners are cleaned up.
Acceptance criteria
Datalabel.Non-goals
FORMATor EXPLAIN detached reruns.Dependencies and sequencing
/*[ … ]*/+ explicit filter activation — an empty parameter cannot omit surrounding SQL #165 and related field types).