Problem
The detached Data view is already dynamic: changing a filter or clicking Refresh reruns the captured query through the shared streaming read seam.
During that rerun, the current implementation paints the in-flight result on every network chunk:
onChunk: () => {
if (myGen === gen && !closed) paint(result);
}
paint(result) rebuilds the complete Table / JSON / Panel surface.
This causes two visible problems.
Empty-result flash
A chunk may contain result metadata before it contains any data rows.
At that moment:
result.columns.length > 0
result.rows.length === 0
The shared result renderer displays:
Rows may arrive in the next chunk, so this message is temporarily false.
Chart churn
When Panel is selected, every chunk:
- destroys the current Chart.js instance;
- rebuilds the Panel view;
- constructs a new Chart.js instance.
The final result is correct, but a long streaming query can repeatedly recreate the chart while data is still arriving.
Relationship to the dashboard
This is the detached-view counterpart of the streaming UI decision already implemented for dashboard tiles.
Dashboard tiles now use the same app.runReadInto() transport, but their onChunk handler updates only the loading label:
onChunk: () => {
label.textContent =
'Loading… ' + formatRows(result.progress.rows) + ' rows';
}
Panel classification and rendering happen once, after the request completes successfully.
The detached view should follow the same commit-on-success UI policy:
stream transport progressively
→ update lightweight progress only
→ keep the previous committed result visible
→ render the new result once on success
This issue does not change dashboard behavior.
Goal
While a detached query reruns:
- keep the previous committed Table / JSON / Panel visible;
- show a lightweight running/progress status;
- do not render the in-flight result;
- do not destroy or recreate the current chart per chunk;
- replace the visible result once, only after a successful current-generation completion.
On failure, cancellation, supersession, or close, keep the previous committed result.
UX
Start
Before Refresh:
42 rows Copy
[previous committed result]
After Refresh starts:
42 rows Copy
Filters… Refresh disabled Running…
[previous committed result remains visible]
Progress
As streaming progress arrives:
Running… 12,400 rows read
Use the streamed progress counter only as progress text. It is not the committed result row count and must not replace the toolbar's existing committed-row statistic.
If no useful count has arrived, display:
Success
After a successful completion:
- assign the new result as
current;
- update the committed row statistic;
- repaint the selected Table / JSON / Panel once;
- record the winning request's bound parameters once;
- clear the running status;
- re-enable Refresh.
Error
On error:
- leave
current unchanged;
- leave its rendered view unchanged;
- show the error in the status line;
- record no recent values;
- re-enable Refresh.
There is no need to repaint current after the error because the in-flight result was never painted.
Superseded request
When a newer rerun supersedes an older one:
- abort the older request;
- ignore every late chunk and completion from it;
- let only the newest request control the status, Refresh button, commit, and recent-value recording.
Close
Closing the detached view aborts the active request and prevents all later UI writes.
Implementation
src/ui/results.js
Keep the existing:
- shared parameter analysis;
- prepared batch;
- token preflight;
- captured session;
- generation guard;
- AbortController;
app.runReadInto() call;
- success-only recent-value recording.
Change only the in-flight presentation policy.
Do not paint in-flight results
Replace:
onChunk: () => {
if (myGen === gen && !closed) paint(result);
}
with a progress-only callback:
onChunk: () => {
if (myGen !== gen || closed) return;
const rowsRead = Number(result.progress?.rows) || 0;
setStatus(
rowsRead > 0
? `Running… ${formatRows(rowsRead)} rows read`
: 'Running…',
);
}
Equivalent wording and factoring are acceptable.
The callback must not call:
and must not modify:
current
statEl
view.current
sort
widths
panelState
chartInstance
Start state
When the request begins:
running = true;
refreshBtn.disabled = true;
setStatus('Running…');
The already-rendered current result stays in the DOM.
Completion
After runReadInto():
if (myGen !== gen || closed) return;
Then handle outcomes in this order:
if (result.cancelled) {
settle('');
return;
}
if (result.error) {
settle(result.error);
return;
}
current = result;
app.recordBoundParams(
src.statements.flatMap((statement) => statement.boundParams),
);
settle('');
paint();
A superseded or close-triggered abort normally fails the generation/closed guard first. The explicit result.cancelled branch is still required as defensive behavior for a current-generation cancellation.
Do not call:
on failure or cancellation.
Toolbar row statistic
The toolbar statistic describes the committed current result.
It must not change during streaming.
It updates only from the final:
after current = result.
Chart ownership
The existing paint() function remains responsible for destroying the previous committed chart before replacing the result view.
Because paint() now runs only on:
- view changes;
- local Table sorting;
- initial render;
- successful result commit;
the active chart is not churned by network chunks.
Shared-code boundary
The following layers are already correctly shared between workbench, detached view, and dashboard:
app.runReadInto() — request, streaming parse, and normalized error/cancel result;
- parameter analysis and preparation;
buildFilterBar();
renderResultView() for Table / JSON / Panel dispatch;
renderResolvedPanel() and the Panel registry.
Do not introduce a generic “refreshable query surface” abstraction in this issue.
Detached and dashboard presentation semantics differ:
- detached keeps the previous committed result visible;
- dashboard replaces each slot with a loading placeholder;
- dashboard participates in a bounded multi-tile wave and reserves generations before queueing;
- detached is one locally owned request with captured session state.
A narrow reusable latest-request controller is tracked separately if desired; it is not required for this fix.
Tests
Previous result remains during chunks
Configure runReadInto() so the test can:
- emit a metadata-only chunk;
- emit a row chunk;
- resolve later.
After each chunk, verify:
- the previous committed table remains visible;
- its row count remains unchanged;
Query returned 0 rows. never appears;
- Refresh remains disabled;
- the status says
Running… or reports rows read.
Commit once on success
Before resolving, place new rows in the in-flight result.
Verify before completion:
- old rows remain visible;
- Copy still targets the old committed result;
- no recent values are recorded.
Resolve the request.
Verify:
- new rows become visible;
- Copy now targets the new committed result;
- the committed row statistic updates;
- recent values are recorded exactly once;
- Refresh is re-enabled;
- status is cleared.
Panel chart does not churn
Open the detached view on Panel and record the current Chart.js instance.
Emit multiple chunks without resolving.
Verify:
- the original chart is not destroyed;
- no additional chart instances are created.
Resolve successfully.
Verify:
- the old chart is destroyed once;
- one replacement chart is created for the committed result.
Error
Emit any number of chunks, then complete with:
Verify:
- the previous committed result remains visible throughout;
- no repaint is required to restore it;
- the status displays
Boom;
- recent values are not recorded;
- Refresh is re-enabled.
Stale and cancellation behavior
Verify:
- a newer rerun aborts the previous signal;
- late chunks from the previous run do not update status;
- late completion from the previous run does not commit or record recents;
- a current-generation cancelled result does not replace the committed result;
- closing aborts the request and prevents later status or paint calls.
Regression
Verify:
- opening the detached view still issues no request;
- filter gating still prevents invalid requests;
- originating
session_id is still passed;
- token-preflight failure leaves the previous result and re-enables Refresh;
- switching Table / JSON / Panel remains local and executes no SQL;
- dashboard progress-only rendering remains unchanged;
- workbench streaming behavior is not changed by this issue.
Files
Expected changes:
src/ui/results.js
- replace per-chunk surface repaint with status-only progress;
- commit and paint only after successful completion;
- remove failure-time restore repaint.
tests/unit/results.test.js
- metadata-only chunk;
- previous-result retention;
- success commit;
- chart-instance churn;
- error, stale, cancel, and close coverage.
No new runtime dependency.
Acceptance criteria
Non-goals
- Progressive Table, JSON, or Panel rendering.
- Changing workbench streaming behavior.
- Changing dashboard tile streaming behavior.
- Moving Refresh outside the detached filter row.
- Changing result caps or session behavior.
- A broad generic query-surface framework.
Problem
The detached Data view is already dynamic: changing a filter or clicking Refresh reruns the captured query through the shared streaming read seam.
During that rerun, the current implementation paints the in-flight result on every network chunk:
paint(result)rebuilds the complete Table / JSON / Panel surface.This causes two visible problems.
Empty-result flash
A chunk may contain result metadata before it contains any data rows.
At that moment:
The shared result renderer displays:
Rows may arrive in the next chunk, so this message is temporarily false.
Chart churn
When Panel is selected, every chunk:
The final result is correct, but a long streaming query can repeatedly recreate the chart while data is still arriving.
Relationship to the dashboard
This is the detached-view counterpart of the streaming UI decision already implemented for dashboard tiles.
Dashboard tiles now use the same
app.runReadInto()transport, but theironChunkhandler updates only the loading label:Panel classification and rendering happen once, after the request completes successfully.
The detached view should follow the same commit-on-success UI policy:
This issue does not change dashboard behavior.
Goal
While a detached query reruns:
On failure, cancellation, supersession, or close, keep the previous committed result.
UX
Start
Before Refresh:
After Refresh starts:
Progress
As streaming progress arrives:
Use the streamed progress counter only as progress text. It is not the committed result row count and must not replace the toolbar's existing committed-row statistic.
If no useful count has arrived, display:
Success
After a successful completion:
current;Error
On error:
currentunchanged;There is no need to repaint
currentafter the error because the in-flight result was never painted.Superseded request
When a newer rerun supersedes an older one:
Close
Closing the detached view aborts the active request and prevents all later UI writes.
Implementation
src/ui/results.jsKeep the existing:
app.runReadInto()call;Change only the in-flight presentation policy.
Do not paint in-flight results
Replace:
with a progress-only callback:
Equivalent wording and factoring are acceptable.
The callback must not call:
and must not modify:
Start state
When the request begins:
The already-rendered
currentresult stays in the DOM.Completion
After
runReadInto():Then handle outcomes in this order:
A superseded or close-triggered abort normally fails the generation/closed guard first. The explicit
result.cancelledbranch is still required as defensive behavior for a current-generation cancellation.Do not call:
on failure or cancellation.
Toolbar row statistic
The toolbar statistic describes the committed
currentresult.It must not change during streaming.
It updates only from the final:
after
current = result.Chart ownership
The existing
paint()function remains responsible for destroying the previous committed chart before replacing the result view.Because
paint()now runs only on:the active chart is not churned by network chunks.
Shared-code boundary
The following layers are already correctly shared between workbench, detached view, and dashboard:
app.runReadInto()— request, streaming parse, and normalized error/cancel result;buildFilterBar();renderResultView()for Table / JSON / Panel dispatch;renderResolvedPanel()and the Panel registry.Do not introduce a generic “refreshable query surface” abstraction in this issue.
Detached and dashboard presentation semantics differ:
A narrow reusable latest-request controller is tracked separately if desired; it is not required for this fix.
Tests
Previous result remains during chunks
Configure
runReadInto()so the test can:After each chunk, verify:
Query returned 0 rows.never appears;Running…or reports rows read.Commit once on success
Before resolving, place new rows in the in-flight result.
Verify before completion:
Resolve the request.
Verify:
Panel chart does not churn
Open the detached view on Panel and record the current Chart.js instance.
Emit multiple chunks without resolving.
Verify:
Resolve successfully.
Verify:
Error
Emit any number of chunks, then complete with:
Verify:
Boom;Stale and cancellation behavior
Verify:
Regression
Verify:
session_idis still passed;Files
Expected changes:
src/ui/results.jstests/unit/results.test.jsNo new runtime dependency.
Acceptance criteria
Query returned 0 rows.Non-goals