Skip to content

Editor and sidebar overhaul, CodeMirror everywhere, AI stop fix (2.2.4) - #98

Merged
broisnischal merged 30 commits into
masterfrom
feat/editor-sidebar-overhaul
Oct 5, 2026
Merged

broisnischal merged 30 commits into
masterfrom
feat/editor-sidebar-overhaul

Conversation

@broisnischal

@broisnischal broisnischal commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

Releases 2.2.4 on merge (release:patch). The release notes are in .changeset/release-2-2-4.md, which auto-release.yml folds into CHANGELOG.md.

What's in it

  • Sidebar. An Objects tab (views, routines, triggers, sequences, types, with their definitions and CREATE templates) and a Queries tab (saved queries in folders).
  • SQL editor. Statement actions, variables, quick fixes, folding, editor settings, and trigger/routine completion with checks before CREATE. Editor tabs are kept across restarts, and queries save in place.
  • Every editor on CodeMirror. Monaco is removed, and the build is 19 MB instead of 37 MB. The code loaded before first paint is 2.49 MB, down from 3.05 MB.
  • Schema Diagram. A Hierarchy view; Data Model is renamed.
  • AI.
    • Stop and Esc always end a reply. A stopped turn can no longer hijack the next one or leave unanswered tool calls that break the chat.
    • HTML in a reply renders as text.
    • The free tier falls back to its other model when one is busy.
    • Tool arguments sent as {}{...} are parsed, and charts accept any data shape.
    • Inline code and keyboard keys are restyled.
  • Shortcuts. ⌘R refreshes the open table. Fixes chords that fired twice or did nothing, and titles show each platform's chord.
  • Dependencies. devalue and DOMPurify patched. Svelte pinned to 5.55.9, because later versions emit invalid JS for some JSDoc casts.

Checks

  • npm test, npm run check, vite build and cargo check: see the latest commit's run

broisnischal and others added 17 commits October 5, 2026 17:34
The release bumped package.json; npm install brought the lockfile level.
Drop table and truncate only worked on Postgres and SQLite, drop view sent
DROP TABLE, and copying a database was Postgres-only. admin.rs now builds
the drop and truncate SQL per engine (CASCADE only where it means
something) and copies databases on Postgres (template), MySQL/MariaDB and
ClickHouse (table by table, INSERT ... SELECT on the server) and SQL Server
(backup and restore), dropping a copy whose tables fail. SQL Server and
ClickHouse gain a database list to switch between. The dialogs show each
engine's own quoting, and Redis and PostHog no longer offer drop or
truncate.

Tested against local MySQL 8, MariaDB 11, ClickHouse and SQL Server 2022
on scratch databases.
The Views tab becomes Objects: one tree per schema with views (materialized
ones included), functions, procedures, triggers, sequences, types and
events, as far as each engine has them. Extension-owned functions are filed
under their extension (vector, pg_trgm), each overload is its own row, and
the tree renders only the rows on screen, so 3,000 functions scroll at
under 2ms of script a frame. Opening an object gives runnable DDL, New
opens an editor tab with a per-engine template, and drop shows the exact
statement first.

The statement splitters (editor and backend) keep a BEGIN ... END body in
one piece, and MySQL falls back to the text protocol for statements the
prepared protocol refuses (CREATE PROCEDURE, TRIGGER, EVENT). The command
registrations for the copy and drop work land here too.
Completion had no type list: `CREATE TABLE t (id varc` offered nothing and
`x::tim` offered current_time. Each engine now has its types, suggested in
column definitions, ALTER ... ADD/ALTER/MODIFY, CAST, RETURNS and after
`::`. DDL positions get their keywords instead of columns, keywords match
from the start only, Postgres-only functions and snippets stay on Postgres,
and Enter on a word already typed out breaks the line instead of
re-inserting it.

Statement snippets (sel, ins, upd, ct...) end with `;` when they finish the
statement they are typed into; clause snippets and a snippet typed inside a
bracket go in as written (sql-terminator.js).
…ders

Every save filed a new copy: nothing tied a tab to the saved query it came
from. Tabs now carry their saved query's id and Ctrl+S updates it in place
(Ctrl+Shift+S saves a copy). Every SQL tab is kept per connection instead
of one draft, and switching databases no longer writes SELECT 1 over the
new database's draft.

History moves an existing entry up instead of duplicating it, records
failed runs with their error, and times a script by its total. Saved
queries can live in one level of folders, shown in a new Queries tab in the
sidebar; old ones stay at the root, so none are lost.
- Ctrl+/ comments the line without opening the shortcuts panel: a key the
  editor acted on no longer reaches the app's own hotkeys (the same bug made
  Ctrl+W close two tabs).
- Statement actions: a `Run ...` chip at the right of the statement under
  the caret, or a full row above every statement. Lines keep room for the
  chip, so it never covers text, and moving the caret never shifts lines.
- Each statement that ran shows its time and rows after its `;`, with a tick
  or cross in the gutter.
- `$name` and `${name}` join the existing `:name` variables, in the same
  panel, now one field per variable.
- A faint `;` after a finished statement, written by Tab; problem squiggles
  show in the editor's own tooltip with a fix button (add ;, close a bracket
  or string), and Ctrl+. applies it.
- Fold arrows beside the line numbers, Fold all / Unfold all, and the
  editing basics in settings: tab size, tabs or spaces, kept indentation on
  Enter and Shift+Enter, closing brackets, Enter or Tab to accept, current
  line, whitespace, caret blink.
- History, Saved and Charts live in the results bar instead of a side
  panel; the toolbar's copy menu and Save are cleaned up.
sqlRunEffects reads a script and says whether it changed the catalog, the
schemas or data, which tables it wrote to and which kinds of object it
created or dropped. It splits with the editor's splitter, so the DELETE
inside a procedure body or a dollar-quoted function is the routine's, not a
write of its own.
Hierarchy draws the tables on the page top to bottom in foreign-key order,
like a roadmap: each table sits under the tables it points at, and lines
leave a table as one trunk and branch to the tables that reference it. ELK
lays it out (network-simplex layering and placement: 320 tables in 1.2s;
Coffman-Graham took 23s and put root tables at the bottom). Cards show the
catalog's row estimate as a bar, hovering or selecting a table lights its
whole lineage, Enter in the find box steps through matches, and lines into
hub tables can be left out and counted on the hub's card.

The view picker is one line per view, the hub switch is one toggle, and
Ctrl+F reaches every view's search (the code's own find in Mermaid and DDL).
Starters sit just above the message box with an icon for what each does.
SQL handed over by the editor's Ask AI arrives as a removable card instead
of a fenced block typed into the box, and sent SQL shows as code in the
bubble. Under the Mono font preset, chat prose reads in a proportional face
(--font-reading). Each turn tells the model what is open, including when
nothing is, so "explain the open table" no longer gets "I can't see your
screen".
…tables exactly

Each listed table carries created_at where the engine keeps one (MySQL and
MariaDB CREATE_TIME, SQL Server create_date) and create_order where it does
not (the Postgres OID, the SQLite catalog rowid, the DuckDB object id), for
the sidebar's new Created sort. Checked on a local MySQL and Postgres.

MySQL caches TABLE_ROWS for a day, so the sidebar kept listing old counts
after an INSERT; below 100k rows the count is now exact, as on Postgres.
Counts just under a million read 1M, not 1000k.
- A SQL run refreshes what it changed: the touched tables' counts in place,
  the table list after DDL, and open table tabs that show the rows
  (waiting for COMMIT in a transaction). The sidebar keeps its rows and
  scroll while it does.
- The sidebar gets the Objects and Queries tabs, a Created sort, and its
  bar's refresh and + act on objects on that tab; dropping a view from
  Objects closes its tabs.
- Saved queries run from the sidebar, F5 and Ctrl+Enter go through the
  console, so variables and the LIMIT apply.
- Ctrl+F reaches the schema diagram's search; settings gain the sidebar
  and editor options; the "N row(s) affected" message reads the reply's
  rowCount.
A trigger made from the SQLite template was created with `UPDATE table_name`
in its body, and then failed on every write to its table. Two causes: the
editor never enabled multiple selections, so a template field used twice
(`ON ${4:table}` and the body's `UPDATE ${4:table}`) kept one cursor and the
other copy its placeholder; and a picked completion wrote at the main cursor
only. Both now write every copy.

Completion knows a CREATE TRIGGER's head - timing, events, the ON table, FOR
EACH ROW - and keeps the trigger's table across the body's own statements, so
NEW. / OLD. (SQL Server inserted. / deleted.) list its columns. In a Postgres
trigger function NEW. reads the table of the CREATE TRIGGER that runs it.

The engines accept a body that cannot run: SQLite and MySQL do not look its
tables up, PL/pgSQL checks syntax only. sql-object-check.js reads each CREATE
TRIGGER / FUNCTION / PROCEDURE / VIEW / EVENT against the schema the editor
knows and reports tables and views that are not there, NEW / OLD and SET
columns the table lacks, and a BEGIN with no END - leaving alone what it
cannot know (other schemas, calls, CTEs, tables the script creates first,
SELECT ... INTO a variable, non-SQL bodies). The editor underlines them, and
Run stops with the first one and a Run anyway.

The sidebar's minimum width is the header row's own (6 tabs and 3 actions,
320 at a 16px rem): at 180 the tab strip clipped under the actions.
The Mermaid views read their colours off the live theme tokens, but the
diagrams in the assistant's replies still used the per-theme hex palette that
had drifted from the themes and fell back to grey for any theme without an
entry. Both now read the same tokens through mermaid-theme.js, and the chat
sets every variable the renderer reads on the SVG - left unset, --line and
--accent resolved from the page, where they are the app's chrome tokens and
drew the arrows in the accent fill. The palette in the registry is gone.
Monaco was 10.8 MB of the 37 MB frontend build for the editors still on it:
the JSON tab, the table's JSON and Text views, DDL, ORM schemas, the data
diff, the policy SQL editor and the ORM runner. They now use the same
CodeMirror editor as the SQL console, and the build is 19 MB.

- CodeTextView replaces MonacoTextView for large read-only documents, with
  the same guards: plain text above 4 MB, no fold arrows past 50k lines.
- cm-languages.js maps the views' language ids onto CodeMirror: SQL, JSON,
  TypeScript, Prisma (a real mode; Monaco borrowed Rust's), and CSV / TSV /
  Markdown tables, which keep their column colouring. CodeEditor takes any of
  them through `lang`.
- The ORM runner fed Monaco's TypeScript service a generated .d.ts.
  cm-orm-complete.js reads the same model off the text before the caret:
  db. / builder / table. members for Drizzle; prisma. models and methods, and
  inside a call the keys that fit - arguments, columns, AND / OR / NOT, a
  column's filters - for Prisma. Its chords, Mod-S (now a re-indent) and Vim
  (@replit/codemirror-vim, as in the SQL editor) carry over.
- monaco.js, monaco-env.js, monaco-themes.js, monaco-delimited.js, the
  1,100-line Monaco theme presets and the Monaco SQL formatter hook are gone,
  with monaco-editor and monaco-vim. Editors that run their own Vim are
  marked data-vim-editor instead of being found by Monaco's class.

Dependencies:
- devalue 5.9.4 and DOMPurify 3.4.16 for their advisories (3 high, 3
  moderate, 2 low). Svelte is pinned to 5.55.9: from 5.55.10 the compiler
  prints `key: /** @type */ (key)` as an invalid shorthand property and
  `Object.keys(/** @type */ (x)).length` as invalid JS. The four casts it hit
  are rewritten so a later upgrade compiles them too.
- @codemirror/lang-javascript in; svelte-check and typescript as dev
  dependencies with `npm run check` (errors) and `npm run check:all`. It
  reports 0 errors; the warnings it raised in RelationTreeNode (props read
  once, <svelte:self>) are fixed, and jsconfig drops the deprecated baseUrl.
What loads before the first paint went from 3.05 MB to 2.49 MB, and the
build sheds another 8 MB.

- sql-formatter (257 KB) was in the startup set through DataTable's static
  import. The grid loads it when idle; a review opened before that shows the
  raw SQL and the formatted text once it lands, unless it was edited, so
  showDmlReview stays synchronous for the rebuild effect.
- shiki's full bundle built all ~290 grammars into the app (8.7 MB) for code
  blocks that are nearly always SQL, JSON or a few languages. shiki/core with
  ~30 explicit grammars, each still fetched on first use; aliases map fence
  names onto them and anything else is plain text.
- Phosphor's ~110 components were 70% of the startup icon chunk (414 KB)
  though it is the third icon set and not the default. They load when it is
  chosen (icon-family.svelte.js); until then <Icon> draws the Hugeicons or
  Lucide glyph for the name. The chunk is 114 KB.
The value helpers DataTable used for copy, export and editing - CSV, Markdown
and SQL-literal escaping, the JSON text of an object cell, Postgres array
literals, value equality, line folding and the array / pgvector / oversize
checks - depend on nothing but their arguments, and none of them had a test.
They move verbatim to cell-format.js, with tests for the quoting each one
does. The grid's own sqlLiteral becomes cellSqlLiteral: it differs from the
two in sql-batch-update.js and dml-preview.js (booleans, objects, non-finite
numbers), so it is not merged into either. The hover-button comment that had
drifted above it sits on cellButtonRects again.
@broisnischal broisnischal added the release:minor Bump minor version (0.x.0) label Oct 5, 2026
The run mark had 0.55em before it and 0.4em after it, the 1em mark overflowed
its 1.05em border-box cell, and the text started a fixed 10px past the gutter
line - three different gaps around one line, and a px one that drifted
against the em ones as the text followed the app zoom. One gap, 0.5em of the
editor's text, now sets every step: either side of the mark (content-box, so
it is centred), after the line numbers, and from the gutter line to the text.
The statement actions' first label follows the same gap.
With line numbers and fold arrows on - the defaults - the arrows' column sat
between the numbers and the gutter line, a strip empty until hovered, so the
gap before the line was the numbers' 0.5em plus 12px against 0.5em after it.
The SQL editor draws its own fold column on the mark's side of the numbers
(CodeEditor's stays where it is for the other editors), sized in em with the
same 0.5em after it: mark, arrow and number step across at one gap, and the
line has the numbers 0.5em before it and the text 0.5em after it either way.
Opening a table from the sidebar leaves focus on its row, and Mod+R (and F5)
with focus in the sidebar reloaded the table list alone, so pressing it right
after opening a table left its rows as they were - new rows from another
client or a trigger never showed. With a table tab active it now reloads that
table's rows first, then the list.
@tanstack/hotkeys calls preventDefault() on a matched event before running
the callback, so inside the Mod+R callback `e.defaultPrevented` was always
true and the guard meant for "a SQL editor already handled this" returned
every time: Mod+R refreshed nothing outside the editor. Confirmed by logging
the event in a table tab - every press arrived prevented. Mod+R now registers
with preventDefault off, so the check sees only other handlers, and the
callback prevents the event itself.
One key, two actions - a page's own listener and the global hotkey both fired:
- Cmd/Ctrl+Shift+B in the SQL tab toggled the status bar and History, and in
  the AI chat the status bar and the conversation list.
- Cmd/Ctrl+Shift+T in the AI chat started a conversation and reopened a tab.
- Cmd/Ctrl+S in the SQL tab, outside the editor, saved the query twice.
- Cmd/Ctrl+R on the schema page refreshed twice (now that Mod+R works).
- Cmd/Ctrl+Alt+Left/Right in a table jumped to the first / last column and
  switched the sidebar section. The global hotkeys yield to the page that owns
  the key; for the arrows, focus decides: the sidebar when it has focus, the
  table's columns otherwise.

Cmd/Ctrl+F on a page with no search of its own focuses the sidebar's filter,
as the sidebar's listener meant to - it never ran, because the hotkey library
marks the key handled before any later listener sees it.

The chat box: redo (Cmd/Ctrl+Shift+Z) compared e.key to "z", which is "Z" with
Shift held, so it could never match; and Cmd/Ctrl+Backspace cleared the whole
message - on Windows and Linux the word-delete chord every other field honours.

Titles: about thirty tooltips spelled chords as macOS glyphs (⌘K, ⇧⌘C, ⌘↑),
which Windows and Linux showed as they are. comboTitle() prints ⌘⇧P on macOS
and Ctrl+Shift+P elsewhere, and every hand-written one goes through it.

The shortcuts list had the tab bar under Mod+Shift+T (it is Alt+Shift+T; the
former reopens a closed tab) and the ORM runner's Mod+S as Format (it
re-indents). Tests cover comboTitle on both platforms and the list.
…h model" plainly

The free gateway is rate-limited per device and serves small models:
- Long conversations summarised their old turns with a second model call,
  spending the same daily quota and tripping the same rate limit - which is
  where "The free AI service is temporarily unavailable" showed up after a
  while. On the free endpoint history now slides instead (historyBudget: 24k
  chars, last 6 turns, no summary call), so requests are smaller and faster.
- A 504 is retried like 502/503, and once its retries are spent the stream
  falls back to the gateway's other alias (stroke-free <-> stroke-free-fast)
  before the error reaches the user. The hint says which it is doing.

The prompt told the model to answer "hi" by saying who it is and naming two
things it could do with real tables, so a small model recited its opener and
listed the schema. A greeting now gets one short line asking what to do.
"Which model are you" was a guess ("a transformer model"); the prompt now
carries the profile's model as people name it (modelDisplayName) and the
answer is that, in one sentence.
Asked to create a table, the assistant replied "I don't have enough context -
please provide the column names and their data types", and said it again after
"create yourself, don't ask me". Two rules made it: one told it to answer any
request with something missing with exactly that sentence, and the SQL rules
said DDL goes in a bare block that is not run. Now the details a request leaves
open - a table's columns, types and keys, sample rows, a name - are the
model's to pick from the request and the schema's conventions, said in one
line and done; it asks only when what to do is unclear, and never after being
told not to. CREATE / ALTER / INSERT the user asks for run through
execute_sql; a bare block is for SQL the user wants to review first or a
read-only connection. Destructive statements still go through the confirm.
…odels sooner

Replaying the app's own request against the free gateway: the model answered
"create me the table" with execute_sql(CREATE TABLE ...), but streamed the
arguments as an empty "{}" and then the object, so they joined to
'{}{"sql": "CREATE TABLE ..."}', JSON.parse threw, and the statement never ran.
normalizeToolArgs() reads the top-level objects and merges them (later keys
win); well-formed arguments pass through untouched, unreadable ones are left
for the caller's error.

The "AI service is busy" retries waited out the backoff twice on the same
overloaded free alias before trying the other. The same requests replayed six
times all succeeded (1.2-1.5s to the first token after a cold first call), so
the failures are the gateway's intermittent ones: on the free endpoint the
other alias is now tried at the first failure, then the usual backoff.

The greeting rule quoted "what can you do", and a small model opened its reply
to "hi" with those words. It now carries an example of the sentence wanted and
no phrase worth echoing.
"This view hit an error: spec().data.find is not a function". render_chart's
`data` came as a JSON string - its length passed the "has data" check, and the
chart view then called .find on it. Models also send execute_sql's whole
{ columns, rows } result, rows as arrays with or without a header row, or one
array per column. chartRows() turns each into row objects and anything else
into an empty list, which the tool reports as "no usable data, pass execute_sql's
rows" instead of crashing. The tool handler, the chart renderer (and the map
and gauge charts it hands the spec to) and the chat's inline preview all read
the normalised rows, so a chart saved in an older chat opens too.
Inline code was a bordered, 500-weight chip that read like a key cap and
never wrapped, so a backticked query ran past the message. It is now a
faint foreground tint at the text's weight that wraps with clone
decoration. A backticked shortcut renders as <kbd> caps sized in em.
The message footer aligns the time with the text edge, mirrors for the
user's turns, and is tall enough for its 24px buttons.
Stop returned unless a live controller was in place, so a code-block run
(which set loading with no controller) or a stopped turn that cleared
the next turn's controller as it settled left a spinner nothing could
end. Each turn now owns a sequence number and its own controller: Stop
always finalises the UI and disowns the turn, a disowned turn leaves
shared state alone, and the stream drops chunks after an abort. An abort
that fired before the Tauri stream's listener was attached now cancels
the request instead of letting it run out.

Requests repair the tool-call pairing (unanswered calls get a cancelled
answer, stray answers are dropped), so stopping between a tool call and
its result no longer breaks every later message with a 400.

Raw HTML in a reply rendered as real elements: a <style> block or a
full-window element restyled or covered the app. It now renders as text
except attribute-free inline formatting tags, and links keep only web
and mail schemes.
@broisnischal broisnischal added release:patch Bump patch version (0.0.x) and removed release:minor Bump minor version (0.x.0) labels Oct 5, 2026
@broisnischal broisnischal changed the title Editor and sidebar overhaul, CodeMirror everywhere, trigger checks (2.3.0) Editor and sidebar overhaul, CodeMirror everywhere, AI stop fix (2.2.4) Oct 5, 2026
@broisnischal
broisnischal merged commit c043bd2 into master Oct 5, 2026
1 check passed
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 5, 2026
@broisnischal broisnischal self-assigned this Oct 5, 2026
@broisnischal
broisnischal requested a balanced review from Copilot October 5, 2026 17:57
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

release:patch Bump patch version (0.0.x)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant