Repository navigation
Editor and sidebar overhaul, CodeMirror everywhere, AI stop fix (2.2.4) - #98
Merged
Merged
Conversation
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Releases 2.2.4 on merge (
release:patch). The release notes are in.changeset/release-2-2-4.md, whichauto-release.ymlfolds intoCHANGELOG.md.What's in it
{}{...}are parsed, and charts accept any data shape.Checks
npm test,npm run check,vite buildandcargo check: see the latest commit's run