Skip to content

Release 2.1.0: code editor for cells, steadier startup, ER diagram zoom - #87

Merged
broisnischal merged 29 commits into
masterfrom
fix/bug-v2.0.0
Sep 24, 2026
Merged

broisnischal merged 29 commits into
masterfrom
fix/bug-v2.0.0

Conversation

@broisnischal

Copy link
Copy Markdown
Collaborator

Merging with release:minor cuts 2.1.0: auto-release bumps the version, folds the changeset into CHANGELOG.md and tags v2.1.0, and the tag builds and publishes the release.

New features

  • Cell values open in a CodeMirror editor with line numbers, folding, and find and replace.
  • ER diagram: Ctrl+scroll zooms at the cursor, scroll pans, Shift+scroll pans sideways.
  • npm run tauri:build:mac / tauri:build:win build installers locally. Windows cross-compiles from macOS or Linux.

Fixes

  • Startup: the window stays hidden until its first finished screen, and the webview backdrop is painted on Windows and Linux too. On Windows the window used to flick between two blacks for about half a second.
  • Reconnect: a quick startup reconnect opens straight onto the tables. A slow one shows the overlay after 700ms.
  • Closing quits: closing the last window quits the app. The tray icon and hide-on-close are gone, since every close left a hidden instance running.
  • Clipboard: no permission prompt on Windows from the connect dialog.
  • Grid: no flicker when switching tables or opening related rows, and related rows stay up between lookups.
  • Columns menu: Hide all stays reachable after relationship columns load.
  • Keyboard on Linux: Shift+Tab moves up the sidebar, database and connection lists, and the connection focus ring only shows for keyboard focus.
  • License: activation holds the trial screen through the celebration, and the confetti stays on that screen.

Release notes

Every release said "No changelog available for this release." In the notes step, node -e "..." VERSION="$VERSION" passed VERSION to node as a script argument, so process.env.VERSION was undefined and the lookup never matched. VERSION is now exported, and a local run of the step against v2.0.0 produces its full section.

Checks

  • 661 frontend tests and 128 Rust tests pass. cargo check and npm run build are clean.
  • Windows cross-build succeeded.
  • Startup checked on Linux (Hyprland).

broisnischal and others added 29 commits September 23, 2026 10:23
The connect dialog read the clipboard the moment it opened and again on
every window focus and visibility change. On macOS each read can raise the
system paste prompt, so simply opening the dialog, or switching back to
the app, asked for permission the user never meant to give.

The read now happens on a click into the paste bar. Focus alone does
nothing, so the bar still takes focus on open and Cmd+V then Enter works
as before. Typed text still wins and a string that was cleared stays
cleared; the read is skipped entirely when the bar already holds text.
App mount faded the page in while AppLockGate was still reading the
keychain (up to 1.5s), so what faded in was an empty page. The shell then
popped in unfaded on top of it, and the reconnect overlay after that.

The page now stays at opacity 0 until the first screen that is actually
finished: the shell once it has decided between onboarding, the connect
dialog, the welcome screen and the reconnect overlay, or the lock screen,
the trial screen or the crash screen. A 2.5s failsafe reveals it anyway, so
the window can never be left blank.
A screen recording of the release build, read frame by frame, showed two
things dev hides (its FPS overlay keeps WebKit compositing every frame).

Opening the related-rows dock shortens the grid, and resizing the canvas
backing store lets WebKit show the new bitmap in the old box for one
frame: the grid, stretched by old/new height. The canvas now stays as tall
as the viewport has been, clipped by the scroll container, and never
taller than the content, so it cannot add scroll range the table did not
already have. A shrinking viewport no longer resizes it at all, and the
frame after any viewport change is painted in the same tick so a closing
dock never uncovers stale pixels.

Opening a table swapped the whole grid for a loading placeholder, which
also destroyed the canvas, and a fetch that landed just past the delay
flashed a spinner for a single frame. The grid now stays mounted with the
placeholder drawn over it; for the first 220ms the canvas holds the last
table's pixels so a quick switch lands as one swap, and every spinner
(grid, empty result, related rows) fades in so one frame of it is
invisible. The toolbar's disabled dim waits 200ms too.
The database name, engine, schema, table count, host and user repeated
what the title bar and status bar already say, and pushed the tiles down.
The new tab now opens on the logo, Quick access and the tiles.
npm run tauri:build:mac and tauri:build:win (and tauri:build:local for
mac-x64, linux or the host) build installers on your own machine instead
of in CI. Windows builds natively on Windows and is cross-compiled from
macOS or Linux with cargo-xwin; the script checks for NSIS, clang-cl,
llvm-rc and cargo-xwin, says how to install what is missing, puts the
keg-only Homebrew llvm on PATH, and lists the finished artifacts.

Bundled DuckDB does not compile under clang-cl as shipped: it marks
deleted functions dllexport. DUCKDB_STATIC_BUILD empties that macro, which
is right for a static link, and it goes in through CL because cargo-xwin
overwrites CXXFLAGS_<target> and _CL_ lands after "--" as a file name.
The paste bar in the connect dialog called navigator.clipboard.readText(),
which is permission gated: WebView2 puts a "wants to see text and images
copied to the clipboard" Block/Allow panel over the app the first time it
runs.

The bridge in src/lib/clipboard.js already routed reads through the Tauri
plugin, but it fell back to the native web API whenever the plugin read
threw, and that throws routinely on an empty clipboard or one holding an
image. So the panel appeared anyway.

readClipboardText() now reads only through the plugin and returns '' for
anything else: empty clipboard, non-text, or browser dev where the plugin
isn't bundled. The native read fallback is gone, and
navigator.clipboard.readText is bound to the same helper so a stray call
from a dependency can't raise the dialog either. Writes are untouched,
they never prompt.

Also refreshes package-lock.json, which was stale at 1.24.0 against a
2.0.0 package.json.
The test that every component imports the Svelte lifecycle functions it
calls matched `mount() {` in a CodeMirror panel object as a call to
Svelte's mount(), and failed on a component that never uses it. A name
followed by a parameter list and a body is a method definition, so the
call pattern now skips that shape.
CodeEditor.svelte wraps CodeMirror 6 for JSON, HTML, SQL snippets, stack
traces and long text: line numbers, active-line highlight, its own find
and replace panel, and language modes for JSON and HTML. CodeMirror owns
its layout, caret, selection and undo, and it is a fraction of Monaco's
size, which is the editor the cell panel deliberately does not load for
one cell.
The cell panel faked a code editor with a <textarea> over a coloured copy
of the text. That only lines up while both boxes lay text out to the pixel
identically, and they drift on wrap width, scrollbar space and font
fallback, so the caret ended up over the wrong character. The raw pane is
now a CodeEditor.

The JSON tree pane is gone. The editor already shows the structure
(colours, folding, brackets), and the tree re-ran JSON.parse over the
whole value on every keystroke to draw a second copy of it. What is left
of it is the notice that the text on screen is not the whole value.

Structured text opens unwrapped so the gutter numbers its lines; prose
keeps wrapping, and Alt+Z flips it either way. Focus waits for the
lazy-loaded editor to exist, so opening the dock really does put the
caret in it.
Inside the cell panel's CodeMirror (its find bar included), Mod+F is the
editor's own find and Mod+H / Mod+Alt+F its replace. The shell's hotkeys
took those keys first and pulled focus up to the table's row search or
the find-and-replace panel, and the sidebar's Mod+F handler moved it to
the table filter. All three now step aside for a target inside
.cm-editor, and the sidebar also respects a key the editor already
marked handled.
inManagedEditor() matched anything under .cm-editor, which includes the
inputs in CodeMirror's search panel, so those fields got no word delete,
no undo and no shield from the global hotkeys. CodeMirror manages only
its text surface, .cm-content, and that is now the element that counts.
On WebKitGTK, Shift+Tab reaches the page as GTK's ISO_Left_Tab keysym,
not "Tab", so the key-name check skipped the row handler and the
browser's own Shift+Tab jumped to the filter instead of the previous row.
The check now accepts the physical key (e.code) too.

Row navigation also found the current row through document.activeElement.
The row's checkbox span is tabindex="-1", so a click near the icon focused
the span, which is in no row list, and every Tab or arrow from there fell
through. It now uses the row the event came from.
Same WebKitGTK ISO_Left_Tab keysym as the sidebar: Shift+Tab in the status
bar's database picker and in the saved-connection list compared e.key
only, so on Linux it moved focus out of the list instead of up it. Both
now accept e.code === "Tab" as well.
Saved-connection rows styled :focus, so clicking a row left the focus
outline on it as if it had been reached with Tab. The row now uses
:focus-visible. While that outline is up, the row's selection bar fades
out: the outline is drawn 2px inside the row, so the bar sat right
against its edge and read as a doubled border.
The columns menu had one toggle that read "Show all" whenever anything
was hidden. Incoming foreign keys load after the table opens, so hiding
everything before they landed left the relationship columns visible with
something already hidden, and the button flipped to "Show all" with no
way left to hide them.

The menu now offers each action whenever it means something: Hide all
while any real or relationship column is still visible, Show all while
any is hidden, both at once in between.
Every lookup replaced the panel's data with an empty loading result
first, so clicking another row's key blanked the table, headers and all,
until the result landed. The panel now draws the last settled result
until the next one arrives and dims it once the load has run long enough
to show a spinner, so a lookup reads as one swap. The held result is only
reused for the same relationship; another one would be the wrong table.
- The confetti draws on a canvas the form owns rather than the one
  canvas-confetti appends to <body>, which outlived the screen: the app
  mounted underneath and the last particles fell across the workspace.
- The confetti chunk is fetched when the key field is first touched, not
  on success, so no chunk load sits between "Activated" and the first
  particle.
- "Verifying" only shows once a check has taken 200ms; a local check
  answers in a frame and the label swap was its own flicker.
- A previous error stays up until the next result replaces it, instead
  of collapsing the line and jumping the button on every attempt.
- New onsuccess (key accepted) and onactivated (celebration over)
  callbacks let the caller move on at the right moment, between 1.2s and
  3.2s after success.
- The field and button use the shared Input and Button components.
activateLicense() refreshes the license status before it resolves, so
the gate saw "not blocked" mid-activation and mounted the whole app in
the same tick the confetti started. That made the celebration stutter
and hid the "Activated" state it was for.

LicenseGate now holds the trial screen once it has shown, until the
screen reports it is done. TrialExpiredScreen turns its copy into a
welcome when the key is accepted, fades out when the celebration ends,
and then hands over. The copy is also two short lines (what to do, then
the reassurance) instead of one sentence balanced into three.
The label sat in the header, a full section gap away from the tiles, so
it read as a caption for the logo. It now sits in the same gap-2 column
as the tiles, the way "Jump to" sits with its list, and both labels use
the same tighter tracking. The column widens to max-w-xl so the four
tiles have room.
The page boots at opacity 0 and stays there until its first finished
screen, and a transparent root showed the webview's white backdrop for
the whole of startup. The boot script in index.html now sets <html>'s
background inline to the base light or dark --background (#f7f7f7 /
#080808), the same values as LIGHT_SURFACE / DARK_SURFACE in lib.rs, so
window, webview and page all come up the same colour. Inline because the
theme stylesheet arrives with the bundle, well after the first paint.
What shows before the page paints is the webview's own backdrop, not the
window background behind it, and it defaults to white on every engine.
Only macOS had it set (underPageBackgroundColor). set_webview_backdrop()
now covers all three:

- WebView2: ICoreWebView2Controller2::DefaultBackgroundColor, queried for
  rather than assumed, so an old runtime keeps the default instead of
  failing.
- WebKitGTK: webkit_web_view_set_background_color.

It runs at startup and again when the OS flips light/dark. webview2-com,
windows-core, webkit2gtk and gdk are already in the tree through
tauri/wry and are pinned to the versions wry resolves, so no second copy
of either binding gets linked.
A window visible from build() put every pre-paint layer on screen in
turn: the host surface, the webview backdrop and the maximize resize. On
Windows that read as the window flicking between two blacks (#080808 and
#111111) for about half a second before the app appeared.

The main window and every window opened with open_new_window are now
built with visible(false). revealApp() shows the window through a new
reveal_window command before it fades the page in. It has to show first:
a hidden WebKitGTK window never fires requestAnimationFrame, so the fade
would wait forever. reveal_window is an app command rather than the core
window.show because the capability file only grants core window
permissions to the "main" label.

tao keeps MAXIMIZED across the hide, and show() issues SW_SHOW then
SW_MAXIMIZE, so the OS still records a real maximized state and
minimize -> restore keeps working. A 4s Rust failsafe shows the window
if no JS runs at all, on top of the existing 2.5s JS one.
A warm reconnect lands in a few hundred ms. Showing the ring and
"Reconnecting to ..." at once flashed them for a moment and then faded
them out over the shell, which on a cold start read as the window
flickering. The overlay's plain surface still covers the wait at once;
the ring and the label fade in only if the connect is still going after
350ms.
Revealing the window on the reconnect overlay meant three screens in
about half a second: "Reconnecting", an empty shell with a skeleton
sidebar, then the tables.

A startup reconnect now keeps the window hidden until the connection is
up and the table list has loaded, and falls back to showing the overlay
after 700ms (RECONNECT_REVEAL_MS) so a slow connect never looks like
nothing happened. Every other start (onboarding, no saved connection,
disconnected last time, auto-reconnect off) reveals on its own first
screen, and a failed, cancelled or timed-out reconnect reveals on
whatever screen it ends on.

The overlay's exit fade is 120ms, and it is skipped when the overlay
leaves before the page has been revealed (isRevealed()): that fade would
otherwise play over the shell right as the window appears.
Closing the window only hid it to the system tray, so every close left a
live Stroke process behind, and each relaunch added another tray icon
next to the old ones. The tray (its Open / Quit menu and the icon that
swapped with the OS theme) and the hide-on-close handler are gone.
Closing the last window now quits the process, and the exit hook still
reaps the OmniRoute proxy so nothing keeps running in the background.

Drops the tray-icon feature from tauri and the two tray images from the
bundle resources.
Every wheel event zoomed the diagram, so a large schema could only be
moved by dragging. Ctrl+wheel, the gesture people reach for to zoom,
never reached the canvas at all: the global zoom blocker swallows it
anywhere outside a [data-zoom-surface], and the canvas was not one.

The canvas is now a zoom surface. Ctrl/Cmd + wheel (and a trackpad
pinch, which arrives as ctrl+wheel) zooms at the cursor, a plain wheel
pans, and Shift+wheel pans sideways on a mouse with no horizontal wheel.
Line and page deltas are converted to pixels so a notched wheel moves a
sensible distance.
Adds the CodeMirror cell editor, the ER diagram wheel controls, the
startup and tray changes, the related-rows, columns-menu, keyboard and
license fixes, and the new tab's Quick access label, next to the entries
already there.
The notes step ran `node -e "..." VERSION="$VERSION"`, which hands node
VERSION=2.0.0 as a script argument rather than an environment variable.
process.env.VERSION was undefined, the script looked for a
"## [undefined]" header, and every release fell back to "No changelog
available for this release." VERSION is now exported before node runs.

Also shortens the changeset to one plain line per change, and drops the
em dashes from the install section of the notes template.
@broisnischal broisnischal added the release:minor Bump minor version (0.x.0) label Sep 24, 2026
@broisnischal broisnischal self-assigned this Sep 24, 2026
@broisnischal
broisnischal merged commit 0d53799 into master Sep 24, 2026
1 check passed
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 24, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

release:minor Bump minor version (0.x.0)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant