Skip to content

refactor: redesign notebook cell dimensions and responsive views - #606

Open
emrberk wants to merge 33 commits into
mainfrom
refactor/notebook-dimensions
Open

emrberk wants to merge 33 commits into
mainfrom
refactor/notebook-dimensions

Conversation

@emrberk

@emrberk emrberk commented Sep 1, 2026 •

Copy link
Copy Markdown
Member

Summary

Requested: stable chart sizing across viewport changes

Reported problem: charts squashed and stretched while moving windows or opening the Editor panel, so the chart shape did not hold.

  • Store the pane layout per cell (paneView) and make it independent of the viewport. Under 480 px the compact tier used to hide the editor and stretch the chart to the full cell height; widening collapsed it back. The tier now affects only the toolbar.
  • Keep the result pane at its own height when the editor is hidden, instead of editor + result height, so toggling the editor never resizes the chart.
  • Apply a grid south-edge resize to the result pane only. The old proportional scaling of editor and chart is gone.
  • Give charts a 296 px floor and every pane a 2400 px ceiling. Drag, keyboard, and agent writes clamp to the same bounds.
  • Suppress grid item transitions while the container width changes, and never animate item height, so cell boxes land in one step.
  • Hide overlapping x-axis labels so a narrow chart thins them instead of colliding.
  • Show the zoom slider and wheel zoom from the measured width and data density, with one option structure so a resize never remounts the chart.
    • Thresholds (per rendered mark, from plot width / slot count): slider when a bar or candle body is under 6 px, or a line/scatter point is under 1 px; wheel zoom at 3x those floors. A stack counts as one column; pie has no zoom.
  • Keep Reset Zoom available when a resize drops the slider (covered by e2e).

Note: the chart width still follows the container. This PR stabilizes the height; it does not lock a width-to-height ratio.

Other improvements

  • Replace grid-height and maximized-view agent semantics with independent editor_height, result_height, and view controls. Snapshots report the stored view, mode, and semantic heights.
  • Add a per-cell Stop control for cancelling initial query runs and chart fetches.
  • Treat hiding a result as an explicit discard action that clears the current result and saved snapshot while preserving the preferred layout for future runs.
  • Preserve run history and copy matching saved result snapshots when duplicating notebooks, without switching the active tab.
  • Return faithful JSON previews and preserve semantic dimensions in snapshots.
  • Add keyboard resizing to pane handles (arrow keys, Home, End) with ARIA value attributes.
  • Compare statements by formatter identity so whitespace and casing edits keep the chart frame and run nothing.

Validation

  • yarn typecheck
  • yarn test:unit (2,160 tests across 95 files)
  • focused notebook snapshot/tool/controller tests (252 tests)
  • yarn build
  • targeted ESLint and git diff --check

Deferred follow-up

  • Update mcp-server-questdb for the new notebook tool contract.
  • Bump the MCP server version.
  • Update EXPECTED_MCP_VERSION in this repository after that release.

@emrberk emrberk changed the title Refactor notebook cell dimensions and responsive views refactor: redesign notebook cell dimensions and responsive views Sep 1, 2026
emrberk and others added 13 commits September 2, 2026 18:36
Replace isViewMaximized/wideView/compactView with one stored
preferredView. The rendered pane layout derives from that preference,
result availability, and the responsive tier; resizing never rewrites it.

- Persisted isViewMaximized migrates to preferredView on read and import.
- Agent reads report preferred_view plus live view/tier from the
  renderer's hydration-aware pane state.
- Drop legacy tool inputs: editor_visible, is_view_maximized, grid h
  back-solve, and the set_cell_view_maximized tool.
- Reveal a result hidden by an editor preference when the user runs,
  or switches to Table/Chart, in any tier.
- Reserve grid rows for a multi-statement result when any statement
  renders a grid, not only the first.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ceLPBG4yUmAWH1zqYD9w8
…ontract

- Cells resolve their pane layout during render. The grid expansion delay
  is gone, so tool responses and the rendered pane always agree.
- Grid transitions run for drag, drop, and width changes. They are
  suppressed only while the notebook container is being resized. Item
  height never animates, so a pane switch and its box land together.
- The measured grid container width is published once per notebook and
  read by set_cell_layout, instead of travelling through every cell.
- apply_notebook_state rejects changing an existing cell's kind.
- Markdown cells report preferred_view, view, and result_height as null.
  view and result_height inputs are ignored for markdown instead of
  rejected. auto_refresh is dropped for markdown.
- Tool descriptions and the notebook prompt describe the new contract.
- e2e/questdb submodule bumped to current master.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sKRGPKxxoNvBSKEPrEP4K
…fter persist, statement key memo

- Stop button in the cell header for a first run or first chart fetch:
  cancels the run, or aborts the chart round with a cancelled canvas state
  and Retry. Draw from an empty cell counts as a first run, so its
  validation can be stopped too. Refreshes never show it.
- Cancel markers: a launched statement keeps the unverifiable
  "Cancelled by user" error; a queued or never-started one is recorded as
  cancelled by the user, and the sequential cascade tells an abort from a
  failure.
- A deleted cell's in-flight run is superseded, so the agent is told the
  cell was deleted instead of that its result was cleared.
- Snapshot rows drop only after the document write lands; the live
  controller's transitions settle after the write and report
  persist_failed on failure.
- Per-consumer statement key memo and per-frame result key cache, so a
  keystroke over a large script formats one statement.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…le, linear key passes

- delete the 500-entry identity map, the per-consumer key memo, and the
  per-frame WeakMap; normalizeStatementIdentity calls the formatter directly
- charts compare statements by formatter identity: a whitespace or casing edit
  keeps the frame and runs nothing
- an edit-triggered chart settle carries every statement with chartable rows
  and executes only the edited ones; poll ticks and manual refresh still run all
- CellBottomContent derives its tab list from the engine debounced queries,
  so a keystroke never re-keys the cell
- the engine keeps slotKeys and identities per entry, derived once per SQL
  change; grid rounds carry frame keys through commits instead of re-keying
  per slot; reconcile and hydration take precomputed keys
- resultsEquivalent skips the identity compare for byte-identical queries
- statementIdentityPasses.test pins formatter calls per event

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@emrberk
emrberk marked this pull request as ready for review September 8, 2026 10:33
@github-actions

github-actions Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

Web Console deploy preview

Preview Commit Logs
https://pr-606--web-console.netlify.app ff2c4b4 build log

emrberk and others added 10 commits September 10, 2026 16:10
…agent freshness

Address the review findings on notebook cell refresh and resize:

- normalizeStatementIdentity falls back to trimmed text when the statement
  contains a backslash. The MySQL-dialect formatter reads \' as an escaped
  quote, which QuestDB does not, so two statements with different literals
  collapsed to one identity and the grid kept stale rows.
- Chart rounds record a fetch time per slot. A settle that carries unchanged
  rows keeps their original time instead of restamping them with the edit
  time. The times persist in the snapshot and re-enter the engine on
  hydration through seedRefreshState, which replaces seedRefreshErrors.
- Hydration re-keys snapshots saved with trimmed-text statement keys to the
  formatter-based keys, so refresh errors and the active tab survive the
  upgrade. The re-keyed snapshot is rewritten once. Reconciliation takes
  precomputed result keys so the formatter runs once per result.
- The split handle keeps the drag in local state and writes the store on
  drop, and in grid layout also when the box needs another row, instead of
  on every pointer move.
- hasAgentVisibleCellHeightChanged compares the pane heights the agent reads
  in both layouts. The split handle signals once per drag against the cell
  it started from, and the edge handles signal on commit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Statement identity used sql-formatter's MySQL dialect, which uppercased
keyword-named aliases and removed the space in number literals. Edits
such as `as rank` to `as Rank` or `1. e5` to `1.e5` kept the old result
as current although QuestDB returns a different column or value. The
formatter also grew faster than linearly with statement size: a 500-row
INSERT froze the tab for about 0.6 s at every typing pause.

- normalizeStatementIdentity calls `format` from @questdb/sql-parser
  directly with a pinned `capitalize: true`. It keeps literals,
  identifiers and aliases as written, and identity never follows the
  editor's keyword-casing setting.
- Statements above 8 KB keep their trimmed text as identity. The
  formatter costs about 1 ms per KB and runs several times per edit.
- The backslash fallback is gone. The QuestDB parser reads `'\'` as one
  literal and returns SQL it cannot read unchanged.
- @questdb/sql-parser bumps to 0.1.19, which adds `format`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…, fold legacy maximized heights

Results now take the text of the statement they belong to at every accept
point (reconcile, chart accept, settle write), so raw-text readers such as
resolveDraw and snapshots agree with identity keying. A whitespace or casing
edit keeps the saved chart config instead of falling back to inferred series.

Statement identity no longer formats on render, notify, or run paths:
sameStatementIdentity compares raw text first, the tab frame and pane sizing
claim results by text and take the engine's slot keys, FrameKeys derives a
frame's keys from the statements that wrote it (strict prefix, so a run that
lands inside the edit debounce never mis-numbers a case-variant duplicate),
and the debounce pass runs inside batchUpdates. A frame that kept every row
under new text is re-persisted so hydration can release the cell again.

Cells persisted with isViewMaximized fold the editor height into the result
pane once, on the read that infers the pane view, so an upgrade keeps the
size main showed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
emrberk and others added 6 commits September 27, 2026 14:52
…t fetch stopped

A result now carries the time its rows were fetched (fetchedAt, stamped in
singleResultFromExec). The status line reads the refresh stamp first and
falls back to that time, so an edit that re-persists kept rows, a reload,
and a draw-to-table switch after a chart carry no longer show the save or
settle time over older rows. The chart settle keeps carried result objects
under the new text instead of rebuilding them. savedAt stays the write time.

A reveal (scroll back into the near band, tab shown again) no longer
re-sends a chart fetch the user stopped. Retry, a SQL edit, and an
auto-refresh tick still fetch again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…b from its run, fold legacy chart heights

- A stopped first chart fetch on an auto-refresh cell no longer re-sends
  on scroll-back or tab-show: the stop stamps lastFetchedAt and a reveal
  restarts the poll with a full-interval wait, so the next tick fetches.
- Error and cancelled results carry fetchedAt like success results, so a
  reload dates every tab from the run, not from a later edit's save time.
  cancelledResult() replaces the hand-built cancelled literals.
- A legacy maximized chart with no stored result height folds its default
  height on read, so upgraded agent-made charts keep their size.
- A chart settle round persists the frame it shows: carried rows are no
  longer dropped from disk when the edited sibling statement fails.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A result's fetch time had four homes — result.fetchedAt, the engine's
slotFetchedAt map, the frame timestamp, and snapshot.savedAt — and the
status line fell back through them, so each review found one more path
to a wrong time. Replace the paths with a model:

- fetchedAt on the result is the only persisted fetch time. Every
  constructor stamps it (singleResultFromExec, errorResult,
  cancelledResult); no result literals remain.
- slotVerifiedAt is a session-only overlay for a poll that returned
  identical rows. It is never persisted, seeded or carried, so an
  identical tick still costs no frame write. The status line shows the
  later of the two; after a reload it shows the rows' true fetch time
  until the next tick.
- The frame timestamp is the run token, not a time: InlineResultTable
  takes it as runToken, StatusNotification no longer receives it,
  ChartResult drops it, and poll freshness reads frameFetchedAt.
- Hydration folds fetchedAt once into results saved without it, from
  the snapshot's legacy per-slot stamp or its save time, and rewrites.
  The snapshot's slotFetchedAt is read only there and never written.
- A restarted poll loop sleeps until its next tick is due, for fixed
  and adaptive intervals alike, so a reveal after a Stop never re-sends
  the stopped query early.
- ResultGridPanel memoizes its data source on columns, dataset and
  timestamp instead of the result object.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e current cell

ResizeHandle binds its document mousemove and mouseup listeners at
mousedown and called the onResize and onResizeEnd captured in that
render. Since the split handle started writing the store when the grid
box needs another row, that stale closure compared every move against
the drag-start cell: after the first row crossing every move wrote the
store, and a drag back into the start rows left the box one row stale
until mouseup. The listeners now read the callbacks through refs synced
in an effect, so the orchestration compares against the fresh cell and
writes once per row.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…etry

The Table and Chart segments locked with native disabled while a run or
chart fetch was in flight, so activating "View chart" from the keyboard
disabled the focused button and Chromium dropped focus to the document
body. They now lock with aria-disabled and a click guard, as the Run and
Draw toggles do, and keep focus.

DrawCanvas's Retry unmounted under focus when the refresh replaced the
cancelled state. When Retry is the active element it now hands focus to
the cell toolbar before publishing the refresh, the same handoff the
Stop button uses on unmount.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…run, re-key legacy snapshots per snapshot, fold imports before the clamp

- apply_notebook_state returns `results_cleared`, the cells whose whole
  result the apply discarded; both shells add the snapshot-only cases
- an auto-refresh change resets the poll interval and the first sleep is
  capped at one interval, so Auto never resumes on a fixed interval and a
  clock set back cannot stall the loop
- a run that Stop aborted before any statement launched reports
  "cancelled" to the agent instead of "error"
- a legacy snapshot is detected by its missing fetch time, and every key
  maps by position, so a legacy key that spells another statement's head
  key lands on the statement it described
- a notebook import folds legacy maximized heights before the pane clamp,
  matching the in-place read

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant