Skip to content

Changelog ​

All notable changes to PlumeSQL are documented in this file.

The format follows Keep a Changelog and the project adheres to Semantic Versioning.

0.21.0 2026-09-24 ​

Added ​

  • Annotation help above every command in a script. The hover card and the @ completion of a plain script's annotations now answer in the docstring of every command, not only the first: a later command's offers what is read there (@extension, @view, @inputs, @ask, @noask, @var, @file, @dir, @pick), the head the whole set. Every card still ends with its Reference link. The Annotations Reference's own overview catches up with the pinned button's places, the preview tab, the result views and the Outline, and the New Query Extension names.

  • Annotations in the Outline. The Outline lists a script's annotations with its commands: the file header's under its own row, always disclosed, a command's docstring lines under the command with its other children, each with the name, the value and a gloss of what PlumeSQL makes of it (the pin's place, the view tab's title, the refresh interval, what an input asks for, the connection it resolves to, a color swatch). A line nothing reads is flagged and the tooltip says why: an unknown name, a block detached from every command, an annotation that belongs in an extension file, a value the parsers refuse. The row's menu goes to the line, copies it and opens the Annotations Reference at its entry; the snap bar counts them.

  • Completion opens after WHERE. The space after a word that opens an expression (where, and, or, on, having, by, set, when, then, else, returning) now offers the statement's columns on its own when the statement has relations to draw them from, and the space after from, join, into or update offers the relations; the list used to wait for a dot, a letter or Ctrl+Space, which read as the columns not being there. Any other space stays quiet, as before.

  • One set of run parameters for scripts and query extensions. A query extension's own command reads the spellings a plain script always read: $1, $seconds, psql's :who, :'who' and the identifier :"tbl" all name inputs, declared by @var when a line names them and asked by their spelling when none does (a $name or :name there used to reach the server untouched and fail; a $1 used to be refused); a button's $pid stays the clicked row's column. The other way round, a plain script's run parameters take @var, @file, @dir and @pick (-- @var seconds "Running longer than (seconds)", "30" labels the field and gives it a default; -- @var $1 "How many", "10" the positional one; -- @var tag=v1 answers without asking), in the header for every query or above one query, exactly as -- @inputs reaches. An action document's form answers are kept in the session like a script's, so a restart reopens the form as it was answered. A lone quoted prompt (@file dump "Dump to") reads without its quotes.

  • A pinned action says where it stands. -- @toolbar takes an optional first word: start (the title bar's left cluster, as before), end (its right end, before the layout buttons), left / right (the side strip of that dock, above its tabs, the same as left-top / right-top), left-below / right-below (that strip, under its tabs) or left-bottom / right-bottom (that strip's foot, above the app menu), and three more surfaces: statusbar (a chip in the status bar), editor (the active script's toolbar, after its connection) and results (the first result's header). At results an @for action is offered for the TABLES the result's statement reads: one click when the query reads one, a pick among them when it reads several, the object placeholders filled from the one picked; Table details and a new Table indexes starter (each index's definition, size, reads, and the ones never read) are pinned there, so the tables behind any result are one click from their state and their indexes. A strip pin is the icon alone with the name as its tooltip, pressed while its tab is active like a title bar pin, and every place clips its overflow under a fade with a "…" button listing the rest, so a long list never runs over the title or the tabs. The row menu's Pin and Unpin still arm and disarm the same line, place and all. A pin opens its action in the PREVIEW tab slot, like a click on a file (the next preview replaces it, so pins no longer pile up tabs); a second click on the pin while its tab is active keeps the tab, the double click's meaning. The row menu's Pin to Toolbar became Pin to, a submenu of every place with the current one checked (the pin's own right-click menu has the same), so a pin is placed or moved without touching the file by hand, the line's icon and name kept; the "…" menu of a crowded cluster checks the action whose tab is active. The starters use the places: Activity and Locks keep the left cluster and Idle in transaction and Long running join them there, psql moves to the right end and Backup joins it, and Table sizes, Database sizes and Vacuum candidates stand under the right strip's tabs, Wraparound (transaction age) and Replication are chips in the status bar, and the editor toolbar gets none by default (it is dense enough; the place is there for your own), each with an icon of its own (a fresh installation; an existing store keeps its files, Restore Starter Extensions writes only the missing ones).

  • The title bar's layout buttons are a setting, off by default. The four VS Code buttons at the right end (toggle the left sidebar, the panel, the right sidebar, the layout menu) each have a key and a command palette entry, so the new global setting Layout buttons (Appearance) hides them unless asked for, the way VS Code's workbench.layoutControl.enabled does; pinned actions at the end place then take the whole right end.

  • A chart beside its grid. The results pane's side by side layout no longer pairs the grid with its messages alone: the left is always the grid, the right is whichever tab you click, Messages or any result view, so a chart draws next to the rows it draws and a click on a bar highlights its row without switching faces. The toggle shows whenever there is something to pair, both sides stay mounted (toggling never redraws a chart), and the sash is remembered per workspace as before. A script says it too: -- @view <extension> beside opens the run side by side (the layout is yours to flip back; only with beside is a problem), and the view's menu writes the word for you: Beside the Grid, a checkable row that adds or removes beside on the query's -- @view line (adding the line when there is none) and shows the result at once; the grid's Extensions menu has Open beside the grid for this query for the same. And the pair can stack: Split Vertically (the result's dots menu, the view's menu) puts the view or the messages above the grid, pgAdmin's dashboard order, the sash turning horizontal and the choice remembered per workspace.

  • The console line runs query and command extensions. extensions lists every kind now, one property per file stem: beside the grid extensions, the query (.plumesql.sql) and command (.plumesql.run) ones, with kind, description and where they are pinned. Each does what its pin does: extensions.current_activity.run() asks it (a command runs in a terminal tab), open() opens the file, pin("statusbar") and unpin() edit its @toolbar line as the Pin to menu does. The table and completion know the new kinds; help.extensions says it all.

  • The Activity starter has a Chart tab. The current activity query now carries a -- @extension $global/Extensions/Activity/activity-chart.plumesql.js as Chart line, and a new Activity grid starter beside it draws what pgAdmin's dashboard draws: a rolling graph of the backends by state (total, active, idle, in transaction, waiting), one sample per @refresh tick, hover for a reading, a legend that hides a line, Reset and Copy samples in the header, no library and nothing fetched. The grid stays one click away and the chart keeps its series across refreshes. It is also the shipped example of a query extension naming a grid extension: with the chart file gone the name is an error in Problems, the grid works as before, and Restore Starter Extensions writes the file back.

  • A view says when to ask. A result view with inputs can carry ask: 'first' | 'missing' | 'never' | 'always': first (the default, what every view did) opens the form on the first draw of a result so the user confirms guessed columns; missing opens it only while a required input has no answer, so a view whose defaults are exact draws at once (the Activity chart says so, and needs no -- @noask in the query); never and always are the extension's own @noask and @ask; a file of several views sets the word once with a // @ask <word> marker at its top. A script's -- @ask / -- @noask lines still win: the file that calls decides over the file that is called.

  • An attached extension that applies nothing is a warning. A -- @extension or -- @view line whose name resolves but whose every view fails its when(columns) guard and every rule its column match against the query's last result gets a WARNING in Problems saying so, with the view's reason and the result's columns (the Activity chart over a result with no state column, say); it clears the moment the result fits. The name check stays an error; this is the quieter case where the line looked like it worked.

  • A view's picks can live in the script. The configure dialog of a result view (which column is X, the chart kind, how many rows) gets a second checkbox, "Save in the script": Run then writes every answered input above the query as a -- @inputs line (quoting a value with a comma or an =, wrapping a long set over several lines, merging into the query's own lines when it already has some, so a second view's picks keep the first view's keys). The file becomes the picks' home for that query: they are visible, they survive an edit of the SQL (session picks are keyed by the query's text), the view draws from them without the dialog, and the dialog opens pre-filled from them with the checkbox already ticked. The editor completes the line's keys (the inputs the views in scope declare, inserting key=) and values (the result's columns for a column input, the options of a choice, the default), explains a key on hover, and Problems flags a key no view declares, a value outside a choice's options or a non-number for a number input, and a column the query's last result does not have. Hovering an input's label in the dialog shows its key, the name it goes by in the line.

  • -- @inputs answers query parameters too, and @ask / @noask say when the form opens. A script with $1, $name, {name} or :name binds the values from an -- @inputs line above the query (1=42, id=7) without the run parameters form, and the form's Save in the script writes what you typed above each statement that uses it. Two new annotations reach like @inputs: -- @ask asks every time with the values prefilled (-- @ask since, until asks for those alone), -- @noask never asks (a view draws with what it has; a parameter without a value refuses the run in the Log, never a silent NULL). Both forms (a view's, the run's) carry a Next time control that writes them; the editor completes and explains their keys, and Problems flags an unknown key, @ask with @noask on one query, and @noask over an unanswered parameter.

  • A view tab can be titled from the script. as <title> after a name on -- @extension or -- @view (-- @extension sanddance as Flights by hour) titles the extension's view tab for that scope; :<tab> picks one view of an extension that declares several, a title with a comma goes in quotes, and the query's own line wins over the header's. Completion, Go to Definition and the unknown-extension check read the name alone.

  • The console sees the extensions. A new extensions global on the Log's console line lists the loaded grid extensions (name, store path, rules, views, @for / @query scope, whether each applies to the result on screen), and extensions.<name>.explain() answers the question nothing answered before, why a rule does or does not apply: the line that forces it or its scope's verdict with the reason, then every rule with the columns it matches, its when guard and what it formats or which rule outranked it, and every view. results.rules is the other end, which rule shapes which column. open(), apply() and extensions.reload() do what the grid's menu and header do.

  • A runaway view can be stopped. extensions.running lists the views alive (drawing for how long, or drawn) and the @refresh timers, each with a kill(); extensions.<name>.kill() tears down every live view of an extension. A stopped view shows a calm face and is drawn again by Reload, its tab or Run in its dialog. The Log entry a hung render writes after 90 seconds now carries a Stop the view button.

  • A view input can describe itself. An input takes an optional description, one short sentence, shown on the label in the configure dialog and on the key in a -- @inputs line (hover and completion). The starters describe the inputs whose labels cannot say it all (SandDance's facet, sort and scheme, the plots' folds and samples, the pivot's rows and columns) and shed the parentheses those labels carried.

  • Actions are extensions. PlumeSQL's extensions come in three flavors named by a double extension: .plumesql.js (grid), .plumesql.sql (a query extension, what was a .action.sql) and .plumesql.run (a command extension, what was a .action.run). The old names stay recognized everywhere and no file in a repository is renamed; the Scripts and Workspace Scripts stores are moved to the new names once at startup (the starters from Actions/ into Extensions/, beside the grid starters), said in the Log. Menus, the palette, the settings (the Actions category folded into Extensions), the Annotations Reference and the file marks (EXT for a query extension) say extension. In the Explorer and Search every extension wears the feather, in its flavor's hue (accent for a query, green for a command, violet for a grid), a row's @color still overriding it; the bolt is the Run Extension button's alone.

  • Settings descriptions caught up: the results spool (no row caps, the memory and free disk space settings), the Markdown copy format, the console's copy and save, the downloaded libraries' home.

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • Results are stored whole. The result row and size caps are gone (results.spoolMaxRows, results.spoolMaxBytes; a value left in a settings file is reported as ignored): every row of a result stays scrollable, and the console reaches any of them by index (results[124000000].reveal() scrolls the grid there, a bare results[124000000] prints the row). The disk is guarded by a new free space reserve (results.spoolReserve, 2 GiB by default) instead; how much of a result stays in memory before it spills is results.spoolMemory (32 MB by default, up to 2 GB): under it the spool stops storing rows and counts the rest, the stats line says so (spooled first 3,200,000 (disk reserve)), and a row asked for past the stored ones answers with the folder and the free space seen, the reserve one click away (Results Settings on the Log row). Storing every row costs about 23 ns a row: the disk write moved off the drain's path to a writer goroutine. The System page's PlumeSQL files gained a "Results on disk" block: the spill folder, the results held right now with the tab holding each, and a Clean button for the spill folders of PlumeSQL processes that died (the startup sweep's work, now on demand; on Windows the sweep no longer waits a day, it asks the process).

  • A console line under the Log, over what is on screen. One line of JavaScript at the bottom of the Log panel, the devtools console's idea for PlumeSQL's own state: results[123].first_name reads a cell of the result on screen, results[123].reveal() selects and scrolls the grid to the row, results.count / results.columns / await results.fetch(from, n) page past the window, results.first / middle / last are rows like results[i], and results.grid is the grid showing the result with every action on it; the globals form one hierarchy (tab.results, tab.connection.db, tab.outline, tab.ast, the globals their aliases); printed, db.tables is a table of the tables and db.tables.actor the CREATE TABLE in the Log's code block; a table in the Log shows log.tableRows rows at first with Show more and Show all under it; a dictionary object carries its columns, usedBy, deps and facts (tables when printed), its ddl and the tree's Script menu as script.select, script.insert.returning ... (each printed as code, .copy(), .script() into a new unsaved tab), browse() and reveal(); tab.run(), tab.save(), conn.connect(), conn.disconnect(), conn.url, results.grid.hide() / show(); every object prints as a table or a line, never a JSON dump; psql's commands work on the line (\dt, \d film, \sf f, \conninfo, \?), answered the editor's way, and in both the editor and the line a bare word at the start offers them with the backslash added (filter(…), the header's own server-side filter; filters, count, reveal(i), select(), copy(), visible); completion is focused on the line's own globals and everyday JavaScript, never the DOM; help is an index and help.results, help.db … one topic each; tab (name, path, dirty, savedAt, statements, lastRun), conn (state, tx, pid, connectedAt, ageMs, serverVersion, searchPath), outline[i] (the Outline's statements with the guard's verdict and the tables each names, reveal() puts the cursor there; the raw tree is tab.ast), and db.public.actor.columns.first_name over the connection's object dictionary (db.tables / db.views / db.functions resolve by search path; await db.public.actor.ddl()). Everything is a property with completion, generated from the live result and dictionary and fed to the editor's TypeScript service; methods only act (reveal, ddl, fetch, copy). Enter runs, Up / Down recall the history (kept per workspace), $_ is the last value, help() lists it. The answer is a Log entry (the expression as its code, the value as its message; a row or rows as a Markdown table drawn in place, with Copy as Markdown under it; anything else as JSON in the detail), so it filters and copies like everything else; db.public.actor.reveal() walks the Objects tree to the row; grid.filter(results.columns.first_name.contains('x')) applies the header's own server-side filter to the grid (grid.remove, grid.clear, grid.filters, grid.rows). See docs/CONSOLE.md.

  • A Markdown viewer of PlumeSQL's own. A small renderer (lib/markdown.ts, no dependency, escaped text only, so a database value can never become markup) and a Markdown component draw headings, paragraphs, emphasis, code spans and fences, lists, quotes, rules and GFM tables with alignment in the app's typography; the Log's console tables are its first use, a rule's console.table draws the same table (it used to be a text one), and anything that hands the Log a markdown body renders the same way. The console line has a console of its own too: console.log lands in the Log, not the browser's devtools, and console.table draws the table.

  • The Log tells this session from the earlier ones. Entries older than the running PlumeSQL instance's start (every earlier session's) fold under one row, "N entries from earlier sessions", so the panel opens on what happened since PlumeSQL started; unfold them (the row, or the header's Earlier chip) and they read dimmed under a "PlumeSQL started · time" divider, the way a devtools console greys what a navigation left behind. Filters, the errors badge and the file are untouched: the entries are all still there.

  • A query can open on its chart: -- @view and -- @inputs. Sometimes the chart is the point and the grid is noise. -- @view plot-focus above a query applies that extension as -- @extension would and makes its view tab the one showing after Run, the grid a click away; -- @view plot-focus only hides the grid tab for that result (Messages and Problems stay); :<tab> picks one view of an extension that declares several. -- @inputs x=ts, series=a,b, top=1000000 answers the view's inputs in the file, so the chart draws with no dialog (the values are that query's defaults: a pick made in the dialog still wins, and Reset restores them; a view with a required input still open asks first and draws on Run). Both sit where -- @extension sits and reach as far (a query, or every result from the file's header). -- @view completes from the loaded extensions, an unknown name is the same error in Problems, F12 on the name opens the extension, and the grid's Extensions picker writes the line (Open on its view for this query / for the file). With @refresh, a saved script becomes a live chart that needs no hand at all.

  • Charts built for a million rows: ECharts, μPlot and a WebGL scatter. Three new view starters take the whole result, not a window: ECharts (Apache's canvas library, from a CDN: line / area / bar / scatter / pie, a range slider and wheel zoom, a line of a million points thinned on the fly with its peaks kept), μPlot (a 45 KB canvas line chart that draws a million points in milliseconds, drag to zoom, a live legend), and Scatter GL, hand-written WebGL with no library at all: one vertex per row on the GPU, so a million points pan and zoom at full frame rate, hover reads the nearest row and a click selects it in the grid, coloured by a category or a numeric ramp. All three page the result in through ctx.rows, in bounded pages.

  • Two table views: a pivot and a column profile. Pivot folds the whole result (rows × columns × a sum / count / mean / min / max) into a table with totals down and across, cells tinted by size, sticky headers and a Copy as TSV button for a spreadsheet. Column profile reads every column of a result you do not know: nulls (as a bar), distinct count, min / max, mean, and a histogram for a number or the most common values for text, in one pass over a million rows. Both are hand-written, nothing to fetch. They seed under a new Extensions/Tables/ folder.

  • Six more Observable Plot starters, after the gallery. Calendar (one cell per day, weeks across and weekdays down, a row per year, a value folded per day), Ridgeline (a value's distribution per group as overlapping ridges), Horizon (many series folded into compact bands, one row each), Box plot (quartiles, median and outliers per group, vertical for a few groups and horizontal for many), Heatmap (two categories coloured by a folded value, numbers printed on a small grid) and Ranked (the top N categories by a folded value as labelled horizontal bars). Every one folds, bins or samples a million rows before Plot draws.

  • Seven more formatting starters. Percentages with a small bar (*_pct, ratio, rate, share; a fraction reads as a percent), milliseconds as durations (mean_exec_time, duration_ms, latency … read as 1.2 s, 4m 12s, 2h 05m), JSON on one line with the pretty print on hover, short UUIDs (the first block, the whole id on hover), arrays as a comma list (PostgreSQL's {a,b,"c d"} parsed, the count on hover), negative numbers in red, and timestamptz in the viewer's local time. The NULL marker starter now also marks an empty string as (empty), since SQL tells the two apart.

  • A result view has the app's own right-click menu. Right-clicking inside a view used to raise the engine's frame menu ("Open Frame in New Window", "Inspect Element"); the view now swallows that and opens PlumeSQL's menu: the view's own header buttons, Configure View, Reload View, Copy as Image (the view rasterizes its largest drawing, an SVG or a canvas, to a PNG on the theme surface and the app puts it on the clipboard) and Copy as SVG (the markup, for a chart drawn as one), Open Source, Open in Tab, and the Extensions settings, the same words as the header and the results' dots menu.

  • An unknown -- @extension name is flagged, not ignored. A -- @extension whose value matches no loaded extension used to be dropped silently; now it is an error, squiggled under the name in the editor and listed in the Problems tab, so a typo does not fail quietly (an ambiguous bare name, one two loaded files share, is flagged the same, with the fix: name it by its store path). The check is static (no server) and runs on its own switch, even where the dynamic parser is silenced; the error clears the moment the name resolves.

  • A grid rule can carry a when(columns) data guard. Any .plumesql.js rule (a formatter, a computed column, a companion) may declare when: (columns) => …, a self contained predicate over the result's columns ({ name, type } each). The rule then applies only where it returns true and does NOTHING elsewhere, so a computed column is added only for results that actually have the columns it reads. It is the twin of a view's when: the way to say "add a name column only when both first_name and last_name are present", which a match (it selects existing columns) cannot express.

  • Result views gain buttons and richer inputs. A .plumesql.js result view can now declare actions, buttons PlumeSQL draws in the view's header that run against the view's live ctx (in the sandboxed iframe, so they can read the last draw); and its ctx gains a host bridge the iframe cannot reach on its own: ctx.copy copies text to the clipboard, ctx.selectRow(index) selects and reveals that row in the result grid (a click on a bar takes the user to its row, the view swapping back to the grid to show it), and ctx.rows(offset, limit) fetches a further window of the result BEYOND the bounded render window (extensions.viewRowCap): the host pages the result store, so a chart or report can draw far more points than the window holds. A view's inputs grow beside column and choice: number, text and multi-column (every value still reaches render as a string). The "Result view (bar chart)" starter shows all of it: a Top-rows number input, a "Copy values" header button, and bars that select their row on click. The action functions never leave the sandbox; only their labels and icons reach the host to draw the buttons.

  • The extension editor's lightbulb offers TypeScript's own quick fixes, no AI. Ctrl+. (⌘. on macOS) over a diagnostic in a .plumesql.js / .grid.js / .js editor now offers the compiler's built-in fixes: a spelling correction ("Change spelling to 'total'"), an added await, a JSDoc @type, and the rest of TypeScript's code fixes, plus "Ignore this error" and "Disable checking for this file". Not every diagnostic has a targeted fix (an operator type mismatch, say, still reads "No quick fixes available"); those are for a human, or an annotation, to resolve.

  • A .plumesql.js / .grid.js / .js editor shows its own mistakes in the Problems tab, the SQL parser's twin for JavaScript. Monaco's TypeScript service already checked these files (a syntax error always, a type error when the file opts in with // @ts-check) and drew the squiggles; now those diagnostics also feed the Problems tab and the status bar's problems item, exactly as the SQL parser's findings do for a script. A JavaScript editor has no run, so the pane it never mounts on its own opens from the status bar item or Toggle Problems, carries only the Problems tab, and clicking a row reveals the spot; the item's counts update live as the worker settles. The row menu is Go to Problem and Copy Message (the SQL parser verbs do not apply), and the dynamic-parser toggle and its settings, which are the SQL parser's, stay out of a JavaScript file's Problems header. The grid shows a focused set of columns for these (Line, Severity, Code, Message); the PostgreSQL error fields a SQL problem carries (Condition, Detail, Hint and the rest) are always empty for a JavaScript diagnostic, so they are left out.

  • The native desktop menu reaches PlumeSQL, not just the editor. The menu bar gains Go (Go to Tab or File, Go to Object, Browse Table or View, Back, Forward), Run (Run Query, Run Action, Cancel Query, Select / Previous / Next Command, Format Selection), Connection (New Connection, Switch Connection for This Tab, Disconnect, Check Connections) and Help (Keyboard Shortcuts, Annotations Reference, System, Account), and View grows a Command Palette entry, the bottom-panel and Problems toggles, the four Focus targets, Color Theme and Zoom. Each item runs the very command its F1 palette twin does (through one window.__plumesqlCommand bridge), so the menu and the palette read the same and stay in step. These items carry no native accelerator: the web app keeps ownership of the keystrokes.

  • Read the extension API's types: F12, TypeScript highlighting, and navigation. Go to Definition (F12) or Peek on PlumeSQLExtension (in the /** @type {PlumeSQLExtension} */ annotation the starters carry), or on any of its members (a rule shape, a view, a callback parameter's type), opens the extension type declarations in a read-only viewer, so an author reads exactly what an extension may return and what each callback receives. The viewer reads like TypeScript now (interface / type / readonly and the primitive types tint as keywords), and Go to Definition works INSIDE it too: inputs?: ViewInput[] jumps to interface ViewInput, a few lines up. Completion and hover on the type worked already; the type was registered only against the editor's JavaScript defaults, so the jump resolved the definition but had no model to open, and it is mirrored to the TypeScript defaults now, the same fix a CDN library's types already carried. The /** @type {…} */ annotation itself is syntax-highlighted as well, the tag and the type name, in every extension editor; an extension's own type / string object keys stay plain.

  • @ts-check is offered in the extension marker completion. Typing // @ in a .plumesql.js / .grid.js editor now lists @ts-check beside the PlumeSQL scope markers (@for, @query, @description, @color). Type-checking an extension is opt-in per file and the directive worked already (the starters carry it); it just was not discoverable from the completion.

  • New Extension is a first-class gesture, with templates. Creating a grid extension (.plumesql.js) used to be reachable only from a result's Extensions picker. It now sits beside New Script and New Action everywhere new files are made: the Explorer's node and pane menus on any script store or the workspace, the F1 command palette ("PlumeSQL: New Extension…"), and, on the desktop, a native File > New submenu (New Script, New Extension, New Action, New Command Action). Every starter's default export is typed (/** @type {PlumeSQLExtension} */) so completion works from the first keystroke, and two drawing starters join the formatting ones: a hand-drawn bar-chart result view and a Chart.js view pulled from a CDN. A file made from the Explorer is written UNSCOPED, recognized by its .plumesql.js name and applying nowhere until it is attached (the grid's Extensions menu, or a script's -- @extension line).

  • A script applies grid extensions to its own results (-- @extension), per query or whole file. A .sql script or query action names the grid extensions it wants with a -- @extension <name-or-path> line, and WHERE the line sits sets its reach: directly above a query (in that query's own comment block, no blank line between) it shapes THAT query's result; in the file header (a blank line before the first query, where @connection lives) it shapes every result the file produces. So one script can try a different extension on each of its queries. It names the extension by a bare NAME (its file's stem, e.g. bar-chart) or a store path ($global/…, $scripts/…, $workspace/…); either way the extension's formatting rules and result views attach whatever the extension's own // @for / // @query scope says. Several extensions are several @extension lines (or one comma separated). It is the counterpart, from the file's side, of a // @for marker inside the extension: the file opts IN, so an extension shared between scripts stays free of the paths that use it. Typing -- @extension completes from the loaded extensions, and Go to Definition (F12) on the name opens the extension's source file. The grid's Extensions menu writes the line for you: For this query adds it above the query, Always for <file> to the header; Scope to this query / directory still scope the extension itself. Unscoped extensions (no marker) are the natural fit for @extension: general purpose, opted into per query.

  • A result view can import modules, including from a CDN. A view's module and every file it imports are shipped to the sandboxed iframe as a graph, so a view can import a helper from a store ($global/, $scripts/, ./) or an ES module from a CDN (import Chart from 'https://…/chart.js/auto/+esm'). PlumeSQL fetches the CDN modules on the main thread (transitively) and hands the iframe blob modules, so its connect-src 'none' sandbox never relaxes, and neither the parse nor the compute Worker ever touches the network. A CDN host must be on extensions.remoteScriptHosts (the same allowlist and one-click "Allow host" the scripts attribute uses); local store imports need no allowlist. A host the allowlist does not name leaves the extension pending (unloaded) with one actionable "Allow host" line rather than a view that renders and then fails, and it loads on its own the moment the host is allowed. The Chart.js starter now imports the library instead of declaring a scripts URL.

  • Type-aware JavaScript in the extension editors. A .plumesql.js / .grid.js / .js file now gets Monaco's TypeScript language service: real, receiver-aware completion (String(x). offers string methods, not a guess), type hovers, signature help, and go to definition, in place of the older curated list. It also knows the extension's OWN vocabulary: annotate the export /** @type {PlumeSQLExtension} */ (the starters do) and the grid formula's value / row / ctx and the view render's data / ctx / inputs complete with their real shapes. It loads lazily, only when the first extension editor opens, so startup is unchanged; the compiler target is locked to the app's own build target so completion matches the runtime the extension runs in. Adding // @ts-check at the top of a file turns the type help into error squiggles for real mistakes (a typo on a typed value, a wrong argument count), opt-in per file so an extension without it is never surprised by red; imports against the stores never squiggle as unresolved. The starters carry it.

  • The editor learns a CDN library's types. When an extension imports an ES module from an allowlisted CDN (import Chart from 'https://…/chart.js@4.4.1/auto/+esm'), PlumeSQL fetches that library's .d.ts types in the background and maps the import to them, so Chart completes, hovers and signature-helps with its real shape instead of being any (and under // @ts-check, a genuine misuse squiggles). It reads the types from the CDN the way editors do: the X-TypeScript-Types header (esm.sh) or the package's own package.json (jsdelivr, unpkg). Editor only and best effort (a library that ships no types simply stays any), fetched on the main thread and only for hosts already on the extensions.remoteScriptHosts allowlist, cached for the session. Go to Definition (F12) and Peek on a symbol imported from a CDN now work too: they open the library's .d.ts in a read-only viewer tab (or peek it inline), the same as jumping into any source. The Chart.js starter shows how to type a held instance across renders (/** @type {InstanceType<typeof Chart> | null} */, since a default import is a value, not a type name, and a bare let x = null would widen to any).

  • CDN downloads are cached offline and can be cleared. The ES modules a view imports and the .d.ts types the editor fetches are now saved under the global dir (remotecache/), so a restart or a dropped connection does not mean downloading them again, and they work offline. They show up in System settings > PlumeSQL files as a rebuildable Downloaded libraries cache with a Clean button, and PlumeSQL logs a one-line notice the first time it downloads from a host in a session. The page still does the fetching and the allowlist check; the server only stores what the page hands it (no network of its own).

  • Chart views ask which columns to use instead of guessing. A result view can declare inputs (a column pick narrowed to a type family, or a choice of constants like bar / line / area), and PlumeSQL asks the user for them in the same form RUN actions use, rather than guessing which column is the label and which the value (which a result with several numeric columns gets wrong). An input with no default is required, so opening the view tab prompts for it before it draws; a sliders button in the view header reopens the form to change a pick. The picks are remembered per extension and query (so re-running the same query in any tab reuses them) and survive a restart. The chosen values reach render as ctx.inputs. The three chart starters now pick their columns this way, and the Chart.js starter also chooses bar, line or area.

  • An Extensions settings category. Grid extensions (.plumesql.js / .grid.js) get their own settings, the sibling of the Actions category: Enable extensions (extensions.enabled, a master switch that leaves every extension loaded and editable but applies none when off, no rule and no view tab), View row window (extensions.viewRowCap, how many rows a view's render receives, default 2000), and Extension run timeout (extensions.runTimeoutMs, how long a .plumesql.js may run before it is stopped, default 2000). All four are per workspace or global, so a repo can carry its own extension settings (including the allowed remote script hosts its chart views use). A result grid's menu reaches them with an Extensions Settings entry (Workspace / Global) in its closing settings group, beside Results Settings and next to the Extensions picker.

  • A result view can load a charting library from a CDN. A view declares the libraries it needs with scripts: ['https://…/chart.umd.min.js'], and PlumeSQL loads one only when its host is on the new Allowed remote script hosts setting (extensions.remoteScriptHosts, under a new Extensions settings category, comma separated and empty by default so no host is allowed). The script is fetched once, cached in memory and by the browser's disk cache, and handed to the view's sandbox as code, so the view's strict isolation (no database, no app, no navigation) is unchanged. When a view wants a host the list does not name, the Log shows a one-click "Allow this host" answer; allow it and the view fetches its library and draws on its own. A new Bar chart (Chart.js) starter extension is the worked example.

Changed ​

  • Releases live on GitHub. Every version is a release on github.com/hercstack/plumesql with the signed bundles and the updater manifest as assets; the desktop updater reads the latest release first and plumesql.com second, and the site's download links resolve there.

  • PlumeSQL lives at plumesql.com. The account, the licence and connection sync talk to account.plumesql.com, the update check and the desktop updater read plumesql.com, and the About card and the Account page link there. The previous host keeps answering the API for builds that still call it, so an installed 0.20.0 signs in and updates as before.

  • The AI assistant is not part of this release. The System page's AI assistant section stays, with its key and model controls disabled and a line saying the assistant comes in a later release; the Anthropic SDK is no longer built into the app. A key stored by an earlier development build stays in the vault untouched.

  • One checkbox on a view's configure dialog. "Save in the script" is the "don't ask again" where a script exists: the -- @inputs line it writes answers instead of the dialog, so the separate session flag is gone there (a view promoted to a tab of its own, with no script to write into, keeps "Don't ask again"). Picks made without saving last while the app runs and no longer: they used to be kept in the session and to travel from one query to an edited one on their own, which read as stale values from nowhere. The -- @inputs line is the one memory that outlives the app.

  • The MCP server is not part of this release either. plumesql mcp exits saying so, the app's /mcp endpoint answers the same, the AI agents section on the System page keeps its description with the Start button disabled and no commands to copy, and a remembered "MCP over HTTP on" setting is kept but not honoured. Both come back with a later, paid release.

  • A view's menu is on its header too, and a view may keep the right mouse button. The view header ends in a dots button that opens the view menu (the right-click's twin, every pane's shape). A view that drives the mouse itself declares contextMenu: false and a right-click inside it opens nothing; the SandDance starter does, since a right drag rotates its 3D view, and its panel now says how the mouse works (drag pans, right drag rotates, wheel zooms, a click selects). Copy as Image works on WebGL views now: every WebGL context a view creates keeps its drawing buffer unless the view asks otherwise by name, so a deck.gl or SandDance frame reads back instead of the "cannot be read back" refusal.

  • A view follows a pane resize without drawing again. Opening the Log panel or moving a splitter used to render every view from scratch after the size settled, seconds of blank on a SandDance of 300k rows. A view may now declare resize(root, size, ctx) and fit the drawing in place; the SandDance starter does, re-projecting its scene in a frame. The new extensions.viewResize setting, also a checkable Keep Size on Resize row in every view's menu, chooses redraw (the default: refit, through the view's own resize when it has one, else by drawing again) or keep (the drawing stays at its size and scrolls, whatever the view could do).

  • The Explorer filters by file kind, for the whole tree. The filter window's file section is a list of kinds (SQL scripts, query extensions, command extensions, grid extensions, connection sources, other files), each a checkbox, any combination, and a kind is a kind in every root: hiding the query extensions hides them under Scripts and Workspace Scripts as much as under the workspace, and a query extension goes although it is a .sql file by name (the kind is read the way the icon is). Connection sources are the very files the Connections tree discovers connections from (.env, appsettings, docker-compose, schema.prisma, alembic.ini, database.yml, application.properties and the rest, kept in step with the scanner by a test), so those files now show; a .json of any other name is an other file and hidden by default. The pane menu's File Kinds offers the same list. Two pattern lists sit under the kinds: Also include (other files by name beside the kinds, **/*.md; the old pattern setting, its key kept, its old preset values read as empty) and Exclude (new, files.exclude: whatever matches hides, in every root and whatever its kind, a matching folder with everything under it). Before, the filter was three fixed patterns that applied to the workspace root alone, still spelled the old grid extension name (so a .plumesql.js in the workspace was missing under "PlumeSQL files") and could not tell a query extension from plain SQL.

  • The product is PlumeSQL. Plume is now PlumeSQL everywhere it shows its name: the window, the app bundle (PlumeSQL.app, product name and bundle identifier io.github.hercstack.plumesql), the plumesql command and its plumesql-dev build, the PLUMESQL_* environment variables, the application_name a session announces, the folder context menu entries, the desktop menu, and every document. The three extension flavors follow: .plumesql.js, .plumesql.sql and .plumesql.run (the .plume.* spellings stay recognized for good like .grid.js and .action.* before them, and the stores rename theirs once through the rename-extensions-plumesql bootstrap step). Nothing is lost on the way: the global dir moves from plume to plumesql (and .plume to .plumesql beside a portable binary) on the first start, whole; a workspace's committed .plume/settings.json keeps working until a .plumesql dir appears beside it; the vault key stored in the OS keyring under the old service name is read once and copied under the new one; the Windows DPAPI blob keeps its entropy; the old "Open with Plume" folder verb is removed when the new one registers, and an installed Plume.app still answers the command line. The GitHub repository, the Go module path and the download site keep their names.

  • The Observable Plot gallery is now six standalone chart starters. The one observable-plot extension that carried six views (Chart, Focus, Density, Regression, Streamgraph, Beeswarm) is split into six single-view extensions under Charts/ObservablePlot/ (plot-chart, plot-focus, plot-density, plot-regression, plot-streamgraph, plot-beeswarm), each pickable on its own from the New Extension menu and each still self-contained. They also lean into large results the way the SandDance starter does: a much higher Top rows default per chart (Density 30000, Focus 20000, Regression 15000, Streamgraph 10000, Beeswarm 8000, Chart 5000), pulling past the render window with ctx.rows. The preamble they share (the Observable Plot import, the palettes, the row and column helpers) lives in ONE shared.js module beside them, which each chart imports; shared.js is a plain .js helper, so it is not itself an extension, and Restore Starter Extensions seeds the whole family together.

  • The formatting starters fit their columns, and name themselves for it. Each formatting starter now matches only the columns it belongs on, so it is safe to apply widely without reshaping unrelated cells: the money starter (renamed amount-usd, USD for amount-like numbers) matches an amount/price/cost/total/balance/salary name AND a numeric type; the date starter (date-only) and the relative-time starter both match a *_at name AND a timestamp type; the byte-sizes starter matches a *size/*bytes name AND a numeric type; the status starter matches any *status name. The combine starter (renamed full-name) adds its name column right after last_name and only when both first_name and last_name are present. The starters' menu labels now describe what each does, and the three renamed files change the name their -- @extension line uses (money → amount-usd, dates → date-only, combine → full-name).

  • The starter extensions are reviewed and upgraded. Every starter now ships UNSCOPED: it carries no // @for / // @query marker, so a new extension applies nowhere until you attach it (the grid's Extensions menu, or a marker you add) rather than defaulting to the whole workspace. The charting starters are rebuilt for real use: the Chart.js starter gains a type picker (bar / line / pie / doughnut / radar / polar), a top-rows limit, a theme-aware palette and a "Copy data" button; a new Observable Plot starter draws a line / area / dot / bar chart that handles a lot of points (a numeric x is scaled correctly, not sorted as text); a new Vega-Lite starter (scatter / bar / line / area / tick) that GROUPS and AGGREGATES for you (mean of Y per X, count, ...), loading its runtime the classic-script way and rendering with Vega's AST interpreter so it runs inside the view's eval-free CSP; a new SandDance starter (Microsoft's 2D / 3D data explorer) that exposes SandDance's whole Insight as inputs: every chart type (scatterplot, barchart, density, stacks, strips, grid, treemap), 2D / 3D, the column roles (x, y, z height, colour, size, sort, and Facet for small multiples) and a colour-scheme picker, animated between any two settings, loaded lazily the classic-script way (it and its deck.gl runtime are ~1.9 MB) and drawn with WebGL, so it wants a GPU-capable browser; and the hand-drawn bar view stays as the no-library, interactive example. Both library charts take a multi-column input, so several columns compare side by side as coloured, legended series (edit the list to add or drop one), and a Palette (Vivid / Cool / Warm / Mono) and a Title input to restyle without touching the code. A view input's kind reaching the pick form was also fixed: number, text and multi-column inputs were silently dropped before, so they only worked through a render-time fallback. A new Trend vs previous row formatter marks every number column with an up / down arrow and colour against the row above. Every starter still leads with // @ts-check and passes it (a test keeps all of them clean).

  • A result view renders once when it opens, not two or three times. Opening a view used to run its render several times in a row: the iframe's own ResizeObserver fired a redraw immediately after the first paint (same size, no real change), and the host could re-post the same data as its readiness effects settled. A view that pulls its whole result inside render (ctx.rows, the charting and SandDance starters) therefore re-paged the ENTIRE result across the sandbox two or three times just to open. The iframe now redraws only when the size actually changes (a forced draw on open, on a refresh and on an input change still always runs), and the host skips re-posting an unchanged view, so a big result (tens of thousands of rows) is pulled and drawn once. A genuine resize, a @refresh and a reconfigured input still redraw as before.

  • A fresh run shows the data grid first; a chart view is opened explicitly. A new run in a script tab used to reopen whichever view (chart) was showing before and re-render it over the new rows, so a query landed on a chart, over columns it might not even fit, and the view's runtime reloaded on every run. A run now drops back to the result grid and unmounts the views, so the data shows first and a chart loads only when its tab is clicked. A result DOCUMENT (a promoted view, a dashboard) is the chart's own tab and keeps its view across a refresh, unchanged.

  • A result view with inputs asks before it draws, pre-filled, and offers to stop asking. Opening a view that declares inputs now always opens its pick dialog first, the values it remembers (or sensible defaults) already in the fields, and nothing renders until you press Run (renamed from "Draw", since a view can report as well as draw). A column input pre-fills the first result column matching its types, and successive column inputs take DISTINCT columns (x, y, z land on different numeric columns), so a chart draws from the defaults without a single pick. A "Don't ask again for this view" checkbox skips the dialog next time and renders with the remembered values; Reset forgets them and restores the defaults in place (the open dialog and the chart both update at once), and disables itself once the fields already hold the defaults, so it offers itself only when there is something to forget. The picks and that choice live per view in the session. Under each field the dialog names what it defaults to (what Reset restores) rather than an action-style {placeholder} a view input never had, and the form validates a view input itself (no server judges it): Run waits while a column or choice holds a value it does not offer, or a numeric field a value that is not a number, the field flagged in red until it is fixed. The SandDance starter's Y is a primary axis again (it auto-fills like X) rather than an optional blank.

  • The Observable Plot starter is now a gallery of interactive examples. Where it shipped one flexible chart, it now ships six self-contained views in one extension: Chart (the flexible line / area / dot / bar it always had), Focus (a master / detail chart: a small overview you DRAG across to brush a window, and the large detail chart above zooms to it, with a per-series crosshair on hover, a dot and value for every series under the cursor; the Y axis anchors to 0 only when 0 is near the data), Density (a hexbin heatmap coloured by the count per cell, for a result too dense for a scatter), Regression (a scatter with a fitted trend line and confidence band, one per group), Streamgraph (the series stacked as flowing bands with a wiggle offset, for composition over x) and Beeswarm (one dodged dot per row, so a value's whole distribution shows without overlap, split into a row per group). Keep the views you want and delete the rest. A charting starter's Title input is now optional (it was marked required, so every activation prompted for a title before drawing).

  • The seeded starter extensions now mirror the New Extension menu. A fresh installation, and Restore Starter Extensions, write every starter the New Extension menu offers (minus its Blank template): the twelve Formatting rules (booleans, money, byte sizes, dates, time ago, grouped numbers, a coloured status column, marked NULLs, row numbers, a running total, a combined column and a per-row trend) under Extensions/Formatting/, and the five Charts (a no-library bar chart, Chart.js, the interactive Observable Plot gallery, Vega-Lite and the SandDance 2D / 3D explorer) under Extensions/Charts/. The set is generated from the same templates, so a starter is identical whether you create it from the menu or restore the seeded set. Previously the seeded set was a small hand-written subset, so most starters (a running total, a trend, a status column, and the rest) were reachable only through the menu.

  • A grid rule can match a column type by its base name, or a RegExp. A match: { type: 'numeric' } now matches numeric, numeric(8,2), numeric(12,4) and every other precision (the modifier is ignored), and 'character varying' matches character varying(255); an exact 'numeric(8,2)' still matches only that. type also accepts a RegExp, tested against the full type text, for a family match: the "Numbers grouped" starter now uses type: /^(smallint|integer|bigint|numeric|real|double)/ to format every number column, whatever its type or precision. A cell descriptor's class accepts a custom class name alongside the five built-ins (an extension cannot ship the CSS for one, so style stays the way to a custom look), and the docs spell out that a view's scripts list is for classic UMD / global libraries, an ES-module dependency being a normal import instead.

  • Every starter extension turns on the type check and is clean under it. A starter from the New Extension menu now leads with // @ts-check (alongside the /** @type {PlumeSQLExtension} */ it already carried), and each is written to pass it, so a generated extension gives real type help and a clean Problems tab from the first keystroke rather than only once the user opts in. The "Time ago" starter is the one that needed the work: its units list is typed so the relative-time formatter type-checks, and its isNaN reads the timestamp's number. A test seeds every starter and checks it reports nothing under @ts-check (the chart starter's CDN host still needs allowing, as before).

  • The editor's toolbar and menus are tuned for JavaScript, not just SQL. A .plumesql.js / .grid.js / .js editor now carries the commands its SQL cousin has that apply to it: Toggle Line Comment, Toggle Block Comment, a Format button (a safe reindent, for a plain .js too, not only a grid extension), and the generic Find and Replace — which were hidden behind the SQL gate though they work in any editor. The SQL leftovers are gone from these editors: the right-click Insert Annotation entry (which opened an empty menu, since a JavaScript file has no action annotations) no longer shows; Ctrl+Enter and the palette's Run query are not bound (a JS file has nothing to run, and used to reach the SQL run path); and the toolbar's and editor's settings menus drop the SQL-only groups (Assistance, the dictionary and PostgreSQL-docs hovers, the SQL inlay hints, and the statement marks / lens / run glyph), keeping what applies to any editor (word wrap, minimap, font, whitespace, line numbers, the general hover switch).

  • The command palette (F1) is grouped by area and easier to type. The commands dropped their repeated "PlumeSQL:" prefix (it was noise for reading and for typing), and the palette now groups them under area headings (Go, Run, Connection, File, View, Editor, Preferences, Help), the recently used still on top. An area name also filters: typing "view" narrows to the View group even when a command's own name does not contain the word. The dozen-plus editor-setting toggles that crowded the list collapse into one "Toggle editor setting…" entry (Word Wrap, Minimap and Inlay Hints stay direct), each showing its current state; and the palette gains the connection commands it was missing: New connection, Switch connection for this tab, Disconnect, and Cancel query. A duplicate "Open settings" entry is gone.

  • The grid's Extensions menu is now a searchable picker, not a nested submenu. As extensions multiply, a submenu that lists every one (each with its own apply-modes submenu) grows unwieldy. The grid menu now carries a single "Extensions…" entry that opens a filterable picker: New extension, then every loaded extension (the ones already applied here first, marked). Picking one opens a second picker with the ways to apply it to this result (apply here once, always for this query, always for this file or folder, or open its source) — the same choices as before, just searchable and one line deep.

  • New file icons for extensions and actions. A PlumeSQL extension (.plumesql.js / .grid.js) now wears PlumeSQL's own mark, the feather, instead of the grid glyph. An action file wears the action bolt (PlumeSQL's one mark for an action, the same one an action tab and the Run Action button wear) instead of the "ACT" lettermark, and a command action (.action.run) wears a terminal, so a SQL action and a RUN action read apart at a glance. The marks are the same in the Explorer, on the tab, and in search, and a row @color still tints them.

Fixed ​

  • The Outline follows the cursor into a command's docstring. The cursor on an annotation or comment line attached to the command below it now marks (and with automatic expand, opens) that command's row, as the Select lens already treated those lines; before, the Outline answered nothing until the cursor reached the first code line.

  • The macOS title bar carries the pinned actions. On the macOS desktop the row under the traffic lights held nothing but the @toolbar pins, and with none pinned it shrank to a thin empty band. The pins now sit in the title bar row itself, after the traffic lights and before the workspace title (the layout trio keeps the right edge), and that second row is gone on macOS for good: one row of editor height back. In the browser and on Windows and Linux the row stays, now at a fixed height whatever it holds. In both rows the pins now have MEASURED room (half the row minus half the centered title group, which grows with the workspace name), so they can no longer run under the workspace title as they did with many pins; a pin keeps its natural width instead of squeezing to "Blo…", what does not fit is clipped under a fade, and a "…" button beside the pins lists every pin, the clipped ones included. And a pin reads PRESSED while the tab it opened is the active one (the status bar's Log toggle idiom: a wash, a hairline and a stripe along the bottom edge, in the pin's own color when it has one), so the row says which of its actions is on screen.

  • F12 on a relative -- @extension ./name.plumesql.js opens the file. Go to Definition resolved bare names and store paths only; a relative path now resolves against the file the line is written in.

  • A -- @inputs value added by hand shows. A view configured once through its dialog stored every field it showed, blanks included, and a blank stored pick shadowed a -- @inputs title="..." written later; an edited query could also inherit another query's picks over its own line. Only a change is a pick now, a blank never beats the file, and a query with its own @inputs inherits nothing.

  • Refresh and Reload wait while a view draws. The document's Refresh and the view header's Reload are disabled, with their tooltip saying so, while the view is rendering (the first draw or a redraw), so a second click cannot queue another draw behind a slow one. The button dims while it waits, and a tooltip already showing when the text changes under the pointer re-reads it instead of keeping the old one.

  • A slow redraw of a view says so. Reload View, a refresh or new inputs cleared the view and left an empty pane until the chart was drawn again, seconds on a big SandDance. The same "Rendering view…" face now shows when a redraw takes longer than a beat (a quick redraw, a dashboard's refresh of a small chart, shows nothing and never flickers).

  • Switching tabs no longer drops a result view. A script tab showing a chart (an extension's view) fell back to the grid whenever another tab showed in between, and the chart had to be opened, and drawn, again. The view showing is now the tab's own, kept in the session like its Results / Messages / Problems pick, and the pane itself stays alive while another tab shows: a tab switched back to finds its chart as it was, camera and selection kept, nothing drawn again (a background tab showing the bare grid is let go, a script's or a document's alike). And a view already drawn for a result shows again on its tab without asking its inputs or drawing again; Configure View is the road to new picks. A fresh run still shows the grid first, as agreed.

  • Editing a query no longer loses a view's picks. A view's inputs (which column is X, Y, colour, size, the chart kind, the row count) are remembered per query text, so adding a column or changing a filter used to open the view with its defaults again. An edited query now inherits the view's newest picks made on another query whose columns it still has; picks naming a column now gone are passed over, and "don't ask again" stays with the exact query it was set on.

  • A result view no longer blanks and redraws while the mouse moves over it. In the desktop app, SandDance on a large result went blank on hover and came back when the mouse left, resetting its camera; at smaller sizes it flashed. Its hover tooltip overflowed the view's document by a few pixels, WebKit's scrollbars took room, the body shrank, the view's resize observer redrew it from scratch, the tooltip vanished, the body grew back, and round again on every mouse move. The view's viewport no longer scrolls (the body does, when a view outgrows the pane) and the redraw watches the viewport, which a scrollbar never changes.

  • No more row pixels flashing above the grid header. On a fast scroll, above all in the desktop app, a few pixels of the rows bled past the top edge of the grid for a frame and showed above the column header, unclipped by the scroller's edge. The scroller now clips its own painting with a clip-path, a composited clip that holds in every frame.

  • An extension's view shows its connection in the status bar too. A result document (Run Extension's tab) left the status bar's connection segment empty while a script tab and the extension's source tab showed theirs. Now the segment carries the document's connection with the same readout and the same click to change it the document's header offers, locked with the same words when the file names its connection (@connection). The bar also keeps its shape from tab to tab: on a result document or a browse, where a script shows the editor's line:column and selection, the grid's current cell (row:column, the column's name in the tooltip) and its selection (1 cell, 3 rows × 2 columns, 4 rows, all rows) stand in the same two places, so the connection segment no longer jumps. The cell readout opens Go to Row, the grid's Go to Line: a row number selects the row and scrolls it into view (the console's reveal road, so a row far down a spooled result comes up just the same), row:column stands on the cell. The selection readout opens the grid's selection commands the way the editor's opens its own: Select All, Select Row(s) and Select Column(s) of what is selected, Clear Selection.

  • A vault that cannot be opened no longer blocks the app. When the encrypted connection vault will not decrypt (its key lost or replaced), the app used to fail its first request and start with nothing: no connections, not even the ones discovered from a .env, no session. Now the saved connections are simply missing until the key is back, the reason is one Log entry (verbatim, from the meta payload), and everything else (discovered and environment connections, scripts, files, the session) works as ever. The entry carries a Start a fresh vault button, which sets the old file aside as vault.json.undecryptable (kept, never deleted) so saving works again; and the vault now records which kind of keyring sealed it, so one sealed with the key file but opened with the keychain says exactly that, with the way back (PLUMESQL_KEYRING=file).

  • The Explorer moves and copies between its roots in every direction. Dragging a row onto the Scripts, Workspace Scripts or workspace root (or any folder under them) offers Move Here and Copy Here whatever the source root, for folders as well as files, and Cut, Copy and Paste in the row menus and on the keyboard do the same instead of refusing ("not supported yet"). A script store still takes only what its contract names (.sql, .run and .js): a file of another name has no drop target there, and a folder holding one is refused whole by the server, naming the file, in the Log. Before, a .plumesql.run or a .plumesql.js could not be dragged into Workspace Scripts at all and a folder never crossed a root.

  • SandDance and Vega-Lite views now draw. Their libraries compile chart expressions at runtime with new Function, which the result view's iframe Content-Security-Policy forbade (script-src had no 'unsafe-eval'), so SandDance rendered its panel but an empty canvas and Vega-Lite drew nothing, both without a visible error. The view sandbox adds 'unsafe-eval' now; the real boundary around a view is the sandbox itself (opaque origin, no parent, no database, connect-src 'none'), which eval does not weaken, and the view's own module code could already run arbitrary JavaScript regardless. The SandDance starter also now sits in the app's own palette rather than a white rectangle with unstyled controls: the canvas and axes follow the theme (dark or light), the chart fills the pane, and SandDance's control panel (it ships no CSS of its own) is a compact column on the right styled like the app's panes, with the legend and the Select & Filter buttons beneath the chart knobs.

  • Annotation completion keeps up as you type past the @. In a plain SQL or command file, comments do not quick-suggest (a comment is prose), so only the @ and space trigger characters opened the annotation list: typing the rest of a name (@e, @ext) or a value after a space did not re-open it, and you had to delete back to the @. Typing anywhere on an annotation line (-- @…) now re-triggers the list, so a partial name filters and a space after a keyword shows its values, while ordinary prose comments stay quiet.

  • A result view no longer draws on a white ground in dark theme. A view's sandboxed iframe left its document transparent so the dark element behind would show, but a sandboxed frame's base canvas paints white, so every chart (the bar starter, Chart.js, Observable Plot, Vega, SandDance) sat on a white rectangle under a dark app. The host now paints the iframe document itself with the theme surface, repainting on a theme flip, so a view's own background matches the app in both themes. The hand drawn bar starter also gained room to breathe and its labels: padding, an uppercase column header, a row number down the left and the value down the right (tabular figures), a faint track behind each bar, and negatives in a distinct colour from positives. The Chart.js starter gained the same breathing room (a layout padding around the plot).

  • A dashboard @refresh no longer flashes or jumps. Two things moved on every tick. A result view (a chart or report from a .plumesql.js extension) was torn down and rebuilt: its sandboxed iframe reloaded, the module re-imported and the loading overlay swept, a visible flash. And a "Refreshing…" banner appeared above the pane for the duration of the run, pushing the grid down and letting it snap back, a jump every few seconds. Now the view stays mounted across runs and re-fetches the new rows into the LIVE iframe (redrawing the chart in place, no reload, no overlay, the previous rows kept until the new ones drain in), and the "Refreshing…" banner shows only on the FIRST run, when the pane is empty; a refresh that already has rows updates them silently in place. Tabular results already patched in place; charts and the surrounding layout now do the same. The action document's meta line grows a "Refreshed just now" readout that ticks back through "12s ago" and flips to "just now" on each tick, the visible proof a silent refresh happened (the same readout the chart view's header already carries), so a successful refresh stays out of the Log where it would only flood it.

  • A dashboard action whose file is deleted no longer floods the Log. A query action's result view re-reads its .action.sql from disk on every @refresh tick (it runs the file as saved); with the file gone, each tick's read failed and logged, so a dashboard left open logged the same "Reading …" error hundreds of times. The auto refresh now logs a read failure once and stays quiet while it persists, re-logging only on a changed error or a manual Try again (the refresh timer's own rule, like the parser's); the view still shows the failed state, and a recovered read makes a later failure news again.

  • Grid extension annotation help matches the rest of the editor. A .plumesql.js / .grid.js // @color now draws the colour swatch and opens the palette picker, the same as a SQL or command action's @color; and hovering // @description, // @color or // @ts-check explains each, beside the @for / @query hovers already there. A plain command (.run) file's @ completion no longer offers the SQL-only annotations (@extension, the @format-* family, @parse, @readonly, @confirm-writes) its header would silently ignore, leaving the presentation pair it actually keeps.

  • The command palette (F1) opens on the PlumeSQL commands, not a menu of Monaco's quick-access modes. F1 used to land on Monaco's provider help list ("Go to Line", "Go to Symbol", "Show And Run Commands") until you knew to type, because the standalone editor registers that help provider on the empty prefix when the first editor mounts, stealing the default from PlumeSQL's own command provider. The help provider is now registered early enough that PlumeSQL's registration wins, so F1 shows New Extension, New Action, New Script and every other PlumeSQL command straight away (Monaco's own editor commands stay one typed > away). An e2e guards it, alongside the extension editor's snippets.

  • Opening an extension no longer logs a Monaco service error. The JavaScript language service's outline path instantiates an editor component that depends on Monaco's tree-views DnD service, which PlumeSQL's selective build had not registered, so opening a .plumesql.js logged "ic depends on UNKNOWN service treeViewsDndService" once the editor went idle. The service is now registered (it is tiny; a missing service, not a feature), and an e2e guards against this class of missing-service error.

  • Restoring the starter extensions refreshes the Extensions picker. After "Restore Starter Extensions", the grid's Extensions picker now lists the restored extensions at once, instead of keeping its stale list until a later gesture reloaded. The restore relisted the file tree but did not reload the loaded extension set the picker reads from.

  • Auto-saving an extension now applies it. Editing a .plumesql.js (or a helper .js it imports) and letting autosave write it re-reads and re-applies the extension to every open result and view, the same as an explicit save did. Before, the autosave path recognized only the older .grid.js, so a change sat on disk without taking effect until an explicit Ctrl+S or a window refocus.

  • A broken extension edit no longer blanks the result. When a save leaves an extension unparseable (a syntax error mid-edit, a failed import), the last version that worked stays applied to the grid and views while the error surfaces (inline in the editor, or the Log once the file is closed), instead of dropping the formatting and charts until the file is fixed. Fixing it re-applies at once.

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • Reload extensions, by command or per view. A "PlumeSQL: Reload Extensions" command (F1) re-reads every extension and its helpers from disk and re-applies them, for when a change did not pick up on its own. A result view (a chart) gets a Reload button in its header that re-reads and re-renders the view without re-running the query, for quick iteration on a chart. A manual reload reports any extension error to the Log there and then (with the message and an Open the file button), even for the file you are editing, so the reload's outcome is never silent.

Changed ​

  • The editor now uses Monaco's own JavaScript grammar for .js / .plumesql.js. The previous hand-rolled grammar had no regex-literal rule, so a /.../ with a quote inside it (as in /[&<>"]/g) opened a string whose colour then leaked over the rest of the file. PlumeSQL now vendors Monaco's complete, maintained JavaScript Monarch grammar and language configuration (proper regex literals, template string interpolation, hex/binary/float numbers, JSDoc comment continuation on Enter, #region folding, smarter auto-closing), keeping only the @for / @query marker tint on top. Editing a grid extension now reads like VS Code.

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • Richer JavaScript completion and hovers for extensions. The curated JS surface the editor offers (still no language service, by design) gains more of the standard library extensions use (Set / Map / Promise, Intl formatters, more Array / String / Number / Date and RegExp methods, .format / .test / .toLocaleString), and the DOM / SVG surface a view's render(root, data, ctx) draws with (document.createElementNS, root.innerHTML, setAttribute, style, clientWidth …), plus an "svg element" snippet. Member completion now also fires after an expression result, not just a plain name: String(x)., arr[i]. and 'text'. offer the method union instead of the globals list.

  • "Open in tab" reuses an already-open result document instead of stacking duplicates. Promoting the same statement to a tab of its own (from the grid or a view) again just shows the tab it already opened, matched by the statement, its source file and connection.

  • .plumesql.js extensions gain @description and @color, and @for accepts globs. A // @description <text> head comment shows the extension in the file tree, and // @color <name> gives it an identity colour (tree row and tab mark) from the same palette actions use, so extensions read like every other file. The @for script target can now be a glob (report*, *.sql, scripts:reports/*.sql, **/summary*) instead of only an exact name. The editor completes all of it.

  • Result views: a .plumesql.js extension can add a tab that renders the result. Beside a file's format and computed-column rules, it can declare views: [{ tab, when?, render }]; a result whose @for / @query scope (and the view's when(columns) guard) matches grows that tab beside Results and Messages, and the view's render(root, data, ctx) draws into a sandboxed iframe with the result's rows. Isolated (no network, no navigation, no app access), theme-aware, and its ctx.log goes to PlumeSQL's Log. The Results tab (and Messages, Problems) stays clickable while a view tab is open, so you switch back to the grid from either side. A view has its own header, the grid header's twin (the command and the rows / execution-time readout on the left), with two actions on the right: Open source (opens the extension file behind the view) and Open in tab (promotes the chart into a result tab of its own, opened on the chart). A view's render runs through a ResizeObserver on the iframe body, so a chart draws once the pane has laid out and re-fits on every resize, instead of drawing once at a size that could still be zero or stale (which left it blank or half-painted until you switched tabs and back). A view is mounted the first time its tab is opened and kept in the DOM after, so switching back to a chart is instant instead of rebuilding the iframe and re-fetching every time; until the first render lands, a theme-coloured loading shimmer covers the iframe rather than a white flash. A starter chart view (a self-contained SVG bar chart of the first numeric column) ships as an example. See docs/RESULTVIEWS.md. Views are new and still settling; imports inside a view and paging are follow-ups.

  • .plumesql.js is the PlumeSQL extension file (rules and views), replacing the narrower .grid.js, which is still recognized during the transition. A plain .js in a script store is a helper module an extension imports.

  • A .plumesql.js is an extension by its name alone, and scope is separate from identity. The .plumesql.js name makes the file an extension (no @for marker needed the way a legacy .grid.js still does); @for and @query only SCOPE it. A .plumesql.js with no scope marker is recognized but applies NOWHERE until it is attached to a result, and a file may now carry SEVERAL @for and @query lines, which combine as a union (the extension applies to any of them).

  • Starter grid extensions, bundled and restorable like starter actions. A fresh install now seeds a handful of ready-made .plumesql.js extensions into the shared Scripts store under Extensions/: Formatting ones (a relative "… ago" companion column beside each _at timestamp, amount columns as currency, booleans as Yes/No, a leading row-number column) and Charts (a Bar chart and a Line chart of the first numeric column, drawn as a self-contained SVG). They ship UNSCOPED, so they apply nowhere until you attach one to a result from the grid's Extensions menu. "Restore Starter Extensions" in the Explorer pane menu writes back any that are missing, never overwriting your own.

  • Apply an unapplied extension to a result, from the grid menu. The grid's Extensions menu now lists every loaded extension not already shaping this result under "Apply an extension here", each offering three ways to attach it: apply here once (nothing written to disk, cleared on the next run), always for this query (appends a @query marker, the result's source table when it has one so every form of that table matches), or always for this file or its folder (appends a @for marker). This is how an extension saved with no scope, or scoped elsewhere, is pointed at a result.

  • Go to Definition (F12) in .js and .grid.js editors. F12 (or Cmd/Ctrl+ click, or Peek) on an import specifier opens that module file; on an imported symbol it opens the module it comes from (Peek lands on the export inline); on a helper or constant declared at the top of the same file it jumps to that declaration in place. Hand rolled, like the rest of the JS editor support (no language service).

  • Companion columns: add a computed column beside each matched column. A rule with both match and add now adds a new column next to every column the match selects (rather than formatting it in place), reading that column's value. One rule fans out to one column per match, so "a … ago column after every timestamptz" is a single rule. add is a header template where {col} is the matched column's name ({col} ago → created_at ago), and at: 'after' (the default) or 'before' picks the side.

  • A .grid.js rule can share helpers and lookups with the rest of its file. A rule's fn now runs as the live function it is, so it can call helper functions and read constants or lookup tables declared at the top of the same file, instead of each formula having to be self contained. Write the shared formatting once and every rule uses it. (Keep a shared module binding a constant: PlumeSQL may compute rows in separate passes, so use prev / prevRow for a running total, not a module variable.)

  • A .grid.js extension can import helpers from another file, in any script store. Put shared formatting in its own .js file and import it, so one library serves every extension. Two specifier forms resolve: a $store/ alias with the @for store words, as SvelteKit's $lib (import { money } from '$scripts/lib/format.js', also $global/ and $workspace/), reads that store from its root, and a relative path (./format.js, ../lib/format.js) reads the importing file's own store and folder. A helper file needs no @for marker; edit it and save, and every extension that imports it recomputes. Bare package names and URLs do not resolve (a grid helper lives in a store, not a package tree). The JS editor eases it: an import snippet, path completion inside from '…' (the $store/ aliases and every importable file), and completion of a named module's exported symbols inside the { … }.

  • Plain .js files edit with the same JavaScript support, not only .grid.js. A .js file in the workspace is now listed (added to the default file patterns), wears a JS mark, and opens in a JavaScript editor with the grammar, completion (globals, members, the String / Array / Number methods, keywords, snippets) and hover the grid extensions have, minus the grid vocabulary (no @for, no rule keys or descriptor) and with no run bar, since it is a document, not SQL. It is a hand-kept list, not a language service. A .grid.js still gets the grid help on top; a bare .js gets just the JavaScript.

  • The .grid.js editor completes and explains plain JavaScript. Writing a formula now offers the globals it uses (console, Math, JSON, Object, Number, String, Array, Date, Intl, parseInt, …), their members after a dot (console.log, Math.round, ctx.log), and after any other . the common String / Array / Number methods (toUpperCase, slice, replace, map, filter, join, toFixed, …), plus the keywords and a few control-flow snippets; hover explains each. It is a hand-kept list, not a language service (PlumeSQL keeps Monaco's TypeScript worker out), so there is no type inference: after value. you get the String, Array and Number methods together. And a returned descriptor completes by hand: its keys (value, class, style, align, title, html), then after class: the exact class palette (fmt-ok, fmt-bad, fmt-warn, fmt-muted, fmt-strong) and after align: the values, so there is no guessing which classes exist. And a parameter hint in a fn: ( … ) list shows all four parameters a formula may take (value, row, prevRow, ctx) with the current one highlighted, so which are available is a hint away, not a memory test.

  • A grid extension's console.log shows up in PlumeSQL's Log. A formula's console.log / info / debug / warn / error (and dir, trace, group, assert, and console.table, rendered as a small text table), plus a deliberate ctx.log(...), are captured while the on-screen window computes and shown in the Log, so you can debug an extension without the browser devtools. It reads like the browser console: each line leads with its real level (log / info / debug / warn / error), names the column it came from, and consecutive identical lines fold with a ×N count. A console.error is a red error row like any other, but never takes the Log over (it is debug output, not an app failure). Output is capped per compute (a safety valve, since a formula runs once per row); a copy runs quietly.

  • A grid extension can scope to a whole TABLE, not just one query. A // @query whose value is a single relation name the dictionary knows (// @query users, or schema.table) now applies to every form of that table's result, matched by the result's own source relation rather than the query text: select * from users, select * from users where id = 1, a browse of the table from the Object tree, an FK step onto it. It works wherever PlumeSQL identified a single source relation (the case that also makes a result editable), and does not need the DDL snapshot loaded. The @query hover explains which it is.

  • You can step between a grid and the extensions shaping it, both ways. A result's right-click gathers grid extensions under one Extensions node: the ones actually shaping this result first (each naming the columns it touches, opening its file or bringing an unsaved draft forward), then the other saved extensions in scope, then New extension, under greyed section headers so it reads at a glance. The reverse is in a saved extension's own editor right-click (Go to a result it shapes): the open result tabs that extension applies to, each a click away, even when they are not the active tab.

  • A grid extension's // @for marker can target a whole store, or several at once, and its value explains itself on hover. A store word with an empty name is a store wildcard, every result from that store whatever the script name (// @for scripts: the workspace Scripts store, workspace: the workspace files, global: the shared store); a bare // @for scripts reads the same as scripts: (the store), not a script literally named scripts. Several comma-separated targets are a union, so // @for scripts:, workspace: covers both local stores while leaving the shared one out. Hovering a @for value names the exact stores and their physical directories on disk, then, dynamically, WHICH open results the scope matches right now, by name (honouring the file's @query too); a @query value hover shows the normalized query and the same live list. So the hover answers concretely what an extension will format, not only the abstract rule. Completion offers the store wildcards and the both-local-stores union.

  • .grid.js files open in a JavaScript editor. A grid extension file now has syntax highlighting (a hand-rolled JavaScript grammar: keywords, strings including template literals, comments, numbers) and wears a JS mark in the Explorer and on its tab, so writing one reads like code instead of plain text. It completes what fits the cursor: a whole-rule snippet at the array level, the rule keys (match, add, at, priority, fn) inside a rule, and inside a fn the formula arguments (value, row, prevRow, ctx) plus a return descriptor snippet whose class and align values are offered as a dropdown, so what a formula may return is a keystroke away. And it is checked as you type: a file that will not load, or whose default export is not an array of rules (a single rule or { rules } object also count), underlines the error on the export default line and clears it the moment the code is fixed (the check runs in the grid's own Worker, so a runaway file cannot hang the tab). While a .grid.js is open in a tab its errors live there, inline; a broken file that is NOT open is logged instead (closing a broken one re-scans and surfaces it), with an Open the file button on the entry. The Log logs a broken file once, when it becomes broken, not once per scan, and dismisses the entry on its own when the file is fixed, so a corrected extension clears from the Log.

  • The result grid's context menus are consolidated. Every one now follows the same order (value, clipboard, the row, the column, then Grid Rules / Columns / Results Settings) and ends on the same grid group, so a given action sits in the same place wherever it applies. A right-click on a data cell reaches its column's own actions and the grid group, not only the header's menu; the three-dots menu, the empty header past the last column and the blank space past the rows all carry Grid Rules and the grid group too, instead of the generic app menu. To keep the cell menu short, its column actions fold under one "Column: <name>" submenu (a column header, being column context, keeps them inline), and a column's filter options are grouped under a single "Filter" submenu on every menu that shows them.

  • Grid column rules, as .grid.js extensions. Reshape a result grid with small JavaScript rules: reformat a column (a boolean as a coloured TRUE/FALSE or Yes/No, a number grouped and right aligned, a timestamp as a date), or add a computed column (a row number, a running total, two columns joined). Every rule lives in a .grid.js file (there are no session-only rules): the file exports an array of rules (as many as you like; a single rule may be written without the array, and the { version?, rules } object is accepted too), each a format rule ({ match, fn }, matching a column by exact name, a name regular expression, PostgreSQL type or position, combinable) or a computed column ({ add, fn }, placed by at: 'end' the default, 'start', or { before } / { after } a matched column). A rule's fn runs once per row over (value, row, prevRow, ctx) in a worker (a slow one never freezes the tab), and returns a value or a descriptor (value, class, style, align, title, html, strings only). Computed values thread into the next row's prevRow (a running total accumulates), copy along with the real columns, and show ERR (a formatter falls back to the raw value) with one Log entry when a formula throws. The format is specified in docs/GRIDJSSPEC.md. A block's cells are computed the moment the block is fetched, so they are ready as the rows paint: a ruled cell stays blank until its value lands rather than flashing the raw value first, and the compute stays in the Worker so the rows themselves never wait on a slow formula. A cell whose value takes long enough to notice shows a quiet glowing skeleton (only after a short delay, so a fast compute never flickers, and never under reduced motion). Copying a selection runs its formulas in the Worker too, so a slow one never freezes the copy. The cached cells are dropped with their block so the cache no longer grows without bound.

  • Formatting and computed columns live inline in the grid's menu, and apply as you write. A result's right-click offers, over a real column, Format <column> and Add Computed Column, no longer folded under a "Grid Rules" entry. The Format templates are TYPE AWARE, so a column is offered only what fits it: Yes / No and TRUE / FALSE on a boolean, thousands separator and currency on a number, UPPERCASE / lowercase on text, date-only and Time ago (relative time, the exact value on hover) on a timestamp, As a tag (an HTML demonstration, with the value escaped) and Dim NULL on any. Applying a format to every column of a type (all boolean columns, say) is its own entry, Format all columns of a type, beside Add Computed Column: it does not depend on the clicked column, so it shows even where no column is in context (the dots menu, the empty header, a computed column's header), with one submenu per column type present in the result. Running total is offered only on a numeric column. Add Computed Column places the new column where you pick: After or Before this column, At start, or At end. Not over a column (the dots menu, the empty header, a computed column's own header) Format <column> drops away, leaving Format all columns of a type and Add Computed (At start / At end). Pick one and PlumeSQL opens an UNSAVED .grid.js that applies to the result at once, as a draft, scoped to the current script or query by a marker, with no dialog and nothing to run again; a template opens quietly so the result stays in view, Custom opens focused to write. Tune it live, then save to keep it. The menu also lists the saved extensions in scope to open, and offers New extension…, a picker of a blank file or one of eleven worked examples. A computed column now carries the same layout menu as a real one, so it can be hidden and frozen at either edge (freezing overrides its at placement for the view); only Browse stays off, since a computed column has no source relation.

  • A computed column can be filtered too, on small results. A real column's filter runs on the server over the whole spool; a computed column exists only in the browser, so filtering it runs there: PlumeSQL loads the result, computes the column over every row and keeps the matches. Because that needs the whole result in hand, the Filter entry appears for a computed column only when the result is complete and within a size cap; a larger result keeps its computed columns unfiltered. Real-column filters still run on the server and combine with a computed one. A By Value filter on a computed column matches by substring (its value is text), and the match is highlighted in the cell the same as any other filtered column (formatted real columns highlight now too).

  • A funnel on the column header is a fast path to its filter. Hovering a filterable column's header shows a small funnel (always shown, in the accent colour, while a filter is active); clicking it opens the column's filter menu right there, without the right-click. It is the same filter the right-click menu still carries, and appears only where filtering applies (a computed column's funnel shows only while the result is small enough).

  • A // @for marker makes a file an extension and scopes it. Grid extensions are found where action files are, and the same way: the opened workspace itself, the workspace's own Scripts store and the shared Scripts store. But a .grid.js name is generic JavaScript, so what claims a file is one required leading marker, // @for, the same idea as an action file being known by its annotations. A .grid.js without it (a build output, a grid component a JavaScript project in the workspace holds) is left alone: not loaded, not run, no grid icon. Because the marker is the identity, a team keeps its extensions in source control by committing the .grid.js files into the repo, beside its .action.sql files. The marker's value is the scope: // @for global (every result, everywhere; keep it in the shared store), // @for workspace (every result here; keep it in the repo or workspace store, and it wins a formatter tie over a global one), or // @for <script> for one script (matched by name, so several files can target the same script; qualify it global:, scripts: or workspace: to tell apart same-named scripts across the stores; global and workspace are reserved words). A second marker, // @query <query>, narrows to one query, written out in full so it reads plainly (matching ignores whitespace and a trailing semicolon). A rule authored from a column is scoped this way automatically. Extensions show in the Explorer beside scripts and actions (the default file filter now lists .grid.js), load on startup and re-read the moment one changes, so a rule written once follows its scope across every query. Each fn must be self contained (it is compiled on its own).

  • The .grid.js editor knows the format. A grid extension opens in a JavaScript editor with no SQL run bar or connection picker (it runs nothing), and completes what a rule needs: the rule shapes, the callback arguments, the return descriptor with its class and align values, the // @for marker with its global / workspace / store-qualified values and // @query, and, when the file was authored from a result, that result's columns as row.<col>. The markers are highlighted like an action file's annotations; hover explains the whole vocabulary (rule keys, fn arguments, ctx, the formatter classes, the markers and the @for values), and hovering the structure (export default) shows the whole format at a glance (an example, then a short reference); and a Format button (⇧⌥F) reindents it safely (whitespace only). The generated files carry no spec header now, lead with their // @for marker, and are written already formatted (expanded, one field per line), since the editor is the reference. A generated fn names only the parameters it uses (a positional prefix, so (value, row) when it reads the row but not prevRow or ctx); a Custom starting point keeps the full (value, row, prevRow, ctx). The Concat all cells template lists this result's columns by name ([row.a, row.b, ...].join(', ')) rather than iterating the row, so it reads as concrete code to edit.

  • Grid extensions are marked where you see them. A .grid.js that IS a PlumeSQL extension (one carrying the // @for marker) wears a violet grid glyph (its own icon, not a lettermark) in the Explorer, on its tab and in search results, so a rule file is told from an ordinary script at a glance; a .grid.js without the marker wears the plain file icon. And a result column a rule shapes, a formatter re-rendering it or a computed column, carries a small violet dot in its header, so the transformed display is never mistaken for the raw value. The dot's tooltip names the extension shaping the column, and the column's menu offers Edit (open that .grid.js, or bring its unsaved draft forward) and, for a single-rule file, Delete, so a rule is traced and changed from the column it affects, not only authored there.

  • A running total is correct end to end on a small result. A computed column that looks back with ctx.prev (a running total, a cumulative) threads over the whole result when it is complete and small enough to compute at once, rather than resetting every block as you scroll; a larger result keeps the per-block reset (use SQL for a true cumulative there).

  • More lightbulb refactors on a SQL statement. Beside Expand *, the lightbulb (⌘. / Ctrl+. elsewhere) now offers, where each fits: Collapse a full column list back to *; Add an INSERT's column list from the table (a generated column stays out); Qualify bare columns with their table or alias; Remove a trailing comma; Add GROUP BY for the non-aggregated columns of an aggregate query; Add LIMIT 100; and Extract a FROM subquery into a CTE. Each reads the same object dictionary and scope as completion, backs its edit with a single undo step, and appears only where the statement supports it.

  • Expand a * into its columns. With the cursor on a select * (or a qualified t.*), the lightbulb (⌘. / Ctrl+. elsewhere) offers "Expand * to columns" and "Expand * to columns, one per line": it writes the relation's columns from the object dictionary in catalog order, qualifying each with its table or alias when the FROM names more than one, and aligning them under the star for the multi line form. A multiplication or a count(*) is never mistaken for a column star. Real tables and views for now; a CTE's or a derived table's * is left as is.

  • The toolbar's Hints and Hovers buttons pick their kinds. Each now wears a small caret and opens a menu on click: Inlay hints with Function hints and Column type hints under it, Hovers with the object hover and the reference cards. The tooltip says which kinds are on, so "hints on" no longer hides that Column type hints (off by default) are why a name: text hint is missing; the family's own shortcut still toggles all at once.

  • PlumeSQL flags a forgotten semicolon between two statements. When two SQL commands are written one after another with no ; between them, the seam is underlined and the lightbulb (⌘. / Ctrl+. elsewhere) offers to insert the ;. The same finds list in the Problems pane, where a row's menu inserts the ; for that seam or, when a file has more than one, Fix All Missing Semicolons in a single edit. PostgreSQL's own parser both finds the seam and confirms it, so a multi line single statement, a column alias, a set operation or a ; inside a string or a dollar quoted body is never flagged. It runs in the page with no server, so it works offline and on a connection whose parser is silenced; editor.flagMissingSemicolons (on by default) switches it.

  • The terminal panel holds more than one shell. A + in the terminal panel's header opens another integrated terminal, and the shells show as small tabs beside it (each labelled by its shell, with a close when more than one runs); click a tab to switch, and New Terminal, Restart Shell and Close Terminal are in the panel's overflow menu too. Each terminal is its own server PTY, so it survives a reload (reattach replays its scrollback) and the open set is remembered per workspace; on restore the shells still alive come back and the reaped ones drop. A terminal can also be promoted into a tab of its own (the arrow on its strip tab, or Move to Editor Area in the menu), filling the editor area beside your SQL; Move to Panel in the terminal's right-click menu sends it back. Closing the tab ends its shell.

  • The editor toolbar can wrap, and its layout resets. A new switch (Wrap on Narrow Widths, in the toolbar's right-click menu and as editor.toolbarWrap) lets the tool buttons flow onto a second row when the bar runs out of room, instead of the right group covering the left; off by default, the connection readout and Run never cut either way. And Restore Default Layout in the same menu puts the pinned buttons, the group sides and the floating groups back to their defaults in one click (the view switches, large buttons and wrap, stay).

  • DO blocks read like routines. An anonymous block (DO $$ … $$) gets everything a function or procedure body had: its variables complete inside the body and carry their type on the hover, Go to Definition, Find References and Rename walk between a variable's declaration and its uses, the Outline lists its variables, the block folds, and the dynamic parser checks its statements with the variables typed (a RETURN with a value is refused, as in a procedure). PL/pgSQL is the block's default language, as PostgreSQL has it; LANGUAGE sql and the rest are read by what they say. And do in the completion list offers the whole block as a snippet: the dollar quotes, a DECLARE with one variable, a BEGIN … END with a RAISE that uses it, the variable's name and value as the stops.

  • PL/pgSQL completion. Inside a routine body the list knows the language, which the SQL word list never did: its control words (raise, elsif, loop, perform, execute, get diagnostics, when others then ...), and the spots the language shapes tightly enough to answer alone: after RAISE the levels, each bare and as a whole statement with a placeholder and a variable; after the message USING, and after USING its options; after EXCEPTION WHEN the everyday condition names with their SQLSTATEs, others then first, sqlstate '…' as a snippet; after GET DIAGNOSTICS its items, the stacked form its own; after RETURN, FOR … IN, FOREACH … IN, EXIT and CONTINUE what follows. The constructs come as snippets beside the words, each closing its own block: if (with and without elsif), for over a query and over a range, foreach, while, loop … exit when, case, a nested begin … exception … end, execute format(…) into … using …, get diagnostics, return query, select … into strict, a cursor's open, fetch and close, assert, perform.

  • Statement templates at a statement's start. Beside do, the completion list offers a whole statement as a snippet whenever the cursor stands at a statement's start (and while a create [or replace] lead is still being typed): a PL/pgSQL function, a plain SQL function, a SQL function with a standard begin atomic body (PostgreSQL 14 and later), a set-returning returns table function, an immutable parallel safe SQL function, a PL/pgSQL procedure and a begin atomic procedure, a trigger function and the create trigger that binds it, a table (identity primary key and audit timestamps) and a range- partitioned table, a view and a materialized view, a composite and an enum type, a schema, a sequence, a domain, a row-level-security policy and the alter table … enable row level security that arms it. Beside the DDL, the everyday statements: a do (test) block that arranges, asserts and rolls back (repeatable throwaway tests), the full write matrix (insert, insert … returning, and every on conflict action: do nothing, do update, their returning variants, and on PostgreSQL 19 and later do select and do select for update, whose returning is required), insert … select, update and delete with their returning variants, a merge into … using … (the standard-SQL upsert, PostgreSQL 15 and later), a common table expression (with … as (…)) and its recursive form (with recursive), a create index, an explain (analyze, buffers), a copy … from/copy … to, and grant/revoke. Each carries a distinctive label (function (sql), table (partitioned), materialized view, do (test) ...) and filters on both its leading keyword and its distinguishing words, so create, function and sql all reach the SQL function and do test reaches the test block. The shapes are written in one house style: lowercase throughout, _-prefixed arguments and variables, language/as/$$ each on their own line, four-space bodies.

  • The object tree's write scripts fan out. A table's Script and Copy Script menus group the write statements into insert, update and delete submenus, each with the full set: insert and its returning form, every on conflict action (do nothing, do update, their returning variants, and, when the server is PostgreSQL 19 or later, do select and do select for update, whose returning is required), and update/delete each with a returning variant. Every form is written from the table's own columns and its primary key (the conflict target, the SET list and the WHERE key), the same as before. A table also scripts a merge into … using … (matched on the key, from its columns; PostgreSQL 15 and later), a create index, a truncate … restart identity cascade beside the plain one, and rename, analyze and vacuum. grant and revoke join the table, function, schema and sequence menus, rename the index, trigger, type, schema and sequence menus. And the extended-statistics and foreign-server objects now offer a drop, like every other object type.

  • Documentation for the catalogs, the builtins and the extensions. Every object of pg_catalog and information_schema, every builtin function and type, and every member an extension owns has a Documentation entry in its row menu, a button in its DDL header and a link on its hover, opening the page that documents it: a catalog's own page, a statistics view's section inside the monitoring chapter, a function's cell of its category page, a type's family page, a contrib module's page (pg_trgm's for similarity), PostGIS's page per function, Citus's API section, TimescaleDB's reference page; an extension documented elsewhere (pgvector, pg_cron, pg_partman) opens its home in the browser. The page opens as a reference tab with the reference's own words: a catalog's description and its whole column table, a function's signatures and description, and the hover shows the description line. A catalog's columns, which have no comments of their own, read their descriptions from the same excerpt: the object tree's column rows and the column hover say what relkind means, and a catalog object's row in the tree carries the reference's description where a comment would stand, as does the completion list's details pane for a catalog object, a builtin or an extension's member. Resolution goes through the reference's own book index (one fetch per major, then cached like the excerpts) and the extensions' indexes (PostGIS's reference page, Citus's API pages, TimescaleDB's sitemap), so a name is never guessed into a wrong page; everything fetched lands in the documentation cache (docs/GLOBALDIR.md) and serves offline after the first ask, and the keyword hover's switch (Keyword and operator hover) governs every fetch a hover or a row makes. The dictionary gained ext, the extension that owns a member (docs/OBJECTINDEX.md), which the hover now names too.

  • Citus in the object tree. A table Citus distributes reads DISTRIBUTED on its row instead of TABLE, a reference table REFERENCE, and the object hover says what the extension manages it as: the distribution column and shard count, the table it is co-located with, a reference table replicated to every node, a local table in the metadata, a table distributed with its schema. The facts live in the DDL snapshot's object dictionary (citus, docs/OBJECTINDEX.md), read by one query over the extension's own catalog that runs only when the extension is installed; the dictionary type underneath stays table, so Browse Data, the statistics and every @for table action keep working. The DDL files recreate the tables: a distributed table's file carries, between its CREATE and its constraints (a foreign key between two distributed tables needs both distributed), the create_distributed_table call with its column and shard count, co-location written by NAME (a group's first member by name is created with colocate_with => 'none', the others with the leader), a reference table's its create_reference_table, a table added to the metadata by hand its citus_add_local_table_to_metadata, and a distributed schema's own file its citus_schema_distribute, whose tables then carry nothing. A table distributed or undistributed moves the snapshot's change check like a column would. @for gains distributed table and reference table (docs/ACTIONSPEC.md 8.28), and the starters a Citus folder: every table the extension manages (each opening its shards), the shards of the table an action was opened on, the cluster's nodes, and the moves the rebalancer would make. The citus and citus_internal schemas join the Object filter's system schema groups. The version matrix gained a Citus server (citusdata/citus, amd64 only, so emulated on an Apple silicon host), on which the fixture's distributed, reference, local and schema-distributed tables replay and round trip byte for byte.

  • pg_cron and pg_partman starters. The pg_cron folder lists the scheduled jobs (each opening its recent runs) and the recent runs of all of them, refreshing on its own, and schedules or removes a job from a form, asking first. The pg_partman folder lists the partition sets (each opening its partitions from the catalog), runs the maintenance and sets a set's retention, asking first; the extension's schema, an installer's choice, is asked in the form and inlined as an identifier, quoted by the server. Forty-eight starters now.

  • Hypertables in the object tree. A table TimescaleDB manages reads HYPERTABLE on its row instead of TABLE, and a continuous aggregate CAGG (TimescaleDB's own shorthand, the full words would not fit a row) instead of VIEW (which is what the catalog holds under that name, over an internal materialization hypertable); the object hover leads with the same word and says what the extension manages it as (the time column and chunk interval, the space partitioning, whether compression is enabled; an aggregate's source hypertable, materialized only or real-time). The facts live in the DDL snapshot's object dictionary (timescale, docs/OBJECTINDEX.md), read by one small query over the extension's information views that runs only when the extension is installed, so a server without it pays nothing; every existing snapshot rebuilds once on its next check to learn them. The dictionary type underneath stays table or view, so Browse Data, the statistics and every @for table action keep working. @for gains the two words hypertable and continuous aggregate (docs/ACTIONSPEC.md 8.28), subsets of their dictionary type, and the starters' TimescaleDB folder six actions on them: a hypertable's chunks (range, size, compressed or not), its compression (chunks and bytes before and after), its policies and background jobs (on a continuous aggregate too), and three that write and ask first: compress every chunk not yet compressed, add a compression policy, add a retention policy, the ages asked in a form. And the DDL files recreate them: a hypertable's file carries, after the table's own statements, the create_hypertable call with its chunk interval (the integer now function with it), every further dimension, the compression settings (segmentby and orderby where TimescaleDB 2.17's settings view exists) and its compression, retention and reorder policies; a continuous aggregate's file is the CREATE MATERIALIZED VIEW ... WITH (timescaledb.continuous) that made it, with its own query, its materialization mode and its refresh policy, in place of the CREATE VIEW over an internal table the catalog holds under that name, and its owner clause in the one spelling the extension takes. A policy or a compression setting changing, which the extension keeps in a catalog of its own, now moves the snapshot's change check like a column would. The version matrix (make test-versions, the schema matrix workflow) gained a seventh server carrying the extension, on which the fixture's hypertable and aggregate replay and round trip byte for byte; verified on TimescaleDB 2.1 and 2.29.

  • Zoom. The whole app zooms in the desktop app, VS Code's View: Zoom In, Zoom Out and Reset Zoom on VS Code's keys (⌘= and ⌘- on macOS, Ctrl+= and Ctrl+- elsewhere, the numeric pad's 0 for the reset), with the wheel and ⌘ held (Ctrl elsewhere), the browser's gesture, from the Zoom Out and Zoom In buttons right of the workspace title (the Go Back and Go Forward pair's mirror, with the percent between them once off 100%, a click on it resetting), from the app menu's Zoom submenu with its readout, and from the command palette. It is the desktop shell's own webview zoom, so everything scales together, the editor, the grids and the terminal included, and it persists as the global window.zoom setting. In a browser the browser's own zoom does this on the same keys and PlumeSQL stays out of its way. The editor's own text size (the editor.fontSize setting, a menu away in the editor's Text group, the palette's Editor Font Size: Bigger, Smaller and Reset) gains three toolbar buttons in the View group, smaller, bigger and back to the default, each saying the size now.

  • Inlay hints. Small labels drawn inside the text, VS Code's idiom, read from the object dictionary and never from the server: the parameter's name before a bare-value argument of a routine call (film_in_stock(p_film_id: 1, p_store_id: 2); a written expression already reads and gets none), the declared type after a SELECT list's bare-column items (f.title: character varying(255), resolved through the statement's aliases and FROM targets), and a routine's return type after its call. Each kind is its own setting under Editor, Inlay hints, with one master switch over them (off until asked for: Alt+I, the toolbar's Hints button, the menu or the palette), and VS Code's per-line length cap is a setting too (43 characters by default, 0 for none). Editable SQL buffers only, and only the statements on screen.

  • A play glyph in the gutter runs the statement. New switch (editor.statementRunGlyph, on by default, also in the lens cog's menu): a play glyph in the gutter on every statement's first line, VS Code's test-runner idiom, which costs no line above the statement and runs it on a click through the same select-then-run gate as the lens. The glyph wears the run guard's color the way the statement marks do, so what a statement is and how to run it are one sign. Run is all a glyph can do, so it stands BESIDE the lens instead of replacing it: the lens's own list says what the lens carries (empty is no lens at all), and neither switch hides the other's commands.

  • Every editor setting is a menu away, and the inlay hints have a switch on the toolbar. The editor toolbar's menu and the editor's right click (Editor Settings) carry every Editor setting in the Settings page's own groups: a switch is a checked row, a choice a submenu with the current value checked, a number or a text a row that opens the input, and the settings page link stays at the end. The statement lens opens its OWN menu there instead of a row spelling its semicolon list: the commands, glyph and settings entry the lens cog offers, built once for both doors. What is set once and forgotten, or owned by a surface of its own, stays on the Settings page and out of the menus: the inlay hints' length cap, the large script threshold and its paused services, whose door is the strip over a large script (it now ends with the settings entry too), the toolbar's own four (its buttons, sides, floats and size), whose door is the toolbar, and the Run group (the run key, run without a selection, run parameters) and the JSON indent, habits rather than switches. One builder serves the whole menu, and the palette's Editor Settings picker writes by the same rule (the workspace when one is open, global otherwise, global-only settings always global). The editor's own faces are switchable from the toolbar: a new View group, the last one on the left, carries four masters that are their own readout (accent while on): Hints (every inlay kind; the kinds are in the editor menu), Hovers (every kind), Lens (the row above every statement, a switch of its own now, editor.statementLensShow, beside the list of what it carries) and Gutter (the play glyph). Function hints come on with the hints: the return types are on by default like the parameter names. Inlay hints as a whole, useful one minute and in the way the next, get the palette's Toggle Inlay Hints and a key of their own, Alt+I (keyboard.toggleInlayHints, rebindable; PlumeSQL's own combo, VS Code ships none, beside Alt+Z for word wrap). The master switch is plainly on or off: VS Code's two hold-Ctrl+Alt modes were carried for a day and dropped, a key beats a chord you have to keep holding.

  • The editor's own knobs are settings. Font family and ligatures, line height, cursor style and blinking, the current line highlight, occurrences highlight (the word under the cursor lit everywhere, new), bracket pair colors, indent guides, sticky scroll for scripts (the statement's first line pinned while its body scrolls, new) and the lightbulb, each under Editor with VS Code's default where VS Code has the option. Nothing about the editor is assumed any more: every one of them switches.

  • Scripts from the object tree. An object row's menu carries Script and Copy Script: the statements one writes about the object, generated from the dictionary. A table gets select, select with its foreign keys joined two levels out, count, insert, insert returning, insert on conflict over the primary key, update and delete by key, alter, truncate and drop; a view its select, a materialized view its refresh; a function its call, a procedure its call statement, a sequence its nextval, currval, setval and restart; an index, a trigger, a constraint, an enum, an extension, a role, a publication and a schema their few; everything its COMMENT ON. Values the template cannot know are $1..$n, so Run asks for them. Script opens a TEMPORARY script tab on the object's connection: it runs like any script and wears the unsaved dot; Save asks where to put it (Scripts, Workspace Scripts or Workspace Files) and then the name, closing it asks Save or Don't Save, and Don't Save loses it. Copy Script puts the text on the clipboard.

  • Edit Comment. An object row's menu and a column row's menu open a comment editor: one form with the object's comment and, for a table or view, every column's, prefilled. Save writes one COMMENT ON per changed field in one run on the object's connection (an empty field removes the comment), the server's answer goes to the Log verbatim, and the object re-renders so the tree, its tooltips and completion carry the new text.

  • Follow the editor in the object tree. A crosshair button in the Objects header (and Follow the Editor in the pane menu, the setting Follow the editor cursor under Objects) lights up and reveals the object under the editor's cursor as it moves: a table in a FROM, a function in a call, the relation behind an alias or a column. Off by default; the editor keeps the focus.

  • Actions attached to a kind of object: @for. An action file that says @for table (or view, routine, sequence, any dictionary type, several at once) appears in every such object's row menu and in the open object's header menu (Run <action>, directly while there are up to five, in an Actions submenu beyond that) and, while that object's DDL tab or browsed rows are the active tab, as a group of pins in the title bar after the standing ones (a hairline and the object's name in front), and runs on that object: {schema}, {name}, {object} and {type} are the object, bound like every other placeholder. One view per action and object. Four starters ship in an Objects folder: a table's size, rows and maintenance state, a routine's calls, a sequence's state, and a pg_dump of one table (the starters seed into an empty store; see "Getting the starters back" in docs/ACTIONS.md).

  • Big folders open at once. A folder with more than 200 rows (the system catalog's functions) renders its first 200 and a "Show N more" row that brings the rest, instead of stalling on three thousand rows.

Changed ​

  • The file Explorer loads lazily, one folder at a time. Opening a workspace no longer walks the whole directory tree up front: the Explorer reads a folder's contents only when it is opened, so even a very large or deeply nested workspace appears instantly and each folder costs one directory read. The tree starts folded (VS Code's first open) and restores exactly the folders that were left open. One consequence of not reading a folder before it is opened: a single-child folder chain is no longer compacted into one row under the workspace files (the scripts roots, read in full, still compact).

  • Turning off the Explorer's Workspace Files root now skips the scan. Unchecking Workspace Files (the Explorer menu, or the Workspace files root setting) no longer just hides the row: it stops scanning the opened directory altogether. It is the way to work with scripts only, and to keep PlumeSQL fast in a very large workspace. The scripts and global scripts roots skip their listing the same way when switched off.

  • The Outline side panel opens with Cmd/Ctrl+Shift+X. Its old combo, Cmd/Ctrl+Shift+O, is VS Code's Go to Symbol and belongs to Go to Object, which shared it and could never fire from the keyboard.

  • One name for the picker on every surface. The status bar, the quick pick and the tab well all say it goes to a tab, a file or a database object; "schema object" is gone.

  • The System page says the AI assistant is still being built and promises no shortcut for it.

  • The editor's dual line-number gutter, the Outline snap bar, the Log panel's rows and the Settings and System pages' scroll spies do their per row and per line work once, not on every paint or scroll event.

  • The MCP status is polled once for the whole app instead of twice, and the status bar's activity dot beats while the poll runs.

  • The connection vault is decrypted, and the OS keyring asked for its key, only when the vault file actually changed, instead of on every read.

  • Settings files are parsed once per change instead of on every read.

  • The terminal's scrollback is a real ring buffer instead of recopying its whole window per chunk once full.

  • Filtering a large result reads its window in one pass, and a case-insensitive filter no longer allocates per cell.

  • An action's input form asks the server for all its parameter types in one round trip.

  • Settings, the account cache and the workspace session are written atomically, like the vault.

  • The System page's PlumeSQL files list shows every file PlumeSQL keeps in its global directory.

  • The session carries a tab's on-disk baseline only when it differs from the tab's text.

  • Saving, renaming or deleting an action file re-lists that root alone; a successful contact re-lists the connections only when it healed something; the parser's throwaway shot waits out the window after a failed dial.

  • The object tree's find waits for the query to settle before re-filtering, and the pinned, Frequent and New shelves share one built tree.

  • The sticky folder stack measures the tree's rows once per change instead of once per scroll frame.

  • Deleting a folder reads its files in parallel batches, with the activity dot beating for the work.

  • Vocabulary: the object tree says "DDL snapshot" and "Object filter" where it used to say "schema snapshot" and "schema filter".

  • A partial snapshot refresh drops the dependency edges of untouched files whose targets it reaped.

  • The Explorer no longer follows an action's answer. Running an action from the title bar, an object's row or the pane used to move the Explorer to the action's file, away from the script being worked in; an answer (a query's result document, a command's terminal) is not the file, and the tree now stays where it was. The action's source face, the file being edited, follows like any script, and the answer's header gained Reveal in Explorer for the deliberate walk to its row.

  • A folder in the object tree opens on its own button. A click on a schema, a type family or a shelf now only expands, collapses or selects it, as in every tree. The document holding every object under it (read-only, for the editor's find) opens from a button at the row's right edge, faint until the row is under the pointer, VS Code's inline actions, and from the row menu as before; it opens as a real tab, not the preview slot. The "Open a folder as one document" setting (schema.folderView) and its pane menu row are retired: a stored value is ignored quietly.

  • The status bar's parser chip is two switches. Its menu used to write the file's @parse line in four shapes (squiggles, Outline marks, both, off), a display choice dressed as a switch, and said nothing of the static readers. It now reads "Parser: static + dynamic" and offers the two switches the chip exists for: Static analysis (the readers in the page, off for a slow machine or a huge script) and Dynamic parser (the server asked as you type, off for a server that must not be touched without an explicit command). Each row is checked as it applies to the tab; a row a connection, a group or the file's own @parse line decided is greyed and says so, with "Open where it is decided" below. Where the findings show moved to its own setting, parser.showIn (both, markers, outline; Settings only), and editor.liveParse is plainly on or off; a stored shape still reads. The chip's silence now names the parser: "Dynamic parser off". Both switches are in the command palette too.

  • The two kinds of hover switch apart. A SQL buffer answers with two different things and they had one and a half switches between them: the object hover (what the dictionary and the snapshot know about the name under the pointer, no query) had none of its own, and the master switch was the only way to silence it. It gets one now (editor.hoverObject, on by default, also Toggle Object Hover in the palette), beside the reference switch (editor.hoverReference), which keeps the keyword and operator cards that read the PostgreSQL documentation. Either kind can go quiet with the other left alone, and Editor hovers stays the master over both. The three rows also say which source they are: "from the dictionary" against "PostgreSQL docs".

  • The object tree follows the editor by default, for your own objects only. Follow the editor (schema.followCursor) is on from the start now, and it skips PostgreSQL's and an extension's schemas, the ones the tree dims: on pg_class or now() the tree stays where it was, lit as it was, instead of opening pg_catalog's three thousand rows every time the cursor crosses a builtin. The same predicate orders and dims the tree's roots, so the two cannot disagree about what is yours.

  • The empty script's placeholder teaches the psql commands. A second line under the run gesture says "Type -- @ for annotations,
    for psql commands (\d film, \dt, ?)", the one place a fresh script had to discover them. The placeholder wraps now: Monaco drew it as one line cut with an ellipsis, which hid the tail at the width a narrow window leaves.

  • The feather moved to the app menu. The PlumeSQL mark and name left the top left corner; the app menu's button at the foot of the side strip (account, settings, theme, about) is the feather now instead of three lines, the one place the mark shows.

  • The object row's menu is a third of its former length. Refresh, Pin and Hide are submenus over the row and its levels, the three copies are one Copy submenu, and the view switches (Object Statistics, Extension Objects, Order Objects by Size) left the row: they are the pane's choice and stay in the pane's menu, one click away on its header.

  • Completion, the Outline and the run guard read the statement through PostgreSQL's own parser. What is visible at the cursor (the relations and aliases in scope, the CTEs and derived tables with their columns), what the Outline lists under a statement (its CTE nodes with their bodies, the objects each body references) and what the guard and the gutter marks take a statement for (a data change, a schema change, a DELETE without its WHERE) now come first from the same parser the server runs, in the page, so a USING join, a LATERAL derived table, a MERGE source, an XMLTABLE or JSON_TABLE, the arms of a UNION, a subquery three CTEs deep, a trigger's function, a COMMENT ON target, a data-modifying CTE and EXPLAIN ANALYZE all read exactly as PostgreSQL reads them; a column merely named update inside a WITH no longer counts as a data change, and a DELETE without a WHERE inside a CTE now counts as the dangerous one it is. The tree-sitter grammar and the token heuristics still answer wherever a half-typed statement has no complete tree yet. The parser loads once per page, beside the grammar, on the first SQL editor; the Static analysis setting silences every reader together.

Fixed ​

  • A grid rule that loads but does nothing is now flagged in the editor. A .grid.js rule PlumeSQL silently dropped (a formatter whose match names no column, so the common match: { match: {…} } typo, a rule with neither match nor add, or one missing its fn) is underlined as a warning as you type, instead of just quietly never applying.

  • A grid extension authored from a partial run names the right query. Formatting or adding a computed column from a result that a selection (or a single-statement run) produced wrote a // @query marker for the buffer's FIRST statement instead of the one that produced the result, because a run numbers its statements within the text it executed, not the whole file. The marker (and the scope match that reads it back) now derives from the run's own SQL, so the query it names is the one you ran.

  • An inline comment no longer corrupts a grid extension's @query marker. The marker is one line, so a query with a -- ... comment mid statement had the comment swallow the rest of the query once it was flattened. The query is now compared with its comments stripped (string literals kept intact), so the marker reads as clean SQL and a query that differs only in its comments or whitespace still matches.

  • Starting PlumeSQL in a filesystem root or the config dir no longer crawls the whole machine. The workspace file listing refuses a filesystem root or the global plumesql dir (a note in the Explorer says so, and Workspace Files can be turned off there), and every workspace listing now stops on a wall clock and a directory cap as well as the file cap, so a very large or slow tree returns a truncated result instead of hanging.

  • Reserved words come out quoted in generated SQL. quoteIdent now quotes a PostgreSQL reserved keyword (a column or table named order, user, end, default ...), the way quote_ident does, so Script and Copy Script, the DDL tab titles, and the editor's Expand and Qualify actions produce runnable SQL instead of a bare select order from t.

  • The lightbulb's SQL refactors keep to the cursor's own scope. Qualify columns and Add GROUP BY no longer reach into a subquery's or a UNION arm's columns (which turned an independent subquery into a correlated one); Expand * ignores a star inside a function call like count(t.*); Add an INSERT column list writes it after an AS alias; and the missing-semicolon fix places the ; before a trailing comment, not inside it where it did nothing.

  • An identity column stays out of a generated INSERT. A GENERATED ALWAYS AS IDENTITY column is excluded from the Script and Copy Script insert and merge templates, like a stored generated column, so the statement runs.

  • The Log panel no longer loops when it is empty. Watching an empty Log with the panel open could trip Svelte's effect_update_depth_exceeded; the seen-watermark update is now untracked.

  • A canceled availability check no longer marks a healthy connection down. When a probe request is canceled (the client left, or a newer probe superseded it), the cut-short dial is not recorded as a failed Test, so the connection's dot stays as it was instead of latching red with a backoff.

  • An action's result spill file no longer leaks on a mid-stream re-run. Disposing a once-run result while its rows were still draining could re-create the spill file it had just removed; a disposed result now refuses further rows.

  • Forgetting a connection clears the rest of its state. Deleting or unsharing a connection also drops its captured search_path, its per-connection tree state (expanded folders, filter, hide and pin lists), its pending refresh timers and its view scroll positions, so a rediscovered connection reusing the id starts clean.

  • The heuristic FROM and object readers are steadier. A comment's apostrophe (-- customer's list) no longer blanks the query up to the next quote and hides FROM targets; a FOR UPDATE OF t row lock is not read as a table; a schema named like a directory (tables, views, functions ...) reads as a SCHEMA; a quoted primary-key column name parses whole; and a truncated text never splits an emoji in half.

  • A result cell's hover tooltip no longer covers the screen. The native title was the whole cell value, so a multi-kilobyte cell (a stack trace, a long JSON string) painted a box across the window; it is capped to a short preview now, with the full value a Shift+Enter away in the value tab.

  • The Outline no longer shows a RETURN at the end of a DO block, a procedure or a void function: the PL/pgSQL parser appends an implicit one the source never wrote, and it was listed as if it had.

  • Go to Object could never fire from the keyboard: it shared its default combo with the Outline side panel and the first binding always won.

  • The keyboard cheatsheet listed an Open Snippets shortcut for a panel that no longer exists.

  • Every gesture the session records now schedules its save: a stacked pane's splitter, a document view's scroll position, a hidden discovered connection, the Log's seen mark, the Actions pane's pin, an action tab's face and the Problems toggle used to wait for an unrelated change or the quit-time flush.

  • F6 reaches the bottom panel's terminal again while a command action's terminal is open, and the panel's terminal stays reachable after that action tab closes.

  • The connections pane's view switches and the Problems and Messages column pickers save globally when no workspace is open, instead of logging an error and doing nothing.

  • A required run value explicitly set to NULL no longer blocks the run with no field marked as missing.

  • The Annotations Reference's "A plain SQL file" entry jumps to its section again (the nav and the section carried two names).

  • The keyboard shortcuts cheatsheet takes focus when it opens, and its text can be selected.

  • Signing out, copying a command action's command and reading the team's groups report their failures to the Log instead of dropping them or painting them inline.

  • Every copy to the clipboard goes one road: a refused write is reported to the Log, and "Copied" is written only when the copy happened.

  • The Account page's "Create one" link opens externally on every kind of click; a middle click or a Cmd click used to navigate the page away.

  • A background rescan of discovered connections (every listing, every changed or vanished candidate) no longer disconnects live sessions; the new parameters apply from the next connect on, as the rule says.

  • The SSH agent socket is closed after every tunnel dial, so availability checks on a tunneled connection no longer leak a file descriptor each.

  • A notice raised before a statement's first row is filed under that statement in Messages, not under the one before it.

  • A failed export leaves the file you picked exactly as it was; it used to truncate it first and delete it on the error.

  • Column types and editability resolve again after a run that included DISCARD ALL.

  • The catalog fence cleanup gives up after ten seconds, so a server that stops answering no longer blocks closing and reaping every session.

  • The event log's rotation happens before the answer says it did, and no longer races an open append on Windows.

  • A file path copied to the clipboard on Linux is percent-encoded, so a path with a space pastes correctly.

  • A path with an apostrophe no longer breaks the Windows PATH command on the System page.

  • The Log's fix buttons (Add the plumesql command, Open group, Open settings.json, Use the ID, Show the run, the Reference door) survive a restart: every fix kind is read back from the log file through one registry shared with the writer, so a kind cannot be forgotten again.

  • Replacing a preview tab frees everything closing it frees, so a replaced preview never dials the parser's throwaway connection and closing the next tab returns to the previous one.

  • A DDL snapshot ensure that arrives while another build is running is queued and built when the slot frees, instead of being marked done with no snapshot until a manual Refresh.

  • One failed dial makes one Log entry with its Edit connection button; the connection picker and the new-script path no longer add a second.

  • A tab closed while its dial was in flight no longer leaves an orphan backend.

  • A raised files.frequentLimit is honoured on restore, and recent objects restore per connection under schema.frequentLimit.

  • Copying a Log entry, the whole Log or an object's script reports a refused clipboard write; a refused copy no longer dismisses the entry.

  • A failure while PlumeSQL starts is reported in the Log as "Starting PlumeSQL".

  • A failed file or object listing goes to the Log, with Try again on both trees (the object tree rebuilds its snapshot, the Explorer lists again), instead of a red banner that replaced the tree's rows.

  • A failed result range fetch is reported once in the Log, and the rows it could not fill stop pretending to load.

  • A quick connection switch can no longer let an older object listing land over a newer one.

  • The "Show N more" row opens the rest of the folder from Enter and Space, instead of trying to open the folder as a file.

  • The Explorer's File Statistics, Descriptions and Sort switches are on its pane menu always, not only once the workspace has recent files.

  • The Explorer shows the shared Scripts root when PlumeSQL opened no workspace, as its tooltip promised.

  • The connection editor no longer repeats its snapshot status request on every connection refresh.

  • The Object filter window's rows are reachable from the keyboard: one Tab stop, arrows inside.

  • The group editor and the connection editor offer one colour palette, so a colour picked in one is offered and shown as selected in the other.

  • Two row actions with the same label no longer collide in the results grid, a cancelled pointer ends a grid or panel drag the way a released one does, tree and search timers no longer fire into a pane that has gone away, and Space works in the connections tree the way it works in the file trees.

  • The editor no longer keeps every definition preview it ever built: the models behind Peek Definition live in a bounded set and the oldest are released.

  • Typing inside a half-written statement, hovering in a large script and editing a file full of dollar quoted routines are all much cheaper: the healing parses, the action header scan, the routine variable lookup and the typed literal scan are each bounded now.

  • The completion's statement slice and the run's statement split read strings, quoted names and nested comments the same way, so both put the boundary in the same place.

  • Turning Static analysis off or on refreshes the Outline at once instead of leaving what it computed the other way.

  • LEFT, RIGHT, REPLACE and TYPE render as the keywords they are, and the misspelled builtins (regexp_matchs, phraseto_tsqury, databse_to_xml, sense_rank, pg_collaction_is_visible, pg_last_commited_xact) are gone from the suggestions, correctly spelled.

  • A line holding a quoted name with two dashes in it no longer silences that line's SQL completions.

  • Copying a statement or a folder document's section from the editor lens says so in the Log, and a refused clipboard write is reported instead of swallowed.

  • The DDL snapshot renders the same through a borrowed tab session as over a dial: the build pins DateStyle, IntervalStyle, TimeZone (UTC), extra_float_digits, standard_conforming_strings and bytea_output, so a tab's own SET no longer makes timestamptz bounds, interval defaults or float constants flap, and the role fingerprint no longer moves with the session's interval style.

  • A GRANT renders its WITH GRANT OPTION, and a table's column-level grants render at all.

  • A LANGUAGE c function renders AS 'file', 'symbol' and replays; window functions enter the object tree and the dictionary.

  • Opening an object during a snapshot build can no longer republish the pre-build object list over the fresh one; snapshot files are written under per-process temp names.

  • A long non-ASCII object name is cut on a character boundary instead of producing a file name the filesystem refuses, which threw the whole build away.

  • A shared catalog store that cannot be built is reported once and retried after ten minutes instead of failing silently on every build; a broken TimescaleDB policy payload fails the build with the error instead of rendering a plain table.

  • The MCP table_stats tool schema-qualifies its functions, and sample_rows accepts a quoted table name containing a dot.

  • A table or view an extension owns (PostGIS's spatial_ref_sys, Citus's citus_tables) came out of the DDL snapshot as a file of its own, with its constraints and indexes as files of their own, none of which could replay (the extension had already created them). They are dictionary members now, like an extension's functions: completion and the hover know them and their columns, the tree carries no file, and the objects on such a table carry none either.

  • An extension's own event triggers (TimescaleDB installs two) came out of the DDL snapshot as files of their own, which could never replay (the extension had already created them); they now stay inside their CREATE EXTENSION, like the extension's functions.

  • A view calling an extension's function (pg_trgm's similarity, TimescaleDB's time_bucket) re-rendered on open with a dependency on nothing (the function has no file), so every open reported a change the next build took back, forever. The re-render now drops that edge exactly like the build does.

  • The editor's right-click menu is one menu, Editor Settings nested inside it. The entry used to open a second menu in place of the first, because Monaco's built-in menu is a flat list that cannot carry a submenu of PlumeSQL's. The editor's menu is now drawn by PlumeSQL: the same entries Monaco would draw, read from its registry with their keybindings and the Peek submenu, and Editor as a real submenu at the end (the settings page link, Editor Settings, is that submenu's last row). Along the way a right click on an inlay hint, which Monaco hands to the browser's Look Up and Translate menu, lands in the same menu; the browser's menu never opens inside the editor (the find widget's text field alone keeps it, for cut, copy and paste).

  • The two cursor-deep copies show only where they can copy. Copy JSON Path As and Copy Array Subscript stood in every SQL buffer's right-click menu and answered a click with a note in the Log whenever the cursor was not inside a literal, which reads as a dead command. They now appear only with the cursor inside the string of a JSON or an array literal (the quotes and the ::jsonb tail are out), and anywhere in a JSON file, which is one literal end to end. The gate reads the spans the literal scan already computes, so a cursor move costs no new work.

  • A psql command's answer leaves the cursor on an empty line. Enter on \? or \conninfo used to leave the cursor at the end of the answer's last line, which still reads as a command (\? closes its own list), so the next Enter ran it again under itself. The cursor now lands on an empty line under the answer, an existing one reused and one inserted otherwise, and Enter there is a plain new line.

  • A routine's positions no longer drift after non-ASCII text. The parser behind the routine analysis counts bytes, the editor counts characters, and a comment in Croatian above a function or an accented literal in its body used to push every position after it to the right by one per extra byte: the parameter and variable nodes in the Outline, Go to Definition on a variable, and the static syntax errors in Problems. Every position now maps through one converter.

  • A routine's outline nodes select the right text when the routine is not the first statement. Clicking a parameter, variable or block node under a CREATE FUNCTION further down a script used to select text near the top of the buffer: the node's offsets were the statement's own and were never moved to where the statement starts.

  • The connection editor's header follows a move in the tree. Dragging an open connection into a group (or moving it from the row menu) left the editor's header reading "No group" until it was reopened; the header and the group draft now follow the connection, unless the group is being edited there and not yet applied.

  • Reveal in Objects Tree lands on the row. The Outline's Reveal in Objects Tree rode the sidebar follow's mechanism, which matched only when the outline belonged to that very DDL viewer; from a script's outline it turned the dock to Objects and stopped. It now asks the tree directly, whichever tab the outline belongs to: ancestors expanded, the row selected, scrolled into view and focused, the way the dependency rows' Reveal in Tree works. A reveal now only reveals, from either place: it no longer opens the object's preview on the way (Enter or a click does that). With the object browser pinned to another connection it says so in the Log instead of doing nothing.

  • The Log panel fits narrow widths. With both docks open the panel is a third of the window, and its header used to wrap "Copy all" onto two lines and push its own folder, settings and hide buttons off the right edge, while the rows squeezed the message into a sliver beside a full width timestamp. The header now folds by its own width: first the text actions and the two icon buttons gather in one More menu (the same entries, Log Settings with its Workspace / Global pair included), then the Errors toggle and the source picker follow, while the filter box shrinks and stays; the Terminal tab's Clear, Restart and settings fold the same way. The rows fold their columns to zero width instead of dropping the cells, which under a fixed table layout had shifted the message into the level column's 64 pixels; the message keeps the room, and below 420 pixels the clock drops its milliseconds.

  • Go to Definition works on extension functions. F12 (or Cmd/Ctrl click) on a member routine like pgcrypto's crypt used to say "no definition found", because it has no snapshot file. It now opens a read-only viewer with the routine's CREATE rendered on demand, which shows exactly where it lives, a C function reads LANGUAGE c AS '$libdir/pgcrypto'. Peek Definition shows the same inline. The viewer is transient (it renders fresh, so it is not restored across a restart).

  • Extension functions have editor support again. A recent change that correctly stopped rendering extension- and type-owned routines as standalone snapshot files also dropped them from the object dictionary, so pgcrypto's crypt, digest and gen_salt, PostGIS's functions and every other extension member lost their completion, signature help and hover (while pg_catalog builtins like encode kept theirs). Such routines now enter the dictionary again as file-less entries: the editor knows them, while the snapshot still leaves them to their owner's CREATE EXTENSION / CREATE TYPE for replay. They also take their place in the object tree, under their schema's functions (or procedures, aggregates) folder, wearing a small ext marker: opening such a row renders its CREATE on demand into a read-only viewer, since there is no file to read, and the tree follows such a viewer to its row like any other. A big extension can bury a schema's own functions, so the Objects pane menu, every row's menu and the Object filter window gain one Extension Objects switch, a setting of its own, offered only on a connection that has any; hidden, the rows go and the tree stops following them, while the editor keeps knowing them. Refreshing the snapshot picks them up.

  • A BEGIN ATOMIC function body is one statement. The statement splitter took the semicolons inside a SQL-standard function body (CREATE FUNCTION ... BEGIN ATOMIC ... END;, PostgreSQL 14+) for statement boundaries, so one CREATE FUNCTION ran as several commands and the dynamic parser flagged the fragments. Between BEGIN ATOMIC and its matching END the splitter now stays in the statement; a CASE ... END inside the body does not close it early, and a plain BEGIN transaction block still splits as before.

Removed ​

  • The sidebar no longer switches panes when you switch tabs, and the "Sidebar follows the active tab" setting is gone with it. Activating a DDL viewer used to turn its dock to Objects, a connection editor to Connections, a file to the Explorer, evicting whatever pane you were reading there. The pane you chose now stays, as in VS Code, and every pane lights the active tab's row whenever it is the one showing (the file in the Explorer, the object in Objects, the connection in Connections), so the tab's home reads in whichever pane is open. A settings file that still carries the old key is read as nothing. "Show Objects when a tab connects" is unchanged: it answers a pick you just made in that dock, not a tab switch, and the Outline's Reveal in Tree still turns to Objects, because you asked it to.

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • Completion reads a half-typed statement through the real parser far more often. A statement that does not parse as typed is healed before the parser sees it: the word under the cursor stands in for what is being typed, the parentheses still open are closed, and the piece the statement stops short of is supplied (the ON a JOIN waits for, the END of a CASE, the list after IN or VALUES, the query after UNION, the value after SET, what an INSERT INTO t needs). Over the corpus' queries cut where a user would be typing, the real parser now answers nine prefixes in ten where the grammar's exact reading answered three; the grammar and the heuristics still cover the rest. An action file's $name and {name} placeholders are read as names of the same length, so its statements are read like any other.

  • psql's backslash commands work in the editor. Type \d film on a line (or -- \d film in a comment) and press Enter: the table's definition appears under the line, from the DDL snapshot, and the line becomes a comment so the script still runs. \dt, \dv, \dm, \di, \ds, \df, \dn, \dx, \dT and \du list what the dictionary holds, with psql's patterns (\dt public.*, \d fil*); \sf and \sv insert a function's or a view's source; \conninfo says where you are; \? lists the commands. Typing the backslash offers them, and a command offers its argument from the dictionary. Nothing is asked of the server.

  • A script with placeholders asks for their values and binds them. Run a statement that carries $1, $name, {name} or psql's :name, :'name' and :"name" and a form asks for each value once, typed by what the server says the parameter is (a number field for an integer, the text area for text), prefilled with the tab's last answers. The values travel bound beside each statement through the extended protocol, never pasted into the SQL (:"name" alone is an identifier and goes quoted), so a query pasted from application code or a psql script runs as it is. The dynamic parser reads a placeholder as a value meanwhile, so :name no longer shows a syntax error. The Run parameters setting leaves the colon style out or turns it off.

  • The input form's fields are the row form's. The form an action's inputs and a run's parameters open in renders every field with the same control a row edit does, one component for both: a segment for a boolean, the enum picker, one element per line for an array with the Lines switch, the format hints, and the NULL checkbox beside every field. NULL is that checkbox and nothing else: a field sends exactly what it holds, an empty one the empty string (an empty typed field used to be read as NULL on its own, and a text field had no way to say NULL at all).

  • A function in FROM completes its columns. After r. in select r. from get_rows() r, the suggestions list what the function's definition says it returns: its OUT and TABLE parameters, the columns of the relation it returns (returns setof actor), or, for a scalar such as generate_series, the one column named after the alias; WITH ORDINALITY adds its column. A column list on the alias (unnest(tags) as t(tag)) still wins.

  • A star inside a CTE or derived table completes the table's columns. After r. in with r as (select * from actor) select r. from r, or a.* inside a subquery, the suggestions list every column of the relation the star stands for, through the object dictionary, beside the columns the select list names.

  • A routine's blocks fold. Inside a CREATE FUNCTION or PROCEDURE, every IF (and each of its ELSIF and ELSE branches), loop, CASE statement, nested BEGIN block and EXCEPTION section is a fold range of its own, under the statement's fold, so a long body collapses branch by branch. A CASE expression inside embedded SQL is not a block and does not fold. The ranges come from the same parse as the rest, with a keyword scan of the body for where each block ends.

  • A routine's variables navigate like symbols. Inside a body, F12 on a variable or parameter jumps to its declaration, Shift+F12 lists every mention with the declaration first, and F2 renames all of them at once, by the parser's reading of the body rather than by text: a column that shares the name, a call, a string or a comment is left alone, quoted names match exactly and bare ones case-insensitively, as PL/pgSQL resolves them. Outside a routine's own names the rename says why it declines.

  • Routine bodies get the static checks plpgsql_check is known for, without the extension. From the parse tree alone, at no server cost: a variable declared and never used, a parameter never used, a variable assigned and never read, statements after a RETURN, EXIT, CONTINUE or RAISE EXCEPTION that can never run, and a function that can reach its end without RETURN (void, SETOF, procedures and OUT parameters excepted; a plain LOOP with no EXIT counts as never falling through). Warnings at the declaration or the statement they are about, on the same squiggles and Problems rows.

  • Completion and hover know a routine's own names. Inside a CREATE FUNCTION or PROCEDURE body, the parameters and declared variables complete with their types (loop variables, found, a handler's sqlerrm and a trigger's new, old and tg_* included), a %ROWTYPE variable followed by a dot completes its table's columns, and resting the mouse on a variable says its type and the line it was declared on, or on a row variable's field the column it is.

  • The Outline reads inside routines. A CREATE FUNCTION or CREATE PROCEDURE row discloses what the routine is made of, from the same PL/pgSQL parse the body analysis runs: its parameters with modes and types, the declared variables, records and cursors, then the body as the source reads it, IF with its ELSIF and ELSE branches, CASE with its WHENs, every kind of loop, nested blocks with their EXCEPTION handlers, and each embedded statement as a leaf (SELECT ... INTO, RAISE, RETURN, PERFORM, EXECUTE). A LANGUAGE sql body lists its statements. Every node reveals its spot on a click, blocks fold and unfold like CTE nodes, the automatic expand opens the whole shape with the current command, and the keyboard walks it like the rest.

  • The status bar switches the parser. Beside the problems count a parser chip reads the dynamic parser's mode for the active script ("Parser: Both", "Parser: Editor", "Parser: Outline", or the silence it already reported: "Parser off", "Parser after connect"). Its menu switches the mode for this file (the Problems header's own choices, writing the same -- @parse line), offers the way back to the connection's setting, opens or hides Problems, opens where the setting is decided (the connection's Parser section, its group, the workspace or global scope) and the Parser Settings in the usual shape.

  • The status bar says when the parser is silent. A tab whose dynamic parser resolved off (the Production group's default, a connection or workspace setting, or the file's own @parse line) used to look exactly like a clean script. The problems slot of the status bar now reads "Parser off" there, muted, with the tooltip naming the level that decided and what it means: nothing goes to the server while you type, routine bodies included, so no problems are reported. A tab whose parser is on but waits for a connect (throwaway dials vetoed) reads "Parser after connect". A click opens where the choice was made: the connection editor's Parser section, the group's editor, or Settings on the scope that holds it.

  • The status bar counts the active script's problems. Before a run the Problems tab does not exist (the parser never opens the pane by itself) and the Outline's marks show only while the Outline is showing, so two squiggles could be all that said a script had problems. The status bar now carries VS Code's problems item, next to the file: "2 problems" or "2 problems, 1 note" in the Problems tab's own words and counting, red for errors, amber for warnings, gone while there is nothing to report. A click opens the Problems tab, run or no run: your gesture may mount the pane the parser never does, and the tab remembers it, so the pane stays through your edits (saying "No problems in this file" once they are fixed) until the next run takes it over; that pane carries its own hide button, the bottom panel's minimize line, and the status bar slot keeps reading "No problems" as its handle while it is up, so what opened it also closes it. Toggle Problems does the same from the keyboard on VS Code's combo (Cmd+Shift+M on macOS, Ctrl+Shift+M elsewhere), in the command palette and the cheatsheet.

  • The Settings page says which defaults a group or a connection can override. Every setting the connection and group editors can override (the Session, Snapshot, Parser and Statistics families) now wears a "per group and connection" chip beside its key, and each category that carries such settings opens with one line saying these rows are the defaults and the editors override them, the most specific level winning. The two surfaces read as one chain from both ends; one shared list drives the chip and the editors' sections, and a test keeps them equal.

  • Every Parser setting can be set per group and per connection. The editors' Parser section now carries the whole family: the dynamic parser (markers and outline marks, one of them, or off), its pause, whether tabs that have not connected are checked, notes, warnings, Did You Mean and the static analysis, each resolving connection over group over workspace over global like every other override, so a production group switches the server-side parse off wholesale and a slow server earns a longer pause. The four seeded groups now carry the defaults their environment earns, on this same layer: Production logs every change run and asks the server for no speculative work (no dynamic parser, no throwaway dial to check an unconnected tab); Local refreshes its snapshot every minute; Staging and Development keep the stock defaults on purpose. Every one is an ordinary override, so change or remove it like your own. An installation seeded before these defaults existed receives them once, on the groups it still has and only where the group sets nothing of its own, announced in the Log like the seed itself.

  • The Connections pane marks the connection the active tab runs on. Switching to a script, file, DDL viewer or action tab lights its connection's row in the connections list, the way the Explorer lights the active file, so which server a tab is talking to reads in the tree and not only in the status bar; the row's tooltip says so. With "Reveal the active tab in the sidebar" on (the default), the row also scrolls into view and a collapsed group opens on the way, the same setting and the same rules as the other trees. Until now only a connection's own editor tab lit its row.

  • The Terminal tab grew a toolbar. Beside the shell name sit Clear (empty the buffer), Restart (end this shell and start a fresh one in the same directory) and a cog into the Terminal settings, the same actions the right-click menu and shortcuts already carried, now where the Log's own controls live.

  • The Log panel narrows, copies whole and shows the day. A filter bar in the Log's tab strip searches every field of every entry (marking the matched text in place, so a row shows why it survived), an Errors toggle hides the informational ones, and a source picker (or clicking a row's source chip) isolates one category, connection, schema, files and the rest (the three axes stack; one X clears them). "Copy all" (or "Copy shown" when filtered) puts the entries on the clipboard with full timestamps for a bug report, and a day separator plus a full date-and-time tooltip on each row's clock keep yesterday's entries from reading as today's after a restart. Every row action still keys on the true entry however the view is narrowed.

  • Function bodies are analyzed as you type, at no cost to the server. The parser cannot see inside a CREATE FUNCTION or CREATE PROCEDURE body, and the server itself checks only the syntax of a PL/pgSQL body at CREATE time (a missing table inside it surfaces when the function first runs). PlumeSQL now reads every routine in the buffer with PostgreSQL's own parser compiled to WebAssembly (libpg_query, the PostgreSQL 18 build, loaded once the first routine appears), which yields the body's variables and every embedded SQL statement and expression, PL/pgSQL syntax errors and shadowed variables included; each embedded piece then goes through the same read-only Parse the dynamic parser runs for the script, the variables replaced by typed NULLs, so the server resolves names, columns, functions and types inside the body exactly as it will at run time. LANGUAGE sql bodies (BEGIN ATOMIC too) are read the same way with the parameters typed. Nothing is created and nothing rolled back: no transaction ID, no lock, no privilege, so it runs wherever the dynamic parser runs, on its triggers and debounce, for the routines whose text changed, on the tab's session or the throwaway dial the parser's own veto allows. Verdicts join the same markers, Problems rows, Outline marks and dismissals; a piece that mentions a variable whose type cannot be named (a record, a trigger's NEW and OLD) stays unchecked, and a relation the body creates before using it is not reported. One parser build serves every server release: what is version-specific is judged by the connected server. The earlier design, a rolled-back CREATE of each routine on the server, is gone with everything around it: the Check body lens and menu action, the check.bodies setting and its place in the group and connection editors, the Problems header's body check chip and the two server endpoints.

  • Arrays reach JSON's level everywhere. A broken array literal in SQL ('{1,,3}'::int[], the $tag$ spelling too) squiggles on the character that is wrong, by array_in's own rules: nesting, quoted elements, backslash escapes, NULL, the [lower:upper]= dimensions prefix, ragged sub-arrays. Resting the mouse on an array cell reads the value (type, dimensions, element count) with the elements one per line; the peek and the value tab carry the same reading in the same words, and the value tab grew a Lines face (one element per line, spelled exactly as stored). The right click copies the cursor's subscript from inside a literal ([2][1], honoring the literal's own lower bounds), && explains itself on hover like the JSON operators, and the grammar stopped lighting functions a connected server does not have (trim_array on 13, array_sample and array_shuffle before 16, any_value and the strict/unique JSON aggregates before 16).

  • Ranges too. A broken range or multirange literal ('[1 5)'::int4range, daterange '...', '{[1,2),[3,4)}'::int4multirange) squiggles at its fault by range_in's rules: brackets, quoted bounds, missing bounds as infinity, empty, the multirange wrapper. The range operators (&&, <<, >>, &<, &>, -|-, @> and <@) explain themselves on hover with a worked example and the version-exact reference link; the overloaded ones only speak where the statement is visibly about ranges, so an integer bit-shift never grows a tooltip. The multirange constructor and aggregates (range_agg, range_intersect_agg) now light up only from PostgreSQL 14 on, where they exist.

  • A form field edits in a tab of its own. The row editor's small box is no place for a large JSON, so every text field grew an expand button beside its toggles: the form folds away (exactly the foreign key pick's dance, nothing typed in the other fields is lost) and the value opens in a real editor tab with the JSON grammar when the type says so, folding, find and room. Accept puts the text back into the field and the form returns focused on it; Revert restores what the field held when the editor opened (typed-but-unsaved form edits included), Original restores the row's STORED value (update mode only: an insert has none, and a stored NULL disables it with the reason, since NULL is the form's toggle, not text); Cancel, or closing the tab any other way, returns the form untouched. A json or jsonb field also gets Format (an explicit gesture: it rewrites the literal, which a json column stores as written) and an advisory validity note in the header line; the server stays the judge of what the literal means, on save. The editor lives only as long as its form: a restart drops both, exactly like a pick in flight.

  • Every builtin function is now in the dictionary: completion, signature help and hover for all ~3,000 of them. A filter had excluded c- and internal-language routines from the snapshot for years, which silently hid the entire builtin library: jsonb_set gave no parameter hints, generate_series no hover, and the shared pg_catalog store held 50 functions instead of about three thousand. The filter is gone; the real rule it was standing in for is named instead (pg_dump's own): MEMBER routines, extension-owned or a type's auto-created constructors, stay with their owner and never render as standalone files, so a snapshot still replays whole. Typing jsonb_set( now pops the true signature with the catalog's own one-line comment beside it, on the exact server version connected. Existing snapshots and catalog stores rebuild themselves once (the render version moved); an extension's helper functions leave the user-schema file tree with the same rebuild, which is where they never belonged.

  • The SQL editor speaks JSON. Hovering a JSON operator finally answers: all fourteen (-> ->> #> #>> @> <@ ? ?| ?& || - #- @? @@) carry a one-line meaning, a worked example and the reference link, with the overloaded three (- || ?) speaking only in statements visibly about JSON, so a numeric minus never grows a tooltip. JSON LITERALS in queries ('{...}'::jsonb, jsonb '...', $tag$...$tag$::json) are parsed as you type and mistakes squiggle INSIDE the string, on the character that is wrong; Copy JSON Path As works from inside a literal too, cursor-deep. And the completion lists gained what a diff against the reference found missing: json_build_object and jsonb_build_object (absent from the borrowed grammar all along), jsonb_populate_record_valid on 16+, and the jsonpath type keyword.

  • JSON grew up: one parser, squiggles, an outline, paths and real formatting. A hand-rolled recovering JSON parser (the SQL side's discipline applied to JSON; no Monaco language service, per the house rule) now feeds every JSON surface. Errors: .json files and the field editor squiggle as you type, on a settle, with VS Code's wording and positions, error recovery keeping the rest of the document lit; the field editor's note names the first problem's line; settings files read as JSONC (comments and trailing commas are theirs), plain .json is strict; duplicate keys warn with the jsonb truth (only the last survives). The Outline: JSON documents (files, value tabs, the field editor) show their tree, keys and array indexes with previews and line numbers, windowed so a monster document stays flat, cursor followed in files, clicks landing in the owning editor. Paths: every outline row and every JSON editor's right click copies the cursor's path as jq, PostgreSQL arrows (->'a'->0->>'b'), a #>> text array or jsonpath: the accessor for the WHERE clause, one click away. Formatting: Format Document (VS Code's own combo) works in every JSON buffer through a fidelity printer that reproduces numbers and escapes byte for byte and only ever moves whitespace (a JSON.parse round trip mangles big integers; this never rewrites content), with the indent a setting (editor.jsonIndent); a .json tab's toolbar carries the four styles as a Formatting button group (Format, short lines kept, compact, sorted keys) and the field editor's header the same four buttons, each one undo step; a commented JSONC settings file declines to format rather than eat its comments; the value tab's pretty face prints the same way. And jsonb gets the truth its type deserves: the field editor OPENS a jsonb value formatted (the type never stores formatting, so the compact wire text is free to unfold, and the line says so), while json, which stores its text as written, opens exactly as stored; the row form's change detection is jsonb-aware, so a purely reformatted jsonb value counts as no change at all: it is left out of the UPDATE, and a form where it was the only thing touched says "Nothing changed yet" instead of writing a new row version for whitespace. And the grid: json and jsonb cells got the geometry treatment's sibling card on hover (what it is, how much, a pretty excerpt), the peek says "object · 14 keys" above the raw value, and the value tab carries the same reading in its line.

  • pgvector values read as vectors, not float walls. A vector, halfvec or sparsevec cell (the column's declared type is the gate, so a JSON array of numbers keeps its JSON face) gets the geometry treatment: resting the mouse shows a card with the dimensions, range, L2 norm and a sparkline of the components; the peek carries the same strip above the raw value; the value tab adds the panel over the text as stored. A sparse vector says its nonzero count and draws its entries as spikes at their true positions. All of it reads the text the cell already holds, no query.

  • TimescaleDB starters: hypertables, continuous aggregates, background jobs. A new starter folder for servers running TimescaleDB: hypertables with their chunk counts, sizes and compression (the name opens the chunks with their time ranges), continuous aggregates with their defining query one click away, and the background jobs (policies, refreshes) with each job's run history, failures verbatim with their SQLSTATE. Existing installations pick them up with the actions tree's Restore Starter Actions; a server without the extension answers in its own words.

  • A geometry value inspects itself: type, SRID, points, bbox and PostGIS's own validity verdict. The value tab's geometry panel now carries the vertex count and the bounding box beside the kind and SRID, and a Validate button asks PostGIS itself (ST_IsValid with ST_IsValidReason, one throwaway connection per click, a gesture every time): a clean shape reads "valid (PostGIS agrees)", a broken one shows the verbatim reason with its coordinates ("invalid: Self-intersection[1 1]"), in the panel where you are looking. The verdict clears when the cell is read again; a server without PostGIS answers with its own error, in the Log, verbatim.

  • PostGIS geometries decode where you read them. A geometry or geography cell holds EWKB hex on the wire; resting the mouse on such a cell now shows a hover card with the shape sketched and its EWKT (the peek's little sister, mouse only: it dismisses on any key, scroll or click, and Space stays the keyboard's way in; a geometry column's cells drop the native tooltip, whose hex dump is exactly the noise the card replaces). The cell peek shows the decoded EWKT above the raw value with a small sketch of the shape beside it, and the value tab decodes it whole: the EWKT text in the editor (exactly what ST_AsEWKT prints, so a copy round-trips; a header toggle returns to the value as stored, and Copy copies the face shown) under the same sketch with its kind and SRID beside it. The decoding is PlumeSQL's own, no query and no dependency: both byte orders, Z, M and SRID in both the PostGIS and ISO WKB spellings, all seven geometry kinds; anything that is not a geometry declines silently and shows as before.

  • A geometry column sketches as one picture. A geometry column's menu grows Sketch Geometry Column: every EWKB value of the column decoded and drawn into one shared bounding box, in a tab bound to its result exactly like a value tab (Refresh reads the column again, a result that is gone is said calmly under the name). Points draw as dots, lines as lines, polygons filled, holes included; the line above the canvas counts what was drawn (geometries, points, SRID, NULLs, anything that did not decode) and says honestly when a monster result was cut ("first N of M rows"). A sketch, not a map: coordinates project straight into the box, no tiles under them. A sketch over a result document restores with the workspace; one over a script's result lives as long as the result does.

  • Geometry fields preview as you type. In the row editor, a geometry or geography field draws a small sketch of its literal under the input, live: the stored EWKB hex of an existing row, or the EWKT being typed (both PostGIS's own spellings and ISO WKT, Z, M and SRID included). Advisory only, never a gate: a literal that does not parse simply shows no sketch, and the server stays the judge of what it means.

  • Extension types name themselves in the grid header. A column whose type the wire does not know (an extension or user-defined type: PostGIS's geometry, hstore, a domain) read "unknown"; the names now resolve right after the run in one round trip through pg_catalog.format_type, which lets the extension render its typmod too: the header reads geometry(Point,4326).

  • Per-column filters on every result grid. A column's menu (the header's or a cell's) grows Filter to This Value, Filter by the column (typed in the bar: contains for text, equals for the rest), Filter to NULL and NOT NULL; active filters show as chips in a bar above the grid with the honest count beside them ("12 of 3,520 rows"), each chip editable and removable, and the filtered column's header wears a funnel. The filter runs over the rows as fetched (the server scans the result's own spool; the SQL is never touched), so it works the same on a script's result, a document and an action, with or without the connection; copy and export take exactly the view the grid shows, filters restore with a document, and everything that acts on a row (editing, value tabs, row buttons) keeps addressing the right one. Matched spans wear the find highlight. A gigantic result is safe to filter by accident: one scan serves every concurrent reader, it stops the moment the filter is cleared, the view holds at most the first million matches and says so ("first 1,000,000 matches of ..."), and while the scan runs the bar reads "filtering" and the activity dot beats for it by name.

  • Pin to Toolbar from the row menu. An action's row menu grows Pin to Toolbar and Unpin from Toolbar, and the gesture is an editor of the file's own @toolbar line, nothing more: unpinning disarms the line in place with the dash that turns an annotation into prose (never deleting a drawn icon or a chosen name), pinning re-arms a disarmed line when one waits, icon and all, and only a file with none gets a fresh bare @toolbar after its @description, so the file stays the one answer and travels pin and all. An open tab takes the edit in its buffer with undo intact (clean tabs save at once; a dirty tab's unsaved work stays unsaved and the line waits for your save); a closed file is edited on disk under the usual stale guard. The toolbar button itself answers a right click with the same menu: Run Action, View Source, Unpin from Toolbar.

  • Role privileges, a starter that views and manages ACLs. A new starter lists every role with its attributes and memberships. The name opens the role's ACL: every explicit grant to it across databases, schemas, relations, single columns and routines, one row per object with who granted what, the relation rows carrying "revoke all" in their own cell (a real REVOKE through @inline, names quoted by the server, confirmed first), and an empty ACL answering in words instead of a bare grid. On the roles list itself, "grant select" and "grant all" ask for a schema and relation (each with a pick) and run the real GRANT the same way; a privilege is a keyword, so the two common ones are two buttons.

  • System catalogs, a starter that opens every one. A new starter lists every system catalog, view, statistics and progress view the PostgreSQL 18 documentation inventories (148 relations, each with what it holds and its family), and the name browses the relation itself through @inline (whole: the spool caps bound a runaway read). On an older server a relation that does not exist yet answers with the server's own error, the honest answer; the list itself opens everywhere.

  • @inline: actions can run utility statements. PostgreSQL accepts no bound parameters in ALTER SYSTEM, VACUUM, GRANT and their kin, and the action format never pastes a value into SQL, so actions could not run them with a value at all. @inline <name> literal|identifier (ACTIONSPEC.md 8.27) expands the named $column or {placeholder} into the statement's text with the quoting done by PostgreSQL itself (quote_nullable / quote_ident, one round trip on the same connection right before the statement), so nothing is ever concatenated out of data and a hostile value comes back inert. Everything not named stays bound, the default road.

  • Server settings, a starter that browses and writes. A new starter query action lists every server configuration parameter (value, unit, type, when it can change, source, restart pending, category), opens a parameter's card on its name (description, bounds, allowed values, shipped default, where the current value came from), and edits through the form: Set value writes ALTER SYSTEM through @inline (the server validates the value in its own words before writing a thing), Reset removes the override, Reload applies what a reload can.

  • Editing without a primary key. A result that maps to one table is editable even when no key is in it. A table whose primary key is missing but which has a NOT NULL unique index is edited through that index exactly as through a key (the form's chip and the column hover say "unique key", honestly). With no usable key at all, inserts just work, and Edit and Delete switch to content matching: the row is located by everything it shows (its text renderings, so every type compares, json included) and must match alone; two identical rows are refused, never guessed between. The write targets the located row's physical locator under the same optimistic guard as every edit, so a row changed or deleted by someone else in the meantime is refused, and the form wears a "matched by row contents" chip saying how the row was found (the tooltip carries the consequences: unique match required, finding the row may scan the table). Undo works for keyless edits too, targeting the exact row version it undoes.

  • A cell's value in a tab of its own. The peek is for a glance; a JSON document or a long text wants the editor. The peek's Open in Tab, the cell menu's Open Value in Tab and Shift+Enter on a cell open the value as a read-only editor tab: the JSON grammar with a Pretty print toggle for json, jsonb and anything that parses as JSON, plain wrapped text otherwise, folding, find, Copy. The tab is bound to its result like a foreign key browse: Go to Cell selects the very cell in its grid, Refresh reads the same place again (a query run again since the read is said, never applied under you), a result that is gone makes the tab disconnected the moment it happens (the broken link, the value kept, Close offered, nothing closed on its own), and over a result document the tab comes back after a restart and reads its place once the document has rows.

  • @pick: a form field's values from a query. An asked @var may carry @pick <name> <query> in the same header: the form's field grows the row editor's chip ("pick from a query") and a button, and the pick is the row editor's own picker: the form folds away, the query's rows open as a result document in pick mode (the full grid, any size), Select puts the selected row's value in the field (the column named like the input, else the first) and the form comes back with everything typed so far. The field stays typeable; the query runs on the action's connection when the picker opens, never when the form does, its values bound, and may read the clicked row's $column and the registry, never the form's other fields. Query and command actions alike, both parsers, the spec (ACTIONSPEC.md 8.26), the reference and the help. Two new starters carry one each: role-members (who is in a role you pick, the list showing each role's powers and member count) and database-activity (backends on a database you pick, the list showing live backend counts and sizes), and a starter command run-sql-file runs a SQL file you pick with psql (ON_ERROR_STOP, the password through the environment), eighteen starters now.

  • Edit table data from a result. When a query result maps to one table and carries its whole primary key, the results toolbar grows New row, Edit and Delete. Edit and New open a form, one field per column, each with its type, flags and foreign key (the column its value must exist in) beside its name and its comment in full below: the control fits the type (a TRUE/FALSE switch for a boolean, a number field, a roomy box for text, JSON and arrays), a format hint rides the empty date, uuid or json field, and a NULL or Default toggle covers the rest. The form is as tall as its fields (up to a ceiling, then it scrolls), moves by its header and resizes from any edge or corner. The row is re-read as the form opens, so you edit what is really stored, and the write is guarded against a row someone else changed in the meantime: rather than overwrite it, the form says so and you reopen. On success the grid patches the row in place from what the database stored (defaults, sequences and generated columns applied), no re-run. Every write logs its command, and offers Undo, which patches the grid back the same way while the result is still on screen and is refused if the row has changed since; a deleted row comes back alone, rows the database removed with it by ON DELETE CASCADE do not, and the delete's own confirmation says so before you agree. While Edit or Delete acts on a row the whole row is selected behind the dialog, so the target is never in doubt, and your own selection comes back when it closes. A write that waits on a lock another transaction holds beats the status bar's activity dot for as long as it waits and can be stopped from the form (Cancel turns into Stop; the write then fails as canceled, said inline, and the form stays to retry or close). Keyboard first: with the grid focused, Enter or F2 edits the selected row, Delete (⌘⌫ on macOS) deletes it and Alt+N inserts one, all three rebindable under Settings, Keyboard; a double-click on a cell opens the form on that column, and the cell's context menu carries Edit Row, Delete Row and New Row. The form opens focused on the selected cell's control, Tab walks it, Ctrl+Enter (⌘Enter) submits, and Escape closes and hands the focus back to the grid (the delete question gives it back the same way, whichever answer). A read-only connection or file, or a view, shows no edit buttons.

  • A result in a tab of its own. An Open in Tab button beside a result's copy button (and in its dots menu) promotes that statement's result into a result document: a tab that keeps the rows exactly as they were (they move server side, no query runs), the statement's text and the connection it ran on, so the script can move on, run again or close and the rows stay. Refresh asks the statement again on a connection of its own, the action tab's way (so the script's SETs, temp objects and transaction are not there); Focus Command selects the statement back in its file while the file still holds it, Copy Command and Open as Script hand the text on. The document IS the results pane: the same header, grid, row editor (it dials a session of its own on the first edit), copy and save. It survives a restart with its text and connection; its rows do not, and it refreshes itself the first time it is shown again, the way a restored action tab does. The grid itself is remembered too, across a refresh and a restart: column widths, hidden and frozen columns, frozen rows, the row height, the scroll position and the selection come back once the rows are in (a place, not a row: the data may have moved), and a promotion takes the script grid's own arrangement along.

  • Compare results side by side. A result document's dots menu (and a script result's, which makes the document first) offers Compare With, listing every other result open in the app: the script tabs' and the other documents', each named the way its own header names it. A compared result joins the document as one more section, the sections stand side by side with vertical sashes between them, and the grids scroll together by row (Scroll Together in the dots menu switches that off). Each section keeps its own connection (named in its header when results compare), its own Refresh, Focus Command, Copy Command and Open as Script, and its own grid arrangement; Refresh All asks every source again in turn, Remove from Comparison lets one go. The comparison survives a restart with the document.

  • Follow a foreign key from a result document. In a document, a value whose column references another table (the object dictionary knows the key) is a link: it opens the referenced row, whole, one step deeper in the same tab, and a bar above the pane grows Back, Forward and a trail of crumbs the moment the document has been somewhere (language › 1), the action tab's trail in shape and in rule: every crumb is a way back, going back asks again, the combos are the action pair's (Alt+Cmd+- and Alt+Shift+Cmd+- on macOS, Alt+- and Alt+Shift+- elsewhere), and the trail is not carried across a restart. The referenced row edits like any table-backed result.

  • Browse Data. A table, view, materialized view or foreign table opens its rows in a result document from three places: Browse Data in the Objects tree row's menu, a Browse Data button in the DDL viewer's header, and a Browse data link (beside Open DDL) in the editor's hover on the object's name; a hover on a foreign key column links to the referenced table too. A table's rows come ordered by its primary key when the snapshot knows one, and the statement asks for the first 1,000 of them (the new Browse row limit setting; 0 lifts it), so a click on a huge table stays cheap: the pane's readout says so ("first 1,000 of about 16,044 rows in 4 ms", the estimate the statistics' when collected), and the limit in it is a picker (presets up to 50,000, or all rows; Show All Rows in the dots menu too) that asks again with the chosen limit and makes it the workspace's Browse row limit from then on. The document is everything a document is: the grid, the row editor, the foreign key links, the trail, Compare With and Save.

  • More doors to a table's rows. A foreign key value links in a script's result too (the click opens the referenced row as a document of its own, the way the form's chip does; the script's result stays where it is); a column header's menu offers Browse for the relation the column comes from and, on a foreign key, Browse Referenced for the table it points at.

  • The Keyboard Shortcuts cheatsheet lists Trigger suggest (the completion list on demand: Ctrl+Space, Ctrl+I; on macOS ⌃Space, ⌥Escape, ⌘I) and says why ⌃Space alone tends not to reach the editor on a Mac.

  • A palette command that replaces the active editor (System, the Annotations Reference, a connection's editor) no longer leaves an unhandled "Canceled" error behind: the command runs once the palette has closed.

  • The dynamic parser reads an action file the way the run sends it: {placeholder}, a header's own @var names and a button's $column are shown to the server as the bound parameters they become (each padded to its own length, so a problem still lands on its character), instead of being reported as syntax errors on every line that has one.

  • Opening a DDL viewer or a schema folder document from the Objects tree no longer turns the dock to the Outline for a beat (or for good, for a folder document): the sidebar follows the tab once, when it knows what the tab is. Nor does opening a .env or .json file, Browse Data, a promoted result or a command action's terminal.

  • The Objects tree walks with a browsed table. While the Objects pane is showing on the document's connection, the relation the document looks at is selected and scrolled into view in the tree: the browsed table, then each foreign key step as the walk goes deeper or back. The tree is never focused and no dock is turned for it.

  • Search in Files. A Search side tab (⇧⌘F / Ctrl+Shift+F, VS Code's Search view): one query with the trees' match case, whole word and regex toggles over any combination of the shared Scripts store, the workspace's scripts, the workspace's files (with include patterns, or the Explorer's) and, new to a SQL client, the followed connection's DDL snapshot (where is this column used in views and functions; read only, found and never edited: a change goes to the database). The results are a tree of files and matches with the match lit; a click opens the file as a preview on the match, a double click or Enter pins it, F4 / ⇧F4 step through the matches from anywhere, Find in Folder… in the Explorer's folder menu narrows the search there. Unsaved buffers are searched too, as in VS Code. The query, toggles and scopes live in the workspace's session. The trees' filter window gives up the combo (its button and pane menu entry stay). And REPLACE (⇧⌘H / Ctrl+Shift+H, the arrow beside the query): a replacement with Preserve Case and $1 groups under regex, previewed in every result row (the match struck, the replacement beside it), put in per match, per file or all at once (Ctrl+Alt+Enter in the Replace field). A file open in a tab changes in its buffer, so undo takes it back; a closed file changes on disk, each match re-checked at its spot first; the Objects snapshot is never written. One Log line records each replace. Dismiss takes a result off the list.

  • An Annotations Reference, and every door leads to it. PlumeSQL: Actions Reference opens a view generated from the registries: every annotation by the header it belongs above, the placeholders, how an action works, and five example shapes with New Action from this example; Copy as AI prompt hands the whole format out as text. The hover of any annotation or placeholder ends with a Reference link landing on that entry, Insert Annotation and the Explorer's pane menu have it, the Log's starter line and a command action's parse problems carry it as a button. New Action asks what shape the file starts as (Blank, Dashboard, Table with row buttons, Ask for values), and the new file's header points at the reference instead of carrying the whole specification.

  • Undo and redo survive a tab switch, and closing and reopening a file. The editor's buffer now has an identity across mounts (the file, the action, the tab), so Monaco keeps a closed buffer's history for the run and hands it back when the same text is mounted again, as VS Code does for a closed file. Not across restarts, which VS Code does not do either.

  • The Explorer opens a directory the way VS Code does. A directory opened for the first time starts folded: the roots open, every folder under them collapsed, and the folding is yours from then on. Folders whose only content is one folder share a row (src/main/java), the row standing for the deepest one; off with the Compact folders setting (Files) or the Explorer filter window's checkbox. And typing into any focused tree (Explorer, Objects, Connections) opens its search with what was typed, so a name is found the way a list is navigated in VS Code.

  • @parse completes its values. Typing @parse offers off, markers, outline and both with a line each, in an action's header and in a plain file's; a test now reads every annotation whose syntax spells its values (a|b|c) and holds the completion to them.

  • One word for an action's door: Run Action. The Explorer row's menu leads with the same two entries for either kind, Run Action (a query's answer, a command's program) and View Source (the file's own tab, whatever the click does); the file tab's toolbar wears the bolt on a command action too, not only on a query's; the palette command is PlumeSQL: Run Action for both. Every shipped query starter carries @face result, so a click on it opens the dashboard, and the starters land in Scripts under Actions (Queries, Commands) instead of Starters.

  • A new .env (or any file discovery reads) is picked up without a restart. Whatever lists the workspace again (a save, a new, renamed or deleted file, the window regaining focus, the refresh timer) and auto save's own writes refresh the connections too, a moment later; the server rescans only when the files changed.

  • A value without a default is required. In the input form, a @var, @file or @dir line that wrote no default holds Run while its field is empty (the field says "required", in red once Run was tried); a written default, "Prompt", "" included, makes the empty answer valid. @file and @dir take the "<prompt>", "<default>" shape too.

  • @face result. An action file may say that a click on its row opens its ANSWER (the view; a command action's program) instead of the file, for dashboards whose text nobody needs to see; source, the default, keeps the click on the file. Run Action, View Source and the title bar button do what they say regardless.

  • @connection <id or name>. An action file may name the connection it runs on, from every door: by its display name, or by its permanent ID where names repeat. The readout shows it with no picker behind it; a name no connection carries puts the reason in the Log and a calm line with Try again on the view; a name two carry runs on the first (Vault, Shared, Discovered, in the tree's order) and the Log says so, with Use the ID rewriting the line. The connection editor shows the ID under the name (Copy ID) and warns when another connection already carries the name; the tree row's menu gains Copy Connection ID; the editor completes @connection with names, or IDs where a name repeats. A command action with one is never asked which database it means. A plain .sql script may carry the line too: its tab then runs there, both readouts show it and open no picker while the line stands (remove the line to pick again), a live session moves only on the user's own Run, and a name nobody carries refuses the run with the reason.

  • An action can ask for a value, in a form. The @var, @file and @dir lines a command action has always had are legal in a query action too, above its own command and above a button's: everything a command asks for opens in ONE form (the row form's shape), a field per input with its prompt as the label, "<prompt>", "<default>" filling the field first (a placeholder, or above a button the clicked row's $column), Cmd+Enter runs, Escape runs nothing. A field's control follows the type the server describes for the parameter (no type in the file): a segment for a boolean, a keypad for a number, the enum picker, a Browse button for a file or a directory. The value travels bound like every other; the refresh timer reuses the last answers. A command action's inputs open in the same form instead of one prompt after another. The starters gain long running statements, which asks how long.

  • Actions are files in the Explorer; the Actions pane is gone. An action is a file whose name says so, name.action.sql (a query) or name.action.run (a command), wherever the Explorer lists it: the workspace, Scripts, Workspace Scripts. Its row wears ACT or RUN, its menu leads with Run Action and View Source, its @toolbar line puts a button in the title bar, and New Action and New Command Action are in the Explorer's folder menus. A click opens the file as a script tab like any other (Run runs the buffer as any script's); the toolbar's Run Action, before Run, opens the action's view in a tab of its own, which asks the file as saved on every refresh and runs on the file tab's connection; the view's Edit opens the file's tab. The view is a result document: the same grid, trail, Compare With, foreign key links and row editing every document has, with what the file's header declares on top (its description, @refresh, @hide, the buttons with @on, @at, @open and @confirm, the placeholders) and a header naming the action, the connection it ran on, the count, the time and the interval. The two action stores of earlier releases are moved into the script stores once, under the action file names, and the Log says so; the starters land in Scripts under Actions, and Restore Starter Actions moved to the Explorer's pane menu. The Actions side tab, its keyboard shortcut and its settings (click behaviour, sort, roots, descriptions) are retired and read as nothing. The file tree patterns list .run files by default now, and the script stores list them too.

  • Statement line numbers stay in SQL editors. A file that is not SQL (a settings JSON, a connection source, a command action) showed the per-statement column too, numbering what the SQL parser made of its text; such a file now shows plain line numbers whatever the Editor line numbers setting says (relative and off still apply).

  • One settings entry per menu section. Every menu that linked into settings with a pair of rows (Open Workspace X Settings, Open Global X Settings) now carries one "X Settings" entry with a Workspace / Global submenu, the same shape in the Explorer, the object tree, the connections tree, the Actions pane, the editor, the sidebar and a result's menus, always at the menu's end. A result's dots menu is shorter for it too: Include Headers lives in the Copy Format and Save Format submenus it belongs to, and the two settings entries close the menu together.

  • The row form's text areas resize by hand. Every multi-line field (text, json, an array's lines or literal) drags taller or shorter by its corner; it still grows to fit what is typed until it has been dragged, then the height is yours.

  • Array columns edit one element per line in the row form. A column of an array type shows its elements as lines (the server's quoting done for you: an element with a comma, a quote or a space is written quoted, a bare NULL line is a null element), and a Lines switch beside it shows the literal instead, the text the server takes, which is what a nested or dimensioned array keeps to. The value stays the literal underneath, so an untouched array never reads as changed.

  • Enum columns have a picker in the row form. A column whose type is an enum the DDL snapshot knows gets a button beside its text box (Alt+Down in the field too) listing the enum's labels in their own order, the current one checked; picking fills the field, and typing stays possible (a label the snapshot does not know yet included). The labels come from the snapshot's CREATE TYPE, no query.

  • The row form's text is selectable. Column names, types and comments in the edit form select like any text. A right click there opens nothing (the app-wide menu used to appear under the form, and its Escape closed the form); inputs keep the editing menu.

  • Pick a foreign key value from the referenced table's rows. The edit form's FK chip folds the form away and opens the referenced table as a browse document in pick mode: a bar above the rows names the field being filled, Select puts the selected row's key into it (Enter on the row does the same) and Return to Edit brings the form back unchanged, as does closing the picker's tab. Everything typed in the form so far comes back with it, and the picker is a full document meanwhile (a missing row can be created there first, then picked), minus what would take it off that table: no Compare With and no foreign key links while it picks. Opening another row edit while a pick waits is noted in the Log with Go to the picker; opening one in the waiting form's own tab replaces that form, and the picker is a plain document from then on.

  • Browse Table or View in the palette. Beside Go to Object, the command lists the object browser connection's tables, views, materialized views and foreign tables (frequent ones first) and opens the pick's rows as a result document.

  • Save. A save button beside a result's copy button (and Save All or Save Selection in its dots menu, the copy's twins) opens the desktop's own Save As dialog and writes the result into the file, the painted columns in their painted order and, for a selection, its rows: the rows stream from the server's copy of the result, never through the page, so a result the size of the spool cap saves as readily as ten rows. Four shapes, the copy's, with settings of their own under Results, Copy and Save (Save format, comma separated the default for a file, with a byte order mark so a spreadsheet reads accents right; Save with headers). The Log records the path and the row count.

  • One Copy, in the shape you chose. A copy from a result grid (the key, a menu's Copy Selection or Copy All, or the new copy-all button beside a result's dots, which takes every row in one click) follows three settings under Results, Copy, switched from the result's dots menu (which also copies the selection or every row) too: the format (tab separated, which pastes into a spreadsheet; comma separated, RFC 4180; a JSON array of arrays; JSON objects keyed by column name, typed by the column so numbers, booleans, json values and NULL travel as themselves), whether the column names lead (on by default; a single cell always copies its value alone), and the row cap (a larger copy is cut to it and the Log says so). Spreadsheet text now quotes a value holding a tab, a line break or a double quote the way Excel, Sheets and Numbers write their own clipboard, so it pastes back into one cell instead of splitting rows. Every grid menu (a cell's, a row number's, a column head's) carries Copy Selection and ends with the section's Columns list and its Results settings pair, what the dots menu offered alone.

  • Hidden columns leave a seam. A column you hide from a result no longer vanishes without a trace: a narrow dashed seam stands where it was, the spreadsheet convention, its tooltip naming what is folded there, and a click on it brings exactly those columns back (the header's Columns menu still lists them all). A column an action hides by design leaves none. Hiding starts on the column head: a right-click there selects the whole column (as one on a row number selects the row) and offers Hide This Column (Hide These Columns for a selected span) and, once anything is hidden, Show All Columns; the full checklist lives in the section's dots menu.

  • Freeze panes. A column head's menu offers Freeze Column at Start and Freeze Column at End: the column moves to that edge and stays there while the rest scroll under it, a hairline marking the edge, so an id or a name keeps you oriented across a wide result wherever it sat. Freeze several and they stack in their own order; a selected span freezes together; unfreezing puts a column back where it came from. Frozen columns never take the whole width (the menu withholds a freeze that would leave no room to scroll), the keyboard never scrolls a frozen target out of sight, and Unfreeze Column or Unfreeze All Columns undoes it. Rows freeze the same way from a row number's (or a cell's) menu: Freeze Row (Freeze Rows for the selected rows) moves the row to the top under the header, where it stays while the rest scroll, a reference row against the others; more stack beneath it in their own order, and Unfreeze Row or Unfreeze All Rows lets them go. Like the hides, a freeze lasts until the next result.

  • Multi-line cell values read as multi-line. A result cell whose value holds newlines or tabs no longer collapses them to invisible spaces: a compact cell paints a dim glyph where each break is (the copied value keeps the real character), so you can see at a glance that a value is more than one line.

  • Set the height of result rows. Drag a row's bottom edge in the number gutter to make every row that tall at once (uniform, so the grid stays fast), and the cells wrap their real newlines; double-click the edge to return to a single line. A column's width resets the same way, on a double-click of its edge.

  • Peek a cell's full value. Press Space on the selected cell (or pick View value from its menu) to open the whole value, newlines and all, in a viewer you can wrap, select and copy from. Escape closes it. Reads the long, the multi-line and the JSON that a single grid line cannot show.

  • The dynamic parser: PostgreSQL itself checks the script as you type. Every statement of a connected SQL tab goes through the server's own parser and analysis (Parse without Execute, psql's \gdesc mechanism, nothing runs): syntax errors and unknown names show before anything executes, debounced after each change, on connect and after every run, with verdicts cached per statement so retyping costs nothing. Problems paint as editor markers (squiggles, hover with the verbatim message, F8) and in the Outline: the command's row wears the class glyph, and the verbatim error text rides as a child node directly under it, always disclosed with its line number on the right, a click revealing the spot in the editor. The new Dynamic parser setting (also in the Outline pane's menu) picks markers, outline, both (the default) or off. The parser borrows the tab's own session so names resolve under your search_path and temp objects, and only on its terms: never waiting behind a query, never entering an open transaction. A tab that has not connected yet gets a connect-analyze-close shot instead, on its first look and then on every debounced change, the same tempo as a connected tab: a throwaway dial with the same session configuration validates the buffer and closes, the tab staying "not connected" throughout, so problems exist before the user ever connects (the dial counts as a real contact, and a server the checks know is down is not asked). Once the parser holds problems, the results pane opens for them, run or no run. Its verdicts also stack up as a third results-area tab, Problems (N): the grids' own shape over the parser's data, one column per field of the server's message plus the Line it points at (columns pickable, problems.columns), the Message cell a link that reveals the spot, present before the first run, which is the tab's whole point. A statement using something an earlier statement of the same script would create can report a false problem for now; the run stays the truth, and large scripts pause the parser with their other services.

  • A file can declare itself read only, and the server holds it to that. Two new annotations (ACTIONSPEC.md 8.17): @readonly puts the file's tab session under default_transaction_read_only, dialed in, followed LIVE as the line is typed or removed on a connected session, and re-checked at every run, so a write is refused by PostgreSQL itself as a plain statement error, function bodies included, exactly like the connection's own Read only flag; @confirm-writes raises the production guard to Ask on both axes, one dialog per run naming the line as the reason. A file may only tighten what its connection allows, never loosen it, so a script that travels cannot switch anyone's safety off; adding the line hardens even an already-open transaction mid-flight, removing it frees the session from the next transaction, and the connection readout shows read only while it holds.

  • A file can carry its own formatter options. The new @format-* annotations (@format-keywords, @format-commas, @format-indent, @format-width, ACTIONSPEC.md 8.16) override the Formatting settings for THAT file alone, one line per option, so one script keeps one style and its neighbour another and the style travels with the file. The values complete in the editor like the color palette does, the toolbar's File Formatting menu writes and clears the lines as checked picks, and every format path (the lens, Format Selection, the editor menu) reads them live from the buffer.

  • File pattern presets in the Explorer menu. The Explorer's pane menu (the hamburger, and the right click on empty tree space) grew a File Patterns submenu: one-click presets for the common shapes (SQL, SQL + config, SQL + docs, Everything), the current one checked, with Reset to Default and the scoped settings pair beside them, so narrowing the tree to SQL no longer means typing a glob in Settings. The plain workspace tree's pattern funnel offers the same presets, same writer, same scopes.

  • The obvious connection picks itself. A new tab, script or first-time file that inherits no connection now takes the obvious candidate on its own: the workspace's own discovered connection when there is exactly one (the .env beside your scripts wins over whatever the vault holds), else the only connection there is. Run on a tab with nothing picked adopts it the same way instead of refusing, and its tooltip names what that press will take. Picking, never connecting: the tab stays "not connected" until its first run, two or more candidates are a real choice and stay yours, and a deliberately detached tab keeps its none.

  • Edit Connection from the panes that name one. The Objects pane's menu grew Edit Connection for the browsed connection, and the Actions pane's the same entry for the connection its actions run on, the gesture the connections tree's row menu already offers. A team shared connection does not carry it, exactly like the tree: its parameters are the portal's to change.

  • Outline object nodes disclose their columns and parameters. The objects a statement references grew the Objects tree's chevron: a relation opens its columns, a routine its parameters, straight from the dictionary already on hand. With automatic expand on, the command under the cursor opens WHOLE, its objects' columns included, and stays open wherever inside it the cursor moves: writing the select list is exactly when the columns are needed (the first cut opened only the object whose name the cursor stood on and folded it the moment the cursor left, which was backwards). Other commands stay shut, and manual chevrons still fold within the current one. The rows drag into the editor like their Objects tree twins, and a click finds the owner in the script. Dependencies stay the Objects tree's business on purpose: the outline says what a command touches and what that is made of, not where it leads.

  • The Outline washes the whole current command. While the editor holds the keyboard, the command under the cursor highlights as one block: its head keeps the stronger accent and its disclosed children (CTE nodes, references) wear a softer coat of the same color, a remembered selection melting in rather than sitting grey among them. The moment focus moves into the Outline itself the wash lifts and the pane's own cursor and selection take the stage, returning when the editor does.

  • Detail sections fold. The headers inside an object's disclosed details (columns, parameters, depends on, used by) are collapsible rows now: click or Enter folds a section, the arrow keys walk and fold them like any tree row, and a wide table's columns get out of the way of "used by". Folds restore with the workspace like the rest of the tree's view, everything stays open by default, and an active find still shows a lit match through a folded section.

  • Detail rows drag like their objects. The object tree's disclosed rows now drag into the editor exactly like the object rows above them: a column or a named parameter drags its name (quoted only where PostgreSQL needs it), an unnamed parameter its $N position, and a "depends on" or "used by" row the signature of the object it names.

  • Insert Annotation in the editor menu. The editor's right click grew Insert Annotation: the vocabulary this buffer actually takes (@description and @color for a script or file, an action's own set in an action, #-commented for a command), straight from the registry, so the menu grows with it. The insert writes a valid line wherever the cursor is: an empty line becomes the comment, a bare marker is completed rather than doubled, a comment with content continues on a fresh line below, code gets the comment above, and a color opens its palette immediately.

Fixed ​

  • Gigantic values no longer freeze the grid, the peek or the form. Measured on an 8 MB jsonb cell: the peek went from 540 ms to 20 ms (it now shows the first 256 kB with an honest note; Copy and the value tab still take it whole), the row form from a one-second freeze to instant (a value past the inline edit limit never enters a textarea: the field shows its size and is edited through its editor tab, the full value written whole), typing in the form's other fields from ~35 ms to ~7 ms per key, and typing in the field editor from ~105 ms to ~10 ms per key (the JSON validity note now parses on a pause instead of every keystroke, dirty tracking rides Monaco's version id instead of comparing megabytes, and Wrap defaults off for documents past 1 MB, measured as the dominant per-key cost; the toggle stays). Grid cells were already capped at 10 kB with a size marker; their native tooltips now share that cap. The inline edit limit is the new results.inlineEditLimit setting (kB, default 100; 0 removes it).

  • An outside click no longer discards a row form holding work. A click past the dialog closed it, whatever was typed; now it closes only an untouched form, and one with anything typed, toggled, picked or carried back from a field editor shakes its head and stays (the footer already counts what would be lost). Cancel and Esc stay the explicit ways out, unchanged.

  • An errored run with nothing to show opens on Messages. A run whose grids all came back empty and whose outcome is an error used to open on the empty grid, the error hidden behind the Messages badge; the error is the outcome, so Messages is now the default face for exactly that run. Any rows anywhere keep Results in front as ever, and a tab picked by hand always wins.

  • The executed statement no longer hides under the results pane. The first run's pane mounting was supposed to keep the cursor's statement on screen, but the reveal raced the split (it measured the editor before the shrink landed) and a statement low in the file vanished under the pane. The reveal now fires again on the layout change itself, so the statement just run stays visible.

  • An untouched nullable field no longer inserts an empty string. Inserting a row sent '' for every field left alone, which a non-text column refuses (a PostGIS geography answered "parse error - invalid geometry" for it). A field left empty is now simply omitted from the INSERT: the column takes its default when it has one, NULL otherwise, and the field's placeholder says so ("empty = NULL").

Changed ​

  • The results strip's corner says how long a run has been running (a live 00:00:01 clock beside Cancel) instead of a timeless "running".

  • Go Back undoes the actual last move, drills included. Walking a result document's own trail (a foreign key followed, an action's @open, the document's own Back) now records into the app's navigation trail, so the title bar's Back first walks the drill back inside the tab and leaves it only past the point where you entered, its tooltip naming the step either way; Forward re-enters the drill the same way. The document's own Back and Forward stay as the scoped shortcut they were. A restored document deliberately starts at its own statement, so step points do not survive a restart (the trail's file and view points do, as before).

  • The starters reorganized by feature. The shipped actions now land in a feature folder each (Activity, Performance, Storage, Replication, Settings, Roles, Reference, Schema Management, Backup and Restore, PSQL) instead of Queries and Commands. Two starters retired as duplicates: database activity (current activity answers it) and role members (role privileges absorbed it: a Members step per role, with grant membership and revoke membership managing it, and the table grant buttons moved onto the privileges step, where the grants show). Five starters added for the questions DBAs actually page on: idle in transaction (with terminate), replication status (standbys and slots, lag and retained WAL, and it says so in words when there is no replication), sequential scans (where an index might be missing, with each table's indexes one click away), database sizes, and transaction age (wraparound headroom per database, the aging tables of the current one a click away). The title bar pins are three: Activity, Locks and psql; top queries lost its pin because it needs an extension most servers do not preload, and a pin must never be a dead button (the file says how to pin it back where the extension runs).

  • A temporary object's row says so: the object tree, the DDL tab titles, the footer signature and the pickers label an object in the session's temp schema TEMP TABLE (TEMP VIEW, TEMP SEQUENCE) instead of a bare TABLE.

  • Switching to a script no longer flips the sidebar to the Outline. The follow evicted whatever pane you chose in that dock on every touch of a SQL tab. The follow keeps its identity reveals (a file lights its Explorer row, a DDL viewer its Objects row, a connection editor its tree row); the Outline, a helper pane, is opened by you and serves the active tab whenever it is open.

  • Row heights are per row. Dragging a row's bottom edge used to set every row's height at once. It now sets that row's, the spreadsheet way: with the row inside a whole-row selection every selected row moves together, and with everything selected the uniform height moves. A double-click on the edge fits the row to its content (as tall as its tallest cell, capped at the viewport), and a second one returns it to the single line; with everything selected it resets. A taller row wraps its cells' real newlines; its neighbours keep their single line. The virtualization stays exact over a sparse map of the rows dragged taller, and the heights ride the grid's state through a refresh and a restart with the rest.

  • The bolt is the mark of an action's view everywhere. The tab, the view's header, the status bar readout and a title bar button without an icon of its own wear the bolt the toolbar's Run Action wears, instead of the angle brackets, which were the retired Actions pane's. One icon per verb from here on: the bolt asks an action wherever it is asked from (the action view's own toolbar and header included, which wore the script tab's play), the play runs a SQL buffer, the circular arrows refresh a document.

  • Files and directories alone open their pickers directly. A command whose only inputs are @file and @dir with no default (the run-sql-file and restore starters) goes straight to the picker, in written order, with no form in between; a typed value, a default or a @pick still brings the form.

  • A command action's tab is its process. Closing the tab while its program (psql, pg_dump, pg_restore) still runs used to leave the program running unseen until the terminal reaper took it, up to twenty minutes later. It now stops the program, and asks first, the dirty file's way: Stop and close, or Cancel to keep both; Close Others, Close All and the other bulk closes ask once for every running tab. The stop is logged and raises no failure alert. A long job means keeping its tab; nothing ever outlived the application itself.

  • An action's view opens beside its file. Run Action from an action file's tab now opens the answer (a query's result document, a command's terminal) right after the file's tab instead of at the end of the strip, and the bolt in the toolbar fills while the view is open, leading to it and asking again on a click (never a toggle, so it does not wear a pressed chip). Its tooltip and the plain Run's say what each does: the bolt asks the file as saved in a tab of its own, Run runs this buffer here as a script.

  • A colored tab wears its color in the mark. A tab on a colored connection (or an action with @color) used to carry a wash of the color, a stripe along its top edge and a repainted underline, the grammar every other tool has too. The color now lives in the SQL, ACT or DDL lettermark alone, painted as a small label with the session dot on its corner, dimmed at rest and full when hovered or active; a tab with no lettermark paints its icon instead. The tab's face and the accent underline are every tab's again, in every strip layout. The results pane's tab strip follows: its Results tab leads with a small painted box instead of the wash, and Messages and Problems, which are not the connection's, carry no tint at all.

  • Result columns name their type the way PostgreSQL does. A column header reads "integer", not "int4", "character varying(255)", "numeric(10,2)", "timestamp with time zone" and "integer[]" for an array, the exact spelling of format_type, decoded from the type and its modifier that every result already carries. Nothing extra runs to find them out: the user's own statement stays the only query.

  • The result header hover describes the column from the object dictionary. A user-defined type the driver leaves unnamed (an enum, a composite, a domain) now reads by its real name instead of "unknown", and a column that maps to a real table column carries its origin, its full definition (NOT NULL, default, identity, generated) and its comment in the header tooltip, with a primary key marked. It all comes from the schema snapshot PlumeSQL already keeps, so still nothing runs but the user's own statement; a connection with no snapshot yet simply shows the name and type as before.

  • Parser verdicts survive edits elsewhere in the file. A statement's verdict keys on its own text, so typing above it, deleting a line below it, a paste, a reorder or an undo leaves its squiggle, its Outline node and its Problems row standing, line numbers following; only the statement being edited loses its verdict until the next pass validates the new text. The blink of every face on every keystroke is gone.

  • A run's messages keep their lines after an edit around the run. The run's text is found again wherever it moved, so the Messages links, the run's squiggles and the header's select-back stand through edits elsewhere in the file; an edit inside the executed text retires them, and an undo brings them back.

  • The results strip's run readout hides on the Problems tab. "0 rows, 3 messages in 2 ms" is the last run's account, and the Problems tab speaks about the file, so the readout shows only while Results or Messages is what the pane displays; the Problems header carries the file's own line.

  • The column picker moved from the grid's corner to the section header's dots. Every result, Messages and Problems section header ends in the standard three dots; the menu nests the grid's columns under Columns (checked rows, Show All Columns) and carries the section's settings pair (Results, or Parser for the Problems tab, which also offers Restore Dismissed Problems there). The Problems header separates the dots from its mode buttons with a divider. An action's header menu gained the same Columns submenu, so the picker has one home everywhere; the grid header's right click still opens it in place.

  • The dynamic parser has its own Settings section. Parser, between Results and Outline, holds the mode (moved from Editor; the key is unchanged) and the pause after the last keystroke before a check (parser.debounce, 800 ms). An unconnected tab used to wait out a fixed 30 s between its throwaway dials, the wait behind "I changed the statement and the error took a while to show"; it now checks on the same pause as a connected tab, one throwaway connection per check. Four switches beside them, all on by default: whether unconnected tabs are checked at all (parser.checkUnconnected, the veto on background connects), whether an object the script creates earlier is a note or an error (parser.notes), whether the server's parse-time warnings show (parser.warnings), and whether Did You Mean offers names (parser.suggestions). A second group, Static analysis, holds the structural analyzer's own switch (parser.staticAnalysis): off, the Outline's object nodes and the completion's scope fall back to the plain heuristics, the escape hatch for a construct the grammar misreads.

  • Every grid has its own right-click menu. Copy (the selection; a right click outside it selects the cell under the pointer first, the spreadsheet convention) and Select All, on every grid in the app: results, Messages, Problems, an action's. Rows add their own verbs after a divider: Messages offers Go to Message where the message knows its line and Copy Message whole (DETAIL, HINT and SQLSTATE included); Problems offers Go to Problem, Copy Message and Dismiss This Problem.

  • The Problems tab has a header, and a file can pin its parser mode. The tab speaks about the FILE, not the last run: its header names the file, counts its problems ("2 problems in this file", a click copies it) and carries the dynamic parser's mode as four exclusive buttons (Off, Editor, Outline, Both). The buttons write the file's own @parse line (ACTIONSPEC.md 8.23), a new annotation that overrides the Dynamic parser setting for that file alone and travels with it; the pinned mode is underlined, clicking it again removes the line and the file follows the setting again. Off says so plainly in its tooltip: it hides the tab for this file, and the way back is the line in the header. The Dynamic Parser submenu is gone from the Outline's menus (the header is the mode's home now; the workspace default stays in Settings).

  • The Outline's error node reads as a problem at a glance: a wash of the class color behind the row with a stripe on its left edge (the Log's language for a live error), icon, text and line all in the class color, and the command's own row wears a lighter shade of the same, so the whole node is the trouble spot. The SQLSTATE now explains itself in place: its condition name (Appendix A's undefined_column, the name a PL/pgSQL handler catches it by) shows as the Problems tab's Condition column and in the node's tooltip with its class, and Copy Condition Name is one click away. Beyond naming the problem, the menus (the node's and the row's, one shared list) now help fix it: Did You Mean offers the dictionary's closest existing names for a missing column, table, type or function, each a one-click replacement of the misspelt token (the lightbulb on the squiggle offers the same replacements above Dismiss); Open TABLE x opens the relation a missing column was looked up in; Open SELECT Reference opens the failed command's own synopsis in the reference viewer, the document that helps a syntax error. A missing object that an EARLIER statement of the same script creates is no longer a red error but a muted note ("created on line 3": the parse cannot see it yet, the run will), counted apart in the tab's badge and the header. And the parser now reports what the server WARNED about while parsing (a nonstandard escape in a string), not only what it refused.

  • After a run, the results area shows what the run produced, by priority: Results when there are results, else Messages, else the command's own header line (rows affected and time), else nothing at all; the tab picked on a previous run no longer carries over. With a single tab there is nothing to pick.

  • A parser problem can be dismissed as "not an error". A temp table another script creates, a name that exists only at run time: the lightbulb on the squiggle (Ctrl+.), the Outline error node's menu and the Problems row's menu all offer Dismiss This Problem, and the problem leaves every surface at once. The dismissal keys on the file, the statement's own text and the SQLSTATE, so it survives restarts and the statement moving around, and retires by itself the moment the statement's text changes. Every dismissal is a Log entry carrying Restore; the Outline pane menu restores a file's dismissals in bulk. The error node and the Problems row also grew Go to Problem and Copy Message, and the Dynamic Parser mode switch is one shared submenu wherever it appears.

  • A connection error dismisses itself when the connection answers again. Every Log entry that records a failure to REACH a connection (a refused dial, a lost session, a snapshot, statistics or parser shot that could not connect) now remembers which connection it is about, and the moment that connection answers, through a check, a dial or any other real contact, those entries dismiss themselves; a stale "connection refused" from before the server was started no longer reads as news after a restart. The dynamic parser's opening shot retries on the same signal instead of staying silent for the rest of the run when it caught the server still starting. The link survives a restart (it is written to the log file now, not only held in memory), a successful run heals a connection whose session was lost mid-flight (not only one a check had marked down), and the parser's own failures carry the connection whichever path hit it. The panel now keeps the two apart at a glance: an undismissed error wears a stronger wash and a red edge stripe, a dismissed one steps back (muted text, dimmed row), so healed history never reads as a live problem.

  • Run messages appear in the editor as markers. Everything a run's messages locate (the same resolver as the grid's line links, one implementation for both) now also paints into the editor as Monaco model markers: a red squiggle under the word an error's Position names, amber under a RAISE WARNING's line in the DO body, dimmed info under notices, each hover carrying the verbatim message with DETAIL and HINT in psql's voice and the SQLSTATE as the marker code, marks on the overview ruler, and F8 / Shift+F8 walking the problems, all Monaco's own machinery. Hash-gated like the links: an edit clears them on the spot and an undo back revives them; a large script skips the work along with its other paused services.

  • Every message links to its line. A message's text in the grid is now a hyperlink (the grids' standard link-cell shape: the value is the button, selection and copy untouched, Enter fires it from the keyboard, the tooltip names the line) that selects the line it came from in the editor. The chevron is retired everywhere it lived (an action's row links and row buttons included): a link reads as the app's links do, accent with underline on hover, and what the click does is the tooltip's job. Finest location wins: an error's Position field (a character index into the text the run sent, "syntax error at or near"), then a PL/pgSQL context naming "inline_code_block line N", whose line counts inside the DO body's own string starting right after the opening dollar quote, and everything else lands on its command's first code line, a named function's frames included (its source is not in the buffer, the call that ran it is). All of it recomputes through the run's remembered coordinates and holds only while the buffer still matches the run (the select-back's hash gate): edit the file and the links retire on the spot.

  • Experimental: the result stack is resizable, and Messages stacks the same way. When one run yields several results, a sash between neighbouring sections drags their shared edge, the same gesture as the results/messages split; the other sections keep their size, and a re-run starts the stack even again. The arrangement is per run and deliberately not persisted yet: an experiment, kept or discarded on feel. The Messages side now mirrors the results side exactly: one section per command holding the messages that command produced, the same clickable statement name and icons, the same sashes, with the counts in the statistics' place ("1 warning, 1 notice, 1 command completed" where a result says "3 rows") beside the same execution time, each half a click-to-copy. Command tags (the protocol's CommandComplete messages, "SELECT 3") show only on request: they carry nothing beyond the tag, so the new Show command tags setting (Results category) defaults to off, hiding their rows, sections and counts, and a run whose commands sent nothing else shows no Messages tab at all; notices, warnings and errors always show. The results strip grew the pane's own menu behind the standard dots button (right of the split toggle), and a right click anywhere in the pane that no grid or name claims opens the same menu: the switch lives there, with the standard scope pair of Results settings links under it. The dots glyph itself now has ONE definition (lib/icons.ts) that every surface imports, the grid corner's column picker included. The Messages tab's badge now classifies everything, not warnings alone: errors (red x), warnings (amber triangle) and informational messages (dimmed i), each with its count; and each messages section's header carries the command's gravest message whole, its class's icon and color on it, truncated to the line with the full text in the tooltip and copied by a click. The class glyphs are now small drawn icons with ONE definition (lib/icons.ts, VS Code's visual language: a circled x, the warning triangle, a circled i) on every surface that marks a message class.

  • The Messages grid shows the wire protocol verbatim, one field per column. The grid's columns are now exactly the fields of PostgreSQL's own error and notice messages, nothing invented: the Severity column (formerly Level) shows what the server actually said (NOTICE, WARNING, INFO, and so on; the made-up "OK" is gone), a command completion is honestly a row with only its tag, an error row carries the message field alone with severity and code in their own columns, and the full field set is available from the column picker: Position, Internal position, Internal query, Where, Schema, Table, Column, Data type, Constraint, File, Line, Routine. The "message" counts in the header bar and the Messages tab count leveled messages alone, never command tags. Row coloring keys on the protocol's untranslated severity, so a localized server still colors its warnings.

  • The results header names the statement and remembers the command. Each result leads with the statement's own name in the outline's shape ("SELECT film"), computed by the outline's own reader; a single result also lends its name to the Results tab itself, and two small icons beside the name select the command back or copy it in one gesture. Beside them sit the statistics in the voice SQL people know ("4 rows. Total execution time: 00:00:00.006"), each half a CLICK-TO-COPY of its bare number, every copy recorded in the Log; the bar above sums the run ("4 rows, 1 message in 6 ms") with the executed-at wall clock in its tooltip. The results tab and the statement name wear the tab's own identity color (the file's @color first, else the connection's), through the ONE shared implementation the tab strip now uses too, so the whole column reads as one file. The run remembers the command that produced it: the name's tooltip shows it whole, right click copies it, and while the buffer still matches the run (a content hash guards it) a click on the name SELECTS the command back in the editor. The CSV and JSON links are gone for now; export returns as one systematic pass (docs/BACKLOG.md). Under it all, per-statement durations are finally REAL: the clock used to start after the server had already executed (a pg_sleep reported under a millisecond), and now runs from the request.

  • The statement lens leads with Run. The default order is now Run, a widened divider, then Select, Format and Copy, so the one command that acts stands apart from the ones that arrange. The divider is a vocabulary word of its own (gap in editor.statementLens, Divider in the cog menu), placed anywhere between command groups; leading, trailing and doubled gaps drop themselves.

  • Every grid carries the columns button. The corner ellipsis (and the header's right click) the Messages grid had is on every result grid now: the main results and an action's answer let you hide and show THIS result's columns, ephemeral on purpose (the columns belong to the query; the next run starts whole), with Show All Columns one click away and the last visible column refusing to go. The Messages grid keeps its richer menu, and it now shows ALL its columns by default (code, detail and hint included): trimming is a choice, not a default.

  • The formatter defaults to a four-space indent, and the toolbar's format group grew up: the Format Document button is gone (formatting works on what you SELECTED; the lens formats one statement, and a whole-document rewrite touches code you are not looking at, though the VS Code-parity paths, Shift+Alt+F and the editor menu, stay). In its place: a File Formatting button and a Formatting Settings button, each a menu of one-click picks for keywords, commas, indent and width.

  • The editor gutter has its own shade. The line number margin sits one quiet step off the editor's background (lighter in the dark theme, darker in the light one), so the margin reads as a margin instead of dissolving into the text area.

  • The Objects bar counts by type. The pane's meta line now reads the inventory, importance first: 15 tables · 7 views · 9 functions and so on, instead of when the snapshot was refreshed. The freshness, the build time and the total moved to the line's tooltip, where they were already written out in full, and a narrow sidebar clips the tail exactly as before.

  • The edge is the connection, the tab is the file. The colored hairline across the content's top edge now always shows the color of the connection the active tab runs on, and never anything else. It used to show the file's own @color when there was one and the connection's otherwise, so the edge answered no question; now the tab and the tree row wear the file's color, the edge wears the connection's, and each color has exactly one meaning. A tab with no connection draws no edge.

  • A new script starts empty, like a new file. The header note that taught @description and @color is gone: both doors now open to the same ghost text, whose tail now points at the one gesture to know (Type -- @ for annotations), and the pair teaches itself through the annotation completion and hover inside any comment (files carry the pair exactly like scripts). This also mends the reuse rule: "new script" reuses an untouched one again, and it now recognizes the old teaching notes by shape, so scripts created back when the note's wording differed count as untouched too instead of piling up.

Fixed ​

  • An action's grid carries every grid feature. The action view used to drop the whole statement header, and with it Copy all rows, Save As, the dots menu (the copy shape, the Columns switcher, Open as Script, Compare With, the Results settings) and the spill warning. The header now goes slim instead of away: the same controls in the same words as every other grid, minus only what the action's own header already carries (Refresh, the file) and what a one-shot run cannot do (row editing).

  • An @on button column's NULL cells paint as nothing instead of the grid's NULL marker: NULL there means "no act on this row" (the spec's a-cell-with-no-value-is-no-button rule), not a data value to read.

  • Completion inside comments knows its place: on an @inline line the second word offers exactly its two kinds (literal, identifier) and the first the action's own columns, and the SQL completion stays quiet in every comment (it used to drown an annotation's values in half the catalog; a comment is prose).

  • Browsing a temp table from another session could silently lose a row. PostgreSQL's own fence fires only when a block of the table is read, so another session's EMPTY temp table quietly reads as 0 rows and the first insert into one extends the relation without tripping the check, losing the row where the owner never sees it (the second write then failed with "cannot access temporary tables of other sessions"). Browse Data is therefore not offered on a temp relation anywhere (the editor hover, the object tree's row menu, the DDL viewer's header, a grid column's Browse doors, the Go-to picker, which opens its DDL instead), and the server refuses any edit or row read aimed at a pg_temp schema the session does not own, before anything runs. The temp table stays fully editable where it lives: the grid of its own script tab.

  • A temp table reaches the editor services again. The automatic after-DDL snapshot check always dialed a fresh connection, where the session's temp schema does not exist, so a CREATE TEMP TABLE never reached the tree or completion until a manual Refresh; the check now borrows the active tab's own idle session, the same way the manual Refresh does (busy or transacted sessions fall back to the dial). And the captured search path is re-read after CREATE and DROP tags too: the first temp object conjures pg_temp into current_schemas, which only SET used to trigger.

  • The type list offers itself where a type belongs. Completion knew the type spots (a cast's ::, a column definition's second word) but only answered Ctrl+Space there; the second colon of a cast and the space after a column's name in CREATE TABLE or ADD COLUMN now open the list on their own (the connection's own types first, then the everyday built-ins in the order a hand reaches for them, text through uuid, the serials in a column definition, then the long tail), and CAST(x AS ...) counts as a cast spot too. A space anywhere else stays quiet.

  • A run at the bottom of the script stays in view. The results pane mounting on the first run took its height from the editor and could leave the executed line hidden beneath it; the cursor is revealed once the new layout stands.

  • A command with no result set always shows its tag. With Show command tags off (the default), CREATE TABLE and its kind answered only in the section header while the Messages side stayed empty; a statement that produced no rows now shows its tag there whatever the switch says, which keeps governing the tags that would repeat the results side.

  • Typing no longer summons the results pane. With the pane closed, the dynamic parser's problems mounted it (and its Problems tab) the moment a statement was momentarily wrong under the keystrokes, and unmounted it when the sentence completed, over and over. A problem while typing is a state, not an event: before a run it lives in the squiggles, the hover and the statement dots, and the pane exists only for what a run produced (in flight, rows, a query error), where the Problems tab keeps its home.

  • Completion offers the catalog's relations unqualified. Typing from pg_stat_a offered built-in functions and never pg_stat_activity: the bare-name list left pg_catalog out wholesale, to keep its thousands of functions from burying the user's objects. Its hundred and forty relations (pg_stat_activity, pg_class, pg_settings and the rest) now complete in a FROM or JOIN spot, after the user's own, each row saying pg_catalog; the functions stay behind an explicit pg_catalog. as before.

  • The documents and the help say what the app does. ACTIONS.md described the command input flow as one prompt after another (it is one form), a New Action header carrying the whole specification (it is a pointer, and the file lands as action1), the connection picker opening on its own (it is a line with a button), VS Code's Back and Forward combos (they are PlumeSQL's own), three completions where the editor offers every annotation of the header, "Run again" where the header reads Refresh; ACTIONSPEC-QUICK.md's tables are generated from the registry now and held to it by a test; GLOBALDIR.md gains the object browser's and the command actions' pins and says the @connection name is re-read on restore. In the app: the action view's calm state reads "The run failed", the New Action pickers at the palette and in the Explorer are one list (and the palette gains New Command Action), a query action's header problems carry the Reference button like a command's, an @open navigation's @confirm is no longer painted as danger, the Annotations Reference's plain-file section uses the editor's own words, the title bar buttons' and the command tab readout's tooltips name the connection the click uses, the "row buttons" example runs a real act (a statistics reset) instead of promising an ANALYZE it did not run, three Log wordings say "button", and a stale line naming the retired Actions pane is gone.

  • The two parsers honour the specification, in one voice. A # header line with more than one space before its @ was dropped in silence by the command parser; # lines below the command were read as header; {password} and unknown names passed unchecked in every message value (@confirm, the alerts and hooks, an asked input's default), in both files; the query parser skipped the @var value rules the command parser had (no other @var, no unknown name, a value nobody reads); a "", "default" declaration read its default as the prompt and stayed required; a $1 written by hand in a query action's own command or a pick silently received the parser's first value; @file name=x was accepted; @on a hidden column was said only after the first run; @format-*, @parse, @readonly and @confirm-writes took any value; @toolbar last-wins kept the first line's icon; <SVG in capitals became the label; column names were compared case-sensitively and could not be double-quoted; @env accepted a repeated or malformed key; a refresh under a second was a loop; a run of dashes read as prose in a query where a run of hashes read as header in a command; an unclosed quote in a command passed; a comment between a command's continued lines joined it as arguments; a hook clash was reported on the wrong line; a @pick in a command did not count as needing a connection; and the same mistake had two wordings across the two parsers. Each is diagnosed now, with one message, and tested.

  • Six starters answered wrongly. long-running listed backends idle in a transaction as running statements (their query_start is the start of a statement long finished); it lists active backends only now. non-default-settings listed settings initdb wrote at their shipped value (shared_buffers, max_connections, DateStyle and more); a setting equal to its built-in default no longer counts as changed. vacuum-candidates' navigation opened the locks on the table, which is not what holds dead rows back; it opens the backends holding a snapshot, oldest first. The backup, restore and dump-objects starters announced "finished" on any exit code; each now has a success and a failure dialog of its own. database-activity's pick asked every database's size, which needs CONNECT on each and failed whole on a hosted service; the size is asked only where allowed. backup-globals let pg_dumpall dial postgres and template1 instead of the connection's own database. The three activity starters now share one column set.

  • Two starters overwrote their file on a second run the same day (dump-schema, backup-globals, named with {date}); they are named with {timestamp} like the backup. database-activity and role-members open on this connection's database and user as their defaults. "Schema" in pg_dump's sense left the two dump starters' wording (the word means the namespace object in PlumeSQL).

  • The starters are proven on every supported release. The seed execution test now runs against the version matrix (make test-versions, and the schema matrix workflow on a starter change), creates pg_stat_statements where the server allows it instead of skipping top-queries, and the counts (ten queries, eight commands) are asserted, so a lost starter fails a test rather than a first run.

  • The Explorer follows an action. Opening a file from the shared Scripts store (where the starters live) did not light its row, and an action's view (a command's terminal, a query's result document) lit nothing; both now light and reveal the action's file, as any file tab does.

  • A running command action is no longer killed for being unwatched. The terminal reaper counted attached streams and keystrokes only, and the command view attaches for the tab on screen alone, so a long dump or restore died twenty minutes after the user looked at another tab ("exit -1", the failure alert). A program a command action runs is now reaped only after it exits and sat unread for the ttl.

  • The integrated Terminal no longer attaches to a command action's program. The terminal panel took the first live terminal, a running pg_restore included, so keystrokes went to it and Restart killed it. A command's program is reached by its id alone now.

  • A command action's Run button is Stop while the program runs, and asks first; it used to wear the stop icon and re-run on a click.

  • Completion hooks fire when the program ends, whichever tab is on screen. @alert-on-* and @open-on-* waited for the user to return to the tab; a tab off screen now asks the server every two seconds.

  • The same action can no longer run twice. F5, the pane's Run, a refresh tick or the view showing again during the file read, the form or the confirmation started a second program or a second drain; a run is marked in flight from its first step. F5 also does nothing while a form or a question is on screen, and a form displaced by another answers its run as dismissed instead of leaving it waiting forever.

  • Closing a tab mid-run cancels the statement. The drain went on to the end into a spool nobody read and then wrote onto a tab that was gone; the tab's close now aborts the request, which cancels the statement server side.

  • A failing refresh timer no longer reopens the Log every tick. The same error every three seconds is one entry; a new entry (and the panel) comes only when what is wrong changes.

  • A run that threw unexpectedly left its tab refusing every Refresh.

  • Stopping a command cancels what it was running on the server. The kill sent a hangup alone, so a psql running pg_sleep died while its backend slept on until the sleep ended. A stop now sends Ctrl+C first (psql, pg_dump and pg_restore cancel their statement on it), then the hangup, then a kill for a program that ignores both; PlumeSQL's exit stops every program side by side instead of one after another.

  • An action tab that cannot run no longer says it is running. A failed dial left a spinner reading "Running…" under the "is not answering" strip, forever with a refresh timer; the spinner now shows only while something runs. Before its first run an action tab says Run, not Refresh (the header button and the strip alike), and a dismissed input form leaves the tab saying what the action asks for instead of "Refresh to load the rows", which reopened the form. The script tab's "Run a query to see results" no longer shows under a document's own strip.

  • One menu per grid. The results pane's own dots (and the right click on its empty parts) are gone: they held one switch and a copy of the settings entries every section's dots already carry, a hand's width apart. Show Command Tags moved to the Messages section's dots, where the rows it governs are.

  • The keyboard steps over hidden columns. Arrows, Home and End in a result grid used to land on a column you had hidden (an invisible active cell, and one more keystroke to get past it); they now walk the painted columns only, and the edges are the first and last visible ones.

  • The Log keeps its message readable in a narrow panel. With both docks open the bottom panel is a third of the window, and the Log's fixed time, level and action columns used to leave the message a sliver that broke word by word. The panel now folds by its own width, least important first: the level word goes (the dot already carries it), then the hover actions (Copy and Dismiss stay in the row's menu and on the keyboard); the time stays, whatever the width.

  • Your column hides survive in Messages and Problems. Hiding a column in the Messages or Problems grid no longer snapped back the moment a new message arrived or the parser ran another pass while you typed: the grid rebuilt its whole model on every content change and reset your picks with it. The hides, and the measured column widths, now hold across content changes and reset only when the visible column set itself changes.

  • A query tab no longer sticks on "conn busy". If a result stream was interrupted mid-drain (the page went away as rows were still arriving), the tab's connection could be left undrained and every later query on it failed until the session was reaped. The connection is now always drained or closed on that path.

  • The desktop app opens the folder you chose. On Linux and Windows, a folder dropped onto a window or opened from the file manager could reopen a previously opened folder instead of the one you picked; the new window now always takes the folder you gave it.

  • The desktop window no longer freezes on a stalled handoff. Handing a dropped or opened file to the local server ran without a timeout, so a wedged server could hang the whole window; the handoff now bounds its wait.

  • A case-only rename works. Renaming Dvd.sql to dvd.sql was refused with "a file with that name already exists" on macOS and Windows, whose filesystems are case-insensitive: the existence check found the file being renamed and mistook it for a conflict. A target that is the source itself now passes; renaming onto a genuinely different existing file stays refused.

  • One background dial failure is enough. When a background surface proves a connection down (the stats collection, say), the OTHER background work (the snapshot ensure and auto refresh) now defers instead of dialing into the same wall and repeating the Log entry. Nothing latches: the mark ages out in under a minute, the availability check keeps probing, and the server coming back wakes everything as before. Your own gestures (Try again, Refresh, Run, Test) always dial regardless.

  • The freeze after coming back to a script is dead. Switching the sidebar away from the Outline and back (F12 to a DDL and return, or plain side-tab clicks) locked the whole app in an effect cascade (effect_update_depth_exceeded): remounting the Outline with the parser already loaded ran a ready-callback synchronously inside an effect, and its counter increment read the very state it wrote, making the effect its own dependency. Ready-callbacks now fire in a microtask and the increment is untracked, and a new e2e scenario drives the exact churn so this class cannot return unnoticed.

  • COMMENT ON references resolve in the Outline. A table.column chain (COMMENT ON COLUMN thing_data.thing_code) sought as schema.object and missed; the chain's head now claims the relation node, the same resolution Go to Definition uses.

  • Go to Definition works in statements with no FROM. A table.column chain inside COMMENT ON or GRANT resolved only through the statement's aliases, of which such statements have none, so F12 and Cmd-click died there while the hover answered fine; the chain's head now also resolves as a bare relation name.

  • The Outline reads like the Objects tree. Same row rhythm and text size, the statement verb as the muted type prefix inside the label ("SELECT thing_data" the way the tree says "TABLE users"), the referenced name in the tree's own text, the gutter line and line count as the muted numbers on the right where the tree keeps its counts and sizes, the same chevron and icon sizes, and the trees' focus ring instead of a heavier fill.

  • Opening a huge script no longer freezes the UI. A 725 KB schema dump has over three thousand statements, and the Outline built a DOM row for every one of them the moment the tab opened (the freeze ran to seconds in the desktop shell). The Outline now renders only the rows in view (49 instead of 3184 for that file), resolves each row's references lazily as it scrolls into view, fills the snap bar's reference count in idle slices, and recomputes on the typing pause instead of on every keystroke; the statement marks in the editor recompute on the same pause. Nothing about the pane's behavior changed: same rows, same keyboard, same menus.

  • Typing in a huge script is instant again. Every keystroke paid a hidden quadratic walk: the annotation scope resolver sliced the whole text once per statement to learn a line number, and the completion provider asks it on every quick suggest, so a schema dump cost near a second per keystroke. One forward scan now answers all the line numbers (925 ms became 31 ms on a 725 KB dump), the statement gutter looks its numbers up by binary search instead of walking every statement per rendered line, and the @color swatch scanner skips files that never say @color instead of walking every line on every change.

  • The dual line numbers fit big files. The "both" gutter (script number and statement number side by side) sized itself for seven characters, and a four-digit script line squeezed the columns into each other; the gutter now measures what it actually renders.

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • Setting descriptions render their code as code. Keys, vocabulary words, values, file names and placeholders in every setting description (the Settings page and the connection editor's override hints alike) now wear a quiet mono chip instead of blending into the prose, so a vocabulary like select, run, copy, format reads at a glance. All 131 descriptions swept; descriptions are authored with backticks from here on.

  • The statement lens grew commands, and a cog. Above every statement of an editable SQL buffer the lens now offers Select, Run (select and execute in one click; the guard still asks where a group demands it), Copy and Format (that statement alone, through undo). Which commands show is the editor.statementLens setting, a semicolon list whose description names the whole vocabulary; the cog at the end of every lens edits the list in place, and an empty list hides the lens (the setting is then the way back).

  • Object comments stand out. The comment leads the hover and the suggest details as a quoted block set apart from the machine facts (types, stats), for tables and columns alike, and an Outline object node carries its object's comment in its tooltip. Comments are where the business meaning lives; they read like it now.

  • Run selects before it executes. With no selection, Run (the button, Ctrl+Enter or F5, the palette) now selects the command at the cursor instead of sitting disabled: the visible selection says exactly what the next press will execute, and nothing ever runs unseen. Pressing again runs it; moving the cursor collapses the selection and the next press selects afresh. The run.wholeScript setting keeps its meaning (a bare press runs the whole script), and the button is disabled only with no connection or an empty script. The button says which of the two it will do: a small play inside a dashed box (the Selection tools' own vocabulary) when the press will select, the filled play when it will execute.

  • Comment rows in the Outline have their own menu. Select Comment and Copy Comment, instead of falling through to the pane's tab menu (Move Outline to ..., an answer to a question nobody asked there).

  • The Outline follows the cursor by shape. Automatic expand, on by default: the command under the editor cursor opens whole, CTEs and references included, and every other command folds as the cursor moves. Toggled from the Outline header (beside the up and down steppers) or its new three-dots menu, which also carries Collapse All and the pane's settings pair; the setting lives in a new Outline section of Settings. Off keeps expansion fully manual.

  • The Outline discloses a query's CTEs. A WITH statement's row now opens into its CTEs, each CTE into what its OWN body references: dictionary-confirmed objects under the node whose body wrote them, and a jump row for a CTE that reads a sibling CTE (click lands on that CTE's definition). The CTE skeleton needs no connection at all, clicking a CTE selects exactly its body, and its context menu can select, run or copy that body alone. References found through the parser also land exactly where they were written (click and Peek use the real offsets). A statement that does not fully parse still shows the CTEs that did (a half-typed token mid-query must not fold the node flat); only one with no recognizable structure keeps the old flat behavior.

  • Large scripts pause their per-statement services. Above a size threshold (Settings, Editor, Large scripts: 512 KB by default, 0 for never) a script pauses the services that walk every statement on every edit: the Select lens, the statement marks, per-statement folding and the statement half of the line numbers. A strip above the editor says so, chooses which services this covers, and its Enable anyway turns everything back on for that tab (until the tab is reopened). Completion, the formatter and the Outline stay on: they no longer care how big the file is.

  • The toolbar has one menu. The "..." button used to open a different menu depending on whether any buttons were hidden (a bare command list one moment, the full customize menu the next), and neither matched the right click. Both gestures now open the same menu, and it no longer lists the hidden buttons as runnable rows (that list grew gigantic with everything hidden): a hidden command keeps its shortcut and its palette entry, and its group's checkbox brings the button back.

  • Format buttons on the toolbar by default. Format Document and Format Selection now ship in the default button set, in their own group after the comment pair. A customized toolbar keeps its own list, as ever, with the new commands waiting in the toolbar menu.

  • A SQL formatter. Format Document (Shift+Alt+F, VS Code's combo) and Format Selection (Ctrl+K Ctrl+F; only the statements the selection touches), also in the command palette and as toolbar buttons. A statement that fits the line width stays on one line; a longer one breaks into its clauses, and lists or condition chains still over the width break one element per line. Comments stay exactly where they were written. The new Formatting section in Settings holds the choices: keyword case (lowercase by default, uppercase or leave-as-typed), trailing or leading commas, indent width and line width. Two guarantees over all of it: a statement that does not parse is never touched, and every formatted statement is re-read and compared token by token with the original before it replaces anything, so the formatter can decline to format but can never change what the SQL says.

  • Completion understands the statement itself. A real PostgreSQL grammar (tree-sitter, generated from PostgreSQL's own parser and loaded lazily as WebAssembly) now reads the statement under the cursor: CTE names complete where a relation is wanted, a CTE's or subquery's columns complete after its alias's dot and in expression spots, VALUES and function aliases carry their declared columns, and only the relations actually visible at the cursor are offered (an outer query's alias reaches into its subqueries, a sibling subquery's no longer leaks out). In expression spots the statement's own names lead the list: its aliases, CTE names and subquery aliases come first, with the dot after any of them narrowing to columns. All of it works with or without a connection; the previous heuristics stay as the fallback while the parser loads or when a statement is too broken to trust.

  • Workspace settings stay out of the repository by default. They live in the global plumesql dir beside the session now (localsettings/<hash>.json), so a casual toggle never creates a .plumesql dir in your repo. An existing .plumesql/settings.json always wins, which is how a team's committed settings take over on checkout; the Settings page's Workspace tab shows where the file lives and moves it between the two homes in one click (the one way PlumeSQL ever materializes .plumesql), and the settings.workspaceStorage global setting restores the old always-in-repo behavior for whoever prefers it. Existing workspaces with a .plumesql dir keep working untouched.

  • The Selection toolbar leads with the command trio. Select Command grabs the statement at the cursor (a gap resolves to the next one), Select Previous and Select Next step through the statements exactly like the Outline's arrows, clamped at the ends, and all three ride the palette too; with Run executing the selection, select-run-next-run becomes a two-button rhythm. Select All and Select Line follow behind an in-group divider, and Select to Bracket left the default set (the group's right-click brings it back for whoever uses it).

  • Two sidebar quality-of-life gestures. A right-click anywhere a sidebar pane left unhandled (its bars, empty states, margins) opens that pane's own tab menu instead of the generic app fallback, so the sidebar's context always beats the app-wide one. And a schema object drags its SIGNATURE: grab a row in the Objects tree or an object node in the Outline and drop it on a writable editor, and the qualified name (a routine's argument list attached) lands at the pointer, the caret walking with the drag; a pointer drag like every other in the app, deliberately not HTML5 drag and drop, which the desktop shell's webview never receives (Tauri's native handler owns it for file-drop-to-open). The toolbar joined the first gesture too: a right-click on its empty stretches opens its own customize menu instead of the fallback.

  • The sidebar follow routes each dock by what the tab is about, and it is ON by default now. A DDL viewer turns the dock that owns Objects to it, a SQL buffer turns the Outline's dock to the Outline, and a file's Explorer reveal rides along in its own dock, all independently in one switch of the active tab (it used to pick a single pane, so coming back to a script left the Objects tree standing). When two of those live in one dock the more specific pane wins: a script shows its Outline over its Explorer row. The Follow the active tab switch still governs all of it (off restores the old stillness), a hidden tab is never followed to, and the follow now also fires on a file's FIRST open, not only on later switches to its tab (the activation used to run before the tab knew it was a DDL viewer, which is exactly why it sometimes switched and sometimes did not).

  • The Outline: the active script's structure as a side panel. A new sidebar tab lists the script one row per block, shaped like the Objects tree (the chevron leads, then the icon): a statement wears its SYMBOLIC head (SELECT DISTINCT, CREATE TABLE, EXPLAIN ANALYZE, WITH DELETE for a CTE that hides one), the first referenced name muted beside it, a verb-family glyph, the statement marks' colors on the lead bar, its gutter line number and line count, with the full first line in the tooltip; a comment header keeps its own first line as the label. The pane's own snap bar counts the commands, the confirmed references and the lines, and steps through the command selections with its up and down buttons. Clicking or pressing Enter on a row selects that statement in the editor (arrow-key browsing previews without stealing focus, and the walked-to row keeps a quiet wash when the keyboard leaves, the trees' own selection manners), and the row under the editor's cursor stays highlighted and followed. Under each statement sit the OBJECTS it touches, dictionary-confirmed only (the structured spots plus function calls, resolved under the connection's search path), wearing the Objects tree's type icons and its label shape (the dimmed type prefixing the name); a click finds the name where the script wrote it (selection, nothing opens), Enter or a double click opens its DDL, the right-click menu carries the editor's own vocabulary (Go to Definition, Peek Definition, Select All Occurrences, Reveal in Objects Tree, Copy Name; statement rows answer with Select, Run and Copy Statement), a chevron unfolds them per statement (ArrowLeft/Right on the row do the same; they start collapsed, so a long script reads as its commands first), and one name is one node, whatever the overload count. Revealing the Outline fetches the snapshot exactly like revealing the Objects pane, so the nodes need no detour through it. The tab joins the sidebar family fully: its own visibility checkbox and side in the strip menu and settings (it ships pinned right, beside the database panes), Cmd/Ctrl+Shift+O, the dock stacking, and per-workspace session like every other tab.

  • Statement marks: the run guard's verdict, visible before the run. A thin bar beside the gutter colors each statement by what it would do: violet for a schema change, amber for a data change, red for a dangerous one (TRUNCATE, or UPDATE/DELETE with no top-level WHERE), muted for opaque code (DO, CALL, EXECUTE); hovering the bar says it in words. Reads wear nothing, so the marks stay a signal instead of a wash. Editable SQL buffers only, both themes, and togglable like the other editor view switches: the editor.statementMarks setting, the toolbar's right-click menu and the command palette.

  • Standalone comment clusters are their own blocks in the editor. A run of comments separated from the code by a blank line (a file header, a commented-out region) folds as one block, wears no statement numbers (the bare dot, like the gaps) and carries no Select lens: it is not a command. A comment touching a statement's first line is its docstring and stays part of that statement's block, folding, counting and selecting with it.

  • Two sidebar panels can share a dock, stacked with a splitter. Visual Studio's dock column: right-click a side tab and Show X Below Y puts that panel under the dock's current one, both on screen, the splitter between them dragging their share (double-click rebalances); the clicked tab is the SUBJECT of the gesture (testing read it the other way around until it hurt): every side tab, shown or hidden, carries Show Above and Show Below submenus listing the panes currently on screen as destinations, either dock's, and the pick sends the clicked tab there, crossing docks in the same gesture when it must; a stack member repositions the same way and leaves through Separate Panes, and an anchor whose dock has no room for one more pane (the splitter's ~100px floor) simply is not offered, so a short window stops offering rather than producing slivers. The group is PERSISTENT, a true unified tab: clicking another side tab shows that pane alone and the group survives unseen, coming back whole through its own tab (testing caught the earlier model dissolving the group on the first stray click). In the strip the group reads as ONE tab: the member icons alone, in pane order, labels ignored, the selected member's icon at full strength and the rest dimmed; a click on an icon selects that pane, a click on the selected one collapses the dock like any active tab, and right-click keeps every member's own menu. The anchors on offer are EVERY other tab, hidden ones included: picking a hidden anchor brings it on screen first (alone or with its group) and the subject lands beside it. The same tab menu opens on a right-click of any pane's header, so the arrangement gestures live where the pane is looked at, not only on the strip. The strip menu also slimmed down along the way: Move Sidebar Right/Left left it (the per-tab Move entries cover every arrangement, and the sidebar.position setting stays reachable through the settings pair), following the theme toggle out, and the Objects tab gained the visibility checkbox the other three always had (it used to be unhideable outright; now it merely refuses to be the last light turned off: with every tab switched off, Objects comes back); Unsplit on a stacked tab takes it back out, and clicking a tab not in the stack replaces the active pane exactly as before, so nothing changes for anyone who never splits. Every stacked tab lights up in the strip, the arrangement and the split sizes restore with the workspace, and the sidebar follow, Objects-on-connect and the browser pin all prefer emphasizing a pane that is already on screen over replacing anything. A stacked pane's own ⋯ menu leads with its Unsplit (the strip's menu carries the same move, twin doors), the strip tells the emphasized pane from the other members (every member wears the wash, the one holding the keyboard the stripe, and screen readers hear the strip as multiselectable), the strip's menu opened from the keyboard anchors beside the focused tab instead of the window corner, and no pane can be dragged below about a hundred pixels whatever the dock's height. The model takes any number of panes; the menu is the deliberate gesture, and dragging a tab into the stack was considered and declined (docs/BACKLOG.md).

  • A script folds by its statements, and can number by them. The editor now parses the buffer's statement structure as it changes (one shared parse per edit, feeding every consumer) and offers each multi-line statement as a folding region, so a long INSERT collapses to its first line exactly as a folder view's object does; folded state stays with the tab like the rest of the editor view state. And the Editor line numbers setting grows two levels beside the script's own: per STATEMENT, each counting from 1, which is the numbering PostgreSQL's own LINE n uses in an error message, since a run sends the statement alone (the gaps between statements carry no number), and both side by side, the new default: script·statement, a gap line ending on the bare dot with the columns held still. And every statement of an editable SQL buffer (scripts, workspace files, SQL actions; read-only viewers and the folder view stay as they are) wears a Select lens above its first line: one click puts exactly that statement under the selection, and Run executes exactly the selection. The settings JSON editor picks up indentation folding along the way.

  • The editor toolbar arranges by groups. Comments are their own group now, apart from the selections (what a group is decides what tears off, docks and hides as one). The bar's right-click menu (Choose Toolbar Groups) checks whole groups on and off instead of listing every button, and the single buttons moved to the GROUP's own menu, a right-click anywhere on the cluster (or its floating toolbar), not only on the grip, next to its Float, Move and Hide entries. And a dragged group now shows a caret at the exact slot a release would dock it into, computed once for both the caret and the drop so they cannot disagree; the drop lands at that slot even when hidden or floating groups sit in the layout list.

  • Keywords answer Go to and Peek Definition, like objects do. The language's own ladder: the keyword hover keeps its excerpt (the purpose line and the synopsis head), Peek Definition (Alt+F12) on any keyword opens the statement's command's WHOLE synopsis grammar in the inline frame, the full SELECT included, which never fit a hover, and Go to Definition (F12, Cmd/Ctrl+click) opens the command's REFERENCE VIEW: a document tab of its own (the keyword's answer to the object's DDL viewer) wearing the same read-only code face as the peek, the purpose line and the versioned URL as comments above the whole grammar; the URL is clickable and is the deliberate way to the page's prose, a keyboard gesture never launches the browser on its own. The view restores with the session, keeps its scroll and stands in the Go Back trail like every other document view. The dictionary is asked first, so an unreserved keyword that names one of your objects keeps its object definition, and the Keyword reference hover switch governs all of it: off still means no postgresql.org fetches. The server now caches the whole synopsis (the hover clips client side); pages cached by earlier builds refetch once and the offline fallback still serves them.

  • Accepting a function or procedure suggestion inserts a runnable call. Every argument is a tab stop holding a valid value literal for its type ('', 0, false, '{}', now(), array[]::text[], 'empty'::int4range, NULL::enum_type where nothing better exists), selected as the cursor lands on it, so what remains is typing values; the old placeholders were the parameter NAMES, which made every accepted call invalid until each was replaced. The positional row inserts only the REQUIRED arguments (the snapshot now records how many trailing arguments carry a DEFAULT, which is exactly what a call may omit), and a second row inserts named notation with every argument, _execution_id => '', marked "named" in the label; a routine with unnamed or VARIADIC arguments offers the positional row alone. Every OVERLOAD is now its own row too, disambiguated by its signature in the label, where before only the first variant was reachable. Snapshots from before this refresh on their next build; until then every argument reads as required.

  • The long settings categories read in subsections. Editor (Run, Text, Display, Hovers, Toolbar), Appearance (Application, Sidebar, Tabs) and Connections (Discovery, Safety, Availability, Tree, Session) each carry small subheadings on the Settings page; a search thins the headings with the rows, deep links and the section nav are untouched, and the short categories stay as they were.

  • The editor's view switches, one gesture from where you sit. The toolbar's right-click menu now carries Word Wrap, Minimap, Line Numbers, Whitespace Characters, Hovers and the Keyword Reference Hover as checked rows next to the settings links it already had, and the command palette gets the same six as single commands plus Editor Font Size: Bigger, Smaller and Reset. Toggle Word Wrap rides VS Code's Alt+Z, configurable in Keyboard settings and listed in the cheatsheet. Every one is a real settings write (workspace scope when one is open, like every pane menu), so the Settings row shows it as modified with the reset one click away.

  • Picking a connection for a tab shows the Objects tree. The moment a tab gets its database, the Connections tree's job is done and the schema is the result worth looking at, so the dock that owns the Objects pane turns to it. Politely: only when that dock was showing Connections (any other pane keeps your arrangement), only the active side tab changes (a collapsed dock stays collapsed, focus never moves), only on the pick itself (never when the dial or the snapshot answers later), and a browser pinned to a different connection skips the turn. A new Appearance setting, Show Objects when a tab connects, turns it off, from Settings or the sidebar tabs' right-click menu, next to the follow and reveal switches it joins.

  • Every per-connection policy now has its workspace and global default. Four new settings under Connections: the production guard's two axes (structure changes, data changes), the change log and the commit gate. One inheritance chain everywhere, most specific explicit value winning: global settings, then workspace, then the connection's group, then the connection. Every control in the connection and group editors says what it inherits and from where, with Open workspace settings and Open global settings one click away (and Edit in group where the group decides), the Check availability control's manners on every row.

  • Transactions carry a chronicle and a commit gate, per connection. Two new connection properties, beside the production guard in the editor's Properties (with the same group inheritance: the stricter of the connection's word and its group's wins). "Log structure and data changes" writes an INFO entry to the Log for every run that changed structure or data, the SQL riding as the entry's code the way an action's run is recorded, with counts read from PostgreSQL's own command tags (what HAPPENED, never a guess): under transaction only (the default), always, or off. "Confirm commit" (on by default) asks before a COMMIT makes the staged work permanent, with what the transaction counted since BEGIN: X structure changes, Y data changes and their rows, grid edits included; a transaction that counted nothing commits without a word, declining runs nothing, and a COMMIT inside a string, a dollar-quoted body or COMMIT PREPARED never asks. The counters ride the session, so a reload does not lose what an open transaction staged.

  • Plain SQL files and scripts carry @description and @color too. Any .sql file in the Explorer or the script stores (and any plain .run file) may open with the presentation pair in its first comment: the description shows under the row (the Explorer's own Descriptions switch, searched by the tree's find) and the color tints the row's mark and name and the file's tab, where it wins over the connection's tint exactly as an action's does. The reading is deliberately lenient, unlike an action's: only the two names are recognized, everything else in a comment stays prose, and nothing unknown is ever flagged, so files carrying other tools' annotations look exactly as they always did. The editor completes, swatches and explains exactly the pair in a plain file's header; while a file is open its tab and its tree row follow the buffer live (the Actions pane's rows follow their open action the same way), a save re-lists the pane for everything else, and a new script starts with a short inert note teaching the pair, from every door that makes one (delete it and nothing is lost).

  • An action can wear a color. @color red in an action's header (either kind of file) colors every surface that stands for the action: its row in the Actions pane (the lettermark and the name), its title bar button (a chip: tinted words over a wash of the same color), its tab (the connection tint's own grammar, a wash plus a top stripe, the glyph and the status bar's tab readout included, and a hairline carrying the color across the content while the tab is active), and the icon in its result header. On an action tab the action's color wins over the connection's tab tint; without @color the connection's tint stays, exactly as before. The value is one of ten names (blue, cyan, teal, green, olive, amber, orange, red, pink, lavender), painted with the connection palette's hues so both themes keep reading; a name outside the vocabulary is reported and the action keeps the ordinary chrome. The editor completes the names behind swatches of their paint, the finished line wears a swatch of its own, and clicking it opens a color picker whose every choice lands as the nearest of the ten names, so the file always holds a word the format knows. A color is a signal, never a guard: the annotation that stops a destructive run stays @confirm. Five starters arrive colored as working examples: the restore red, the two backups green, current activity blue and blocking locks amber.

  • Settings opened from a link show only the section that was asked for. Following "Open workspace settings" from a connection's Snapshot section, the Log panel's gear, the pane menus' settings entries and every other deep link used to land on the full page, scrolled to the section, with every other option crowding around the few that were meant. The page now opens FOCUSED: only that section is shown, a strip under the toolbar says so ("Only Snapshot is shown") with Show all settings one click away, and the category nav switches sections instead of scrolling while the focus holds. Opening Settings plainly (the menu, the shortcut, the palette) always shows the whole page, and typing in the search leaves the focused mode, so one narrowing rules at a time. The focus rides the session like the rest of the page's state. The connection editor answers its own deep link the same way: the Objects pane's Open Snapshot Settings opens the editor on the Snapshot section alone, with the same strip and the same nav-as-switcher. And settings entries in menus now come as a scope pair everywhere: the Actions pane and the appearance menu gained Workspace/Global variants, the shape the Explorer and the connections tree already had.

  • The starter actions seed into a group of their own. A fresh installation's fourteen starters now land under Starters, split by kind into Queries and Commands (folders are groups), so the root of the Actions pane belongs to the user's own actions from day one. Restore Starter Actions writes the missing ones back into that group, directories included. Installations that already have their actions are untouched, as always: the seed never fires on a populated directory, and a restore never overwrites.

  • @var: name a value once. A command action can now declare @var tree={out}/{db}-objects and say {tree} in the command, the @confirm and the alerts, so the dialogs can never drift from what the command actually does; the value may use PlumeSQL's placeholders and the file's inputs, never another @var and never {password} (a name that hid the secret would carry it into the command line), and a declared name nothing reads is reported. Without = the same annotation ASKS: @var jobs How many parallel jobs? opens a one-line input box before the run and the typed text becomes {jobs}, visible where the file uses it, which the box says beside its question. The shipped backup, restore-side dumps and object tree starters now name their output once this way. Specified in ACTIONSPEC.md 8.8; the editor completes and hovers it like every other annotation.

  • The shipped command actions use the new annotations. Backup asks before it runs, naming the exact file and its type ("This writes mydb-....dump, a custom-format pg_dump archive for pg_restore"), and reports with a titled @alert-on-done, honest about either outcome; the schema dump dropped its success alert, because the file it writes already opens in a tab and a dialog over it repeated what was on screen. A @confirm's placeholders now fill in on the page before anything runs, which is what lets a question name the file it is about to write; the password is never among those values.

  • @file: a command action can ask for a file. A restore cannot know its backup's name in advance, so @file dump Pick the backup to restore opens a picker over the workspace's files (newest first, typing filters) before anything runs, and the picked path becomes {dump} in the command, the @env values, the completion hooks and the @confirm, which can therefore name the very file about to be restored. Cancelling the picker runs nothing, exactly like declining a confirmation; a name PlumeSQL already fills cannot be shadowed, and the value travels as one argv element like every other, never through a shell.

  • Three more starter actions. Restore (custom-format pg_restore of a picked backup, asking first because --clean drops what it recreates), the backup's table of contents (pg_restore --list, no question because it only reads, no connection needed at all), and the server's roles and tablespaces (pg_dumpall --globals-only into a dated SQL file that opens on success), which no per-database backup carries.

  • The command line of a .run action explains itself. A curated registry of the PostgreSQL client tools (psql, pg_dump, pg_restore, pg_dumpall, pg_isready, vacuumdb, reindexdb, createdb, dropdb, and plumesql dump-tree) feeds the editor: tool names complete at the head of the command, the tool's flags complete once a dash is typed, enumerable values complete after =, and hovering a flag explains it with a link to the tool's PostgreSQL documentation page, versioned to the resolved tool's own release. Hovering the tool's own word shows what would ACTUALLY run: the binary the tool matrix picked and why, and the command line with the connection's values filled in, the password never among them; a refusal shows as the answer. Hovering a {placeholder} anywhere now also shows its live value on the pane's connection.

  • plumesql dump-tree, and a starter that runs it: the database as a directory tree. The new CLI subcommand writes every schema object into <out>/<schema>/<type>/<name>.sql, byte for byte as pg_restore prints it: one custom-format schema-only pg_dump, one pg_restore, and a cut at the TOC headers the tool itself writes between objects. Nothing is reformatted and nothing is filtered; the connection comes from the PG* environment variables like every other PostgreSQL tool, and the tools are picked to suit the server (one closed-immediately version probe, documented in docs/INTERNALSQL.md). The dump-objects starter action asks where the tree goes and runs it; plumesql as an action's command resolves to the running binary itself, so the desktop app needs no PATH entry.

  • @dir: the directory input. The sister of @file: a picker over the workspace's directories (the root first), the picked path becoming {name} under the same rules, both annotations declaring into one shared name space. A New directory... row names one that does not exist yet: the typed path is asked about and created only on the user's yes, logged, and never in silence; declining creates nothing and runs nothing.

  • A command that finishes off screen says so. When a command action's process exits while its tab is not the active one, the Log gets the exit code and duration, the corner notification shows it, and the entry carries a Show the run button that brings the tab forward, on its result face. A run watched to its end writes nothing: the terminal already says it. The entry goes through the same switch as the run's own record (actions.logCommand).

  • A confirm or an alert can title its own dialog. @confirm and the three @alert-on-* annotations accept their whole value as two quoted parameters, "<title>", "<text>"; anything not matching that complete shape stays one verbatim parameter, exactly as before, so no existing file changes meaning and nothing needs escaping. A button's $column fills into the title like into the question, alert titles expand server side like their texts, and a one-parameter value keeps today's title, the action's or button's own name.

  • Completion hooks for every outcome, not only success.@open-on-success and @alert-on-success grew into two families of three: -on-success (exit 0), -on-failure (nonzero, for the log a tool writes when it fails or a message of the file's own) and the outcome-blind -on-done, which alerts on any completion and opens its file only if it exists, staying silent when it does not. Within a family the specific members may stand together, each naming its own outcome; combining -on-done with either is reported and the specific ones win. All six expand server side, exactly like the two they grew from.

  • The comment annotation format has a specification: docs/ACTIONSPEC.md. The whole definition (the annotation syntax as a general carrier, the action vocabulary over it: the grammar, every annotation and placeholder, the dependencies between annotations, the execution and security requirements) now lives in a standalone RFC-style document written so that anyone can implement the format without PlumeSQL in the room. The syntax names NpgsqlRest's comment annotations as the sibling dialect a future revision intends to unify with. docs/ACTIONS.md stays the product document: how PlumeSQL's pane, buttons, run model and Log surround the format, referencing the specification instead of restating it. The registry drift test now holds the specification's table to the code.

  • Command actions: the other kind of action, which runs a program instead of asking a database. A .run file, a header of # comment lines and one command, answered by a terminal in the same tab a query action answers with a grid. Everything around them is the same: the same two stores, the same tree, the same pins, the same title bar buttons, the same annotation registry, the same tab with two faces. It is executed DIRECTLY, never handed to a shell: the line is split into argv the way a shell splits words, and the program is started on a PTY with that argv. Nothing a shell does beyond splitting happens, so a database called a b; rm -rf / is one argument, and there is nothing to quote and nothing to escape. A long command breaks across lines with a trailing backslash. Four annotations of its own: @env NAME=value for the environment the program runs in, @open-on-success <path> to open what it wrote, @alert-on-success <text> for an outcome the terminal does not make obvious, and the @description and @toolbar both kinds share. The two kinds are told apart before a name is read: a command action's row wears its RUN lettermark in green where a saved query's SQL is in the accent, and the Actions pane's menu gains Sort by Kind (queries first, commands after, each half alphabetical) beside Sort by Name and Sort by Modified. Opening one shows its SOURCE rather than an empty terminal: selecting a row must not start a program, and the file is the thing that says what will run. Until it runs it has only that one face: no "not run yet" panel and no Result button, because there is nothing to show and nowhere to go. Running it is a gesture that says so: Run in the tab's toolbar, Run in the row menu, its own @toolbar button or F5; the tab becomes the terminal the moment it starts, Result appears with it, and coming back to the row later shows that run. A command that needs a connection and has none refuses and says so in the Log instead of running with an empty {db}. A PostgreSQL tool is resolved to a version that suits the connection (the oldest installation still new enough, since no tool works reliably against a server newer than itself); anything else is looked up on PATH. When nothing installed is new enough the run is refused, and the refusal names the version to install, in the Log. Three starters ship with it: an interactive psql, a custom-format backup and a schema-only dump that opens what it wrote.

  • Placeholders: what PlumeSQL already knows, written into an action.{host}, {port}, {db}, {user}, {sslmode}, {conn}, {server} and the clock ({date}, {time}, {timestamp}, and each part on its own). They work in both kinds: in a command each becomes one argv element, and in a query each becomes a BOUND parameter beside the statement, never text pasted into it. Braces are read only where code is, so an array literal, a JSON body and a regular expression's {2,3} are left exactly as written, and a name nobody knows is reported on its own line. Typing { offers them all, each saying where it comes from, and hovering one explains it. {password} may be spent in a command action's @env line and NOWHERE else: not in an argument list, which every process on this machine can read; not in SQL; not in a file name. The page never has it at all. The value is expanded by the server, in the process that starts the program, and what comes back is the command that ran, secret absent by construction.

  • The Actions pane never says "file". What it makes is an action: the root rows' own buttons said New File and made an empty one, which is the Explorer's gesture in a pane that has no files in it. New Action writes the template, New Group makes a group, and the failure message when either cannot be written says the same words.

  • Actions come in two stores now, the Explorer's arrangement exactly. The installation's own (there whatever directory PlumeSQL opened) and this workspace's (which come back with the project wherever it is opened from, kept in the global dir keyed by the workspace, so the opened directory is never written to). The pane is one tree with a root row for each and the Frequent shelf above them, and the pane menu switches all three the way the Explorer's menu switches its own roots. New Action makes one in the store you point at, from a root row, a group row or the pane menu, which names both. An action's IDENTITY became its virtual path (~actions/x.sql, ~wsactions/x.sql), so the same name in both stores is two actions with two tabs, two pins, two title bar buttons and two results; only the file IO resolves it back to a store and a name. The Go to picker names the store beside the path for the same reason.

  • CI runs on demand too (development only). The ci workflow answers workflow_dispatch as well as a push, which the schema matrix has done all along. An Actions outage swallowed a push event on 2026-08-06 and left a commit on main whose pipeline could not be started at all: a workflow with only push and pull_request triggers cannot be dispatched, and there is no run to re-run either.

  • Actions: saved questions you run from the sidebar and get a live grid back from. A new side pane, its own connection (the object browser's, unless it is pinned somewhere else), and a click on a row that shows the ANSWER rather than the code. Edit is in the row menu, one click away, because reading is what a saved question is for. An action IS a .sql file, and that decision carries the feature. Groups are folders, so rename, delete, copy, cut and paste, find, the size metrics, the whole tree and dragging a row into a folder (or onto the blank space, which files it back at the top) all work from the first day; the behaviour is declared in ordinary SQL comments, so the file greps like SQL and diffs like SQL. There is no action format to migrate, no index to keep in step with a directory. The file is a COMMAND with a header above it, then one more command per button, each under a header of its own. That shape is chosen for the EDITOR: a button's command is real SQL in the buffer rather than a string inside a comment, so it colours, completes and folds like the SQL it is, and it may run to several lines. Nothing in the syntax is quoted either, since every annotation takes the rest of its line, which removes the whole class of escaping rules a quoted argument needs and the mistakes they cause. The grid is the results pane's own, and so is the machinery behind it: an action's answer goes through the same drain, the same spool and the same paged reads a query's does. The first window paints while the server is still draining, a million rows scroll off a file on disk instead of through the page, and nothing about it is a second implementation. The one thing an action does not have is a session, so the drain was written against a small interface a session and a one-shot run both satisfy, and the RESULT outlives the connection that fetched it in a store keyed by the action's path: it is dropped when the action re-runs, when its tab closes, on shutdown, or by a sweep that catches a window closed without a word. Eight annotations, each belonging to one of the two headers, which the parser and the editor both know. Above the ACTION: @description gives the tab its line of text and sits under the row in the tree, dim and italic, exactly as a connection's description and an object's comment sit under theirs (the Actions pane menu turns them off, tree.descriptionLength cuts them); @refresh 3s re-runs it on a timer, but only while its tab is the active one and the window is not hidden, since nobody is reading a background tab and the database should not be asked; @hide keeps a column out of the grid while leaving it selected, for the one case that needs it (a $column a button reads takes its value from the result set, so it has to be there). A column nothing uses is a column to leave out of the query, and no shipped action hides anything. Above a BUTTON: @button <label> turns the command below it into a button on every row; @confirm <text> asks first, with $column filled in when the question is asked; @at start|end|before <column>|after <column> says where in the grid it sits, since the leftmost place is not always where the eye is. The two edges stay pinned while the grid scrolls sideways and a place named after a column travels with it; buttons asking for the same place share a column, and one naming a column the action does not show falls back to the start and says so in the Log, once, with the name it got wrong. @on <column> says the other thing about a button's place: that an EXISTING column is the button. Its cells become clickable and keep their values, so reading the value and acting on it happen in one place instead of hiding the column and labelling a button after it. Only the value is the target, so the rest of the cell still selects, drags and copies like any other; the arrows walk in and Enter presses it; the label stops being text nobody sees and names the ACT instead, in the confirmation's title, the Log's record and the cell's tooltip. A column carries one button, @at says nothing beside it, and a column the action does not show (never selected, or hidden) leaves that button a column of its own, reported once like every other place that cannot be found. @open <label> is the other kind of button, and it turns an action from one question into a small report to walk through. Where @button acts and reports, @open ASKS: the command below it runs with the clicked row's values and its answer replaces the grid, one step deeper into the same tab. Databases, then what runs on one of them, then the end of what runs. Nothing about it is a second grammar: @on, @at and @confirm mean there what they mean on a @button, the values travel bound the same way, and the answer goes through the same drain, spool and paging every other result does (one statement through the extended protocol, which the drain grew without a second copy of itself). A button belongs wherever the columns it NAMES are, so the cancel button written for a list of backends stays away from the list of databases and appears by itself the moment a navigation lands on something with a pid. That is also where the Log stops checking: in a file with no @open, a button whose columns are missing cannot run and is reported; once a file navigates, the same absence is the file working as intended. The header grows ‹ › and a trail the moment the tab has been somewhere; every crumb is a way back, and taking a different turn drops what was ahead. A crumb says which ROW it came from as well as what the file called the step (table-sizes › indexes data_points), because otherwise the same word names every visit and a trail that cannot tell two of them apart is decoration; the value is the @on column's, or the first one the command binds, with the whole of it in the tooltip. The combos are VS Code's own Back and Forward, deliberately not Alt+←, which belongs to the browser. Going back ASKS AGAIN, because an action dials, asks and closes: there is no held answer to return to, and one from ten minutes ago is not what belongs behind a Back button in a database client. Run again and the @refresh timer follow the same rule and re-ask the step on SCREEN. A navigation carries a chevron, on its cell and on its button, and an act does not: one click ends a stranger's transaction and the other one just looks around, so they must never read the same. The connection an action needs is asked for on the user's GESTURE and never on its own: a restored action tab with no connection waits with its line and its button instead of opening a picker over a window that has just come up (startup opens nothing the user did not leave open and steals no focus), and clicking the row of a tab that is already open asks again, because that click is the same request as the first one. Two things that look alike are kept apart, and the editor's completions teach it: an annotation's ARGUMENT names a column (@hide pid, @on pid, @at before pid), while $name inside TEXT is that column's value in the row that was clicked. The $ is what makes a value visible in free text, where a bare word would just be a word; an argument needs no such mark, because the whole argument is the name. Writing @on $pid is unmistakable rather than ambiguous, so the $ is dropped rather than refused. $column in a button's command is that row's value, and it travels as a query PARAMETER, never pasted into the text: a database client must not teach its users to build SQL out of data. What reaches the server is PostgreSQL's own $1..$n with the values bound beside it; the name is how the file says which value, and it is checked against what the action selects, so a typo is reported rather than silently binding the wrong column. References are read where CODE is, with the same scanner the editor uses: a $ inside a string, a comment or a dollar quoted body is left alone, which matters because PostgreSQL's own $do$ ... $do$ is made of dollars and identifiers. A label is free text, like every value here, so it carries spaces, apostrophes and quotes without a rule to learn. It is the same on every row, and the row's own values ride in the button's TOOLTIP instead: hovering shows the exact question the confirmation would ask, filled in, which reads better than a button whose width changes from row to row. Asking again is always at hand: Run again in the tab's header, and the run keybinding (F5 by default) with an action tab active, which used to answer "select what to run first" because it went looking for an editor that is not there. An action never borrows a tab session. It dials, asks and closes, so it cannot join an open transaction, cannot see uncommitted work it did not make, and leaves nothing behind. The result belongs to its tab, so switching away and back shows what was found instead of asking again; Run again is the gesture for asking twice. An annotation PlumeSQL cannot read is reported in the Log with the line it is on, never passed over in silence, because an annotation with a typo that quietly does nothing is the worst thing this parser could produce. Once, though, not once per run: an action on a refresh timer would otherwise write the same line into the Log all day, so only a change is news. A $1 written by hand in a row action is reported too: the only values a button sends are the ones its $column references name, so a positional parameter has nothing behind it, and it used to collide with the generated numbering and quietly feed one value to two places. A byte order mark, which Windows editors put at the start of a file, no longer eats the whole header: it is stripped before the file is read and before its SQL is sent, both of which used to fail without a word (the first line no longer began with --, and PostgreSQL refused a statement starting with an invisible character). An action that cannot reach its database says so as a CONNECTION failure, like every other dial: the availability dot, the sidebar and the object browser hear about it instead of the failure stopping at the action's own pane. A NEW action starts as the shortest thing that RUNS (a description, a command, a timer) under a /* */ header carrying the whole specification: the shape of a file, how an annotation is written, and every annotation there is with its syntax and one line about it, grouped by the header it belongs above. The header is GENERATED from the registry, so the two cannot drift: an annotation added there appears in the next new action, in its group, with its own line, and a test holds it. Every annotation in it sits behind a dash, because an annotation is read only when it opens its line: the dash is what keeps the header prose instead of a stack of broken declarations, and the text says so, which teaches the rule by obeying it. The Actions pane has the toolbar every other pane has, and lost the search box nothing else had. Where the Explorer reads "0 scripts · 419 files" and the object browser "refreshed just now · 1.2k objects", this one reads "8 actions · 2 groups", and the magnifier beside it opens the same floating find window the other trees open, with the same toggles, the same "1 of n" counter and arrows, remembered per workspace and restored without stealing focus. An inline filter box in one pane while every other pane searched from a window was the kind of difference a user has to learn for no reason. Ctrl+F in the tree opens it too, as it already did everywhere else, and the box it used to drop into is gone. No filter button here: a bar carries the actions its pane HAS, and an action tree has nothing to filter by. The find itself grew up with the move. It searches an action's @description as well as its name, which is the whole reason the description is on the row, and it LIGHTS UP what it matched, in the name and in the description, wearing the editor's own find highlight exactly as the object tree's has always done. A description turned off shows whole while a match is lit in it, because a hit nobody can see is a hit that failed to answer the question. The highlight was object-tree-only until now, so the Explorer's find lit nothing either; it does now. The Actions pane carries no numbers on its rows, and owns its own order. It used to show a file SIZE on every row, which is the least interesting fact about a saved query (they are all small, and the row already says what it answers), and it showed it because it silently read the EXPLORER's settings: switching file statistics off in the Explorer changed the Actions pane, and ordering the Explorer by size reordered the actions by how many bytes they take. The rows are clean now, and actions.sortOrder is the pane's own, switched from its own menu in the words the other trees use (Sort by Name, Sort by Modified). There is no Sort by Size in this pane, for the same reason no size is on the rows. The editor helps with the headers. Typing @ above a command completes the annotations that belong THERE (the action's own header offers four, a button's offers five), each with the syntax and the explanation from the same registry the parser reads, so the list cannot offer something the parser would refuse; @refresh completes intervals, and @hide, @on and @at before|after complete with the COLUMNS that action's last run returned. Only above a command, because a comment anywhere else declares nothing, and only in an action's own editor: a query tab is not offered any of it. The commands themselves are ordinary SQL in the buffer, so they keep everything the editor already does for SQL, version-matched to the connection the action RUNS on rather than to the tab's own (an action file tab has none, so it used to get the newest grammar PlumeSQL knows: MERGE lit up as a keyword even for an action pinned to a 13). The annotations are COLOURED: an action's editor uses its own flavour of the SQL grammar, one that tokenizes @name inside a comment instead of swallowing the line whole. A flavour and not a change to the grammar, because a plain script that says -- @param must not light up as if PlumeSQL knew what that meant. They keep the comment's italic, so what colours them says "structure" while the italic still says "the server ignores this". What the parser cannot read is MARKED where it is written, and every annotation explains itself on hover: both are the parser's own answers, so the editor cannot say something the file does not mean. Hovering a squiggle gives the message first and the annotation's documentation under it. That pair costs Monaco's hover contribution, 170 KB of the bundle, measured and taken on purpose (docs/WEIGHT.md): a squiggle whose message nobody can read would be worse than no squiggle. The Log still carries the same problems when the action RUNS; the squiggle is for the writing, the Log for the running. A fresh installation is seeded with seven (top queries among them, which needs pg_stat_statements and says so in its own header), and six of them NAVIGATE, so the feature is in front of a new user rather than only in the documentation: current activity, whose pid opens the locks that backend holds while cancel and terminate stay labelled buttons beside it (one file showing both kinds); blocking locks, whose blocker opens everything waiting behind it, which is the size of the problem; table sizes, whose table opens its indexes; unused indexes, whose table opens its write traffic, because an unused index is paid for on writes and not on reads; vacuum candidates, whose table opens whoever is holding a transaction open on it, which is the usual reason dead rows will not go away; and non-default settings, whose name opens what the setting is, where the value came from and the shipped default it was changed from. Four different shapes of answer between them (a list, a list of what is waiting, a list of indexes, and counters standing up one per row), so the six teach more than one trick. Unused indexes also stopped suggesting indexes that back a constraint or carry the replica identity: dropping one of those takes the constraint or the replication with it, however few reads it has. Each runs unchanged on every PostgreSQL release PlumeSQL supports, 13 through 18, and the shipped NAVIGATIONS are executed by the same test, with their values bound, so the half nobody sees until they click cannot ship broken either. They are ordinary files: edit them, rename them, delete them. The seed fires once per installation and only into an empty actions directory, so a deleted starter never comes back and an edited one is never overwritten. The way back is Restore Starter Actions in the Actions pane menu: it writes the ones that are missing by name, never touches a file that is there, and says in the Log what it wrote or that nothing was missing. A default you cannot restore is a default you cannot safely try deleting.

  • Go Back and Go Forward, VS Code's editor navigation. PlumeSQL now keeps a location history: every tab switch and every larger cursor jump inside an editor becomes a point on the trail, and going back restores both the tab and the spot in it. Two arrow buttons sit left of the workspace title, where VS Code keeps them, greyed when there is nowhere to go; the combos are VS Code's own (Ctrl+- and Ctrl+Shift+- on macOS, Alt+Left/Alt+Right plus Linux's Ctrl+Alt+- family elsewhere), configurable like the rest of the keymap, and the command palette carries both as PlumeSQL: Go Back and PlumeSQL: Go Forward. Each arrow's tooltip names where it would land (the file and its line). The trail remembers FILES, not tabs: going back to a file whose tab has closed reopens the file, and the file-backed points persist in the workspace session, so the trail survives a restart. Only a location with no file behind it (Settings, a connection editor) leaves the trail with its tab.

  • The layout controls: arrangements and postures. VS Code's own four buttons at the title row's right: the three visibility toggles (left sidebar, bottom panel, right sidebar; the filled region says at a glance what is on screen, and a dock with no tabs has no button), and the Layout menu, whose entries change the whole layout in one gesture. The ARRANGEMENTS say which dock hosts which side tab: Everything Left (the classic single sidebar), Everything Right, and Split: Files Left, Database Right (Explorer and Actions beside Connections and Objects); they batch-write the same per-tab side settings the strip's right-click "Move to ..." items write, at the same workspace scope, and arbitrary combinations stay on that right-click, one tab at a time. The POSTURES change the geometry: Write clears the room for the editor (big split, docks and panel away), Inspect clears it for the results, and Reset Layout puts every geometry back to its first-run default, the way back after any amount of dragging (it deliberately leaves the arrangement alone: that is a setting, the user's explicit choice). The menu also carries the three visibility toggles (left sidebar, bottom panel, right sidebar) with their combos and a checkmark for what is on screen, and everything is in the palette too (PlumeSQL: Layout: ..., PlumeSQL: Reset Layout, PlumeSQL: Toggle Left/Right Sidebar). Double-clicking the editor/results sash steps the split through the postures' balances, the fast lane past pixel-dragging. With this comes the missing shortcut: ⌥⌘B / Ctrl+Alt+B toggles the secondary dock (the one ⌘B does not target), VS Code's Toggle Secondary Side Bar.

  • The command palette remembers what you use. VS Code's own behavior: the empty palette opens with a "recently used" section on top, most recent first, and "other commands" below in their usual order; typing filters everything, the recently used still leading. The history keys on stable command ids (labels carry live keybinding hints and change), is capped at 50 and persists with the workspace session.

  • @confirm guards the whole action now, not only a button. Written above the action's own command, in either kind of file, it asks its question before anything runs ("This truncates staging. Continue?"), on every gesture: Run, the row's click, the title bar button, the mount's own ask. A @refresh timer re-asks silently, since the gesture that started it already answered. Above a button it keeps meaning what it always did, and the registry, the completions, the validation, the new-file templates and docs/ACTIONS.md all say so from the same data.

  • An Actions section in Settings. The actions settings move out of Objects into a section of their own, and it grows the behavior switches: Clicking a saved query (run, the default and what a click has always meant, or open, showing the file with Run as its own gesture), Clicking a saved command (open, the careful default, or run, starting the program on the click; a @confirm in the file asks first either way), and Log what query/command actions run, the two switches over the run chronology, both on by default. The pane's sort order and display switches live there now too, still switchable from the pane menu, the same setting behind both; the pane menu ends with Open Actions Settings, the way the Log's cog leads to its own section. And the Actions tree's background right-click now opens the Actions pane's own menu: it used to open the Explorer's, root toggles of the wrong pane and all.

  • The Log shows an entry's code as code. The statement or command a log entry records now rides the entry as its own block: mono, in a quiet box, with a Copy code action of its own (in the row and in the right-click menu) that copies the code without the prose around it. A long body clamps to a few lines with More to open it, and wide lines scroll inside the box, so a long command can never swallow the panel; the corner notification cards show only the entry's message, never the code.

  • Every command an action executes is chronology. A run writes the plainest head there is, <file> on <connection>, with what ran verbatim as the entry's code: the statement and the values bound beside it (a step's row values, the root's {placeholder} bindings), or a command action's command line as resolved by the tool matrix (secrets cannot appear: {password} lives in the environment). A button's act was already recorded with its SQL. All of it as info lines in the Log. The refresh timer's runs deliberately stay out: asking every few seconds is not news.

Changed ​

  • The hovers are yours to tune. Three new Editor settings, VS Code's own semantics: "Editor hovers" (off silences the mouse entirely, while the keyboard's Show Hover, ⌘K ⌘I, still answers on demand at the cursor — the quiet workflow, and the cheatsheet says so), "Editor hover delay" (300, 700 or 1200 ms; a longer rest is usually the better cure than off), and "Keyword reference hover" with a switch of its own, because it is the widest-reaching hover (nearly every word is a keyword) and the only one that talks to postgresql.org — off also stops those fetches, while the hovers on your own objects stay.

  • Go to Definition: F12 and Cmd/Ctrl+click jump to an object's DDL. VS Code's own gesture, on VS Code's own combo: hold Cmd (Ctrl elsewhere) and identifiers become links whose preview card shows the target's DDL; the click, or F12, or the context menu's Go to Definition opens the object's read-only DDL viewer in the preview slot, exactly as a click in the Objects tree would. Resolution is the dictionary's, the same rules the hover follows: a qualified name as itself, a bare one down the search path, an alias through the statement's FROM targets, a column to the relation that owns it. It works from a DDL viewer too, so a schema can be READ by jumping reference to reference. In a browser F12 belongs to the devtools (the cheatsheet says so); the click and the menu have no such rival.

  • Hovering a keyword links the official reference, versioned. Rest on any keyword and the hover names the statement's command and links its page of the PostgreSQL documentation, for the exact major the tab is connected to (falling back to /docs/current before the first contact): "where" inside a query links SELECT's page, "primary" inside a CREATE TABLE links CREATE TABLE's. The pages are a curated table (web/src/lib/pgdocs.ts), never guessed slugs, so a command the table does not know gets no link rather than a wrong one; an unreserved keyword that actually names one of your objects keeps its object hover. The link opens in the system browser through the app's docs door, never in the page itself. And the hover carries a taste of the page: the reference's own purpose line ("SELECT — retrieve rows from a table or view") and its synopsis grammar, fetched by the server (one page per hover at most, cached for the whole run, a failure retried after a minute), the synopsis capped at 18 lines. The first hover on a page never waits out a slow network: it shows the link at once and the excerpt joins from the cache on the next one. Offline changes nothing but the excerpt's absence.

  • The editor grew three PostgreSQL-fluent services, all fed by the object dictionary and costing no query. Signature help: typing inside a routine call shows its signature with the active parameter highlighted, overloads included, from ( through every comma. Hover: resting on any identifier tells its story, the same one the suggest details tell (type, schema, comment, the statistics with their age, a routine's attributes), with aliases resolved through the statement's FROM targets and columns through their relation; below the story stands the DDL itself, a syntax-colored excerpt of the snapshot file the dictionary points to, capped for the eye and cached per dictionary so a snapshot refresh can never leave a stale quote. A novel of a catalog comment is clipped the same way on every small surface (the suggest details, the hover, the signature help), at a word boundary with an ellipsis: a 4 KB comment used to grow the details pane to most of the window and push the statistics and the DDL below the hover's fold; the full text always lives in the DDL file's COMMENT ON. And the completions learned three more spots: after a cast's :: (and in a column definition) they offer types, the user's own composites, enums, ranges and domains leading and the built-in names behind; inside INSERT INTO t (...) and ON CONFLICT (...) they offer that table's columns; and (, , join . as trigger characters so the lists appear as the statement is typed, not only on demand.

  • The suggest details pane tells you what you are about to pick. It now opens by default beside the completion list (fold it and it stays folded for the session), and for the focused row it says what the app already knows, computed lazily and costing no query: the object's catalog comment; for a table or view the statistics the Objects tree collects (~rows, size on disk, seq/idx scans, dead rows), labelled with their age and governed by the same stats.show choice, a missing number staying missing, never zero; for a function or procedure its language, volatility, strictness, security and, when the server tracks functions, its call count and mean time.

  • SQL completions know where in the statement they stand. The suggestion list now follows the cursor's spot: after FROM, JOIN, INSERT INTO and UPDATE it offers relations and set-returning FUNCTIONS (never procedures); after CALL it offers procedures and nothing else, the one place they can run; in an expression spot (the select list, WHERE, ON, SET ...) the statement's OWN columns lead, read from its FROM targets and labeled with the table they come from, aliases included ("t." completes the columns of "from tests2 t"). Tables, views, materialized views, foreign tables, functions and procedures each wear their own glyph and say their type and schema in the row's detail. Accepting a function or procedure inserts the call whole: name, parentheses and a tab stop per parameter, with the full signature and return type shown beside the row. The spot detection is deliberately heuristic text analysis (web/src/lib/sqlcontext.ts, unit-tested): it feeds suggestions, where a wrong guess costs a keystroke, never correctness.

  • SQL completions read the object dictionary. The editor's suggestions for the user's own objects used to come from two introspection queries run once per connection per app run; they now come from the same object dictionary the Objects tree reads (docs/OBJECTINDEX.md), so they know more and cost nothing: every relation with its columns (typed, with comments), user functions and procedures with their signatures, every schema, resolved through the connection's real search_path exactly the way the server would. A qualified pg_catalog. completes the system catalogs (the shared catalog store is part of the dictionary), bare names stay the user's own objects. The dictionary tier is ordered rather than alphabetically mixed: the search path's tables and views lead (each row naming the schema it resolves to), then its routines, then the schemas, with keywords and built-in functions behind them; typing still filters and scores as ever. Completions refresh whenever the snapshot does (the fingerprint check, your own DDL) instead of once a run, and they work from the on-disk snapshot before the database is even reachable. The two retired queries are gone from docs/INTERNALSQL.md, and the MCP get_schema tool now reads the same dictionary instead of dialing.

  • The AI assistant is reduced to its configuration. The inline AI bar, the Fix with AI button and their generate/fix endpoints are removed; a rebuilt assistant will come back systematically. What stays is the System page's AI assistant section (the API key in the vault and the model choice), so nothing already configured is lost, and the MCP server (AI agents) is untouched.

  • The editor toolbar is the user's to arrange. The connection (Run and the readout) still leads and is always there; the editor tools behind it are now one registry drawn as four groups, Selection (with the comment toggles), Save with Undo and Redo, Clipboard, and Find and Replace. Each group opens with a grip, the floating windows' six-dot glyph: the separator you can hold. Drag it and the group tears off into a small floating toolbar over the editor; drop it back on the bar (which lights up to say so) and it docks exactly where it was let go, left cluster or right, in that spot. The grip's right-click menu does the same by name (Float, Move to the other side, Hide), Enter on a grip floats and docks without a mouse, and Dock All Toolbars in the bar's menu brings everything home. Which buttons show at all is a pick too: right-click the bar (or "..." > Choose Toolbar Buttons) and check buttons on and off; what is off waits behind "..." as a menu entry, one level down but never gone, so a rarely pressed button stops costing accidental clicks without losing its command. A Large Buttons switch (same menu, and the Editor settings) grows the whole bar and the floats for eyes the small icons strain. Tools dock left and Find and Replace right by default; the sides, the order and the float spots are global preferences (editor.toolbarLayout, editor.toolbarFloats), so the same toolbar meets you in every workspace and every file, while the button pick (editor.toolbarButtons) follows the workspace like every pane menu toggle.

  • A group is its members' default now, not their ceiling. The production guard used to escalate: the stricter of the group's and the member's policy won, and a member could not opt out. With the inheritance chain the member's explicit word wins, and the guard gained an explicit Allow for exactly that (Default inherits, Allow overrides a stricter default). Existing metas keep behaving the same until a member explicitly chooses something weaker than its group, which could not even be expressed before. Read only and the legacy confirm-writes flag stay forced, escalate-only, as before.

  • A pinned object browser shares the active tab's session when the tab runs on the pinned connection. The pin used to mean two things at once: "do not follow other tabs" (what pinning is for) and "never borrow a session" (a second, silent veto), so a browser pinned to the very connection you were working on quietly lost Refresh Through Open Transaction and the amber uncommitted marks, with nothing anywhere saying why. The pin now means only the first: pinned to the active tab's own connection, explicit refreshes and the through-transaction catch-up work exactly as in Automatic; pinned to another connection, nothing is borrowed, as before. The standing veto for reading inside transactions remains the "Refresh through open transactions" setting, global or per connection. And the pin is now WORN, not only written: the Objects and Actions pane headers show a pin mark beside the connection's name while pinned, the same mark pinned tree rows wear, with the tooltip spelling out what the mode does. The pickers' words caught up too: the browser's Automatic row now says "follow the active tab, share its session" (following alone undersold the half that matters), while the Actions pane's says "run on the active tab's connection", because an action never shares a session and the two rows must not promise the same thing. And while the sharing is LIVE, the Objects header wears an intact link beside the connection's name, the broken link's counterpart: one glance says the tree is reading through the tab's session, and the mark's absence on the Actions header is the honest difference, since an action always dials its own. A tab whose connection is assigned but not dialed yet wears the link DIMMED: the relationship already exists, the first run makes it live, and the session dot beside the elephant says which state it is in. The wash and stripe used to end at the tab's bottom edge; while such a tab is active, a two-pixel hairline in the same color now runs along the top of the editor and results area, the same continuation an action's own @color gets, so which tab you are IN survives a glance that never reaches the tab strip. And with several colored tabs open the ACTIVE one now stands apart: a resting colored tab wears a faint wash under a dimmed stripe, the active one the strong wash, the full stripe and the active-tab bottom stripe in its own color, where near-equal washes used to drown the difference.

  • @alert-on-success means an ALERT. It used to write a Log line; it now shows a dialog with one OK button, nothing to decide, for the outcome a terminal does not make obvious. The run itself is in the Log either way, and a failure still needs no dialog: the exit code and the output are the answer, already on screen.

  • An annotation reads as structure, not as comment. The name and its value each take a color of their own and drop the comment's italic: upright, because italic is the comment voice ("the server ignores this") and an annotation is exactly the part PlumeSQL reads. In both comment forms and in both kinds of action file, whose headers now light identically; placeholders in a command's values keep their own color on top.

  • One Run on an action's source, not two ways to say it. The source toolbar carried Run and a "show the result" button, and for a query action they were the same gesture twice: a query holds no answer (it dials, asks and closes; its grid re-asks the moment it shows), so showing the result IS asking. The second button now exists only where it means something of its own: a command action with a run, whose terminal is the held answer and where Run would start a second program. And Run from the source now GOES to the answer: it used to run the query behind the source view and change nothing on screen, which is why it read as a button that did nothing. The tooltip says where the answer goes, and the grid's own mount-ask stands aside when a run is already in flight, so nothing asks twice.

  • Every dependency at the day's latest. The new make upgrade (with per-surface upgrade-go, upgrade-ui, upgrade-desktop) moved the whole tree in one morning: pgx 5.10, Svelte 5.56.9, Vite 8.2, TypeScript 7 (the native compiler), Monaco 0.56 (whose new exports map required rewriting every deep import), and the current Tauri 2.x crates. The desktop surfaces deliberately stay within their declared ranges: the shell is signed and carries the auto-updater, so a Tauri major is a deliberate edit, never a morning automatism. The build itself is now warning-clean and FAILS on any new Svelte compiler warning, so it stays that way.

  • The cog is a hamburger now. The button at the strip's foot opens a MENU (account, settings, theme, updates, about), and a cog promises settings directly; the icon now says what the click does.

  • The status bar stands up for itself. It was the smallest status bar in the class, smaller than VS Code's and half of Zed's, and it read that way: a strip that receded into the chrome. It is now a step taller with a step larger text, its resting labels sit brighter than the app's muted grey, the accent tint behind it is stronger and the top edge carries the accent rather than the plain divider, in both themes. What the bar says and does has not changed, only how firmly it says it.

  • The action trail moved off VS Code's Back and Forward combos. It had taken Ctrl+- and Ctrl+Shift+- when nothing else went back; now that the real Go Back and Go Forward exist and own VS Code's combos, the trail rides a pair of PlumeSQL's own, one Alt away: ⌥⌘- and ⌥⇧⌘- on macOS, Alt+- and Alt+Shift+- elsewhere. Rebound bindings are untouched; only the defaults moved.

Removed ​

  • The Ask AI and Explain buttons are gone from the editor's corner. Both will return in a later phase, rebuilt properly; until then the floating pair over the editor's lower right is retired with everything that existed only for it: the visual EXPLAIN modal, its POST /api/ai/explain endpoint, the JSON explain runner and the AI plan interpretation. The AI bar itself stays exactly where it was (⌘I / Ctrl+I, VS Code's inline chat spot), and MCP's explain_query tool keeps its own engine call, which is the one caller left. A button in the title bar opening a modal of its own, with its own layout, its own ordering control and its own reset: a whole small feature answering one question that actions now answer better, in a file anybody can read, change, order differently or delete. The button, the modal, the two endpoints behind it, the desktop menu item and the client types all go; the shipped top-queries.sql asks the same question, wears the same bar chart icon in the same place, and says in its own header what pg_stat_statements needs to be installed and how to reset the counters. The MCP tool of the same name keeps its engine call, which is the one caller left.

  • The Activity panel is gone, and an action took its place. It was one fixed query behind a toolbar button, with two buttons hard-coded onto its rows and no way to change any of it. It is now current-activity.sql, a file in the Actions pane that says the same thing and can be edited: another column, a different order, one more button, a filter for the connection you actually care about. What was a feature is now an example. Its endpoints, its embedded query, its native View menu item and its toolbar button went with it.

  • The Objects pane menu links its own settings. The pane offered only the connection's Snapshot overrides; it now carries the Workspace/Global scope pairs for both Snapshot and Objects settings, the shape every other pane menu already has, so the menu walks the whole resolution chain (connection, then workspace, then global) and leads to where "Open a folder as one document" and the tree's other switches live.

  • A discovered connection can be hidden. A workspace full of configuration files conjures a dozen lookalike rows under Discovered, and those rows cannot be deleted because their source files own them. The row menu now offers Hide: the row leaves the tree (per workspace, the other trees' hide-list pattern), the Discovered node counts it ("3, 2 hidden") and its right-click menu unhides everything at once, while the Hide's own Log line carries Unhide for putting back just that one. Hiding is a tree matter alone: availability checks, stats and connection pickers see the connection exactly as before.

Changed ​

  • The default layout is two docks: files left, database right. The Explorer and Actions tabs keep following the Sidebar position setting (left by default), while Connections and Objects now ship pinned to the right dock, so a fresh workspace opens with the file trees on one side and the database on the other. Anyone's own arrangement wins as always: a tab side that was ever set, by the setting or by the tab's right-click Move, stays exactly where it was put.

Fixed ​

  • A FROM target without an alias no longer hides the next JOIN. The relation scanner's optional alias group consumed the following keyword ("from pg_class join pg_namespace" read JOIN as pg_class's alias), so the second relation vanished from column completion and from the Outline's object nodes alike; a lookahead keeps keywords out of the alias slot.

  • Moving Connections or Objects to the left sidebar works again. Both ship pinned right by their own setting default, and the Move entry normalized its write against the Sidebar position default instead: "move left" wrote null, the setting fell back to its own right, and nothing moved (the new stack submenus inherited the same silent no-op for cross-dock picks). One shared write path normalizes against the tab's OWN default now, so the move is explicit exactly when it has to be.

  • The editor placeholder no longer promises AI generation. It told every fresh script to "press ⌘I to generate it with AI" while nothing is bound to ⌘I and no generation endpoint exists yet (the AI section on the System page configures a key for the planned assistant); the promise returns when the feature does.

  • The sidebar strip's menu names what you clicked. Its settings pair said Appearance, the category its switches happen to live in, where every other menu's pair names its subject. The pair is contextual now: on a TAB it opens that pane type's own settings (Open Workspace Connections Settings on the Connections tab, Objects, Explorer, Actions likewise, the same pair the pane's own menu carries, reachable here even while the pane is not open), and on the strip's empty space it opens the Sidebar's, landing scrolled to exactly that subsection (settings deep links learned to target a category's group). The theme toggle left the menu entirely: the theme is an app preference with a home on this very strip (the app menu at its foot), in the palette and in Settings, and a fourth door in a sidebar menu was clutter.

  • The tab well's row lines span its whole width. In the multi-row layout each row's hairline rode the tabs and stopped at the last tab of the row, leaving the space to its right hanging in no row; the line is drawn by the list now (the rows are a fixed height, so a repeating gradient reaches where a border on tabs cannot, Visual Studio's tab well), and the last row sits flush on the well's bottom border, which reads as the slightly stronger document boundary, exactly as VS draws it.

  • BEGIN and DECLARE count as keywords everywhere. The grammar tokenizes its scope words (BEGIN, END, CASE ...) apart, for the folding pairs, and BEGIN and DECLARE live in that list ALONE, which silently cut exactly those two off from everything reading the keyword list: no suggest entry, no reference hover, no keyword Go to or Peek Definition. The list the features read now folds the scope words in; the tokenizer is untouched.

  • Peek Definition works. The context menu has carried the Peek submenu all along (it ships with the Go to Definition commands), but the peek WIDGET was never registered, so the entry silently did nothing. Alt+F12, or the menu entry, now opens the object's DDL in VS Code's inline frame below the line, fed by the same dictionary resolution as Go to Definition and the hover, and Escape closes it: the middle step of the ladder, hover excerpt, peek inline, F12 full DDL view.

  • A root row's hover buttons no longer tangle with its statistics. The New File and New Folder buttons float over the row's right edge, exactly where the folder rollup and file count sit, and the two shone through each other. While the buttons show, the numbers yield (VS Code's manners for a row's inline actions) and return the moment the pointer leaves; ordinary folder rows, which have no buttons, keep their numbers under the pointer as before.

  • A fresh workspace opens on the Explorer, not on Actions. The left dock's face was normalized in the first paint's beat, while the workspace path was still on its way from the server and the Explorer tab therefore transiently absent, so every never-opened directory latched its left dock onto Actions. The normalization now waits for the session to load, and the default faces say what they mean: the Explorer left, Connections right.

  • A script with no connection opens again instead of crashing the editor. The default SQL language (the one a tab without a server version uses, a fresh global directory's first script above all) was registered at module load, before the signature help and object hover providers existed, so Monaco received undefined providers and threw "undefined is not an object (evaluating 't.signatureHelpTriggerCharacters')" the moment the editor mounted. The registration now runs after every provider is declared.

  • The focused suggestion is readable in the dark theme. It sat on the accent itself, a light pastel there, where no text tops a 2.5:1 contrast ratio; the focused row now wears the selection look the context menus already use (accent-dim behind the ordinary text color, ~7:1), with the matched letters in accent on every row, in both themes.

  • COMMIT reads as the keyword it is, and the whole SQL vocabulary with it. The vendored grammar had no COMMIT at all, and the audit that followed (against pg_get_keywords() on PostgreSQL 13 and 18, the official keyword table) found the rest of the everyday statement vocabulary missing with it: VALUES, CALL, SAVEPOINT, VACUUM, COPY, ON CONFLICT DO NOTHING, WITH RECURSIVE, window frames, cursors, explicit locks, isolation levels, GRANT and foreign-data DDL, some ninety words in all. Every reserved word is now covered, every statement-heading word from the SQL Commands appendix highlights, and the type list learned PostgreSQL's real types (jsonb, uuid, xml, timestamptz, the reg* casts, PostgreSQL 14's multiranges in their version delta) while shedding an upstream typo (CAHR) and two types from other dialects (GEOMETRY, MAP). Version deltas grew PostgreSQL 14's COMPRESSION and SEARCH BREADTH/DEPTH, 17's MERGE_ACTION and 18's NOT ENFORCED. Unreserved words that are overwhelmingly column names in real schemas (name, data, version, path, source, target) deliberately keep reading as identifiers. Completions follow along, since they are built from the same lists.

  • Go Back reaches Settings, editors and every other view, not only files. The navigation trail held document-like views by their tab alone, and the preview slot replaces those tabs freely: open a connection editor, follow its Open workspace settings link, press Back, and the trail had already forgotten the editor and landed on the last code tab. View locations now carry a durable identity like files do (Settings, a connection or group editor by its id or name, Account, System, a schema folder document), survive the tab closing and the restart, and Back REOPENS them through their own front doors; the arrows' tooltips name them even after the tab is gone. And the views remember their scroll: each document-like view's position is kept by the same identity (session state, like Monaco's view state for code tabs), so Back, a tab switch or a restart lands where you left off, with a deep link's asked-for section still winning.

  • A preview tab with a live session is never replaced. Opening a script with a single click, running BEGIN in it and clicking the next thing replaced the preview tab and took its session, open transaction and all, with it: the "run to keep it" promise held only while the run was in flight. A preview tab is now promoted to permanent the moment it dials (a run, or connecting it from the toolbar), so the preview slot can only ever replace tabs that hold nothing, and a session ends only on the user's own explicit act, the standing rule. And the two lives of a tab read apart at a glance now: a permanent tab's title is BOLD where a preview's stays italic and dim, weight against slant, without spending any edge or color the washes and stripes already own.

  • A tree chevron toggles, and nothing else. With Folder Opens as One Document on, clicking an Objects tree folder's expand/collapse chevron also opened the folder's concatenated DDL view, and a connection group's chevron also opened the group's preview: a click aimed at the tree's shape was answered with a document. The chevron now has its own hit area that only expands and collapses (the rule the file rows' detail chevron always followed); the row's text keeps doing both.

  • The global Scripts root's right-click offers New Script. It used to fall through to New File and ask for a name, while the same root's own row button auto-named a script: one gesture, two behaviors, depending on the door. Both doors now auto-name (script1.sql, the first free number) and start the new script with the same note, and renaming afterwards stays the escape hatch.

  • A cleared Log stays cleared across a restart. Clearing emptied the panel and fire-and-forgot the file's deletion, so a DELETE that failed (a server mid-restart, typically) was swallowed and the next start brought every entry back, against the session's exactly-as-it-was rule. The clear now waits for any append still in flight (whose batch would otherwise land after the deletion and resurrect just-cleared lines), then deletes the file, and a deletion that fails is reported in the Log like every other error instead of being swallowed.

  • Clearing the Log no longer asks. The log is a diary of what the app did, not data: Dismiss all beside it never asked, VS Code's own Clear buttons never ask, and the clearing leaves a "Log cleared (N entries)" line of its own, so a mis-click is an explained event rather than a loss.

  • The bottom panel can take (almost) the whole window. Dragging the Terminal and Log panel up used to stop at 60% of the workspace, and often lower: the cap subtracted the editor's split fraction, which is a fraction of a different box, so with a tall editor the panel pinned at a third of the window. The panel now grows to 90%, the only ceiling being what keeps the editor strip and the divider on screen to drag back; the editor and results shrink into what remains, as they always did.

  • A failed action run is an error like every other: the Log, and only the Log. A query action that errored (top-queries without pg_stat_statements installed, say) printed the verbatim error into its results pane and wrote nothing to the Log, and a failed row button did both at once. Errors have one home: a failed run now writes the verbatim error to the Log, which reveals itself, on every gesture (the refresh timer's retries add an entry only when what is wrong changes, so a failing timer cannot take the panel over every few seconds), and the pane shows the calm failed state instead of the words: "The run failed; the error is in the Log.", with Try again beside it. A row button's failure leaves the grid it acted on standing untouched. And a connection that died mid-run used to be presented as if it were the action's answer; it now folds through the broken-connection state (Log, sidebar dot, the tab's broken face), exactly as a script tab has always treated it.

  • A command annotation in a query file is reported, not swallowed. The .run parser always said "@refresh belongs to a saved QUERY", but the .sql parser silently ignored @env, @file and the other command-kind annotations, the exact silence the format forbids. Both directions now diagnose by the kind axis, in either header.

  • Editor help reads dashed annotation names whole. Hovering @alert-on-done (or any name with a dash) showed nothing, and worse, hovering its middle piece showed @on's help, because the editor asked Monaco for the word under the pointer and Monaco's words break on the dash. The hover now reads the token that opens the line, the same rule the parser reads by, so it also stops answering for annotation names quoted inside prose. Completing past a dash was broken the same way: accepting @alert-on-done after typing @alert- inserted the whole name after what was typed instead of replacing it.

  • An action tab reopens on the face it was left on. Viewing an action's source and reopening the workspace used to land back on the result face; the face now rides the session like the rest of the tab's state. A command action with no live run still shows its source regardless, since its answer was a terminal that cannot be revived.

  • An annotation lights up only where it IS one. The editor colored @name anywhere inside a block comment, so a documentation bullet like - @description lit up as if PlumeSQL would read it. The highlight now follows the parser's own rule, in both comment forms: the name must open the line's printable text (whitespace and a block comment's decorative stars may lead), anything else in front makes it prose. The rule itself is now stated in docs/ACTIONS.md.

  • The trees tell screen readers the truth. Every tree row with the treeitem role now carries aria-selected (the Explorer's directories report the drag multi-selection, a group row reports its open editor), the floating Object filter and find windows are focusable dialogs, and the tab strips take programmatic focus properly. Found by driving the build's own accessibility warnings to zero; one real reactivity bug fell out with them (the tree's pinned sticky rows measured their height into a variable the layout never watched).

  • The System page's Client tools description caught up with command actions. It still said PlumeSQL runs none of the client tools; since command actions, a .run file's program runs on your click, and PlumeSQL even hands it the installation that suits its server. The paragraph now says what actually happens, refusal semantics included.

  • macOS: the traffic lights survive losing focus. With the overlay title bar an inactive window's buttons draw with AppKit's vibrancy, which needs a visual-effect backdrop in the window's own layer tree to blend into; PlumeSQL's window background is the webview, a foreign surface, so the inactive buttons composited to nothing and vanished, where every other Mac window keeps them, dimmed grey. The shell now gives them exactly the backdrop AppKit expects: a small visual-effect view under the button cluster, masked to three circles the buttons themselves fully cover, so the material never shows anywhere. An unfocused PlumeSQL finally looks like an unfocused window, not one without controls.

  • One mark means "action", everywhere. The Actions side tab wore the angle brackets while the tab strip, the footer and the result header wore a play triangle, so the same idea had two faces depending on where you looked. The brackets won: the pane, the tab, the footer's tab identity, the result header's icon, the title bar button's default icon and the two store rows all wear them now (the installation's store ringed, the way the two script roots are told apart).

  • @toolbar is written in the order the button is drawn. The line read "label then icon" while the button showed the icon then the label, which is the order every other button in PlumeSQL is drawn in. The button keeps that order and the SYNTAX moved to match it: @toolbar [icon] [label]. A name on the other side of the icon is still taken, since the icon is unmistakable and refusing it would be pedantry; it is simply not the form the specification teaches, so the shipped actions were rewritten and the registry, the documented table and the new file's own header all say the same thing.

  • Restoring a starter brings its title bar button back. Deleting an action took its button away, and putting the action back left the button gone until a restart: Restore Starter Actions writes files server side, and nothing told the title bar. The fix is one answer instead of another notification: the Actions pane publishes the buttons from the SAME listing it draws its own rows with, so however the files got that way (a save, a copy, a rename, a delete, the starters restored) the pane and the title bar cannot disagree.

  • Editing an action reaches the title bar and the tree at once. An action's file IS its declaration: the @description shown under its row and the @toolbar button in the title bar are read from it, and nothing re-read them after a save, a rename or a delete. A changed label, a changed icon, a removed line: all of it sat there until the app was restarted. Writing an action now re-lists its pane and reloads the offers, by whichever path the write took (AUTO SAVE included, which is the one most edits actually take and the one a first fix here missed), and renaming or deleting one reloads the offers too.

  • An action whose connection is down says so, instead of printing the dial report. The pane used to show the driver's whole failure text, four lines of addresses and refusals, in a red block where the answer goes. Errors have one home: the verbatim words are in the Log now (revealed, as errors reveal it), and the tab shows STATE, the reading a broken connection gets everywhere else in PlumeSQL: "postgres (.env) is not answering", with Try again and Pick another. The attempt also counts as a Test in both directions, which is the standing rule for every real contact with a database: a failed dial marks the connection not available exactly as a failed Test does, and the next run that gets through clears it exactly as a successful one does (only when there is something to clear, so an action on a refresh timer does not re-list the connections every few seconds).

  • The window can no longer navigate away from itself. A Backspace with nothing focused blanked the desktop app and brought it back at the splash screen: the engine still walks history back on that key, and PlumeSQL is one page, so leaving it does not go somewhere else, it tears the app down and rebuilds it from the session. The same door was open to the platform's back and forward combos (Cmd+[ / Cmd+] / Cmd+arrows, Alt+arrows), to the trackpad's two finger swipe, and to a file dropped anywhere the app does not handle, which the engine renders in place of PlumeSQL. All of them are shut now, in one place (web/src/lib/nonav.ts, installed before anything mounts). The keys are refused with preventDefault alone and never stopPropagation, so what the app does with them is untouched (the results grid still jumps to its edge on Cmd+arrow, a tree still takes its Backspace) and only the engine's own default is gone; inside anything that is TEXT (an input, a textarea, Monaco, the terminal) not a single key is touched. The swipe has no event to refuse, so it is answered instead: one history entry, pushed again on every popstate, leaves the history with nowhere to go. That half is the desktop shell's alone, because a page in a browser tab that traps the back button has taken something from its reader.

  • Typing a find query no longer changes what a tree has selected. In every sidebar tree, the search moved the cursor to the first match on each keystroke, and preview-follow (the default) opened that row as the preview tab a moment later. The row a tree paints as selected IS the active tab's row, so the selection jumped to whatever the query happened to match first, while the file the user had open a second ago dropped out of the filtered list: they were still describing what they were looking for and the app had already gone somewhere else. Typing narrows a list; it is not navigation. The cursor still lands on the first match, so the arrows and Enter continue from there, but nothing opens until the user actually navigates: the find window's own arrows preview as arrow keys always have, and Enter commits. Escape still puts the tree back as the search found it. The Explorer, the object browser, the Actions pane and the connections tree, all four.

  • Recents stopped forgetting two of their roots on every launch. The session's restore accepted workspace and scripts and dropped everything else, a list written before globalscripts and actions joined it: the Frequent shelf, the Go to picker's frecency ranking and an action's ask count all reset when the app closed, silently, because a history that starts at zero looks exactly like a history nobody made. The roots are one exported list now, checked against the type's own values, and the mapping that decides which list an entry belongs to is one function instead of two spellings that had already drifted (a failed open of an action forgot a WORKSPACE entry of the same name and left the action's own where it was).

  • An action with nothing connected asks, instead of complaining. Clicking one is asking for its answer, so the question it cannot answer on its own ("on what?") is put to the user right there: the same connection picker the pane header opens, opened by the action itself. Answering runs it. Cancelling leaves a calm line and a button where the answer would be, not a red failure telling the user to go and look for a header somewhere else.

  • An action can have a button in the title bar, and its file is the only thing that says so. Beside Top queries, where the Activity panel's button used to be: one click from any tab, whatever is open, and the click asks the action exactly as clicking its row does. @toolbar [label] in the file is the whole story, label included. It began with two halves, the file offering and a Pin to Toolbar gesture recording the user's answer in a setting that could contradict it either way; two answers to one question is one answer too many, and a button in the title bar is part of what an action IS, so it is declared where everything else about the action is declared, in a line anybody can read, copy and version. The gesture and its two settings are gone. That line can DRAW the button too: everything from the first <svg on is read as an icon, so @toolbar Activity, @toolbar <svg .../> and @toolbar Activity <svg .../> are all complete answers, and PlumeSQL sizes what the file shaped. The icon is REBUILT and never passed through: an action file travels between people, and markup rendered as received would hand every one of them a script tag, an onload attribute, a foreignObject full of HTML and a stylesheet that can move anything on screen. What comes out is reassembled from the shapes an icon may have (path, circle, rect, line, polyline, polygon, ellipse, g) with the drawing attributes only, so there is no filter to get wrong: an element this reader does not know is dropped whole, contents and all, and a line whose icon draws nothing is reported and falls back to its words. Two of the shipped actions arrive with one: current activity is a pulse line called Activity, blocking locks a padlock called Locks.

  • An action can be pinned to the top of its pane. Pin to Top in the row menu, the Explorer's pin and the object tree's in the third tree that has rows worth keeping in sight: a strip above the tree, per workspace like both of those, restored with the session. Two pins that answer two different questions, and they no longer share a word between them: Pin to Top is where an action sits in ITS pane (yours, a gesture), @toolbar is whether it has a button outside the pane (the file's, a line of text).

  • An action's header is the header every document-like tab wears. It had a shape of its own: a bold name, a line of description, and two words in two different button styles on the right. The DDL viewer's header was the one the app had already settled on (an icon, the name with its kind beside it, a subtitle, and the actions as icon buttons with three dots holding all of them), so that shape moved into a component of its own (ViewHeader) and both wear it now. The two words became what they always were: Refresh and Edit, as icons, with Back and Forward joining them while the tab has a trail. The three dots are new here and hold every one of those plus Pin to Top, Copy Command and Show the Actions pane. Primary actions inline, all of them in the menu: growth costs a row in a list and never a redesign.

  • An action is one tab, with two faces. Clicking its row opens the answer, Source turns the same tab around to the file, and the toolbar's Run and Result buttons run it and turn it back; Edit from the tree lands on that tab instead of opening a second one. That toolbar reads the ACTIONS connection now, the one the action actually runs on, and clicking it opens that picker: it used to show the tab's own, which an action never has, so it read "no connection" over an action that had one. Two tabs was the obvious shape and the wrong one: they could disagree, because the result ran what was on DISK while the editor held unsaved text, with nothing saying so. The tab holds one buffer now and runs exactly it, and the answer says when the source has moved past it, with Run again next to the sentence. The tab is a file tab as well, so the dirty mark, Ctrl+S, auto save, the stale write guard and the close prompt are the ones every file tab has.

  • An action's result says what it ran. The tab opens with the answer and named the file, so the only way to see the COMMAND was the tree, a right click and Edit. Source shows it in the same tab, the whole file: the header, the command and each button's command, in an editor with the annotations coloured and completing.

  • A button that acts leaves a record. A row action cancels somebody's query or ends their session, and the only trace was a grid that repainted. The Log now carries what ran, with which values, on which connection, and what the server called it; a failure goes there verbatim as well as into the pane. Everything else about an action stays silent, because a refreshed grid is its own answer.

  • An action's result says which database answered. The tab opens with the answer already on screen, and the only thing naming its connection was five grey letters on the meta line, while the footer's connection readout, the one place a user checks "what am I on", vanished entirely for an action tab. Now the header carries the connection's own color, its mark and its full url and opens the picker on a click; the footer reports it like any other tab, saying what is true for an action (no session: it dials per run and closes, so no pid, no age, and never "not connected"); and the tab wears the connection's color like a query tab does. A grid with a terminate button on every row must not leave which server it is talking to to a nickname.

  • The Actions pane picks several rows at once, with the primary modifier or a shift range, and drags them into a group together, exactly as the Explorer does. The menus say how many they will act on. It was the Explorer's gesture alone, for no reason other than where it was first written.

  • Actions are somewhere to go. The Go to Tab or File picker lists them in a section of their own, ranked by frecency like the database objects (asked often and recently floats up), and picking one ASKS it rather than opening its text. An open action tab is reached by name there too, and says "Action" instead of calling itself a scratch query.

  • The Actions pane keeps its own folded groups. It was writing them into the Explorer's set, so collapsing a group called Diagnostics folded a workspace folder of the same name, and the other way round. One set per tree now, carried in the session like the other two.

  • An action opened for editing is no longer filed as a workspace file. Its recent entry claimed the workspace root, so the Explorer's Frequent group offered it back under a path the workspace does not have. Every root that owns files of its own keeps its own entries now.

  • Three ways a SQL scanner could misread a script. Block comments NEST in PostgreSQL, and the scanner took the first */, so a commented-out block with a comment inside it left the tail to be read as code: a DELETE parked in there could trip the production guard, and a semicolon in there could split one statement into two. A dollar-quote tag may not start with a digit, and $1$2 was read as an opener, which swallowed the rest of the file into an unclosed body. And the backslash in an E'' string escapes the quote, which the scanner did not know, so everything after such a string was read shifted by one string. All three are fixed, and each has a test that fails without the fix. Two smaller ones with them: ;;; used to be three empty statements, so three nothings were sent to the server and three empty command tags came back; and the tag matcher copied the whole rest of the text on every $ in a script, which made a file full of $1 parameters parse in quadratic time. A script with 60,000 parameters is now a test.

  • A copy of a connection keeps its snapshot. Every way of copying a connection mints a new id (Duplicate, editing a discovered or shared candidate, Copy to Vault), and everything about a connection keys on that id, its DDL snapshot included. So a copy used to start with nothing and pay for a full rebuild before its object browser could show anything, even when it pointed at the very same database. Copy to Vault was the clearest case: the parameters there are not merely equal, they are the same ones. The snapshot travels now, guarded by the parameter hash, which is the one job that hash has: change the host, the port or the database on the way and the copy inherits nothing, because a snapshot describing another server is worse than none at all. Statistics deliberately do NOT travel, and there is nothing to ask about it: they are a history of what happened on that connection, and one that has never dialled must not claim one. Two entries with identical parameters are two connections with their own history and their own dot.

  • Deleting a connection forgets its usage history. Its vault entry, its metadata and its snapshot already went; the connect and failure counts stayed behind forever. An id is a permanent GUID and is never reused, so a leftover row could only ever be dead weight that grew.

  • Saving can no longer overwrite a change somebody else made. PlumeSQL already noticed, on window focus, that an open file had moved on under it, and reloaded the clean tabs. What it did not do was stop the save: the write was a plain overwrite, so an outside change went silently whether PlumeSQL knew about it or not, and no amount of checking could close the gap between a check and a save. A save now carries what the tab last saw on disk and the SERVER refuses it when the file has moved on, which is where a guarantee has to live.

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • The Explorer can order files by size or by when they changed. The same idea as the object tree's, from the Explorer's pane menu, as three rows with the current one checked. Folders are ordered too, by everything under them and by their newest file, since that is what "by size" plainly means and the totals are already there; folders still come before files, which no order changes. Two differences from the object tree are worth knowing: this needs nothing collected, because both numbers arrive with the directory listing, so the option is always available; and modified exists because VS Code's own explorer.sortOrder offers it. Size is PlumeSQL's addition there, and it is here because the row already shows it.

  • The object tree can order objects by size. Largest first, from the tree's pane menu or from a folder's own right-click menu, which is where you notice you want it. It is ONE choice for the tree and not one per folder: a preference, so it lives in settings rather than in the session, and a tree that sorts differently in different places loses the only thing the alphabet gives you, knowing roughly where a name lives before you look. Only the leaves move. Schema and type folders keep their own order, because a band tells a user schema from a system one and the type order groups tables with tables, and both carry more than a total does. It needs the statistics, so with them off the entry is not there at all rather than quietly falling back to the alphabet. And it only reorders what HAS a size: a view, a function and a sequence have no bytes, so in practice this orders tables and materialized views and leaves the rest alphabetical.

  • A table that dominates its database is marked, in the object tree. The same two tiers the Explorer uses for files, amber and red, on the size of a table. The threshold is RELATIVE and not a number of bytes, because an absolute one would mean nothing across databases: 36 GB is ordinary in one and alarming in another, so a fixed line is either silent everywhere or shouting everywhere. A table is amber at a tenth of everything the database stores and red at a quarter, which is the fact somebody acts on ("what is taking the space") and which scales itself. The tooltip says the share in words, so the colour is never carrying the meaning alone. Only the size wears the mark, never the row count, and only a real table: an index's bytes are already inside its table's and a partition's inside its parent's, so neither is counted twice. A small database stays quiet. Nothing under 8 MB is marked at all, because colour means "worth noticing" here and a share of almost nothing is not. And a marked size takes the row's first slot whatever the metric order says: the width rules drop metrics last-first, so with the default order a marked size would have been invisible on exactly the rows it exists to point at.

  • The Explorer carries numbers too, the way the object tree does. A file shows its size, a folder shows how many files are under it and what they add up to, in the same small right-aligned type, dropping whole values rather than half of one when a row runs out of width, with the tooltip always spelling out every number. The folder count sits in its own slot beside the name, exactly where a schema row puts its object count, so the two never compete and a folder reads "3 78 B" the way a schema reads "634 36 GB". An empty folder says nothing: two zeroes restate what its missing children already say. A size worth noticing before you click it is coloured: amber for bigger than most files here (files.bigSize, 1 MB), red for large enough that the editor will be slow with it (files.hugeSize, 10 MB). Both are advisory and nothing is refused or hidden, both say what they mean in the row's tooltip rather than leaving a colour to carry it alone, and either can be switched off. A folder is never coloured: its total is the sum of files that carry their own marks, and reddening a folder because it holds many small ones would say the wrong thing. The Explorer's pane menu now carries File Statistics, the same check-row the object tree's menu has for Object Statistics, so both trees are switched the same way and neither hides a setting the other shows. It costs nothing to collect. The directory walk already asks each entry for its info to get the size, so the modification time is the same answer's other field, and the folder totals are one pass over a tree that was just built: no file is read and no extra request is made. Which numbers show is files.treeMetrics (Size, Files, Modified; empty shows none), named the object tree's way. Modified is available but off by default, since a third number on a narrow pane only pushes the other two out. A line count is deliberately absent: it would mean reading every file, and it can arrive for free later, out of static analysis of the SQL.

  • Save is reachable without knowing a keystroke, and Save All exists. Saving was a keystroke and nothing else, which is fine while auto save does the work and useless the moment you turn it off. It is now in the editor toolbar (a button that appears only on a tab with a file and is disabled while that file matches the disk, so it doubles as a readout), in the tab's right-click menu above the closers, in the command palette with its combo shown, and in the desktop app's File menu. The tab menu's Save writes the tab you right-clicked, not the active one. Save All writes every unsaved file in one go, sequentially so a conflict on one can ask its question without three more dialogs queueing behind it, and reports the batch once instead of a line per file. Its shortcut is VS Code's, which differs by platform: ⌥⌘S on macOS and the Ctrl+K S chord elsewhere. It is configurable (keyboard.saveAll); plain Save stays on Ctrl+S.

  • A tab says when it has unsaved changes, and closing one asks. Auto save is still the default and still writes a moment after you stop typing, so a tab is dirty for about a second and none of this shows. Turn it off and saving becomes yours, which needs two things PlumeSQL did not have. The close button becomes a filled dot while there is unsaved work and turns back into an X when you point at it, VS Code's exact affordance; the dot shows on every tab, hovered or not, because the unsaved file worth marking is the one you are not looking at. And closing such a tab asks: Save, Don't Save, Cancel. It has to, because closing a tab drops its buffer with no recovery, which was harmless only while auto save had always already written the file. Closing MANY at once (Close All, Close Others, Close to the Right) asks once, naming what would be lost, rather than putting six dialogs in a row.

  • With auto save ON, closing a tab writes it. Auto save fires a moment after typing pauses, so editing and closing inside that moment used to lose the last edit silently, which is precisely what someone who turned auto save on has been told cannot happen. The close flushes it now, and only a write that FAILS falls through to asking, since by then the promise is already broken.

  • Drop a file on PlumeSQL and it opens. It did literally nothing before, and not by omission: the desktop shell's own drag and drop handling was on, so it swallowed the event before the page could ever see it. That handler is now where the feature lives, because it is also the only place that gets real filesystem PATHS. A file from inside the open workspace opens as that workspace's file, so one file is still one tab however it was reached. A file from ANYWHERE else opens as the file it is, editable and savable, with its absolute path in the tab's tooltip. A folder gets its own window, the same rule opening a folder with PlumeSQL has always followed. In a browser a dropped file has no path to give, so PlumeSQL takes a copy of its contents into an unsaved tab and says so rather than pretending; what it no longer does is let the browser navigate away from PlumeSQL to show you the file.

  • The native Open File dialog opens the file, not a copy of it. It used to hand the contents to an unsaved scratch tab that could never be saved back to where it came from. It takes the same route a drop does now, so the file is the file.

  • A file changed on disk asks you, VS Code style. On an explicit save the refusal becomes one question with three answers, in a dialog because you just pressed Save and it did not happen: Overwrite, reload and lose your edits, or neither. It has no safe default, so it has none: Cancel takes the focus and Enter cancels, because a save conflict answered by muscle memory is exactly the accident this exists to stop. Auto save cannot answer a question, so a conflict stops it for that tab instead, marks the tab with an amber triangle whose tooltip says auto save has stopped, and records it once in the Log. Nothing is ever at risk in the meantime: the tab keeps its content and the session carries it through a restart.

  • A file deleted while open goes dirty. The buffer already stayed and the tab already wore a red triangle; now the tab also reads as unsaved, because nothing of it is on disk any more, so nothing of it is saved. Saving puts the file back, which is what the marker's tooltip says.

Removed ​

  • Backup and restore are gone for now. The whole column went with them: the export and import endpoints, the pg_dump and pg_restore wrappers and the native dump and restore. The UI had already lost its way in months ago, when the connections sidebar became a pure list, so what remained was a working implementation nobody could reach. It will come back designed as a whole rather than repaired in pieces. The client tool detection went with it and has since returned on its own terms, below.

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • The PostgreSQL client tools have their own System page section. psql, pg_dump and their siblings are yours to run, not PlumeSQL's, and managing them is now three questions answered in one place: what is installed on this machine (every installation, its version, where it came from and which binaries it actually carries), which of your servers it can reach, and how to add what is missing. Coverage is listed per SERVER VERSION rather than per connection, because eight connections to PostgreSQL 18 are one thing to install; a missing version carries its own Install button. Installing is the same contract as the plumesql command and the file manager entry: the exact command is shown, typed into your own terminal on your gesture, and the page rescans until the tools appear. The connection editor says the same thing about one server, on the line under its version.

  • An offer PlumeSQL makes can now run in a terminal window. The integrated terminal is a real PTY and Windows has none yet, which left the one offer that exists ONLY on Windows, the "Open with PlumeSQL" shell entry, able to do nothing but copy itself to the clipboard and hope. It opens a terminal window there instead, which every desktop has, and the same applies to the plumesql command and the client tools install wherever the integrated terminal is missing. Where there IS one it still wins, because the output stays inside PlumeSQL. Two things make it safe rather than clever: the command is never sent by the page (the request names WHICH offer to run and the server rebuilds it from the same generator that produced what you were shown, behind the terminal endpoints' own header gate), and it travels in a small script rather than through the shell's quoting, because the registry command is nothing but quotes and backslashes and every platform re-parses a command line differently.

  • What it offers to install is client tools and never a server. What is missing here is psql and pg_dump, and a package that brings a database server (and on Windows a service) along with them answers a question you did not ask. On macOS that means Homebrew's libpq, the client-only keg, and its versioned libpq@17 and libpq@16, which PlumeSQL now also scans for; the full postgresql@N formulas are still found if you have them, but never offered. On Linux the client packages were already the right ones and each now leads with the current release. Windows has no client-only package at all, so PlumeSQL offers nothing there and says instead to clear every component except Command Line Tools in the PostgreSQL installer. A test keeps the rule: no install command may name a server package.

  • The File manager section reports what is actually on disk. It told every macOS user "opening a folder with PlumeSQL works here" on the strength of nothing but the platform, while the installed release they were reading it in carries no folder declaration at all: it was written after that release was cut. The readout now looks. It tells three states apart, and only the last one is success: this PlumeSQL is not running from an installed app (a dev build, a browser, a plain server); it is an app bundle whose Info.plist does not declare folders, which no button can fix because Launch Services reads that at install time; or it declares them, and then the note says which gestures actually hand a folder over. Linux is checked the same way, against the desktop entry on disk rather than against the platform. A readout that cannot be wrong is not a readout.

  • The System page is one flat list of sections. It had grown four sections with eleven children, the largest of them a container called "System integration": a heading and one paragraph above five things nobody looks for together (a shell, a file browser, an API key, an agent server, a hostname). The grouping was true and it still cost a level of hierarchy on the way to any of them. There are now eight peers, read at a glance rather than navigated: System information, Client tools, PlumeSQL files, Command line, File manager, AI assistant, AI agents, PlumeSQL service. Every nav entry is a section and every section is a nav entry, saying the same words in the same case, so the highlight can never land on something the nav does not show. Captions inside a section (Installed, Yours, Rebuildable) stay as captions and are deliberately not destinations.

  • The AI and MCP settings moved onto the System page. Both used to be modals, the only two in an app where every other place you configure something is a tab. They are now sections of their own, AI assistant and AI agents, and the top bar buttons open the page at the right one instead of a modal. Where the Anthropic key is STORED did not change and never argued for a page of its own: the encrypted vault, with your connection passwords.

  • The top bar's right side is empty now. The AI, MCP and account buttons all sat there permanently for pages you visit rarely, and the account already had its place at the top of the cog menu. That entry now says "Sign in to PlumeSQL" rather than "Log in", matching the words the Account page itself uses, and carries the plan once you are signed in. Nothing is lost by leaving: a signed-in account whose licence has lapsed is told so verbatim on every query it tries to run, and an account that never signed in is never blocked at all.

  • The AI and MCP buttons are gone from the top bar. Two permanent buttons that said "off" almost all of the time, for pages the command palette reaches by name. The one fact worth seeing without looking for it stayed: while an AI agent actually holds a session, a small green chip appears in the status bar and opens that section when clicked. It is on screen only while it is true.

  • The System page says where the vault key lives. It used to say only that the key is outside the directory, which left the difference between a key in your Keychain and a key in a plain file on disk invisible. Each backend now says so in words, and the file case says where the file is.

  • And whether this machine can reach plume.hercstack.com. Signing in, your licence, cloud sync and the update check all go to that one host, so a proxy in the way explains all four at once instead of being met four separate times with no cause. It costs no extra traffic: the version check already made the call.

  • PlumeSQL says once that a shell cannot reach it. The System page is not somewhere anyone goes looking on their first day, and plumesql . is the thing most worth knowing about, so a start that finds no plumesql on PATH writes one Log entry with a button that adds it. Once per installation, recorded only when the offer is actually made, so an installation that later loses the command still gets its one offer. Every start after that one asks the ledger and stops there: the check behind the offer probes directories by writing to them, and paying that on every launch to re-learn a spent answer is not what featherweight means. It opens nothing and steals no focus: startup opens only what you left open, so this waits in the Log like everything else.

  • Open a folder with PlumeSQL from the file manager. Finder's Open With, Explorer's right-click menu and a Linux file manager can now hand PlumeSQL a folder, and it opens as a workspace. The folder always gets its OWN window: an open workspace is never switched out from under whoever is working in it. macOS and Linux carry the declaration in the build itself, so the System page reports them; a Windows entry is a key under your own user, needs no administrator, and is offered there as a command you can read before it runs (Windows has no integrated terminal yet, so there the button copies it instead of pretending it can run it). The desktop app also accepts a directory as an argument now, and takes it either as a path or as the file:// URI a Linux desktop entry sends.

  • The System page opens with what is running and what it runs on. A first System information section: the PlumeSQL version with a live check against the published release, and this machine's OS, processor, memory, free space and host name. The version card says which of three things is true, never the wrong one of them: an update is available, or this is a development build ahead of the last release, or it is current. The Update button hands off to the desktop app's own signed updater, the same one the cog menu uses; a browser cannot replace the program it is showing, so there it opens the download and says why. A check that cannot reach the site reports that in the card and nowhere else: being offline is the normal state of a laptop, not an error, so it is not logged and not cached.

Fixed ​

  • Starting the MCP server or saving an AI model no longer wipes your settings. Both wrote their handful of fields into the global settings.json with a whole-file write, and that file is the one holding every preference you have: one click on Start and your theme, fonts and keyboard shortcuts were gone. They live in their own integrations.json now, and values from an older installation carry over. The settings file has exactly one owner again.

  • The System page's sections now look like sections. They sat flush against each other while a subsection got 18px of air, so the hierarchy was upside down and the last control of one section touched the next section's title: "Open in Finder" read as if it belonged to System integration. Sections get a real break and a hairline rule now.

  • Clicking a System page section in the left nav sticks. Jumping to the last section landed at the end of the scroll, where a rule meant for scrolling took over and highlighted that section's final subsection instead of the thing that was clicked.

  • About is a card now, not a table. With the machine details gone to the System page, what was left did not need a form: the feather leads, the version and the runtime are chips, the workspace shows its folder name with the full path a hover away, and the two links sit under it. It also takes focus when it opens, so Tab cycles inside it rather than walking the page behind it, and the feather drifts unless you have asked the system for less motion.

  • About stops repeating the System page, badly. The program and config directories were rows in a dialog that could say nothing about them; the System page says what each one holds, how large it is and what is safe to remove. About keeps what is about THIS window (the version, the runtime, the workspace) and offers a button to the rest.

  • Every clear now asks first, and says it is final. Clearing the log, the frequent files, the frequent objects or the new-objects list threw away accumulated history with one click and no question; none of them rebuilds, and unlike a deleted file none of them has an undo. Each now confirms, names what is lost and ends with "This cannot be undone". The System page's cache cleaning says both true things: nothing of yours is in it, and it is rebuilt by using PlumeSQL again rather than recovered. The terminal's own Clear is deliberately left alone; scrollback is not data and no terminal asks.

  • Backup now picks the PostgreSQL client tools by the server's version, and finds them where they actually are. Detection was a PATH lookup and nothing else, so anyone whose tools come from Postgres.app or a versioned Homebrew keg was told pg_dump was "not found" while it sat on their disk. Worse, there was no version choice at all, and pg_dump REFUSES to dump a server newer than itself: a PATH pg_dump 15 could not back up a PostgreSQL 18 database and nothing said why. PlumeSQL now finds every installation on the machine (PATH, Homebrew both prefixes including the client-only libpq keg, Postgres.app, the Debian and Red Hat paths, and the Windows installer's layout) and picks per connection: the OLDEST installation still new enough for that server, so the dump stays as close to the server's own dialect as it can. When nothing on the machine is new enough it says exactly that, naming the newest it has and the version to install, before the run rather than as a failure halfway through it. The backup dialog shows which pg_dump a custom archive will use. The scan is memory only and never written to disk: what is installed is a fact about the machine right now, and a stale answer is the very bug this replaces.

  • A global script reopened as deleted. Restoring the session dropped its root: the check listed the roots by hand and the new one was not among them, so the tab came back as a workspace tab pointing at a file the workspace never had, struck through with "deleted on disk". The check now asks the FileRoot list itself, so a root added to the type cannot be silently dropped on the way back in. The same two-root assumption was hiding in four more places: the root the tree filtered by (the Scripts root could not be hidden at all), the stamp that reloads the tree after a deleted file is recreated, the recent and frequent lists (a global script was recorded as a workspace file), and the absolute path Copy Path and the footer build, which now comes from one helper instead of a ternary in four files.

  • The Explorer's Frequent shelf now arrives COLLAPSED, the way the object tree's has always arrived. It is not a folder of the workspace that was already on screen: it appears on its own the first time something earns a place on it, and unfolding itself at that moment reads as the tree rearranging under you. Open it once and it stays open, per workspace, like every other row. The New in this session shelf is unchanged: it still opens itself when it grows, which is the whole point of it.

  • A checkbox in the connection or group editor's Session, Snapshot and Statistics sections no longer leaves the editor dirty when it is clicked twice. Writing the value a key would inherit anyway now CLEARS the override instead of pinning it: a checkbox has two states and no third one for "inherit", so the old behaviour left the box looking exactly as it started while the Apply bar disagreed, and applying froze that connection against every later change to the workspace or global setting. The text fields are unchanged, because there an empty input already says "inherit", and so are the dropdowns, whose lists carry an explicit "inherit" row.

  • The revert button in those sections no longer shifts anything when it appears. Its space is reserved whether it is there or not, so a hint stops rewrapping, an input stops narrowing and a row of metric chips stops sliding the moment a value is touched; every one of them now sits in the same column, at the trailing edge of the row whose setting it resets. The connection and group editors' own reset next to "Check availability automatically" works the same way.

Changed ​

  • One notion of "new script", and it says where it lands. With two script stores, an unqualified "New script" stopped meaning anything, so every entry point now names its destination: Workspace Scripts, or the shared Scripts when no workspace is open, which also replaces the old silent fallback to an unnamed buffer. The tab strip's plus, the empty state, the palette and the new-script key all do the one thing; the key used to make an unnamed buffer instead, which was a second kind of "new" with no visible difference from the first. An empty script already open is reused instead of joined by another. Every press wrote a real file, so pressing the plus twice left an empty script on disk and they piled up; asking for a blank pad while one is open and untouched is answered by the one that is there. When that script is the one you are already looking at, the click repaints nothing, so it says in the Log why: the standing rule about staying quiet covers gestures whose outcome is visible, and this one has no visible outcome at all. Switching to a blank script in another tab stays quiet, because there the tab change IS the outcome. Removed from the connection row menus, all three of them: a connection is not a place on disk, so "new script here" had no destination to name. A new script still inherits the object browser's connection, and the tab's own dropdown moves it.
  • Deleting a file offers an Undo. The delete logs what it removed and the entry carries the button that puts it back, folders included (every file under one is remembered). The message SAYS how, naming both the button and the key, because a button on a row whose notification card has expired is not something anyone finds by looking. The Explorer also undoes the last delete with the primary modifier and Z, VS Code's own undo in its explorer, and says plainly when there is nothing left to undo. The confirmation says all this instead of the old "This cannot be undone", which is no longer true. The content lives in memory for the run, so the offer honestly disappears on a restart and the button goes with it, the way every LogFix re-resolves rather than dying a dead click. Batches age out after a few deletes and a very large one is not offered at all, because a folder of dumps must not be held in the page just in case. A persistent trash can replace the buffer later without changing the entry or its button.
  • Several Explorer rows at once. The primary modifier and a click toggles a row into the picked set, Shift and a click takes a range, Shift with the arrows extends it from the keyboard and Escape drops it. Delete, Cut, Copy and a DRAG then act on the whole set with one confirmation, one paste and one drop, and their labels say how many. A right click outside the set makes that row the target instead, VS Code's rule, so a menu never acts on rows you have stopped looking at. The set is deliberately transient and never enters the session: rows silently still picked tomorrow would be a trap.
  • The connections tree deletes with the primary modifier and Backspace on macOS too, which the Explorer already did.
  • The Explorer's filter window now holds the roots too, a Roots section above the file patterns, the same four checkboxes the Explorer menu has. Which roots show is exactly what a filter answers, and the object tree's filter window has always been the place its visibility lives; the Explorer's had only patterns and empty folders, so the window was not the whole answer to "what does this tree show". The last visible root cannot be unchecked in either place, and here the checkbox goes disabled rather than silently refusing. Reset to default clears the roots along with the patterns, which it has to, or the button would lie.
  • Global scripts: a third Explorer root, "Scripts", holding SQL shared by every workspace. What used to be Scripts is now "Workspace Scripts", which is what it always was: the scripts of one project, kept out of the project's own directory so they never land in git. A query worth reusing now gets written once instead of copied from workspace to workspace. They are also the ONE root that needs no workspace at all, so a PlumeSQL started in no directory still has them, which was impossible before. Same contract as the workspace scripts (SQL files only, folders allowed), the same New Script, rename, delete and clipboard, and its own checkbox in the Explorer menu and filter window. Stored in the global plumesql dir under globalscripts/.
  • The Explorer's roots carry their own actions, on the right of the root row where VS Code puts them and faint until the row is under the pointer or holds the keyboard: New File, New Folder, Refresh Explorer and Collapse Folders. Scoped to the root they sit on, so the one on Scripts makes a script and the one on the workspace makes a file, a distinction a single header button could not draw. Collapse Folders is new; the other three existed and had no button anywhere. The Objects and Connections trees get Collapse in their panel header instead, since neither has a root row to hang it on: the object tree folds every schema and type folder, the connections tree every group and kind.
  • The Explorer marks every file kind it supports: SQL, JSON and ENV, as a lettermark on the row and on the tab, the same language the SQL and DDL marks already spoke. JSON and .env files used to share the blank sheet with everything PlumeSQL has no grammar for. One function now decides what a file IS, and both the mark and the editor grammar follow it, so a file cannot be tokenized as one thing and labelled as another.
  • The Explorer's file clipboard is on the keyboard, the combos VS Code registers for its own explorer: cut, copy and paste on the primary modifier. The actions were already on the row menu and only the keys were missing. macOS also deletes with the primary modifier and Backspace, VS Code's own second binding for it; plain Delete still works everywhere.
  • A DDL object tab now has a header instead of a toolbar, the shape the other document-like tabs use (Settings, the connection and group editors): the object's icon, its qualified name and type, and under it which database it came from, how old the snapshot is and that it is read only. The toolbar it replaces had nothing to offer a read-only view: undo, redo, cut and copy sat disabled and paste did nothing. The connection is the part that was simply missing before: a DDL tab wrote it nowhere but in the tab's tooltip. The header carries the actions that do apply. Everywhere: Find, which opens the editor's own find so there is one search and it is VS Code's, and Copy, which takes the whole view. On a grouped folder view, which is many objects in one document, it also carries previous and next object, which SELECT the object they land on, the same DDL without its heading comment that the heading's own Select gives you, and wrap at either end; and collapse all and expand all. Every object in it still keeps its own Collapse, Expand, Select, Copy and Open on its heading. A single object additionally offers Copy name, its qualified name quoted the way SQL needs it. The actions are a list rather than fixed buttons, and the header's own three dot menu always holds every one of them. That is where per object type actions will go as they arrive, and adding one is a row rather than a redesign.
  • A side panel's menu moved into its header, the three dots on the right of Connections, Explorer and Objects, which is where VS Code keeps a view's actions. The header gives up the keyboard help it used to carry there: the panel's own shortcut, and the live arrow glyphs that appeared while a tree held the keyboard. A shortcut belongs on the side tab's tooltip and in the cheatsheet, both of which still have it, and the toolbar underneath is one button shorter for it.
  • The keyboard now follows VS Code. Where VS Code has a default shortcut for something PlumeSQL also does, PlumeSQL uses VS Code's combo, per platform. New: the command palette also answers to Ctrl+Shift+P (⇧⌘P) beside F1, Ctrl+P opens Go to Tab or File, Ctrl+Shift+O opens Go to Object (VS Code's Go to Symbol), Ctrl+, opens Settings, Ctrl+Shift+U shows the Log (VS Code's Output), Ctrl+N and Ctrl+W open and close a tab, Ctrl+Tab and Ctrl+PageDown walk them, and F2 renames the row under the cursor in the Explorer, the Scripts tree and on a connection group. Moved: the side panels leave the F keys for VS Code's view family, Explorer on Ctrl+Shift+E exactly as VS Code has it, Connections on Ctrl+Shift+C, Objects on Ctrl+Shift+D, Snippets on Ctrl+Shift+X, and the side panel cycle to Ctrl+Q, VS Code's own view picker. That gives F2, F3, F7 and F8 back to the meanings a VS Code user expects. The AI bar moves from Ctrl+K to Ctrl+I, which is where VS Code puts its inline chat and which frees the whole Ctrl+K plane. Chords work, written and pressed as VS Code writes and presses them: Ctrl+K Ctrl+S opens the cheatsheet, Ctrl+K Ctrl+T the theme picker, and a binding in settings.json may hold one ("Ctrl+K Ctrl+H"). Inside the editor Monaco keeps its own Ctrl+K plane; the two do not fight. Two deviations stay, both deliberate: Ctrl+Enter and F5 run the query, the SQL client convention being older and stronger than VS Code's insert-line-below, and Enter OPENS a tree row on every platform, so rename is F2 everywhere rather than macOS's Enter. Every combo is still rebindable under Settings, Keyboard. On macOS the old Ctrl+C, Ctrl+S and Ctrl+E side panel aliases are gone: they were shadowing Monaco's own start-of-line and end-of-line keys.
  • The DDL snapshot now includes roles, ownership and privileges by default. Roles are objects: a tree that hides them cannot show you who is granted anything, they cost nothing in the object files (they get files of their own under roles/) and they come from pg_roles, so no superuser is needed. Ownership and privileges follow pg_dump, whose defaults these are (--no-owner and --no-acl are its opt-outs): ownership decides who may ALTER or DROP an object and whose default privileges apply, and PlumeSQL shows it nowhere else, while a GRANT only renders where somebody actually made one (an object with no ACL renders nothing, and the owner's own implicit privileges are never spelled out). Measured on a 406 object database: roles add 7 files and 1 ms, ownership one line per object and 4 ms, privileges nothing at all where nothing was granted and 12 ms. All three stay switchable globally, per workspace, per group and per connection. The first refresh after updating rebuilds each snapshot once, because the fingerprint knows which options built the tree it is checking.

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • A System page, opened from the cog menu or the palette. It reports what this MACHINE has and offers actions on it, which is why it is not part of Settings: nothing on it is a preference and nothing is per workspace, so it carries no scope switcher either. It does carry the same section map on the left that Settings and the connection editor have, two levels deep and highlighting whatever is on screen, and it remembers its scroll and place with the tab. Three sections: PlumeSQL files is everything PlumeSQL keeps outside your workspace, with sizes, split by a hard line. What is YOURS (the vault, the shared and workspace scripts, the settings, the connection metadata, the sessions) is listed and never offered for removal, whatever its size. What REBUILDS itself (the object snapshots, the shared catalog stores, the logs) can be cleaned one at a time, saying what it will free before it does, and snapshots left behind by deleted connections are called out separately since nothing reads them at all. The server resolves the request against that same list, so content cannot be reached from here even by a stale page asking for it by name. PlumeSQL deliberately does not browse or edit these in the editor: the vault is encrypted and the session blobs are not text anyone should hand edit, so the section offers sizes instead. Every path the page shows OPENS where it lives: each row, each tool directory and the plumesql command's own location are links rather than text to select and paste. Revealing is still confined to PlumeSQL's own directories, widened by exactly the tool directories its own scan reported and by nothing a caller can name. System integration is whether a shell can reach PlumeSQL. It says whether a plumesql command exists, whether the one on your PATH is THIS PlumeSQL or another copy, and offers to link it, choosing a directory in your home over one that needs a password whenever your PATH already has one. Whether the file manager offers "Open with PlumeSQL" is REPORTED and not offered: on macOS that comes from the app bundle's own declaration and cannot be switched on afterwards, so a button here would be a lie. PostgreSQL client tools is every installation found, with its version, where it came from, its path, and which of the five tools it actually carries (a client-only keg has psql and pg_dump but no pg_isready, and the row says so). Under it, which of your connections can be backed up and by which installation, and which cannot because nothing here is new enough for their server. Scan again rescans; the answer is never written to disk, since what is installed is a fact about the machine right now. It can also install what is missing: the page offers the ways this machine actually has (Homebrew including the client-only libpq keg, winget or Chocolatey on Windows, apt, dnf or apk on Linux), leading with the version a connection is missing. Each option shows the EXACT command on its face, a confirmation repeats it, and it then runs in the integrated Terminal rather than out of sight: it is your own shell, several of them need a password, and the output is yours to read. PlumeSQL never runs a package manager behind your back. Since it cannot watch someone else's process, it watches the OUTCOME instead, rescanning until the new tools appear, and the status bar's activity dot beats and names the job while it waits.

  • A fresh installation starts with four connection groups: Production, Staging, Development and Local, colored red, amber, green and blue. They are a shelf to file connections onto and the one place groups announce that they exist at all; rename them, recolor them or remove them like any other group, and they never come back once removed. Production asks before schema and data changes, which is a confirmation and never a refusal: a connection filed under that name has earned one "yes" before a migration, and the level is weakened or dropped from the group editor in a gesture. Staging deliberately gets no guard, because staging is where migrations are tested and a prompt in front of every statement would fight the one thing it is for. Nothing is seeded at "block" or read only, the two that stop work outright. The seed writes what it did and why to the Log, with a button that opens Production so the guard can be seen and changed: setup done on your behalf is never silent. Only an installation with no saved connection, no group and no connection metadata gets any of it, so nobody who has already started filing sees anything new.

  • Cheap statistics in the object browser. Every object row can now carry what the database already knows about it: how many rows it holds (the estimate ANALYZE keeps), how much disk it takes with its indexes and TOAST, how often it is scanned, how many dead rows are waiting for vacuum, when vacuum and analyze last ran, and, for routines, how many times they were called. A schema or type folder shows the same summed up, counting each byte once (an index's storage already lives in its table's, a partition's in its parent's). A row shows as many of them as the pane is wide enough for, first metric first, and drops WHOLE ones rather than cutting a number in half ("488 K" left over from "488 KB" would read as a complete value and be wrong by a thousand); the tooltip always spells out every one of them plus how old they are. A partitioned table and a TimescaleDB hypertable roll their children up, so a hypertable no longer reads as an empty table with 124 million rows hidden under it. A missing number is shown as missing, never as zero: a relation nobody has analyzed has no estimate, and the counters only count since the last pg_stat_reset.

  • Statistics have their own clock, deliberately separate from the DDL snapshot: one cheap statement (catalogs and the cumulative statistics views, no table contents read, about 56 ms on a 2,500 relation database), cached to disk beside the snapshot so a reopened workspace paints its numbers without dialing anything, and collected again when the object browser is revealed and the cache has outlived "Recollect statistics when older than" (an hour by default; "off" leaves it to the pane menu's Refresh Statistics). Building or rebuilding a snapshot never touches them, and collecting never touches a snapshot.

  • A folder document (a schema or a type family opened as one read-only view) carries the numbers too, but as ONE LINE each, appended to the -- path.sql header every object already has, plus the family's total on the document's first line. A block per object would have cost that view the two things it exists for: its line numbers count from 1 per object, and its purpose is searching the DDL text, which two hundred blocks of prose would bury. On the header line they cost nothing and pay twice, because collapsing everything now gives a table of contents with sizes, aligned in a column. Copying a section, Open object and the folds all still see the bare path.

  • The same statistics ride the top of a DDL view as a comment block, with the age of the numbers. Display only, always: the comment is composed as the view opens and the file on disk never carries it, so the snapshot stays byte for byte what the server rendered and a copy of the buffer is still valid SQL. A routine on a server that does not count calls says so, and says how to turn counting on (track_functions), instead of showing a zero that would read as "never called".

  • The statistics refresh where you are looking: a row's context menu offers "Refresh Statistics for TABLE film" (or for a whole family or schema, following the same levels the snapshot's own Refresh uses), and the pane menu offers "Refresh All Statistics". A scoped refresh narrows the statement and the partition roll-up to those objects, so it stays cheap on a database where the full collection is not, and the objects it touched carry their own age from then on: their tooltip and DDL header say "just now" while everything else still says when the batch ran. It is not written to the cache on disk, which holds the last full collection under one honest timestamp. Worth knowing: the row estimate only moves when the server has ANALYZEd, while the size, the counters and the vacuum times move immediately.

  • Every row's context menu carries the "Object Statistics" switch itself, not only the pane menu and the tree's empty background: a full tree has no empty background to right-click, and a number somebody wants gone should be one menu away from the number.

  • New settings under Objects, each overridable per connection and per group in the editors' new Statistics section: "Object statistics", "Statistics in the tree" and "Statistics in the DDL header" (which of the eight numbers each surface carries, all of them free once collected), "Recollect statistics when older than" and "Statistics in the DDL view". Two lists rather than one because the questions differ: a tree row has a few characters beside a name, a header has a whole line per number, so the tree starts with rows, size and calls and the header with everything. The Objects pane menu carries the same switch and the Refresh Statistics action.

  • Every metric links to the PostgreSQL page that DEFINES it, for the server's own release: what an estimate is estimated from (planner statistics), what a size includes (the object size functions), what "since the counters were reset" means (the cumulative statistics views), what autovacuum does with dead rows. The links sit beside the checkboxes in the editors and beside the settings themselves; the DDL header's "this server does not count routine calls" note carries the track_functions page as text, so it survives a copy of the buffer.

  • A folder in the object tree opens as one document. Clicking a schema, a type family or one of the shelves shows every object under it concatenated into a single read-only tab, each preceded by its path, the way clicking a connection group opens its editor. It exists for the one thing the tree could not do: searching the DDL TEXT (a function's body, a column default, an index expression), which the editor's own find then does with regex, case and whole-word for free. The document reads in tree order, spans the connection's snapshot and the shared system catalogs alike, refreshes with the snapshot, opens in the preview slot (the row menu keeps a copy in its own tab), and says in its first line how many objects it holds, including what a 5 MiB cap left out. For scale, a 3,000 object database renders to about half a megabyte. It also says what it is NOT: the objects come in tree order, not dependency order, so the text is for reading and searching and never a runnable script (a dump is what builds one). The click can be switched off ("Open a folder as one document", in Objects, and a check row in the pane menu); the row menu's "Open in New Tab" opens the document either way. Each object is a FOLD that collapses to its header line, sticky scroll pins the one being read, a band and a margin bar mark where each begins and ends, and the LINE NUMBERS count from 1 per object, so "line 12" means line 12 of that object's own DDL rather than of a concatenation nobody has. Above each object sits a lens with the five things a reader wants there: Collapse, Expand, Select, Copy (its DDL, without the path header) and Open object, which opens that one object in its own tab. All of it applies to this document alone; every other editor keeps folding off, shows no lens and numbers its lines as before.

  • Pick a database instead of remembering its name. The connection editor's Database field grows a "Pick…" button that asks the server what it hosts (one throwaway dial, like Test) and offers the list, marking the current one and any the role cannot CONNECT to; the field stays free text, so a database that does not exist yet is still typeable. The connections tree's row menu gains "Open Database…", which saves a copy of that connection bound to the chosen database and reveals it: the whole "new database on the server" errand without duplicating and retyping. An existing connection to that database is revealed instead of a second one being made.

  • A connection you just created lands visibly. Saving a NEW connection (a copy, or a database opened as its own entry) now reveals its row in the tree, expanding whatever it sits under, and flashes it for two seconds in the Settings page's language, so the copy is never "somewhere in the list". A copy also keeps the original's group, which is where it used to disappear from.

Fixed ​

  • Duplicate now makes the copy, instead of a draft that could not be saved. The copy arrived with every field already filled, so the editor's "did anything change since I opened" test said no, left Apply disabled, and the copy existed nowhere until you edited a field for no reason. Duplicate creates the connection outright now: it appears in the tree, flashes, keeps the original's group, and its editor opens for renaming. A connection that genuinely does not exist yet (New connection) is savable from the first moment too, its button reading "Save connection" and its footer "not saved yet" rather than claiming everything is applied.

  • A duplicated connection lost its password. Nothing in the browser ever holds a secret, and a copy carried no id to resolve one from, so the saved copy had no password and could not connect. A connection made from another one now names its source and the server fills the blanks from it (Duplicate and Open Database alike).

  • The object tree's find searches everything the tree can show. Beside the row label (name and type), a query now matches the object's COMMENT, its column names and comments, and its parameter names. A hit inside the detail rows opens them for the duration of the query, with the match highlighted where it sits (the comment under the row shows whole while lit, so nothing hides past the length cut), so a match is never invisible.

  • Object comments show under their rows in the object tree, the way connection descriptions show in the connections tree: dim, italic, straight from the dictionary the snapshot already carries, their own line breaks preserved, with the full text in the row's tooltip. Both kinds of description now WRAP into lines instead of being cut mid-line; a comment has no upper limit, so what bounds it is a character cut with an ellipsis. All of it is configurable: "Object comments in the tree" (Objects), "Descriptions in the connections tree" (Connections) and one shared "Description length in trees" (Appearance, default 160); switched off, the rows stay compact and the text keeps living in the tooltip. Both panes' menus carry the switch too: an "Object Comments" check row in the Objects pane menu, Show/Hide Descriptions in the connections pane menu. Column comments inside the detail rows share the whole treatment (below their row, wrapping, the same length cut and setting) instead of the old inline single-line clip, and a comment the find lights up always shows, setting or no setting: a match is never invisible.

  • A "New in this session" shelf at the top of the object tree. What appears on a connection while this PlumeSQL session runs (your own CREATE the moment its refresh lands) collects on a shelf beside Frequent, newest first, and the shelf opens itself when something arrives, so a just-created table is one row from the top instead of a navigation exercise. Learned per app run and never written to disk; a DROP or a rollback takes its entry along. The schema.newGroup setting, the shelf's own right click and the pane menu's check row all hide it, and the shelf's right click can also clear the list to start it fresh.

  • Uncommitted objects show in the object browser, marked amber. A new setting, "Refresh through open transactions" (schema.refreshInTx, on by default, per connection overridable): the after-DDL refresh takes the same savepoint-fenced read through your open transaction that the Refresh Through Open Transaction menu item takes per click, and switching the setting off is the standing veto. A table created after BEGIN then shows up before COMMIT wearing a hollow amber dot (on the shelf and in the tree) until the transaction decides: COMMIT settles it into an ordinary new object, ROLLBACK removes it.

  • The object tree's folders show how many objects they hold, the filter window's numbers on the tree itself: every schema, type directory and cluster collection carries a quiet right-aligned count, and the Frequent shelf counts its objects too. A folder counts what expanding it would actually reveal, so a narrowed filter or an active find recounts to match the view instead of quoting a total the tree contradicts.

  • The object browser refreshes itself the moment your own DDL lands. When a run's command tags report a structure change (CREATE, ALTER, DROP and friends, plus DO and CALL, which might hide one), the snapshot gets the same cheap fingerprint check as the Refresh button, debounced, and deferred until the transaction the change sits in ends. On by default; the new "Refresh after structure changes" Snapshot setting (and its per-connection override) turns it off.

  • A snapshot staleness window replaces the blanket focus recheck. The new "Recheck when older than" Snapshot setting (default 12 hours) is the safety net for changes made outside PlumeSQL: returning focus to the window or revealing the object browser now rechecks only a snapshot older than the window, instead of dialing the database on every focus flip. "off" trusts the snapshot indefinitely. Startup behaves the same way: a fresh snapshot shows from disk without a dial.

  • The production guard grew two axes with three answers each. The single "ask before any write" checkbox became two policies on the connection (and group, escalate-only: the stricter wins): structure changes (DDL) and data changes, each Allow, Ask first or Block. Every statement of a run is classified before it is sent (comments, strings and dollar quoting understood, data-modifying CTEs and EXPLAIN ANALYZE included), so a SELECT 1; DROP TABLE x no longer slips past on its first keyword. Block refuses the run with the reason and the way to the connection editor; a DO or CALL body nothing can see into asks. Grid cell edits, row deletes and dump imports follow the data axis. Existing "Production guard" checkboxes keep guarding as Ask on both axes.

  • The Object Filter window is resizable. Drag its right or bottom edge (or the corner) to size it; the size is remembered per workspace, like its position. A side never touched keeps sizing itself to the content, and a remembered size larger than the screen clamps back into view.

Changed ​

  • Opening a database object no longer talks to the database. The DDL viewer shows the snapshot file, full stop: the automatic re-render on open (and the schema.refreshOnOpen setting that governed it, with its per connection override) is gone, along with the quiet-mode backoff that existed only to keep it from hammering a server that was down. Your own structure changes already refresh the snapshot as they run, the tree's row menu refreshes any object, schema or type family on request, and the viewer is getting a deliberate design pass of its own (docs/BACKLOG.md).

  • The object tree's top level reads in four visual registers. The shelves (Frequent, New in this session) wear small caps and the muted color, section furniture rather than folders; the cluster collections (extensions, roles, event triggers, ...) read muted, machinery rather than your work; ordinary schemas keep the full-strength label, they are the content; and the system schemas (pg_*, information_schema, the extension catalogs) dim and italicize. One glance now sorts what kind of thing a row is before the name is even read.

  • The floating windows stand out from the panes below them. The Object filter, the find bars and the Explorer filter hover over panels painted in the same palette and could melt into them; they now wear a visibly brighter edge and a deeper, ringed shadow, tuned per theme.

  • The Object Filter's checkbox now simply means visibility. Everything starts checked (everything is shown); unchecking a schema or an object type hides it, and a partially narrowed schema reads as a mixed box. The old inverse idiom (an empty board that nevertheless showed everything, where checking one schema hid the rest) and the separate crossed-eye state for row-menu hides are gone: one look, one meaning, the same language as the pane menu's checkmarks. The footer grew a "Hide all" next to "Show all", so "hide everything, then check the one schema I care about" is two clicks; unchecking everything is legal here and the footer says "Showing nothing". Unchecked rows go on the hide list rather than freezing a snapshot of today's schema list, so a schema that appears later shows up instead of being swept up by an old filter. Existing saved filters keep working as they were.

  • The Objects pane grew a Refresh button, and its menu slimmed down. Refresh sits in the pane's action bar now (a cheap change check; Shift+click forces a rebuild from scratch), so the menu's Rebuild Snapshot row is gone: the escape hatch is one gesture on the button, and the "unchanged" Log line teaches it. The menu's per-schema visibility toggles stay up to a dozen schemas; past that they fold into one "Filter Schemas…" entry opening the filter window (built for hundreds of schemas, where the menu is not) plus a "Show All Schemas" reset while a filter narrows the tree.

  • "Refresh an object when it opens" is now off by default. It costs one short connection per opened object, and with your own structure changes refreshing the snapshot the moment they run, the on-disk DDL is rarely behind any more. Turn it on (globally, or per connection in the editor's Snapshot section) when others change the database under you and every open must be current.

  • Automatic availability checks are now off by default. Every check dials the server (a backend process, a slot in max_connections, an audit log line), so it is opted into rather than out of: turn the "Check connections automatically" setting on, or switch a single connection to automatic in its editor, which always wins over the setting. Existing explicit per-connection and per-group modes keep working unchanged; the dots simply stay on remembered history until something checks.

  • Snapshot auto refresh is now positioned as the shared-server option. Its polling behavior is unchanged (and still off by default); with your own changes caught as they run and the staleness window covering the rest, the interval is only worth setting where OTHER people change the structure under you.

  • The system catalogs are read once per server instead of once per connection. pg_catalog and information_schema describe the PostgreSQL release, not your database: three databases of one server render them identically, down to the OIDs. They are rendered once now, into a store shared by every connection to that server and keyed by the release and by the catalogs' own contents, so a second connection gets them for the cost of one stat and an upgrade re-keys the store by itself. Your tree and the dictionary look exactly as they did; what changed is that nobody fetches and renders three megabytes of the same thing again per connection, and a PostgreSQL upgrade no longer rebuilds every snapshot you have.

  • An extension's catalogs are in the Objects tree by default, its chunk storage is not. The exclusion list used to treat everything an extension installs as one thing and leave it all out, so somebody writing a query against timescaledb_information.hypertables could not see it. The line is drawn where the cost is: a schema whose size follows the EXTENSION is a catalog and worth snapshotting, one whose size follows your DATA is storage and is not. TimescaleDB's catalogs are 41 objects; its _timescaledb_internal holds one table plus its indexes per hypertable chunk, thousands in production, every name machine made. So the catalogs come in, the chunks stay out, and each is its own checkbox in the connection editor. PostGIS Tiger the same way: the geocoder's functions in, the loaded census data out.

  • The Log says WHERE a refresh found the change. A refresh that re-read only part of the database now names those schemas, and how the object list moved: "DDL snapshot refreshed for shop: 812 objects in 0.4s, changes in sales, reporting (+2 new, -1 removed)". That is the difference between "something changed" and "my migration landed": the user recognizes their own schema. A refresh that had to re-read everything says nothing extra, because naming the whole database is not shorter than silence.

  • The snapshot's files are written several at a time. Writing a tree is thousands of independent small files, so it is now done by a handful of goroutines instead of one. A handful and no more: these are filesystem calls, not work, and past about four workers the filesystem's own serialization turns the parallelism into contention. Measured on an 8,400 object database: a full rebuild 3.3 s to 3.0 s, a refresh after one change 1.23 s to 1.06 s.

  • The Objects snapshot refreshes only the schemas that changed. The change check used to answer one question, "did anything move"; it now answers it per schema, so a refresh re-reads and re-renders only those and carries the rest of the tree over from the last one. Asking per schema is cheaper than asking once, not dearer, because the whole catalog no longer sorts in one go. What a schema's files SAY can change without that schema moving (renaming a column rewrites the views and foreign keys in other schemas that read it), so the set is widened through the dependency map, and anything that cannot be answered that way still rebuilds everything: a snapshot from an older PlumeSQL, one built with different settings, a change to extensions, publications, servers or event triggers, or more than half the schemas at once. Measured on a 20 schema, 8,400 object database: a refresh after one change goes from 5.0 s to 1.2 s. A database where nothing changed is unaffected, that check was always cheap.

  • Rebuilding the Objects snapshot is about 40% faster on macOS. A rebuild writes the whole tree into a staging directory and swaps it in, and it used to hard-link every file whose content had not moved instead of writing it again. On Linux that is exactly the right trade, twice as fast as writing. On macOS it is four times SLOWER than writing, because APFS implements a hard link with extra metadata records, so the optimization was costing 1.3 seconds of a 5 second rebuild. It is now a measured per platform choice, and macOS writes. Measured on a 8,400 object database: 5.0 s to 3.1 s.

  • Dependencies now come from PostgreSQL's own dependency catalog as well as from the generated DDL. What an object depends on used to be read out of the rendered text alone, which cannot see an edge that is never written as a qualified name: the index that implements a constraint, the server a foreign table sits on, the domain a view's column carries. Those appear now. The text is still read, because it is the only thing that can see inside a routine body, which PostgreSQL keeps as an opaque string and records nothing about.

Fixed ​

  • The object browser kept showing a neighbor tab's connection. A query tab whose connection was deleted (or that was deliberately detached) left the browser pointing at whatever the last followed tab used, so the tree said one thing while the tab said another. The follow now reads the ACTIVE tab: a tab that can carry a connection and has none follows as none, and the browser says it has nothing to follow. Tabs with no connection concept at all (Settings, Account, the editors, plain files) still keep the last target, so opening them never blanks the tree.

  • The Object Filter window called every cluster-global family "Extensions". The label map predated the new root families (event triggers, servers, foreign data wrappers, publications), so a database with an event trigger showed two "Extensions" rows, the first of which actually filtered event_triggers/. Cluster rows now wear their own registry labels, and an unknown future family falls back to its raw directory name instead of borrowing another family's.

  • After a restart the Objects pane forgot when it had last checked the database. It knew when the snapshot was BUILT, which is a different question, so it showed the build time until something checked again. The check stamp is kept beside the snapshot now and read back on startup.

  • An overloaded aggregate was four kinds of wrong. A function carries its signature in its name, an aggregate does not: avg(integer) and avg(numeric) are both just avg, with the arguments held apart. Every place that identified an object by its schema, type, name and owning table therefore could not tell one overload from another, and each place broke differently. They all shared one OID, so one comment, one owner and one set of grants covered the lot, whichever won. Their file names were handed out in whatever order the server returned them, so two builds of one database could name the same file differently and the tree flapped forever. A partial refresh pinned every overload to one path and the files of all but one vanished. And re-rendering one of them wrote a different overload's definition into its file. Rare in a schema somebody wrote, the rule in pg_catalog, which is why turning the system schemas on brought it out.

  • Turning the system schema exclusion off flooded the Objects tree. Asking to see pg_catalog also brought in PostgreSQL's plumbing: the TOAST schemas and one empty pg_temp_N for every session the database had ever had, since those outlive the sessions that made them. On a developer database that was twenty three extra roots, eleven of them empty, beside thirteen real schemas. They are not a preference now, the way the tree never shows a table's TOAST relation either. A tab's OWN temporary tables still appear in its own tree.

  • Another session's temporary table rebuilt your snapshot. A pg_temp_N schema is created on first use and stays behind, so the change check saw something new and every connected PlumeSQL rebuilt, always producing an identical tree. The check now covers exactly what the renderer renders.

  • A connection without full privileges got an incomplete Objects tree. The tables and columns came from information_schema, whose views show only what the connected role has some privilege on, so a limited role saw a tree with whole tables missing and CREATE TABLE bodies missing the columns it could not read, with nothing to say so. Both now read the catalog, like everything else in the snapshot: the tree describes the database, not the role's slice of it. What a fully privileged connection renders is unchanged, byte for byte on every supported PostgreSQL.

  • Adding a schema to a publication did not trigger the automatic refresh. PostgreSQL 15 records that membership in a catalog 13 and 14 do not have, so the change check could not read it directly; it now reads the membership through a view every release has, which is also what the rendered file lists.

  • A routine calling a different overload of its own name showed no dependency for it. Every overload answers to the same unqualified name, so an object's own name was ignored wherever it appeared, calls included. Only the first occurrence, the one in the CREATE statement, is ignored now.

  • The AI panel's status failed on a default installation. The global settings file is written with comments in it, the way it is meant to be read and edited by hand, and one of the two readers of that file did not strip them: /api/ai/status answered 500 with a JSON parse error.

  • A table appeared to use its own trigger's function. A table's file carries the table AND its indexes, triggers and policies, and the dependency rows were read from the whole file, so everything those statements referenced was charged to the table: a trigger function came out as "used by" the table, which does not use it, beside the trigger that does. The rows now come from each object's own statement. What a table's own definition names, an inline foreign key included, is unaffected.

  • Refreshing part of the tree after renaming a table could rename its index's file. A partial refresh reuses the file names the last snapshot handed out, but it looked them up by a key that included the owning table, and an index or a sequence keeps its own name when its table is renamed. The lookup missed, the old name was already taken, and the file came back as t_pkey.2.sql while a full build still called it t_pkey.sql: a file that flaps between the two forever.

  • A trigger function appeared to depend on its own trigger. The dependency rows under an object are read out of the generated DDL, and triggers, rules, policies and constraints were being matched by a qualified name that PostgreSQL never gives them: their names live inside their table, not inside the schema. Any reference to schema.name therefore also matched a trigger of that name anywhere in the schema, and since a trigger is usually named after the function it executes, that function ended up listed as depending on it. Tables picked up the same phantom rows. What uses what is unaffected, and snapshots rebuild once on their next check to drop the stale rows.

  • A function appeared to depend on the other overloads of its own name. A file names itself in its own CREATE statement, and every overload answers to the same unqualified spelling, so each one listed its siblings. An object's own name now means itself and nothing else.

  • Adding a value to an enum, or changing anything about a publication, an event trigger, a foreign server or extended statistics, now triggers the automatic Objects refresh. The change check hashes one catalog per object family it renders, and those families had none, so their changes stayed invisible until something else in the database happened to move the check. Adding an enum label has been invisible this way for as long as enums have been rendered.

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • Six object families the Objects tree never showed. Foreign tables now appear with their columns, their server and their options, next to the foreign servers and foreign data wrappers they belong to; extended statistics (CREATE STATISTICS) sit beside the table they describe; and event triggers and publications get their own root folders alongside extensions and roles. A database with an FDW or logical replication set up used to look, in the tree, as if none of it existed. Snapshots rebuild once on their next check to pick them up.

  • Refresh part of the Objects tree. A row's menu now offers Refresh at the same three levels Pin and Hide do: the object itself, its type family, its schema. Only what you name is re-read from the database, so refreshing one table on a large database costs a fraction of a full rebuild instead of the whole thing, and everything outside your pick is left exactly as it was. A dropped object disappears, a new one appears, and the Log gets one line saying what moved and how long it took. The pane's own Refresh and Rebuild are unchanged: they are the home for the whole snapshot.

  • The object dictionary describes objects, not just files. Every object now carries its comment; tables, views and materialized views carry their columns (name, comment and full definition, inherited columns included); functions and procedures carry their parameters (name and type separately), their language and their flags (immutable, security definer, strict, parallel, leakproof, cost, rows). Nothing about this costs an extra query: it all comes from the one query that was already rendering the DDL.

  • Columns and parameters in the Objects tree. The chevron on an object row now discloses its columns (with their type and their comment) or, for a function or procedure, its parameters, above the dependencies it already showed. A column row's menu copies the name, the qualified name or the full definition; nothing there touches the database. Which rows are expanded is remembered with the workspace, like every other tree state, and the rows are keyboard reachable exactly like the dependency rows (arrows in, left arrow back to the object).

  • CI now runs the tests that need a database (development only). Every live test in the repository used to skip there, so a green build said nothing about the catalog fence, the session serialization, the streaming and cancel paths, or the DDL snapshot: 31 tests reported green without executing. The build job gets a PostgreSQL service and both test variables, and the version matrix gets a workflow of its own, on a path filter over the snapshot code plus a manual dispatch. The matrix is never part of the build.

  • A snapshot on a macOS or Windows filesystem could lose an object's DDL. Two identifiers that differ only by Unicode normalization (café written as one rune or as e plus a combining accent) are two objects to PostgreSQL and ONE file to those filesystems, so the second one written silently replaced the first while the tree went on listing both. A decomposed name now carries a hash of itself in its file name and can never share a file with its composed twin.

  • A PostgreSQL version matrix for the DDL snapshot (development only, make test-versions): the renderer and the object dictionary against one container per major release, 13 through 18, on a fixture holding every object family and every naming and escaping trap. It asserts that every rendered file executes again on that server, that a snapshot of the replayed database equals the original byte for byte, that the dictionary matches the catalog, that two builds agree, and that neither a single object re-render nor a scoped build reports a phantom change. Manual only, never part of the build or a pipeline. The first four fixes below are the ones it found on its first run.

Fixed ​

  • A constraint trigger was rendered as a statement no server accepts. It came out both as its own CREATE CONSTRAINT TRIGGER and, wrongly, as ALTER TABLE ... ADD CONSTRAINT x TRIGGER inside its table's file, which made that whole file unusable. Constraint triggers now appear only as triggers, where they belong.
  • A function's OUT and VARIADIC parameters were lost. The rendered CREATE FUNCTION dropped OUT parameters from the signature and then added them back as a bogus RETURNS TABLE, and a VARIADIC parameter lost its VARIADIC. Signatures now come from the server's own renderer, so every mode, name and default is exactly what the function has. The signature is one line where it used to wrap.
  • An aggregate was rendered with its STATE type as its argument list, which is only right for the simplest aggregates and always wrong for an ordered-set one; an INITCOND was not quoted, so an array state produced invalid SQL; and COMBINEFUNC, SERIALFUNC, the moving-aggregate functions, FINALFUNC_EXTRA, PARALLEL and HYPOTHETICAL were dropped entirely. All of them are rendered now.
  • A table with two CHECK constraints could produce two different files from the same database, because constraints of one kind came out in whatever order the server returned them.
  • A NOT VALID constraint was rendered inside its table's definition, where PostgreSQL accepts the words and then marks the constraint validated anyway. It now appears only in its own file, whose ALTER TABLE ... ADD CONSTRAINT ... NOT VALID is the only form that means what the database means.
  • A table created OF a composite type lost that, and came out as a plain table with the type's columns spelled out.
  • An UNLOGGED sequence was rendered as an ordinary one.
  • Comments with a backslash produced SQL that does not parse. A comment holding both a quote and a backslash (it's C:\temp) was rendered with the quote escaped and the backslash left alone, so the statement was invalid and, in lowercase-keywords mode, the formatter went on to rewrite the comment's own text. Comments are now escaped once, the way the server itself does it, and a comment's text is never passed through the formatter.
  • Enum labels containing a quote were escaped the same broken way.
  • An installed extension whose control file had gone missing (after a server upgrade, or a removed contrib package) was silently dropped from the snapshot. The snapshot also no longer reads control files at all.
  • A table's bundled indexes, triggers and policies came out in whatever order the database happened to return them, so two identical builds could disagree about the file. They are ordered by name now.

Changed ​

  • Where comments appear in a DDL file. An object's comment and its columns' comments are now a block at the END of the file, after the definition and before the ownership and privilege clauses, one rule for every object kind. An index's, trigger's or policy's comment lives only in that object's own file, no longer repeated inside the parent table's; COMMENT ON SCHEMA and COMMENT ON EXTENSION are emitted for the first time; a domain's comment now reads COMMENT ON DOMAIN instead of COMMENT ON TYPE. Every snapshot rebuilds once on its next check.

  • A schema with no objects at all now appears in the Objects tree, with its own schema.sql. It exists in the database, and a partial refresh that empties a schema must not make the schema itself disappear.

  • In lowercase-keywords mode the synthesized CREATE SCHEMA statement follows the setting like every other statement; it stayed upper case before.

  • Routines are read from pg_catalog instead of information_schema, which also means a connection whose role cannot execute a function no longer silently loses it from the snapshot.

  • The snapshot query is faster: a dead join that materialized an entire information_schema view on every build is gone, the sequence ownership lookup reads one catalog row instead of every column in the database, and the routines arm no longer goes through information_schema at all. On a 400 table database the full build's main query went from 870 ms to 500 ms, and re-rendering a single object from 870 ms to 18 ms.

  • The object browser follows the active tab. The Objects pane, the status bar readout and the Activity / Top queries panels now show the connection of the tab you are on, switching instantly from the snapshot on disk as you move between tabs; new tabs, scripts and files inherit the last opened tab's connection (never the browser pin, which is about the tree alone). The header dropdown leads with "Automatic" (the follow, the default) and picking a connection there, or "Browse Objects (pin)" in a sidebar row's menu, PINS the browser to it until Automatic clears the pin; the pin is remembered per workspace. When the browser follows a tab whose LIVE session is idle, its explicit operations (Refresh, Rebuild, opening an object) run through that session, so the tree and the DDL viewers see the tab's own temporary tables. Your open transaction is never touched without asking: while one is open, refreshes take their own one-shot connection, and the pane menu offers "Refresh Through Open Transaction", the one explicit way to see your uncommitted DDL in the tree, protected so the transaction survives even a failed or timed-out refresh. The Objects header wears the shared session's state and pid (the same dot and the same backend as the tab's own readout: one physical connection, one identity); with no session to share, or with a pin, it reads "not connected" and dials one-shot as before. While a refresh borrows the session, the tab's Run waits (its tooltip says why); a running query is never waited on: the refresh silently takes its own one-shot connection instead. Background work (auto refresh, focus rechecks) always dials its own connection and never touches any session. Canceling a query no longer costs the session its state: a cancel now stops just the statement, and an open transaction survives it.

  • Every system query PlumeSQL runs itself (introspection, snapshot builds, activity, top queries, SQL dumps, checks) now schema-qualifies every catalog reference, relations and functions alike, so an exotic search_path on the connection can never bend what they read. The one deliberate exception is pg_stat_statements, whose install schema only the search_path knows.

  • The editor toolbar splits in two: the connection (Run and the readout) owns the left half and may grow as wide as its name needs, the editor tools sit at the right edge and are the first to yield when width runs out. The connection is never the part that gets cut.

  • A connection reads the same everywhere it is named: the standard line is "name (Vault / Shared / the discovered source file / this session) · url · vX", and it flips to "url · name (kind) · vX" when the connection.display setting leads with the url. The status bar and every connection picker row now follow it; the status bar appends its own pid, age and, when it applies, "superuser" at the clippable tail, the pickers their group label and the "current" tag. The toolbar deliberately wears just the short form (the name, or the url per the display setting) so the editor tools keep their room, with the full line in its tooltip; the toolbar and status bar keep the transaction chip.

  • The connection readouts stay lean and the tooltip becomes the session's ID card. Only the transaction chip remains on the toolbar and status bar ("tx" amber, "tx failed" red); everything else the server reports about the live session moves into the tooltip, one line per concern: the name with its kind (Vault, Shared, the source file, or "this session"), the url, the basic facts (idle / in transaction / transaction failed, version, pid, connection age) and the reported extras (session authorization with the login user when they differ, superuser, replica, read only, TimeZone). All of it comes from ParameterStatus reports the wire protocol pushes at connect and on every change; none of it ever costs a query. The Objects header still says "replica" while it shares such a session, and the Log's connect line now carries the same full ID card (url, version, pid, authorization, superuser, replica, read only, TimeZone) instead of just the pid.

  • Changing the search path mid-session no longer strands the object dictionary: after a run whose statements included a SET, RESET or DISCARD, the captured search path behind unqualified Go to Object lookups is re-read (one tiny query, only once the session is idle again — never inside an open transaction) and the seek follows the session's real resolution order.

  • Opening a session runs one query fewer: the server version now comes from the connection handshake itself instead of a select version() probe (the readouts show the same "v17.4"; only the compiler/platform tail of the long form is gone). The single remaining query at open is the search path capture behind Go to Object.

  • The connection readouts speak PostgreSQL's own vocabulary now: a tab whose connection has not dialed yet (grey dot, connects on the first run) reads "not connected" instead of "idle", and "idle" is reserved for what PostgreSQL means by it, a live backend with no open transaction (what the Activity panel already showed). The Objects header's empty state reads "Nothing to follow".

  • An open transaction is visible at a glance, on every readout of the connection: the session dot (the tab strip, the toolbar, the status bar, the Objects header) turns amber while a transaction is open and red once it failed, and the toolbar and status bar carry a "tx" chip ("tx failed" once aborted). All of it feeds off the status byte the wire protocol reports with every statement, so the marker costs no query on your session; the tooltips spell the state out and the Objects header reads "in transaction · pid" while it shares such a session.

  • The SQL Explorer is ONE tree now, rebuilt in the Objects pane's idiom: the collapsible Scripts and Files panes, their sash, the inline search box and the glob bar are gone. Three roots live in a single keyboard surface: Frequent (the files and scripts you open most, real frecency now, with Remove from Frequent on its rows), Scripts (the global plumesql dir, as before) and the workspace files. A pin strip rides above the tree, the Objects tree's exact mechanism: Pin to Top from any row's menu, per workspace. The toolbar matches the other panes: the listing counts, find (a floating Explorer find window with case, whole word and regex) and a filter button opening the floating Explorer filter window: the SQL / SQL + config / Everything glob presets, the free pattern list and empty folders, with Reset to default (which roots show is pane composition, not a file filter, and lives in the pane menu). Creation moved to the root nodes' context menus and the toolbar; the files.view setting retired with the panes. Find, filter and pins persist per workspace, and the window/find state comes back exactly as left.

  • The Connections sidebar is a tree now, rebuilt in the Objects pane's idiom. Connections carry user assigned GROUPS (short labels such as production, local, docker), set in the connection editor's new Group field (with a list of the existing groups to pick from) or the row menu's "Move to group...", which also creates new groups; a group node's own menu renames the group inline (a file tree rename) or removes it (its connections stay and fall back under their kind). A team shared connection's group is the team's cloud group, read only. The tree's shape is fixed: group, then kind, then connection, with ungrouped connections under kind nodes below the groups; the kinds are three (Vault, Shared and Discovered, the environment candidate counting as discovered), and the new connection.treeOrder setting flips the nesting to kind first. Group and kind nodes read like the file trees' directory rows: the same chevron, an icon in the icon slot (a rack of servers for a group; a database for Vault, the team for Shared, a compass for Discovered) and a plain label with a dim member count. Rows carry the tinted elephant, name and url (which one leads follows the Connection display setting) and a dim version tag; a connection with a description gets a second line beneath, dim and italic, one ellipsized line with the full text in the row tooltip, and while a find query marks something in it the line wraps out so no match hides behind the cut. The availability verdict lives in the row tooltip and the row text goes red when the last check failed. The old collapsible sections, the inline search box and the origin badges are gone. The pane's toolbar matches the Objects pane: the fleet stat leads ("6 connections, 2 down, checked just now" with the spelled out counts in its tooltip), and the right side holds find, new connection and a three-dot menu (which a right click on the tree's empty background opens too): New Group, an inline draft row at the top of the tree, the file trees' pattern, where the group stands empty until a connection moves in (waiting per workspace, graduating by itself on its first member; a group that loses its LAST member stays standing as an empty one too, and only the explicit Remove group deletes it, a rename never leaving the old name behind); check every connection; two contextual view toggles (Hide/Show Not Available Connections, the connection.showUnavailable setting, healing itself when a check succeeds, and with it on the node counts spell out what left, "(1, 2 hidden)", a container of only-down members standing at "(0, n hidden)" instead of vanishing; Hide/Show Kind Nodes, the connection.kindNodes setting, flattening a group straight to its connections while groups lead, the kind staying told by the row tooltip and the editor; and Group by Kind/Group First, flipping connection.treeOrder; each writes the workspace scope so the choice stays with the workspace, with the global scope as the default) and the workspace and global Connections settings. The find button (or Ctrl/Cmd+F in the tree) opens a floating Connection find window, the object find's twin: it searches names, urls, descriptions, groups, sources and ecosystems, narrows the tree, highlights matches and cycles with n of m. The tree is fully keyboard operable (one Tab stop, arrows, Enter edits, Delete removes a saved connection, Shift+F10 for the row menu) and its expansion, selection, scroll and find state persist per workspace. Arrow navigation honors the same Tree navigation preview setting as the other sidebar trees: "follow", the default, opens the connection's editor preview as the cursor moves, "enter" waits for Enter, one behavior everywhere.

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • The light theme's SQL keywords are blue now (GitHub Light's keyword blue, calm enough for bold, the pair of the dark theme's VS Code Dark blue); the purple read as foreign next to it. The rest of the light palette stays.

  • The Objects roots read in bands now, not raw alphabetics: Frequent, then the cluster collections (extensions, roles), then the user's schemas alphabetically, then the system schemas (whatever the default exclusion list names: pg_*, information_schema, the TimescaleDB and Tiger families) last instead of flooding the top by their underscores. One comparator drives the tree, the type filter window and the pane menu, so the three can never disagree.

  • Every object always lists: the Objects tree's listing is uncapped (the snapshot is a bounded local artifact; nothing may silently vanish from a tree), while the workspace listing's runaway guard rises from 2,000 to 20,000 files with its note kept. Excluding NO system schemas is finally expressible: a lone dash ("-") in schema.systemSchemas means "exclude nothing" (an empty value only ever inherited the level below, so unchecking every exclusion used to silently fall back to the default and pg_catalog never appeared).

  • The object dictionary arrives (docs/OBJECTINDEX.md, its authoritative spec): every snapshot object seekable in O(1) by name in any PostgreSQL spelling (ime, "ime", schema.ime, schema."ime", "schema"."ime", "schema".ime), quoted parts kept exact, unquoted folded to lowercase, qualified keys joined on NUL so dotted names never collide, unqualified lookups walking the connection's REAL effective search path (current_schemas(true), captured at dial time next to the server version) exactly as PostgreSQL resolves. Values carry the DDL file, the type and the dependencies in one step. Both sides share one source (the snapshot's object list, served by GET /api/schema/objects): the server keeps its index for dependency resolution, the client rebuilds its mirror whenever the snapshot loads or refreshes, the foundation for the editor services to come (autocomplete, object navigation).

  • The connections tree pins too: Pin to Top on any connection row's menu (every kind alike) lifts a first-class copy of the row into the strip above the tree, the file trees' exact band, same face, menu, keyboard cursor and editor opening, with the pin marker on both copies and per-workspace persistence by connection id. Pinned rows never drag; Unpin from Top takes them back.

  • The Objects pane grew its three-dot menu, on the toolbar and the tree background's right click alike, the other panes' manners: Refresh Snapshot and Rebuild Snapshot (the rebuild used to hide behind Shift+click on a refresh button, now retired with the button), root visibility checkboxes (Frequent plus every schema and cluster collection, whole-root shortcuts writing the SAME filter state the type filter window edits fine-grained, the last shown root refusing to uncheck) and "Open Snapshot Settings for <connection>", which opens the connection's editor landed on its Snapshot section. The standalone Snapshot settings tab is retired: the connection editor's Snapshot section is the one home of the overrides, and old sessions carrying the placeholder tab simply drop it.

  • Revealing a tab's row in a tree lands it BELOW the pinned ancestor headers now: switching to a file whose row would park at the tree's top edge scrolls a little further, so the row and its whole ancestor chain are visible instead of the highlight hiding under the sticky stack.

  • Tree highlights mean ONE thing everywhere now: the active tab's row. The Objects tree used to keep its last-opened object lit after the tab moved on (or closed), which the two-docks layout made plainly inconsistent next to the Explorer; the lingering paint is gone, and the remembered selection still brings the cursor and scroll back when the tree returns, it just does not glow for a tab that is not there.

  • The sidebar became TWO docks: any side tab can move to the left or the right edge (right-click the tab, "Move Objects to Right Sidebar"), and tabs on both sides mean two sidebars visible at once, each with its own strip, active pane, width and collapse. Two trees side by side for the first time: the Objects tree on one edge while the Explorer stands on the other. Four Appearance settings carry the choice (sidebar.sideConnections/Schema/Files/Snippets: default, left or right), with the existing Sidebar position setting staying as the default side, so everything untouched looks exactly as before. Each dock collapses, resizes and remembers its active tab independently (per workspace, like the single sidebar always did); a dock with no tabs vanishes; Cmd/Ctrl+B keeps toggling the default-side dock. The focus cycle (F6) reaches both docks, the side tab shortcuts land in the dock that hosts the tab, and the panel cycle key walks the left strip then the right one. The floating find and filter windows now share ONE stacking order across every tree, so two open windows from the two docks raise above each other by click, exactly like windows.

  • Connection GROUPS are editable objects now: "Edit group" on a group's row (or its editor's link in the connection editor) opens the group in its own tab, the connection editor's Properties, Session and Snapshot sections over the group's own stored properties (groupmeta.json in the global plumesql dir, keyed by the group's label; a rename moves everything atomically, and Remove group deletes the properties too, offered in the editor's footer as well). What a group sets, its member connections inherit: the resolution for every connection setting is connection override, then group, then the workspace and global scopes, applied by the server on every dial (session parameters, connect timeout) and by the client for the snapshot options and the availability check mode. Every "Inherited" line in both editors names exactly where the value comes from: the group ("Inherited from group "production"😊, this workspace's settings, the global settings or the hardcoded default, and the connection editor always offers "Edit in group" when the connection lives in one, next to the scope links. The Properties section tells its inheritance story the same way: the Color swatches carry an inherit line (the group's dot tints a member without its own color, and the "none" swatch reads as inherit), and a Read only or Production guard forced by the group says so right at the checkbox, with the way into the group's editor; unchecking the connection's own box is the way back to inheriting, since the group's flag can only add, never be undone from below. A group's color tints members that have no color of their own (the tree's rack icon wears it too), and the safety flags are escalate-only: Read only or Production guard on the group is forced on for every member (the grid stops offering edits, the write guard asks, the server refuses writes), a member can only add its own on top. Team shared connections never inherit; their group is the team's. Live sessions are never touched: like the connection editor, the group editor's footer counts the tabs still running on the previous settings and leaves reconnecting to you. A group's row behaves like a connection's in every navigation aspect: the follow preview opens its editor as the cursor lands (the click's toggle schedules it too), Enter opens it deliberately (expansion stays on the arrows) and Delete removes the group, its own confirm included. Group editor tabs restore with the session (dropped only when the group itself is gone), and the Session and Snapshot sections are one shared component now, so the two editors cannot drift apart.

  • The Explorer's root visibility and its Frequent history are settings now: files.showFrequent, files.showScripts and files.showWorkspace (on by default; the pane menu's checkboxes are their one interactive home besides the Settings page and write the workspace scope, and with every root switched off the workspace files show anyway; the filter window sticks to filtering files), plus files.frequentLimit (50 by default), the objects tree's Frequent history dial for files: how many opened files and scripts PlumeSQL remembers per workspace.

  • The Explorer grew its pane menu, on the toolbar's new three-dot button and the tree background's right click alike: the root visibility toggles (Frequent, Scripts, Workspace Files as checkboxes, at least one always on), Clear Frequent (the shelf and the picker's recents restart, logged), contextual creation that SAYS where it lands ("New Script in Scripts", "New File in sub", resolved from the tree's cursor or the active tab's row, hidden when neither points anywhere) and the workspace and global Files settings.

  • The Explorer drags too: a file or folder onto a folder (the Scripts and workspace roots included), with the destination inviting under the pointer and the drop answering with a small Move Here or Copy Here menu at the release point, the JetBrains gesture (dismissing it cancels). Across the roots a drop PROMOTES: a script becomes a workspace file or a file becomes a script, files only, and Scripts accepts only SQL (a refused destination simply never lights up); a moved file's open tab follows it to its new root. The Frequent shelf and the pin strip do not drag: they are shortcuts, not places. Cut, Copy and Paste in the row menus stay the keyboard path, and the app tooltips keep quiet for the length of a drag.

  • Connections drag between groups: a row dragged onto a group node (empty ones included), a kind node (its group when nested, out of any group at the root) or another connection (wherever that one lives) moves it there, with the destination inviting under the pointer and the tree revealing the moved connection after the drop. Team shared connections stay put (their group is the portal's), a plain click still opens the editor, and Move to group remains the keyboard path.

  • The vault kind speaks its own name: the connections tree's node, the editor's kind chip and the pickers say "Vault" instead of "Saved" (which read as the verb), and the candidates' promote action is "Copy to Vault".

  • The connection editor's sticky header tells the whole identity now: the connection's kind (Vault, Shared, Discovered) sits as a small iconed overline above the title, and its group rides the right side as a real picker button (the group icon, the name and a chevron). A click opens the Move to a group dropdown pinned right under the button, with the existing groups, New group and No group; a pick applies at once, the tree regroups, reveals and selects the moved connection, and the editor's own Group field follows, so no two surfaces disagree. Team shared connections show their portal group read only.

  • Preview tabs are optional now (the tabs.preview setting, on by default): off opens every file, object and page as a permanent tab right away, VS Code's enablePreview parity.

  • A DDL viewer tab's tooltip now says what it is: "object of <connection>, a read-only DDL view" instead of the query tabs' "connects on first run", which these tabs never do.

  • The "values in settings.json were ignored" Log entry carries its answer now: an Open settings.json button that opens the offending scope's raw file, where the ignored key is fixed or deleted in place.

  • How the sidebar reacts to the active tab is configurable now, two Appearance settings. "Reveal the active tab in the sidebar" (on by default) is the existing behavior made optional: activating a tab highlights and scrolls to its row in the tree it belongs to, expanding collapsed groups on the way; off leaves the trees exactly as arranged. "Sidebar follows the active tab" (off by default, it moves the sidebar around aggressively) is new: activating a tab also selects the side tab it belongs to (a connection editor selects Connections, a DDL viewer Objects, a file or script the Explorer), so the tree beside a tab is always the one it came from. The sidebar tabs' right-click menu carries both toggles too, next to a theme switch that names the move it makes ("Switch to Light Theme") and an Open Appearance Settings entry. Tabs with no sidebar home (Settings, Account, unsaved scripts) leave the sidebar alone, a collapsed sidebar stays collapsed, focus never moves, and neither the startup session restore nor closing a tab ever switches it.

  • Discovered and environment connections are fully editable now except for their dial parameters: the Parameters and Advanced sections stay disabled (the source file owns them and that is where they are edited), while the properties (color, read only, production guard, availability check mode) join the name, description and the session and snapshot overrides as the user's to set, saved under the candidate's stable id in the global dir and honored by every dial (a read only candidate really refuses writes). Copy to saved connections carries the whole customization onto the new entry. The candidate's info card grew a SOURCE row, the settings page's FILE line: the exact path of the file the parameters come from, with the pencil that opens it in the editor (it replaces the footer's Open source file button). Also fixed on the way: saving a connection's parameters no longer wipes its stored check mode and setting overrides.

  • The connection editor's info card leads with the availability verdict, a green or red dot and a sentence ("Available, checked just now.", red "Not available, checked ..." with the probe's error in the tooltip, "Checking availability..." while a check runs), so the editor tells the whole story without a trip back to the sidebar. The card now reads one fact per row: the availability sentence, the history folded into a sentence ("Last connected ..., 7 connects and 2 failures so far."), the server version verbatim, and the id and snapshot paths each on their own row. The availability rings over the elephant icons are gone, both in the editor's header and on the sidebar rows: the words carry the verdict now (the sidebar row's stat line took over the ring's tooltip, the spelled out state with the check's verbatim error), and a dot in PlumeSQL means a session again, nothing else.

  • The sidebar can dock to either side of the window: the sidebar tabs' right-click menu grew "Move Sidebar Right" (or Left, the label names the move it will make; written to the workspace settings, like the other toggles there), and the sidebar.position setting carries the default. Docked right, the whole row mirrors: the tab strip sits at the window's right edge with its separator flipped, and the width drag measures from that edge. Left stays the default.

  • The connection editor grew a Session section: the PostgreSQL session parameters every dial of the connection starts with, whatever its job (tab sessions, the object browser, snapshot builds, tests, availability checks). Application name (the pg_stat_activity template, whose default is "plumesql {owner}@{workspace}" now instead of a bare "plumesql"; {workspace} is the canonical placeholder, {dir} still works), connect timeout (default 15 s, at last a real setting), statement_timeout and idle_in_transaction_session_timeout as safety brakes, search_path and time zone as session defaults, and a free-form name=value list for everything else PostgreSQL accepts in a startup packet. Each control names its inherited default and links to the workspace or global Connections settings, where every one of them has its default; the connection's own override, stored with the connection, beats both, and everything rides the editor's Apply bar. Live sessions are never touched: the new parameters apply from the next connect, and the footer says how many tabs still run on the previous ones. Each of these parameters carries a "PostgreSQL docs" link, on the Settings page and in the editor alike, opening the parameter's own page in the system browser; a connection that has connected before remembers its server's version, so its editor opens THAT release's documentation (postgresql.org/docs/17/...), falling back to /docs/current when the version is unknown.

  • The object tree grew a Frequent group at its very top: the objects you open most on this connection, sorted by opens (the counter sits dimmed at the row's end), then by name, up to twelve of them. The rows are first-class tree rows, same click, menu, dependency disclosure and keyboard as anywhere else, and the group's expansion is remembered per connection like any folder's. Opening from the group (or from the pinned strip) no longer scrolls the tree to the object's own row, the same exemption the dependency rows always had: a copy is a shortcut, and yanking the tree defeats it. Reveal in Tree on the copy's menu is the deliberate way there. It hides while the tree is being filtered, and it feeds off the same open history as the pickers' Frequent sections, so all of them always agree. The group is optional and easy to flip either way: right clicking it hides it, and while it is hidden every tree row's menu (and the tree background's) carries Show Frequent, so no hunting for empty space; the palette has the same lever ("PlumeSQL: Hide/Show Frequent Objects") and the schema.frequentGroup setting persists the choice per directory. Its menu also offers Clear Frequent, which resets the history for that connection, and a shelf row's menu offers Remove from Frequent, which forgets that one object; both are logged, so the chronology says why the shelf changed. How much history is kept is now yours to set: schema.frequentLimit (the settings' new Objects section, 50 per connection by default) caps the tracked objects, evicting the least used, with the count decaying by age so a daily workhorse survives a burst of one-offs.

  • The editor tab well grew Visual Studio's tricks. Tabs can pin: the pin icon on each tab (or its context menu, or "PlumeSQL: Pin Tab" in the palette) moves it to a pinned row that sits above the normal tabs and stays put until unpinned; dragging a tab into or out of the pinned row pins and unpins it too, and pinned tabs are saved with the directory's session. The well itself knows four layouts, switched from its own right-click menu, the tabs.layout setting or the palette's "PlumeSQL: Tab layout" commands, per directory: multiple rows, the new default, where tabs wrap into new rows instead of scrolling, Visual Studio style; the single scrolling row it used to be, which now grows arrow buttons while the tabs overflow the strip; and a vertical column on whichever side of the editor you like (vertical left, vertical right), with the pinned section on top. Tabs also come in two sizes now (the tabs.size setting, or Small Tabs in the well's menu): large, the shape the well always had and still the default, and small, a compact variant that shrinks the rows and their text in every layout. The tab context menu (and the well's) gained Close All, and, while any tab is pinned, Close Pinned and Close Unpinned close one row at a time.

  • Connections are checked for real, in the background, so their dots say whether a server answers right now. A check is one throwaway connection (dial, select version(), close) and the outcome lives only for the app run: nothing about availability is ever written to disk, so a fresh start knows nothing and learns the truth in about a second. Checks run on startup, when the window regains focus (at most once a minute), on demand from the Connections header button or the palette ("PlumeSQL: Check connections"), and on an opt-in timer. They run in parallel, at most six at a time, with a short timeout (3 s by default) so one dead host cannot hold up the live ones behind it, and one that keeps failing backs off (30 s, then doubling to 5 minutes) instead of being dialed on every focus. Two servers listed twice (a saved entry and the discovered candidate behind it) are one dial, shared. Every real connect, a session, a Test, a snapshot build, updates the same state for free, and a check never touches the stored history: it does not count as a connect.

  • Checking is optional at two levels, because each check briefly costs a backend process and a slot in max_connections on the target server. Per connection: a "Check availability automatically" toggle in the connection editor, where team shared connections default to on demand and everything else to automatic. The row menu's Test already dials the server on demand, so it does not carry a second way to do the same. Globally: connection.checkAutomatically (on), connection.checkTimeout (3000 ms) and connection.checkInterval (0, off) under Connections. The server resolves connection.checkAutomatically too (workspace over global, between a connection's or group's explicit mode and the origin default), so the automatic pass and the editor's "inherited default" always agree.

  • A server that comes back is noticed. When a check flips a connection from unreachable to reachable, the Log says so, the DDL viewers come out of their quiet mode, and the object browser re-checks its snapshot by itself: start the database and the tree catches up without a click. Flips in either direction are logged; an automatic pass that found nothing new stays silent, so checking on every focus never fills the Log.

  • The sidebar tab strip can drop its text and stay a bare icon rail: a new "Tab labels" entry in the strip's right-click menu, below the tab visibility checkboxes, and the sidebar.tabLabels setting under Appearance. Labels stay on by default, so the strip looks exactly as it did until you switch them off. Like the visibility toggles it writes the workspace scope, so the choice stays with the directory. Nothing becomes unnameable: every tab keeps its name and shortcut in the tooltip, and the pane header still names itself. Turning them off only shortens the tabs, the strip keeps its width.

  • The sidebar tabs can also be large instead of the compact default: a "Large tabs" entry in the same right-click menu and the sidebar.tabSize setting under Appearance, small by default. Large grows the icons and their padding (the strip widens with them, activity bar style) and combines with the labels toggle, so an icon rail can be small or large too. The same icons in the pane headers keep their size.

  • The Go to File picker grew into one "go to" surface, renamed Go to Tab or File in the palette: every open tab is listed now (the view tabs like Settings, Account and the connection editors included, and scratch queries too), then the recent files as before, then a new Schema objects section with the schema browser connection's objects ranked by frecency: how often an object's DDL was opened in this directory, decayed by how long ago it last was, so the objects actually worked with float up without fossilizing. Each row shows when, how many times and the object's snapshot path. The history lives in the per directory session (per connection, capped, counts and last-opened times), heals itself when a pick fails to open (the object was dropped), and is forgotten with its connection.

  • A DDL viewer's right status bar slot carries the object itself: its type icon and signature, as a button where a SQL tab shows its connection. Clicking opens a picker with the frecency-ranked frequent objects first and the snapshot's full listing below, searchable, so jumping between objects of a schema no longer needs a trip through the tree; it grows up right-aligned like the connection picker next door. The snapshot's source connection is named in the button's tooltip, not as an elephant readout that would pass for a connection picker. The same picker is one palette command away as "PlumeSQL: Go to Object", offered while the schema browser has a connection and working from any tab (a DDL viewer's own connection wins over the browser's).

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • Opening a database object no longer floods the Log when the server is down. A DDL viewer reads from the snapshot on disk and only then re-renders that one object live, so an unreachable database used to raise an error per object clicked while the content was already on screen. Now the first failure says once, per connection, that the server is not answering and viewers are showing the snapshot (with Try again on the entry); the live re-render is skipped until it answers, and a line says when it does.

  • Whether that live re-render happens at all is a setting, schema.refreshOnOpen (on by default), overridable per connection in its Snapshot settings, which is where it belongs: the cost it trades away is one short connection per opened object, felt over a slow link or an SSH tunnel and invisible on a local database. Off, a viewer shows the snapshot until the next Refresh.

  • The Log panel is fully keyboard operable, the last panel that was not: it is one Tab stop with the rows roving inside (landing starts at the newest), arrows and Home/End walk them, Enter runs what the entry offers, Delete dismisses, the primary modifier plus C copies (a text selection still copies normally) and Shift+F10 opens the entry menu, which now carries the entry's own actions too. The focused row wears the same accent edge as a tree's cursor, and the cheatsheet has a Log panel section.

  • Log entries can answer for themselves, as light links on the row in the same style as its Copy and Dismiss. A DDL snapshot that failed to build (the database was not up, the port was wrong) offers Try again and Edit connection; hiding something in the object tree offers Unhide, so that entry no longer has to send you to the filter window to undo it; a save that failed offers Save again once the tab still holds the change, and a file deleted outside PlumeSQL offers Save to recreate. The offer is recorded with the entry, so it is still there after a restart, and it is resolved against the state as it is now: a connection since deleted, a path already visible again or a rebuild already running simply show no button rather than a dead click, and an entry written by another build shows none either. What an action does is logged in turn, so the chronology stays complete.

  • The Log button carries a quiet dot when something non-error was logged while the panel was closed, where only errors used to signal (their red count still wins when there is one). Its tooltip says how many entries are new, and they count as read while the panel is on screen, so the dot never lights behind a Log the user is already watching. The watermark rides in the per directory session.

  • A new log entry also flashes in the status bar for a few seconds while the panel is closed, so it is noticed without opening anything: log.flash sets the seconds (off, 2, 5 or 10, five by default) and log.flashPosition which half of the bar it borrows (the right one, over the readouts, by default; the left one covers the workspace and the current tab instead). The newest entry replaces whatever is showing rather than queueing behind it, and it does not wait out its seconds when you are already busy: any click or keystroke anywhere retires it. It reads as its own band, a faint accent wash with the accent edge the app uses for what is live, red throughout when it carries an error; the full text is on the tooltip, and clicking it opens the Log and hands the bar straight back.

  • The log file's size cap is a setting now, log.rotateSize (256 KB, 512 KB, 1 MB, 2 MB or 5 MB, 512 KB by default, so nothing changes unless you say so). Two generations are still kept, so the log costs at most twice the cap on disk.

  • The Log says when it rotates. The log file has always been capped (a generation of 512 KB, then the file is retired and the older one deleted, two on disk at most), but the deletion happened silently, so a gap in the history looked like a mystery. An append that trips the cap now reports it back and the app records "Log file rotated at its size cap; the previous generation (512 KB) was deleted" as an entry of its own. The housekeeping also moved off the request path: the append answers as soon as its bytes are on disk and the delete and rename run in the background, one at a time.

  • Hiding and pinning a schema object now leave a trace in the Log, so a choice made days ago can be tracked back: one line naming what changed and on which connection. A hide also says where to undo it ("unhide it in the schema filter window"), since nothing in the tree brings a hidden row back; a pin needs no such hint, the strip shows it. Every reverse action writes its own line, and Show all logs the bulk clear with how many entries it restored. The filter window's own checkboxes stay out of the Log: they are flipped while looking straight at the state they change, and the bulk rows move dozens at once. Until now the only trace of a hide was a recolored funnel and a "3 hidden" in the snapshot line.

  • Dependency rows have a context menu at last, where they had none: Reveal in Tree walks to the referenced object's own row (expanding the branches on the way, moving the cursor and the keyboard there), then the same Copy Content, Copy Name and Copy Qualified Name the object's own row offers, acting on the referenced object rather than the row it hangs under. Pin, Hide and the disclosure stay out: a dependency row points at an object, it is not that object's place in the tree. Reachable with the ContextMenu key too, and when the target is hidden by the filter, Reveal says so instead of quietly doing nothing.

  • The schema tree's row menu finally acts on the object, not only on the tree around it: Open in New Tab (a row click still opens the preview slot, which the next click replaces), Copy Name, Copy Qualified Name (public.users, and "App".t where the identifier needs quoting; a function keeps its argument list, which is what DROP and ALTER want) and Expand or Collapse Dependencies for the objects that have any, next to the Copy Content that was already there.

Changed ​

  • Nothing in the connection editor applies before Apply changes now, the availability check mode included: it used to apply the moment the select changed, the editor's one exception, and it rides the draft like everything else instead (Discard reverts it, the revert button edits the draft). The mode change still writes its line to the Log on Apply, and turning automatic still checks the connection right away. The Settings page stays immediate on purpose: its rows are independent values with their own revert, while the editor is one form describing one entity, tested before it is applied.
  • The info strips share one home and one face now: the first thing in the scrolled content, a bordered card under the sticky header. The Settings scope strip (what the picked scope means, its file, Edit in settings.json) moved out of the sticky chrome into the content, the scope CHOICE itself staying sticky above; the connection editor's stats card moved back above the form from inside the Connection section, so both tabs open the same way: slim sticky header, info card, then the sections. And with nothing sticky about it any more, the scope card stopped being economical: the description and the file each get their own line, the full path shows whole (wrapping when it must, no more front-ellipsis), and the bulky "Edit in settings.json" button became the same small pencil icon the connection card's folder opener uses, the full explanation in its tooltip.
  • The two document tabs' sticky headers share one skeleton now: an identity block on the left (icon plus title), a right-sized search and the contextual controls on the right. The connection editor's header grew the sidebar row's icon, the elephant tinted with the connection's color (live from the draft, so the color picker previews up here), with the availability dot and the version badge; the Settings header grew the cog and its title, its search shrank from a page-wide bar to the editor's width, and the active scope tab is accent-filled so Global / Workspace reads as a choice at a glance.
  • One reset affordance everywhere: the connection editor's "Reset to inherited" and "Reset to default" text links became the same revert button the Settings page's rows use, sitting next to the control it resets and appearing only while an override is set. The system schema group's revert sits in its heading (it resets the whole list), and the roles, privileges and owner toggles each got their own instead of one shared link.
  • The connection editor's stats strip (last connected, server version, connect count, the internal id and the snapshot's folder) moved into the Connection section, under the name and description, instead of floating above the form: the scroll spy files it under Connection and the field search hides it with the rest of the informational content.
  • The Settings categories sit in a deliberate order now, related ones together: Editor, Results and Keyboard (authoring), Connections, Snapshot and Objects (the database side), Files and Terminal (the workspace tools), then Appearance, Session and Log (the application itself). The generated settings.json template follows the same order. And the connection editor's "Connect parameters" section is simply "Parameters": inside the connection's own editor the prefix said nothing, and the navigation reads as six even words.
  • Text you read is selectable now, the way any page of text is. The document-like tabs (Settings, the connection editor, Account, the Snapshot settings placeholder) let their prose, labels, hints, setting keys and stats be selected and copied; buttons and selects inside them stay unselectable, and the interactive surfaces (trees, tab strips, grids, menus, the editors with their own selections) keep the app-chrome behavior. A standing rule from here on: every new document-like view ships selectable.
  • One word for one concept: the opened directory and its settings scope are called the workspace everywhere in the UI now. The Settings page's scope tabs read Global / Workspace (the tab used to say Local while its own description said "this workspace"), the editor links say "Open workspace settings", setting descriptions say "for the workspace" instead of "for the directory", the status bar and pickers offer recent workspaces, and the discovered connection's editor button is "Open source file". Nothing changes on disk or in the API: the settings scope key stays 'local' there, now a documented historical name.
  • Every connection editor control with a settings equivalent carries the same three-part meta line now: the inherited default named ("Inherited default: on"), Reset to inherited while an override is set, and Open local settings / Open global settings. And those links are precise: the Settings tab opens in the right scope, scrolls the EXACT setting into view and flashes its row for a moment, so there is never a doubt about which default a control falls back to. The availability check control's links gained the same precision.
  • The per connection snapshot settings moved into the connection's own editor, as its Snapshot section, and the middle scope changed meaning: the overrides now belong to the CONNECTION (stored with its name and check mode in connmeta.json, keyed by its id) instead of to a connection-in-this-workspace pair buried in the session. The inheritance is one sentence now: global settings, then the workspace's local settings, then the connection's own overrides, applied everywhere the connection is used. The section carries the same controls the old window had (auto refresh, refresh on open, the system schema groups with custom globs, roles/privileges/owner, wrapping and keyword case), each with its inherited default named, Reset to inherited while overridden, and links to the Snapshot defaults in local or global settings; everything rides the editor's own Apply changes/Discard bar, and a content-relevant change still rebuilds the snapshot on Apply. The old Snapshot settings tab stays as a placeholder pointing at the editor (something else may live there later), and the object browser's gear still opens it, its override highlight now reading the connection's stored overrides. Overrides saved by older builds into workspace sessions are ignored; set them again in the editor once.
  • The connection editor's stats strip carries two identity facts next to the version and the counters, selectable like the rest of it: the connection's permanent internal id, and, once a DDL snapshot exists, the snapshot's folder on disk (ellipsized from the start so the deepest segments stay readable) with a button that opens it in the OS file browser. The strip appears for every existing connection now, stats or none; a brand new connection grows its id line at the first Apply.
  • Connection discovery scans the workspace by depth instead of by a folder list: the discover.dirs setting (comma separated subfolder names) is gone, replaced by discover.depth, how many folder levels below the root are scanned (default 2, so config/ and apps/api/ layouts just work with no tuning; 0 scans the root alone, capped at 8). Dot folders and dependency or build trees (node_modules, vendor, target, dist and friends) are never entered and the visit count is capped, so a monorepo never turns a rescan into a filesystem crawl; the cheap file fingerprint still decides whether a rescan runs at all. A discover.dirs value left in a settings file is ignored.
  • The connection editor's "Check availability automatically" control spells out its three levels the way the snapshot settings do. It is a three-way choice now (default / on / off) whose default option names the resolved inherited value ("default (on)"), the hint says where that default comes from, and inline links offer the way back (Reset to default appears while an override is set) and the way to change the default itself: Open local settings and Open global settings jump straight to the Connections section of the right scope.
  • The connection editor's header shows the connection's description under its name, dimmed, one line with the full text in the tooltip; it follows the Description field live as you type and stays out of the way when there is none.
  • The connection editor got the settings page's map. Its fields now live under four sections (Connection, Connect parameters, Advanced, Properties, with more to come), each with a sticky header, and a fixed category list on the left jumps to a section on click while tracking the one being read as the form scrolls. The header grew the settings page's other tool too: a field search that narrows the form to matching fields, unfolding the SSL and SSH groups when the match is inside them. Where the editor was left (scroll, section, search, which groups are unfolded) is remembered with the directory's session like the settings tab's own state.
  • The connection editor's actions moved off the header into a footer bar, the snapshot settings window's pattern made literal: sticky at the bottom of the form's column, always in reach however deep the SSL and SSH sections go, and led by the same trio, Apply changes, Discard and the little status word saying whether anything is unapplied. Discard puts the draft back to its last applied shape, and the extras (Test, Duplicate, sharing, the destructive actions apart on the right) get a second row of their own, restyled to match. The Test result still answers inline, next to the buttons it belongs to. Two buttons left the editor entirely: Connect schema browser and New script were shortcuts to gestures that live where the work is (the object browser's own header picks its connection, the sidebar row's menu starts a script on one).
  • The DDL snapshot's long-line wrapping became a setting, and bundled statements got room to breathe. Wrapping (breaking a statement that PostgreSQL renders as one long line at its clause keywords past 80 columns) happens in memory while the snapshot is generated, a single cheap pass per definition, never a re-read of written files; it is now schema.wrapLongLines, on by default, settable at all three levels like every snapshot option (the connection's Snapshot settings window grew a Formatting section), and flipping it rebuilds the snapshot like any option change. Separately, the statements bundled into a table's file (its indexes, triggers, policies, sequence ALTERs) are now set apart by blank lines instead of being glued line to line; every snapshot regenerates once for that. And the snapshot can spell keywords your way: schema.keywordCase (the same Formatting section, the same three levels) keeps PostgreSQL's UPPERCASE rendering by default or folds keywords to lowercase, touching only keywords, never identifiers, quoted names, string contents or routine bodies.
  • Toasts are gone, all of them, the mechanism included. Every message now shares the one home errors already had: the Log, with a corner notification as its transient face (see below, and never a takeover). What used to pop over the editor ("Row updated", "Copied 3 rows", "Saved x.sql", "Test connected to ...", "Session was recreated...") is a Log line now, each naming its source; refusals that used to toast red ("Pick a connection for this tab first", "Cannot paste a folder into itself") keep landing as Log errors, which reveal the panel as errors always did. And where the outcome was already visible next to the click, nothing is said at all: a manual snapshot Refresh answers in the pane's own meta line ("up to date, checked just now", the Shift+click hint stays in the tooltip and the Log entry), a manual connections check answers in the dots it just repainted. Now a hard rule: no message ever floats over the editor, and none repeats a surface the user is already looking at.
  • The Log panel got handier chrome and truly reachable keys. Its header grew two buttons: one reveals the log's NDJSON file in the OS file browser (the append-only file under the global plumesql dir, for watching or shipping it; its full path sits in the tooltip), one jumps straight to the Log settings. And the keyboard navigation the panel always had (roving rows, Enter runs the row's action, Delete dismisses, the primary modifier plus C copies) used to be unreachable from the very gesture that opens the panel: the status bar toggles, the panel shortcut and the palette command now put the keyboard inside the panel they open, and PageUp and PageDown join the row navigation. An error revealing the Log still never steals focus: reveals are exempt, gestures are not.
  • The status bar flash grew into corner notifications, Visual Studio Code style: new log entries appear as cards above the status bar (right corner by default, the old log.flashPosition setting picks the side), newest on top, at most three at a time. Each card is the log entry itself: same text and source, and the entry's action links (Try again, Disconnect ...) ride along, re-resolved exactly like in the panel. Cards wrap long messages instead of clipping, carry an X, and leave on their own clock (log.flash, now 3 seconds by default), on the X or on an action, no longer because you typed or clicked somewhere else; opening the Log clears them, and repeats refresh their card's counter instead of stacking. Clicking a card still opens the Log.
  • Opening a file or a script no longer connects it. It carries the connection it will run on, and the first run dials it, which is what the tab's own tooltip and the status bar ("the tab connects on its first run") already promised. Opening ten files used to open ten backends and hold ten slots in the server's max_connections; now nothing is dialed until something is actually run. Picking a database FOR a tab from the toolbar still connects right away, because that gesture is about the connection rather than the file.
  • The Log's connect entry names the backend it opened ("pid 4172") and offers to close it again, so a session opened by accident is one click away from being given back. The offer stands only while some tab still holds that very backend: after a restart, or once it is disconnected, the entry stays and the button is gone. Disconnects are logged too, with the pid and what ended them (disconnected here, the tab was closed, the tab moved to another connection), so the Log reads as a complete chronology instead of connects that never end.
  • Every editor tab wears its connection's dot on the SQL lettermark, green while that tab holds a backend and grey while it only carries a connection it has not dialed, the same pairing the elephant already has in the toolbar and the status bar. Which tabs are on a database is now readable from the strip, without visiting them.
  • A pinned object in the object tree wears the same pin glyph as a pinned tab, not a star: one glyph for one idea, wherever it appears.
  • The bottom panel's Hide button stops borrowing the dropdown sign. A chevron pointing down means "a list drops from here" everywhere else in the window (both connection dropdowns, the tab well's Go to Tab or File), so wearing one on a button that folds a whole region away read as a promise it did not keep. It now wears the classic window minimize line, every OS's sign for "tuck this away and carry on". Still not an X: hiding the panel stops nothing, the Log keeps collecting and the terminal's shell stays alive.
  • The active editor tab wears the same faint accent wash as the active sidebar tab, the connection rows and the active file row. It used to switch to the page background, one step of grey away from the strip, which reads only on comparison; now it is a block the eye catches without looking for it, and the accent stripe stays.
  • A preview tab (single click on a file: italic title, replaced by the next thing you open) is now told apart from a permanent one without reading the italics: while it is the active tab, its stripe is grey where a permanent tab's is accent. Color carries commitment, and keeping the tab lights the stripe up. A running preview keeps the running shimmer. The tab's tooltip also says what a preview tab is and that double-clicking, editing or running keeps it.
  • Every contact with a database now updates every place that shows that connection, wherever the contact happened. A tab that dials, the DDL snapshot, one object's re-render and the availability check all report to the same place: a success counts exactly as a successful Test, so the sidebar dot goes green, the object browser starts asking the database again and the tab's broken link clears, and a failure counts exactly as a failed one. No surface can keep showing a verdict another has already disproved.
  • The object browser's header stops claiming a connection it does not have. It read a session held by any TAB on the same connection, so it showed a green dot for something the browser was not doing, with no pid to back it up. It holds no session by design (it dials one-shot connections that close right after), so it now reads "idle" with a grey dot, and goes green with "working" only while it is actually talking to the database: a snapshot check or build, an object being re-rendered.
  • One object's re-render counts as background activity like everything else: the status bar dot beats while it runs and the tooltip names it ("refreshing an object"). It was the one background task that ran invisibly.
  • The object browser stops giving up for good. After a failed refresh it went quiet until you clicked something, so a database started a minute later stayed "not answering" no matter how long you worked. The quiet mode now expires after 20 seconds: the next object you open tries once more, and if the server is back everything catches up by itself. Which is the point, since the usual reason a database is not answering is that it is not started yet.
  • The active sidebar tab is easier to spot. It wore only a 2px accent rail, so with every tab bold and uppercase, telling it from the rest came down to one step in grey: a comparison rather than a glance. It now also wears the faint accent wash the connection rows and the active file row already use, which makes it a block the eye catches without looking for it. The strip was the one place using half that vocabulary.
  • Connection dots tell you what is true NOW, not what was true once, and they say two different things in the two places they appear, because those places mean two different things. In the connections sidebar a row is a connection DEFINITION: a template the object browser and a script tab connect WITH, never an open connection itself. Its dot is therefore always hollow, and answers only whether the server is available:
    • hollow green: the check reached it
    • hollow red: the check failed, or, with no check, the last connect attempt failed; the row's text says which of the two it is
    • hollow grey: nothing checked it and nothing failed, pulsing while a check is in flight Everywhere a real connection is made (the status bar and the editor toolbar for a tab, the object browser's header) the dot is FILLED and means the session: green while one is open, with the pid in the readout, grey while it is idle, and the broken link when the newest thing known is a failure. Until now the sidebar's dot was painted from the stored history alone, which never expires: a database that had been up for an hour still showed red because the last attempt failed before you started it, and green could mean "connected yesterday". Every dot's tooltip spells the state out, says when it was learned, and carries the verbatim error.
  • The editor toolbar's connection button shows the connection at all now: it grew the elephant, tinted with the connection's own color, with the session dot on its corner and the full readout in the tooltip. It used to show a bare name and say nothing about whether the tab was connected. The status bar still spells out "idle" beside its readout, but that word is the first thing clipped when the bar tightens, and the dot never is.
  • The object browser's header explains its "idle" in the tooltip: the browser holds no session by design, it connects on demand, so there the word describes a resting state rather than a tab waiting to dial. Its dot reads exactly like the status bar's and the toolbar's, green with a session and grey without one.
  • A tab that cannot reach its database now says so where it happened, quietly. The failed dial goes to the Log with its verbatim PostgreSQL error and an Edit connection button, the status bar and the toolbar swap the elephant for the broken link, and the results pane folds away instead of showing a full-height red wall about a query the server never saw. Running again reconnects and brings everything back.
  • The bottom panel's X became a chevron labelled Hide panel, with its shortcut in the tooltip: the button folds the panel away, it never stops anything, and the tooltip now says so ("the Log keeps collecting while it is hidden"). An X read as "close this, it is over".
  • Clear no longer asks for confirmation. The log is a diary of what the app did, not data, and the modal cost more than it protected (Dismiss all beside it never asked either); the clearing writes its own entry instead, "Log cleared (247 entries)", so a suddenly short log explains itself.
  • The word "schema" now means one thing in PlumeSQL: the PostgreSQL namespace object (CREATE SCHEMA). It used to name the whole feature too, so a Log line could read "Hid schema public from the schema tree; unhide it in the schema filter window", three senses in one sentence. The feature family follows the PostgreSQL community's own vocabulary (pgAdmin's Object Explorer, the glossary's "SQL object", pg_dump's schema-only sense): the side tab is Objects, the pane is the object browser with its object tree, the floating windows are Object filter and Object find, the on-disk DDL cache is the DDL snapshot, and the per connection overrides are Snapshot settings. Log and toast wording, tooltips, pickers and the settings labels and descriptions (the Schema category is Snapshot now) follow. Setting keys, session state and the on-disk layout are untouched, so nothing needs migrating and old sessions restore as before.
  • The pinned strip wears a faint wash of the accent, the color its pin stars already carry, so it reads as its own band instead of as the tree's first rows: the hairline and shadow alone got lost among identical row styling. Hovering a pinned row deepens the same wash (the shared row hover was within a shade of the new surface and would have vanished), staying a step below the selection tint.
  • The pinned strip and the tree keep their own expanded branches. They shared them, so opening a pinned schema also opened it down in the tree, and expanding in the tree grew the strip and pushed the tree down: the change always landed somewhere you were not looking. Both the folder expansion and the dependency disclosure are per strip now, each restored with the session; a session saved before this starts with the strip collapsed.
  • The schema row menu is grouped now, separated into what acts on the object, what pins and what hides, and Pin offers a level per ancestor the way Hide does: Pin schema public, Pin public/tables, Pin TABLE users, each reading Unpin once that level is pinned.
  • The dependency disclosure says Expand and Collapse rather than Show and Hide, so "hide" means one thing in this tree: the filter's negative layer.
  • The schema tree's row menu can hide any level above the row, not just the row itself: on an object it reads Hide schema public, Hide public/tables and Hide TABLE users, broadest first, so noticing a whole branch is in the way no longer means scrolling back up to find its row. A level already hidden is left out of the menu.
  • A schema or object type hidden from that menu now says so on its own row in the Schema filter window, with a crossed out eye and a struck through name, and one click there stops hiding it. Only individual objects, which have no row of their own, are left in the window's Hidden section. The two layers stay separate on purpose: hiding never rewrites a filter selection, so a schema that appears in the database later still shows up, which materializing "everything" into the selection would have prevented.
  • The schema tree's context menu talks about database objects, not about the files they are cached in: Open in Finder/File Explorer, the two terminal actions, Copy File, Copy Path and Copy Relative Path are gone from it (they acted on the snapshot directory, an implementation detail). Copy Content, which yields the object's DDL, stays, as do Pin to top and Hide. The file trees (Explorer, Scripts) keep all of theirs.
  • A long object name no longer stretches the schema menu across the window: the name in the Hide entry is capped, and a cut inside a parameter list keeps its closing paren, so an overloaded function reads as "Hide FUNCTION make_window(integer, text, date[]…)". A function with eleven parameters used to run the menu past 900px; it now stops around 420px, and short names are untouched.
  • The status bar was decluttered and split into halves: the left is navigation (where am I), the right the active tab's state. The workspace shows its last two path segments now, the full path staying in the tooltip, and wears a small folder mark. The two always-visible reveal buttons are gone; right-clicking the workspace or the tab segment opens the same menu as the file tree rows (Open in Finder/File Explorer, Open in Integrated Terminal, Open in System Terminal, Copy Path and, for the tab, Copy Relative Path). Every tab now names itself in the bar with the same icon as its tab strip entry, the view tabs (Settings, Account, a connection editor) included; a file shows its relative path, a schema DDL viewer its snapshot file path behind the DDL mark. The connection readout moved to the bar's right half, ahead of the cursor and selection readouts behind a small separator, and its picker now grows up right-aligned; a DDL viewer fills that slot with the object's type icon and signature instead (it runs nothing, so there is no session to show). The bar also joined the F6 focus cycle as one keyboard zone: a single Tab stop, arrows walk its controls.
  • Schema DDL viewer tabs wear their object type's icon (the schema tree's registry: table, view, function...) in the tab strip and the status bar instead of the generic DDL lettermark, which stays only as the fallback for an unrecognized path.
  • Long one-line DDL reads top-to-bottom now instead of scrolling sideways: index, trigger, constraint, sequence and policy statements (and the appended GRANT and OWNER TO clauses) longer than 80 columns break at their clause keywords, pg_dump style, e.g. CREATE INDEX ... / ON table / USING btree (...). This applies wherever the statement appears, the table files included: their bundled indexes, triggers and sequence ownership wrap the same way, and an in-table CONSTRAINT ... FOREIGN KEY line wraps keeping its indentation. Quoted identifiers and anything inside parentheses never split, and function and procedure bodies are left exactly as PostgreSQL renders them. Existing snapshots re-render on their next check.
  • The schema tree's find moved from the inline bar into a small floating window, the same always-on-top shell as the filter window: dragged by its header, position remembered per workspace, closed with Escape or its X. Both schema windows say what they are in the header ("Schema filter", "Schema find", ahead of similar windows coming for the other trees) and carry a six-dot grip marking the header as the drag handle. Where they overlap, the window focused or last interacted with rides on top, desktop style, instead of the filter always covering the find. Both windows restore with the session the same way: the find window comes back open with its query and toggles exactly as it was left (matches filtered, nothing focused), just as the filter window already did, and the stacking order comes back too. Everything else is unchanged: Ctrl+F (Cmd+F on macOS) from the tree opens it with the input focused, the tree filters and highlights matches underneath, arrows walk the matches, Enter opens the current one and Escape restores the tree exactly as the search found it. The window no longer squeezes into the sidebar's width (the match counter had to fold into a tooltip below 250px) and the tree does not shift when it opens.
  • The per connection schema settings moved from the floating window to a proper tab, opened by the same cog: one tab per connection, focused if it is already open rather than duplicated, and with room to grow. It states plainly which connection and workspace it applies to, what each option inherits (its default), and how to reset; two buttons open the workspace and the global defaults every connection falls back to. Nothing is saved until Apply, which also rebuilds the snapshot; Discard reverts the edits, and closing the tab with unapplied changes asks first. Every schema setting now goes through Apply, auto refresh included (it used to apply live). The tree stays visible in the sidebar while the tab is open, so a rebuild is still watched as it lands.

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • The schema browser can optionally include roles, privileges and ownership. "Include roles" adds cluster roles under a roles/ folder (CREATE ROLE with attributes and memberships, never passwords; the built-in pg_* roles are left out). With roles on, "Include privileges" appends GRANT statements (from each object's ACL, PUBLIC included, the owner's implicit grant left out) and "Include owner" appends an ALTER ... OWNER TO to every object script. All three are off by default and resolve through the same three scopes as the other schema settings; the schema settings window edits them per connection (privileges and owner disable until roles is on), staged behind the same "Apply changes" button that rebuilds the snapshot. The owner and privilege clauses are derived at the query level (aclexplode), PostgreSQL 13+.

  • The schema browser can now choose which system schemas to exclude, a new "System schemas" setting. Unlike the filter (which hides files that were still fetched), these schemas are dropped at the query level, so their objects are never fetched or written to disk: hiding a large one keeps the snapshot small and fast. It ships excluding the usual noise (PostgreSQL internals pg_*, information_schema, TimescaleDB's _timescaledb_* and timescaledb_*, and the PostGIS Tiger geocoder tiger, tiger_data, topology), and the schema settings window (the cog next to the filter) edits it per connection: named groups with checkboxes, plus free-form custom globs (* matches any run of characters) to add or remove, a "Restore defaults" and a "Reset to inherited". It resolves through the same three scopes as auto refresh (connection override, then workspace, then global), and clearing an entry lets you explore that schema instead. Edits are staged and logged, then applied in one rebuild with the window's "Apply changes" button, so a heavy database is not rebuilt on every toggle. The excluded set salts the snapshot fingerprint, so the manual Refresh, the scheduled auto refresh and the next launch apply a pending change too, and an unchanged set never rebuilds.

  • Files changed outside PlumeSQL are picked up automatically, VS Code style: when the window regains focus, open tabs whose file changed on disk reload (a clean tab silently, keeping its cursor and undo history; a tab with unsaved edits is warned and never overwritten), a file deleted on disk keeps its content in the tab, which is marked deleted (struck-through title, a warning glyph) with auto save suspended so saving recreates the file and closing discards it (a schema DDL viewer whose object was dropped from the database is marked the same way, and clears if the object comes back), and files created or removed outside PlumeSQL appear in or vanish from the Explorer and Scripts trees. The same reconciliation runs once at startup: a tab restored from the last session whose file changed or was deleted while PlumeSQL was closed reloads or is marked right away on launch, not only on the next focus (each tab now remembers its on-disk mtime and size in the session, so the check is a cheap stat, not a blind re-read of every buffer). The schema browser rechecks its connection on focus too, so outside database changes surface. A rebuilt schema snapshot now also refreshes any open DDL tabs in place instead of waiting for a reopen. No filesystem watcher, so the behavior is identical on macOS, Windows and Linux (an on-focus mtime and size check). For keeping PlumeSQL live while it sits visible next to another editor, an opt-in "Auto refresh open files" setting rechecks on a few-seconds cadence while the window is visible (off by default, and it stops entirely while the window is hidden).

  • Schema auto refresh gains sub-minute cadences (15, 30 and 45 seconds) for near-live work and drops the presets above 5 minutes (the check is a cheap fingerprint, so a longer wait is what the Refresh button is for); changing the per connection override in the schema settings window now writes a Log line so the schedule is auditable next to the refreshes it drives.

Fixed ​

  • A saved connection's id is now a permanent GUID, minted once at the first save and never changed again; it used to be a hash of the parameters, so editing the host, port, database, user or the SSL/SSH settings silently moved the connection to a NEW id and everything holding the old one broke: the editor tab lost its connection (no more stats, Duplicate or Delete) and every per-directory reference went stale. Connections saved before this change are not migrated: their hash-shaped ids stay valid and simply never change again; recreate a connection (or start with a fresh global dir) to get GUID ids throughout. Every connection type carries an internal id now: a discovered candidate's embeds its source file's absolute path (same file, same id; the file moves, the id moves), the env connection's is "env", a team shared one's comes from the server. And EVERYTHING about a connection keys on that id: connect history, the availability dot, names, descriptions, check modes and the DDL snapshot cache. A saved connection and a discovered twin with the very same parameters used to share one connect count and one dot; they are two connections now, each with its own history, verdict and snapshot (existing identity-keyed stats, candidate labels and snapshots quietly start fresh under the new keys; snapshots rebuild on first use). The parameter hash still exists as what it always should have been, purely informational: it detects that two definitions describe the same server, which the team sync and promote's duplicate check use, and nothing else touches it. And applying an edit now disconnects nothing, a new hard rule: nothing the user connected is ever disconnected without their explicit action (the old disconnect-on- edit is gone entirely). Connected tabs keep their sessions and the settings they dialed with; the new ones apply from the next connect on, and the editor's footer says so after such an Apply ("2 connected tabs keep their sessions and the previous settings; reconnect them yourself when you want the new ones"), the count melting away as each tab reconnects or closes.
  • Closing the active tab returns to the tab you were on before it, wherever the two sit in the strip. It used to activate the closed tab's positional neighbor, which looked right by accident from an unpinned tab next to the preview slot and broke visibly from a pinned one: open a file from a pinned tab, close the preview, land on some unrelated tab at the strip's end. Activations now keep a most-recently-used history (transient, like the focus), and closing walks back through it.
  • Two SQL highlighting edge cases stopped rendering half-colored. The multi-word type names ("timestamp without time zone", "time with time zone", the precision forms like "timestamp(3) with time zone") now color as one type token: WITHOUT and ZONE are no keywords on their own and used to fall to identifier grey mid-phrase, the way DOUBLE PRECISION always colored whole. And an identifier that starts with digits (TimescaleDB's chunk constraint names, like 36_36_thing_data_thing_id_fkey) reads as one identifier instead of a number glued to a name; digits with underscore separators between groups (1_000_000, PostgreSQL 16 literals) still count as numbers. Two more: a quoted identifier ("timestamp", "MyTable") wears the plain identifier color instead of borrowing the type color, which made a column named timestamp look like the timestamp type; and REFERENCES colors as the keyword it is, next to FOREIGN KEY and ON DELETE, instead of as a type.
  • The Settings page's header explains itself. The scope toggle's two words grew tooltips and a strip right under the toolbar that says what the picked scope means (Global: defaults for every directory you open; Local: overrides for this workspace only, named, falling back to Global), with the file's path and the "Edit in settings.json" button together on that same line instead of a lone path under the search and the button far to the right. Other parts of the app can now open the page on a specific scope too, not only on a category.
  • The Settings page no longer forgets where you were. Switching to another tab and back scrolled it to the top with Editor selected; its scroll position, active category, scope (Global or Local) and search text now ride the tab exactly like an editor tab's cursor and scroll do, so they survive tab switches and restarts both. Opening a category deliberately (the Log panel's gear, the snapshot window's links) still jumps where it says.
  • A tab's connection dot told the truth about the wrong thing. Opening a file dialed a real backend, the Log said so and the dot stayed grey anyway, because the dial wrote its session onto the tab object handed back at creation rather than the one the screen reads: a live backend the UI never learned about. Every surface (the toolbar dot, the tab's new dot, the status bar's pid) now catches up the moment a session opens or closes, and the dial is re-resolved against the open tabs, so no future caller can reopen the hole.
  • Resizing the window can no longer hide the active tab. The strip kept its scroll position while it shrank, so the active tab could end up past the right edge with nothing saying so. The active tab is now brought back into view whenever it activates, opens or the strip resizes, in every tab layout.
  • The object browser's header breaks and heals like the tab's mark. When PlumeSQL's own contact with the server fails anywhere (a tab's dial or run, the snapshot, an object re-render, the availability check), the header swaps its elephant for the broken link, keeps the connection's name in red and says "not answering"; the next successful contact anywhere puts it back to idle by itself. It used to sit on a grey "idle" while the toolbar next to it already showed the link broken.
  • Errors have one home now: the Log. A sweep converted every place that still popped a caught error as a red toast (deleting or promoting a connection, stop sharing, creating a script, opening a file, saving a setting, switching workspace, listing recent directories or snapshot objects, cancelling a backend, toggling the MCP endpoint, resetting statement statistics, the row menu's Test, the About dialog) to a Log entry that names what was being done, with the verbatim error. What the server says about your own SQL stays where it was, in the Messages grid and the results pane; forms keep answering their own buttons inline; toasts keep carrying outcomes and hints, never error traces. Now a hard rule.
  • A connection dying under a live session is treated as what it is. Stop the database while a tab is connected and run: the first run used to print FATAL: terminating connection ... in the Messages grid as if the statement had failed, and the second showed the dial error as a full screen query result, while the status bar kept claiming a live pid throughout. A connection error is never a query result: the run now recognizes the connection-fatal SQLSTATEs (class 08, 57P01..03; a canceled query stays a plain statement error) and the failed redial behind a dead session, and both put the tab into the disconnected state, broken link, no results pane, the verbatim error in the Log with Edit connection on it. Running again redials and restores everything, and a run that answers also clears an unreachable verdict the availability check may have left on the sidebar dot.
  • The results pane no longer breaks when a run fails. Its "Fix with AI" block was declared inside one branch of the template and rendered from another, so any failed run (a broken connection above all) threw ReferenceError: Can't find variable: aiFix and left the pane blank instead of showing the error.
  • Startup no longer opens a tab you did not leave open. When the DDL snapshot could not reach its server on startup, PlumeSQL opened that connection's editor tab for you, which pushed an unasked for tab into a restored workspace and stole the active tab from whatever you were reading when you closed it. The failure was already in the Log with a Try again button on it, which is where it stays. A workspace now reopens with exactly the tabs it was closed with, always.
  • Tree rows line up again whatever leads them. A schema object with no dependencies sat 6px left of one with a disclosure chevron, and in the Explorer a folder sat 4px right of the files beside it: the indent that pays for a missing chevron counted the chevron's width but not the gap after it, and a folder's chevron was 2px narrower than an object's. Three different alignments at the same depth, now one. The chevron slot and the row gap are single values the rows share, so the row kinds cannot drift apart again; the rename and draft inputs and the dependency headers follow the same rule (the draft input used to sit a whole slot to the left).
  • The sidebar no longer twitches when the schema tree takes or loses the keyboard. The header swaps its shortcut hint for the live "arrows navigate" helper on focus, and the two boxes differed by a pixel, so the header shrank and the connection line, the tree and every row jumped up 1px on every focus change (a full reflow each time). The two are the same height now, and the panel header's height is pinned, so nothing swapped into it can move the panel again. All four panels (Connections, Schema, Explorer, Snippets) lead with the same 30px header as a result, where Schema and Snippets used to be 2px shorter.
  • F1 opens the command palette from anywhere in the app now, not only with the editor focused: it was a Monaco editor action alone, so it did nothing while the schema browser, the Explorer, the connections list, the results grid or a view tab had the keyboard. The focused terminal stays the one exception, like every other key it owns (a TUI's F1 is the TUI's), and F1 joined the reserved combos so a rebind cannot shadow it. The cheatsheet moved it out of the Editor section accordingly.
  • SQL highlighting no longer breaks on a dollar quote glued to a word, the common plpgsql ending END$$;: the word swallowed the quote as part of an identifier, so END lost its keyword color and the code-body state never saw its closing tag, which could mis-tokenize everything after it. The word and the tag are separate tokens now, in every position (END$$, END$body$, a tag glued after any identifier).
  • The schema filter no longer mistakes the tree's root collections for schemas: extensions and roles are cluster-global (bound to no schema), yet the filter listed them among the schemas and, worse, spilled the role files into the "All schemas" by-type list as bogus one-file types. They now sit in their own section after the schemas, labeled Extensions and Roles, toggled whole (no type expansion), and the by-type list aggregates over real schemas only. The schema tree and the filter window also wear a small icon per kind now (schemas, extensions, roles, and every object type from tables to rules) instead of the generic folder, and the object rows themselves (dependency rows included) carry their type's icon in place of the repeated DDL lettermark, which stays on the DDL viewer tabs where it tells them apart from SQL files.
  • Opening the schema find with an empty query no longer paints an arbitrary row with the current-match color (and the arrows walk nothing until something is typed, like the editor's find). The light theme's current-match wash is also softened: Monaco's opaque olive reads fine on text spans in the editor but looked broken washing a whole tree row.
  • The schema tree's sticky ancestor rows read as one band now, VS Code style: no separator lines between the stacked rows, the bottom border and drop shadow sit only under the last one (the shadow also no longer bleeds a few pixels up over the row above it).
  • A right click on the schema browser's pinned strip no longer falls through to the app-wide menu: the strip's gaps and padding open the tree menu like the scrolling tree itself, which was missing a handler there. App context menus also paint above the application chrome now, whatever tab is active and wherever focus sits: Monaco homes its menu layer to an off-screen helper container, which stacked beneath z-indexed surfaces (the tree's sticky rows, the editor's tab strip), so a menu overlapping them rendered partially covered. That container now stacks above the chrome, and closing a menu returns focus to where it was opened from. An open menu also dismisses on the first click anywhere outside it, the title bar, the header and the status bar included: Monaco relies on a focus change to close, which WebKit (the desktop shell's engine) does not perform when a non-focusable surface is clicked, so the menu could hang open until Escape.
  • View and materialized view DDL no longer keeps a stray leading space before the body's SELECT: PostgreSQL's pretty-printed pg_get_viewdef starts the definition with a space, which the snapshot now trims (the continuation lines' indentation is untouched), so a view renders as "CREATE VIEW ... AS\nSELECT ..." instead of "AS\n SELECT ...".
  • The status bar activity dot's tooltip tells the truth about what it reflects: it beats during any background activity, a schema snapshot refresh included, not only a running query or a session save. The idle text now says so, and anything busy without a specific label reads as "Background activity".

Removed ​

  • The connection.default setting is gone. It referenced a connection by NAME or by the old relative discovered id, both of which the identity rules outlawed, and it duplicated what already works: picking a connection is remembered per workspace and restored on startup. A value left in a settings file is ignored (and reported as an unknown key, like any other).
  • The query history feature is gone for now, ahead of a later rebuild: the clock button at the tab strip's right edge, the history panel, the history API and the on-disk history.json (nothing writes or reads it anymore). The MCP panel's "Recent agent queries" list went with it, it read from the same store. Cloud run mirroring for signed-in accounts is unaffected. In the clock's place the tab strip corner now opens the same Go to Tab or File picker as the status bar segment and the palette, dropped down under the button.

Fixed ​

  • Hovering a column named after a built-in shows the column. A bare column in a SELECT list whose name is also a catalog function or type (name, timestamp) hovered the built-in instead of the column; it now resolves to the column of its FROM table. A name that is called (name(x)) or heads a type literal (timestamp '2026-01-01') still hovers the function or type. And every hover now carries its own Copy, beside Monaco's whole-card button, that takes the one useful thing: a column's or a variable's name, or an object's full DDL.
  • Hover and Go to Definition find a temp table made with CREATE TABLE AS. A temporary table created by CREATE TEMP TABLE ... AS SELECT (or SELECT ... INTO TEMP) reports a SELECT command tag rather than CREATE, so the session's pg_temp schema was never added to the captured search path and the editor could not resolve the unqualified name, even though the object browser already listed the table under New in this session. The search path is now re-read after these relation-creating SELECT forms too, so the hover, Go to Definition, signature help and completion reach the temp table like any other object.

0.20.0 2026-07-23 ​

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • The schema browser's header shows a live keyboard helper while the tree holds the keyboard: where the panel's focus shortcut badge sits, a small arrow-key glyph pill (↑↓ →← ↵) appears to say navigation is active, its tooltip spelling out every in-tree key (move, expand, collapse, open, pin, find, filter, row menu, jump to the editor) and a click opening the full Keyboard Shortcuts cheatsheet. It reverts to the focus shortcut badge when the tree loses focus.

  • The schema browser refreshes on a schedule now, off by default: a new "Schema auto refresh" setting rechecks the connection every N minutes (a cheap fingerprint check that rebuilds the snapshot only when something changed, like the Refresh button; 1 to 60 minute presets). It resolves through three scopes, most specific first: a per connection override for this workspace, then the workspace (settings local scope), then global. The per connection override lives in a new floating schema settings window, opened by the cog next to the filter (draggable, always on top, position remembered per workspace), whose "Auto refresh" can Inherit the workspace/global value or set its own; a link there jumps to Settings at the Schema section for the workspace and global defaults.

  • Important schema objects can be pinned to the top of the tree: the row menu's "Pin to top" (on an object, a schema or a type dir) keeps it in a fixed strip above the scrolling tree, one click away regardless of scroll, filter or expansion. Pinned rows are first-class copies: a star marks both the strip row and its tree counterpart, selecting or cursoring one lights up the other, they carry the full row menu, expand their subtrees (a pinned schema or type dir opens in place, a pinned object shows its dependency disclosure), and the keyboard runs straight through them: arrowing up off the first tree row continues into the strip. "Unpin from top" removes an entry. The pinned list persists per connection in the session.

  • Anything in the schema tree can be hidden outright: the row menu's "Hide TABLE users" (or "Hide schema public", or a type dir like "Hide public/tables") puts the object on the filter's hide list, the negative layer under the checkbox selection: a hidden path stays out no matter what is checked, and the search does not find it either. The filter window grows a Hidden section listing every entry as a checked "Hide ..." row; unchecking one brings it back, "Show all" clears everything, and the snapshot bar counts them ("· 3 hidden"). The hide list persists per connection in the session.

  • PlumeSQL works without a mouse now. F6 (and Shift+F6) cycles the keyboard focus through the sidebar, the editor, the results and the bottom panel; Ctrl/Cmd+0 focuses the sidebar, Ctrl/Cmd+1 the editor, Ctrl/Cmd+J toggles the bottom panel; F8 steps through the side panels, and each panel is one press away: F2 Connections, F3 Schema, F4 Explorer, F7 Snippets, plus Ctrl+C, Ctrl+S and Ctrl+E mnemonics on macOS, where the fixed app shortcuts now answer to Cmd only and the Ctrl+letter plane is free (deliberately no Ctrl+digit defaults: browsers and macOS desktop switching claim those). Each panel shows its shortcut in its header, read live from the settings. Shortcuts match the physical key as a fallback, so they keep working on non-Latin keyboard layouts. All of these are configurable under the new Settings Keyboard category, and a binding can hold several shortcuts separated by commas. Focusing the sidebar puts the cursor straight into the list, so arrows navigate immediately. Every file tree is one Tab stop with full arrow navigation inside: up and down move, right expands a folder (or opens an object's dependencies), left collapses or jumps to the parent, Enter opens, Space pins, Home/End/PageUp/PageDown jump, Shift+F10 opens the row menu (rename lives there) and Delete deletes where editing is allowed. Opening from the tree keeps the keyboard in the tree, Tab jumps straight to the editor, and the preview follows the cursor as you move (the default; the "Tree navigation preview" setting can make it wait for Enter instead). The schema tree's find bar navigates by keyboard too: the editor's find shortcut (Ctrl/Cmd+F) opens it from a focused tree (in the Files and Scripts trees it drops into the filter box, where Escape clears and Enter moves to the rows), a new search puts the cursor on the first match, the arrow keys walk the matches from the input (the preview following per the same setting) and Enter opens the current one. Closing the search (Escape or the X) puts the tree back exactly as the search found it: expansion, scroll, cursor and selection all restore, and follow-mode previews opened while walking the matches revert to what the editor showed before; a match opened deliberately (Enter or a click) is the point of the search, so it stays. Tab strips and the connections list take arrows too, and Delete closes the focused editor tab. Focus is always visible: a keyboard-only accent ring on the focused control, an accent edge on the pane that owns the keyboard, and an outlined cursor row in trees. A Keyboard Shortcuts cheatsheet lists everything, from the cog menu or the command palette; on macOS shortcuts render with the native glyphs.

  • Sticky folders in every file tree, VS Code style: scrolling inside a folder pins its ancestor folder rows to the top of the tree, stacked by depth, so deep positions keep their context in view. Clicking a pinned row scrolls back to that folder, its chevron collapses it in place. Applies to the Explorer's files and scripts and to the schema browser alike (one shared tree component drives them all).

  • The schema browser is rebuilt around schema snapshots: picking a connection materializes the database schema as a directory tree of DDL files (one file per table, view, function, constraint, index and every other object, grouped per schema by object type, extensions at the root), stored under PlumeSQL's global directory per connection. The sidebar shows that tree exactly like the Files explorer. The snapshot bar carries find and filter buttons on its right: find reveals the editor's find widget transplanted onto the tree, an overlay hanging off the pane's top edge with working match case, whole word and regular expression toggles, an "n of m" counter and arrows (or Enter and Shift+Enter) cycling through the matches, the current one highlighted in the editor's own find match color; the tree filters as you type and Escape closes. Filter picks the object types shown (tables, views, functions...), and that selection is remembered per directory. The tree opens fully collapsed, and its whole view persists per connection in the directory's session: expanded folders, expanded dependency lists, the selected row and the scroll position all come back exactly as left, on a connection switch and across restarts. A Refresh button rebuilds the snapshot on demand and the bar shows when it last ran, how long it took and how many objects it found; a cheap fingerprint check skips the rebuild when nothing changed. Refreshes and their failures land in the application Log.

  • The schema browser's connection is lazy now: selecting a connection no longer opens (or holds) a database session. Every schema operation, the snapshot build included, runs over a one-shot connection that disconnects as soon as it is done, so an idle PlumeSQL holds no backend at all. The schema pane header shows the connection and server version without a pid, and Disconnect simply clears the selection.

  • Snapshot DDL files open as read-only viewers: no Run toolbar, no connection dropdown, no editing, with a "DDL" mark on the tab, the tree and the footer. Objects are named by type everywhere ("TABLE users", "VIEW v_orders"): the tree rows lead with a dimmed type label, the tab titles and the footer read the same, and the .sql extension stays on disk only. The tree search matches what the rows show, so typing a type ("function") narrows to that type too. Opening a file shows the cached DDL instantly and re-renders that one object from the database in the background, updating the buffer when it lands.

  • Schema objects know their dependencies: while the snapshot builds, each DDL file is scanned for qualified references (schema.name in any quoting form) against the snapshot's own object dictionary, so aliases and column tails never false-match. Objects with relations grow a chevron in the tree; expanding shows two groups, "depends on" and "used by" (the inversion comes free), and clicking a dependency opens its DDL. The scan rides the build at negligible cost and refreshes with it; unqualified references are deliberately ignored.

  • The schema tree's right click menu: Open in Finder (File Explorer or the file manager elsewhere), Open in Integrated Terminal, Open in System Terminal, Copy Content, Copy File (puts the real file on the OS clipboard, ready to paste in the file manager), Copy Path and Copy Relative Path.

  • The Files explorer right click menu leads with three OS actions on any file, script or folder: show it in the OS file browser (Finder, File Explorer or the Linux file manager, by name), open the integrated terminal in that folder (revealing the panel and cd-ing the shell), and open the system terminal there.

  • The status bar sits under the content behind a quiet hairline in the divider color, matching the app's other separators, with the normal text and toggle colors that work in both themes.

  • Sessions can identify themselves in pg_stat_activity. The "Session application_name" setting (Connections, global or per directory) templates the application_name PlumeSQL dials with; it defaults to plain "plumesql" and takes {owner} ("schema" or "tab"), {file} (the tab title) and {dir} (the workspace directory) placeholders. For example "plumesql {owner}: {file}" yields "plumesql tab: report.sql", so a DBA can tell, with the pid also shown in the status bar, which backend belongs to what. PostgreSQL clips the name to 63 bytes.

  • The status bar shows the active SQL tab's connection, next to the file: a broken link and "no connection" until the tab has one, then the elephant tinted in the connection's color with the name, url, server version, backend pid and roughly how long the session has been up ("<1m", then 5m, 3h, 2d). A tab connects lazily on its first run, so until then the readout says "idle" with the elephant dimmed, pid and age arriving once the session dials. The tooltip carries the full untruncated line with the closer age and a click hint; clicking switches the tab's connection in place. Absent on non-SQL tabs.

  • Editor readouts in the status bar, right aligned and immune to the squeeze of long paths: the live cursor position (line:column; clicking opens Go to Line/Column right above it) and the current selection as "(2 lines, 102 characters)" or "(no selection)", clicking which opens a selection command group (Select All, expand/shrink, occurrences, multiple cursors). Selection has a permanent readout because Run executes exactly the selection. Both appear only while an editor is open.

  • Go to File. The status bar shows the active file's relative path next to the workspace path, wearing the same icon as its tab; both paths ellipsize from the start when the bar gets tight, so the directory and file names stay readable, with the full path in the tooltip. Clicking the file button (or running "PlumeSQL: Go to File" from the command palette) opens a picker with the open files first and the directory's recently opened files after them, each stamped with when it was last opened. Picking focuses the open tab or reopens the file. The recent files list lives in the per directory session, follows renames, and drops deleted files. The workspace, and the active file when it lives in a subfolder, carry a button in the status bar that shows them in the OS file browser (the file button is dropped when it would only reopen the workspace folder the workspace button already covers).

  • PlumeSQL remembers the local directories you open. A new command, "PlumeSQL: Open Recent" in the command palette, lists them with when each was last opened; picking one makes it the active directory, reloading the whole app on it, without opening a separate window. The list keeps the 100 most recent and hides the current directory and any that no longer exist. The workspace path in the status bar, now led by a small folder icon, is the same picker one click away: it opens the recent directories list right above itself. The command palette itself works with no tab open now: the app level commands (Open Recent, editor settings, theme) are always there, and the editor commands join them while an editor is open.

  • Connection rows in the sidebar are laid out like the editor's extension list. Each connection leads with a PostgreSQL icon tinted in its own color (the color moved off the left stripe and onto the icon), a corner dot for the last attempt (available, failed, or never tried), and the server version badged on the icon. The name carries the connect count on the right, the connection string sits beneath it, and the footer shows the origin and when it last connected.

  • The Settings page is a two pane layout now. The search box and scope switch stay pinned in a top bar that never scrolls away, a category list down the left jumps to any section and highlights the one you are reading as you scroll, and each category heading sticks to the top while its settings are in view, so you always know where you are.

  • The global settings.json is a documented JSONC file now, written on first run: every setting carries a comment header with its description, its options and its default, and each one sits commented out at that default, so the file documents everything without overriding anything until you uncomment a line. An "Edit in settings.json" button on the Settings page opens the scope's file right in PlumeSQL's own editor, with comment aware highlighting; saving your edits applies them at once, the same as changing a setting in the UI. Editing a setting through the UI still rewrites the file as plain JSON, and the documented comments are restored on the next start. A settings.json that will not parse, or one carrying values that are not allowed (a bad type, or a key that is not a setting), no longer fails in silence: the problem is reported in the Log, with the last good settings kept in force. A clean save is noted in the Log too ("Settings loaded from ..."), so a hand edit that takes effect is visibly confirmed.

  • A cog button at the foot of the sidebar strip opens a menu: Account (or Log in when signed out), Settings, a Dark/Light theme choice with the current one checked, Check for updates, and About PlumeSQL carrying the build version, which opens a small about dialog: version, runtime, the program and config directories (one line when they share a place, two when they diverge) and the workspace. Each path is shown plainly with two actions beside it, show in the file browser and open a terminal there; plus the website. The version text left the status bar for it, freeing that space. The updater runs in the desktop app; in the browser it just says so. The theme is a global preference now, applied everywhere: the Settings page no longer offers it per directory, and any old per-directory override is dropped on startup so the switch always takes effect.

  • Connection stats remember failures too: a failed Test or connect records the time, the verbatim error and a failure count. When the most recent attempt failed, the sidebar row says so in red ("failed 2 minutes ago") and the connection editor's stats block leads with the failed attempt, so a connection that needs fixing announces itself; the error text itself lives in the Log, its one surface.

  • A connection's custom color now works for its keep: every tab effectively running on a colored connection is tinted with it (a soft wash plus a stripe along the tab's top edge, text untouched), the results pane carries the color as a left stripe while showing that connection's data, and the schema browser's connection header wears the same stripe, one visual language with the sidebar rows.

  • Saved connections carry a stable internal uid (a canonical version 4 UUID) from their first save, surviving renames and parameter edits alike, unlike the parameter-derived id; and every connection exposes a full sha256 hash of its connect parameters. Both groundwork for future references that must not break when parameters change.

  • The connection editor gained a New script button (a fresh SQL tab already wired to the connection, same as the row menu item), and "Connect schema browser with this" tightened to "Connect schema browser" in the editor and the row menus.

  • Every connect writes a Log entry, so the Log reads as the full chronology: tab sessions log alongside the schema browser's ("Connected to X (tab "query")"), and a successful Test logs too ("Test connected to X: PostgreSQL 17.4"), from the editor and the row menu alike.

  • The connection color palette grew from 5 to 12 colors (blue, cyan, teal, green, olive, amber, orange, red, pink, lavender, violet, slate), all picked to stay readable as stripes and washes in both themes.

  • Every file and script remembers its last used connection, per directory in the session: reopening one reconnects it to what it ran on last (a deliberate disconnect is remembered too), and the memory follows renames and deletes. A file never opened before, and every newly created tab or script, inherits the schema browser's current connection, or opens unconnected when the schema browser is not connected. The startup "Pick a connection" dialog is gone: on startup the schema browser simply reconnects to what this directory used last (or the environment connection, or the team default), and when there is nothing to reconnect to, nothing connects; the schema header and the Connections sidebar are the entry points.

  • The connection rows read the same across every group: one badge slot next to the name (the team group for shared, "env" for the session connection, and the project ecosystem for discovered, moved up from the source line), the url and description below, then the source file or the collected stats. A discovered or environment candidate the user never renamed drops its auto-generated "(source)" name suffix in the row, since the source file already shows on its own line (and in the tooltip); right clicking still opens the file through the menu.

  • Right clicking a connection group header (Saved, Shared, This session, Discovered) opens the same checkbox menu as the title bar's ellipsis, matching the Files sidebar's pane headers.

  • The connection editor got a fixed head: the title on its own line (long names and many buttons no longer fight for the same row), the action toolbar under it in one stable arrangement (destructive and primary actions kept to the right), and the result of a Test or a failed Save shown right below the toolbar, always visible. The form scrolls underneath, so the actions stay reachable however deep the SSL and SSH sections go. Save is enabled only when there is actually something to save: it tracks changes against the opened state, disables again after a successful save or when reverting to the original values, and its tooltip says why it is off.

  • The Terminal and the Log carry the classic context menu (the same one the editor and Files use): Copy (when there is a selection; in the Log with nothing selected it copies the row under the cursor, like its hover Copy button), Paste (terminal), Select All, Dismiss and Dismiss All (log), Clear. The Log's text (time and message columns) is natively selectable now, as are a connection editor's test result and stats lines and the Account page's sign-in error, so errors and version strings can be copied straight off the screen.

  • Connection stats: last connected time (relative and absolute), the PostgreSQL server version and a connect count, collected every time a connection is tested or actually connected to (the schema browser or a tab). Once a connection has any, a compact line (relative time, short version, connect count) shows right in its sidebar row, under the description, alongside the full detail in its tooltip and editor tab. Every connection, saved, shared, discovered or from the environment, opens as a tab now, exactly like Settings or Account, with Test, Duplicate and Connect schema browser with this connection buttons; a discovered or environment connection's tab edits only its name and description, everything else shown disabled with a note that the source file itself is never touched, plus a menu item to open that source file directly in the editor, syntax highlighted as .env or JSON.

  • A connection's sidebar row menu also offers New script with this connection, opening a fresh SQL tab already wired to it. Right clicking anywhere on a row opens the same menu through the app's one shared context menu (the same widget the editor and Files already use), file-tree style; the separate hamburger button is gone.

  • Settings, Account and a connection's editor now behave exactly like file tabs: opening one fills the italic preview slot (replacing whatever was previewed before) until pinned by double clicking the tab or opening a second one, they are remembered and restored on reload, and the bottom panel (Terminal, Log) and the schema browser's connection picker keep working while any of them is the active tab. A preview tab's title is dimmer as well as italic, so it reads as provisional at a glance.

  • Auto reveal for connections: activating a connection's editor tab expands its sidebar group if collapsed and highlights its row, the same logic the Files tree already uses for the active file.

  • Connections in the cloud. Signed in, your saved connections mirror to your PlumeSQL account (secrets encrypted at rest server side) and restore automatically on any machine you sign in on. The sync is asynchronous and never blocks the app; deleting a connection locally removes the cloud copy and stays deleted on this machine even if another machine still carries it.

  • Shared team connections. A company account (new on the web: register as a company, invite teammates with a link, billing per seat) shares connections straight from the app: "Share with team" on a saved connection's menu picks or creates a group, "Stop sharing" on a shared entry removes it (owners and admins can remove anything, members what they shared). Every member sees shared connections in the sidebar under Shared, grouped by badge, and can connect right away; the web dashboard shows what the team shares without editing it. Shared entries are read-only in the app (editing saves a personal copy); they follow the account and disappear on sign out. Their offline cache on disk is sealed with the same encryption and key as the connection vault, so shared credentials are never stored in the clear.

  • The Account page shows the company you belong to, with a shortcut to the team and shared connections page on the web. Its web links open in the system browser (the desktop shell cannot follow external links itself), and the action buttons wrap inside the card.

  • The Connections sidebar groups (Saved, Shared, This session, Discovered) collapse and expand with the same pane headers as the Files sidebar, and their collapsed state persists per directory. A Connections Explorer title bar carries an ellipsis menu with a checkbox per group.

  • The Messages grid columns are configurable: the header context menu (or its ellipsis button) toggles time, level, code, message, detail, hint and location, and dragging a header reorders them, kept per directory through the "Message grid columns" setting.

  • A side by side results layout: the toggle at the right of the results tab bar puts the statement grids and the messages grid next to each other with a draggable divider, so both stay visible without switching tabs.

  • The schema sidebar's connection header is a dropdown now: the PostgreSQL elephant in the accent color, the connection name with the server's major version (v17, v18) and a chevron; clicking it opens the same connection palette as the editor toolbar, with Disconnect closing the schema browser's session. Disconnected, the header wears a red broken link and says so.

  • The footer slims down: connection state moved into the schema pane header, so the left side carries only the workspace path for now.

  • A global application Log, sharing the bottom panel with the Terminal as tabs: everything PlumeSQL has to say lands there in arrival order, messages, connect and disconnect events, and errors, persisted per directory in the global plumesql dir (an append-only, size-rotated file, so the log never bloats the disk) and restored on startup. Errors no longer flash as a toast and vanish: they land in the Log as unread rows, the panel opens on the Log tab when the error has no other surface, and the footer's Log button carries a red badge with the unread count. Each row offers Copy (which also dismisses) and Dismiss on hover, and the panel header offers Dismiss all and Clear. Info confirmations keep their quick toast and are logged quietly. Error rows lead with the action the user was attempting (connecting to which connection, saving which file, editing which table), so the raw error underneath has its context; the copy includes both. Uncaught JS errors are captured too, deduplicated when they repeat.

  • The terminal button in the status bar reads as a toggle: while the panel is open it turns into a pressed chip with the accent stripe along its bottom edge.

  • The active tab's underline stripe animates while that tab's query runs: a bright shimmer sweeps along it under a soft glow. A running tab shows the stripe even in the background, so a long query stays visible from any tab.

  • The results pane splits into Results and Messages tabs. Messages are a grid now, the same grid the results use, selection, copy and keyboard navigation included: one row per notice, command completion or error, in arrival order, with the time the server received it, the level (warning rows amber, error rows red, command rows a muted OK), the message and the PL/pgSQL location. Errors carry their detail, hint and location too, and live only here, with the Fix with AI affordance alongside; a side with no rows simply has no tab, so a DO block run shows just Messages. A manual tab pick always sticks.

  • Grid columns are resizable by dragging the header edge, in results and messages alike (the width is not remembered).

  • The results pane appears with the first run: a tab with no run in flight, no result and no error shows only the editor, full height, without the empty results placeholder.

  • Run executes the selection. With nothing selected the Run button stays disabled with a hint; Select All is the deliberate way to run the whole script. The "Run without a selection" setting brings back whole script execution when nothing is selected. The toolbar grew a selection group after the connection dropdown: select all, select line and select to bracket.

  • An editor toolbar with everything one click away. Left to right: a play button to run (a stop square while a query runs) and the tab's connection as a dropdown that opens the connection palette anchored underneath, grouped like the sidebar with Disconnect as the last option and the current connection marked. Disconnect detaches the tab: it stays disconnected, sidebar connection or not, until a connection is picked again. After the dropdown come line and block comment, undo and redo, and cut, copy and paste; find and replace sit right aligned. Keyboard shortcuts live in the tooltips, and buttons disable when their action cannot apply: nothing selected for cut and copy, an empty undo or redo stack, an empty clipboard for paste (when the browser can tell PlumeSQL without prompting), and no usable connection for Run. A connected tab's tooltip carries its pg_backend_pid; a tab with no connection at all wears a broken link icon. The bottom right corner keeps Ask AI and Explain.

  • Auto reveal: activating a file or script tab expands its folders and scrolls its row into view in the matching sidebar tree, expanding the collapsed pane when needed. A section hidden through files.view stays hidden, and a pane collapsed while its tab is active stays collapsed until the next tab switch.

  • Auto save, on by default: changes in file and script tabs write to their file on disk a moment after typing pauses (the files.autoSave setting turns it off; Ctrl+S still saves on demand with its toast).

  • Scripts in the Files sidebar: a second tree above the workspace file browser holding per workspace SQL scripts, stored outside the project in the global plumesql dir (scripts/<workspace hash>/). New scripts are auto named (script1.sql, first free number) and open right away; folders are allowed and renames and saves enforce the .sql extension. The two trees stack as VS Code style explorer panes: clicking a header collapses that section to the bare header, both open share the space through a draggable sash, one open fills the panel, and the Files and Scripts Explorer title bar's ellipsis menu (or the header and tree context menus) toggles the Scripts and Files sections as checkboxes, never leaving the panel empty (the files.view setting, saved to the directory's local scope). The pane states, the sash position and the scripts tree's collapsed folders persist in the per directory session.

  • The Files sidebar filters what it lists through glob patterns: a semicolon separated list (default **/*.sql;**/*.env;**/*.env.*; **/*.json) editable at the top of the tree, with presets behind the funnel button (SQL, SQL + config, Everything, Reset). Edits save to the directory's local settings; the same files.glob setting lives in Settings under Files, in both scopes. A second setting, files.emptyDirs, controls folders with no matching files: hidden by default (a folder created in the tree stays visible for the session), toggled from the same funnel menu or in Settings.

  • The per directory session now also remembers the schema tree expansion (per connection), the collapsed file tree folders and each tab's messages pane, and the tree expansion survives switching side tabs within a session too.

  • The desktop app remembers its window size and position per directory, including the maximized state, and recenters when the saved bounds no longer land on a connected display.

  • An activity dot at the far right of the status bar: dim while idle, green and beating with rippling rings while a query runs or the schema loads, and one accent colored ring every time the session is saved.

  • Session save delay setting (session.saveDelay): how long after the last change the per directory session is written, default 600 ms. Leave it empty to turn session saving off entirely; a previously saved session still restores on open.

  • Preview tabs, VS Code style: a single click on a file or table opens it in an italic preview tab that the next open replaces, so browsing does not stack tabs. Editing the content, double clicking the tab or the file, or dragging the tab pins it permanently.

  • Tabs reorder by drag and drop, with an insert indicator on the drop edge; the order persists per directory like the rest of the session. The sidebar's tab buttons (Connections, Schema, Files, Snippets) reorder the same way.

  • Context menu on the tabs: Close, Close Others, Close to the Right and to the Left, plus Copy Path and Copy Relative Path on file backed tabs.

  • The sidebar tabs can be switched off per directory: right clicking the sidebar strip offers checkbox items for Connections, Files and Snippets (Schema always stays). The choice saves to the local settings scope and the same toggles live in Settings under Appearance, in both scopes.

Fixed ​

  • The browser/WebView native context menu ("Back", "Reload", "Inspect Element") never shows now: right clicking anywhere PlumeSQL does not handle gives a small app menu (Command Palette, New Script, Settings, Toggle Bottom Panel, Keyboard Shortcuts, About) instead of the native one, the way an application should. Text fields keep their native editing menu and the editor keeps its own. This also removes a glitch where letting the native menu appear left Monaco's context view mis-homed, so the next tree menu rendered with its top items clipped. A dev config flag ("inspect" in plumesql.dev.json) lets the native menu through when you need Inspect Element or DevTools.
  • Context menus no longer clip their top items: the editor host that Monaco appends its context view into was overflow:hidden, so a menu opened over the sidebar above the editor's box (a pinned tree row near the top) lost the part that overflowed it. The wrapper now lets the floating overlays through (Monaco still clips its own scrollable content in its inner guard), so menus render whole wherever they open.
  • A table with no columns at all (TimescaleDB's cache_inval_* signal tables are real examples) renders as "CREATE TABLE ... ();" in the schema snapshot instead of an empty parenthesized block, and a zero-column table that still carries table constraints no longer renders a stray leading comma. The DDL renderer's version now salts the snapshot fingerprint, so rendering fixes like this one re-render existing snapshots on their next check instead of waiting for the database schema itself to change.
  • Deliberately collapsed tree folders stay collapsed: the restored session's active DDL tab (and a tree remounting on a side tab switch) no longer auto-reveals its row by re-expanding the folders above it. The VS Code style auto-reveal now fires only when a tab is actually activated, not when the tree comes back with one already active.
  • A schema DDL tab no longer keeps showing content from before a full snapshot rebuild: reactivating an already open (or session-restored) viewer re-verifies its object in the background, and the read-only buffer updates whenever the fresh render differs, not only when the database itself moved.
  • Opening Settings (or anything else that tears down an editor) no longer logs a spurious "Canceled" error: disposing a Monaco editor cancels its pending work and Monaco rethrows the cancellation as an uncaught error, which the global error hook dutifully logged. Cancellation errors are now recognized and ignored there, the same way VS Code treats them.
  • E'...' escape strings tokenize correctly: the highlighter treated the backslash as plain content, read the quotes of E''' as a doubled escaped quote and left the string open, painting everything below the line as one string. Escape strings now get their own tokenizer state where backslash escapes work like PostgreSQL's.
  • Double-clicking the desktop window's header maximizes reliably now. Two causes: inert header children (the brand, the empty middle stretch, the toolbar gaps) swallowed the events entirely, and the built-in drag handler started the native drag loop on the first click, which could eat the second click of a double click before the app saw it. The header now starts the window drag only once the pointer actually moves, so a stationary double click always toggles.
  • The SQL grammar knows the DDL vocabulary now: ALTER, SEQUENCE, OWNED, POLICY, EXTENSION, MATERIALIZED, ENABLE, SECURITY and a curated set of similar statement keywords highlight properly (the vendored grammar shipped a much shorter list), so snapshot DDL files and hand-written DDL read fully colored. ATOMIC lights up from PostgreSQL 14 on, where BEGIN ATOMIC bodies exist.
  • The right click context menu follows the dark theme now: it was drawn in Monaco's fixed neutral grey, which read warm and brown beside PlumeSQL's cool panels. Its background, text, borders and selection are remapped to the app palette, so it stays on theme in both dark and light.
  • A global error always reveals the Log now, even at startup: a failed auto-connect on launch logged the error but the restored session (which runs just after) reopened with the Log closed, hiding it. The error's reveal now wins over the saved panel state.
  • Saving a connection from its editor no longer tears down its live sessions when nothing connect-relevant changed: the editor's placeholder defaults (sslmode "prefer", ssh port 22) over a connection stored with blanks dial identically and used to read as a parameter change, so a color or name edit dropped the schema browser's and every tab's session on that connection.
  • A file's tree row is italic only while its tab is a preview (replaceable) one, matching the tab title, instead of italic for every open file. Double clicking a tree row pins its tab (upright in both places), the same gesture as double clicking the tab.
  • Deleting a file or script that was open now closes its tab too (folders take every tab under them). It used to leave the tab behind as an orphaned scratch buffer pointing at a file that no longer existed; there is no such buffer concept anymore.
  • A failed connection Test showed the same error three times at once: a banner in the editor, the full text in the stats block and the Log entry. Errors now follow one rule everywhere: they go to the Log, which reveals itself (every error opens the panel on the Log tab now, not only some), and nowhere else. The editor keeps its green success banner and the red "last attempt failed" stat; the Account page's inline sign-in error and the startup connect-fix banner followed the same consolidation.
  • Saving a connection used to close every live session on it even when only the name, description, color or the production guard changed. Cosmetic edits now keep the schema browser and every tab connected; sessions still reset when anything connect-relevant changes (parameters, secrets, certificate paths, read-only).
  • Double clicking a file in the tree could open two tabs for it: the double click raced its own single-click sibling while the file was still being read, slipping past the already-open check. A second open of the same file now waits for the first and lands on its tab.
  • Settings, Account and a connection's editor tab used to take over the whole workspace pane: the bottom panel (Terminal, Log) disappeared and could not be reopened while any of them was active, and the schema browser's connection picker and the F1 command palette silently failed to open (Monaco's quick input needs a focused editor, and none of these tabs had one mounted), leaving "Quick input service needs a focused editor to work" errors in the Log. All fixed; every tab kind now shares the same workspace chrome.
  • Errors from a connection's Test, Save and Delete, and from signing in on the Account tab, now land in the application Log like every other error in the app, instead of only showing inline (Delete failing silently showed nothing at all).
  • A connection's identity (the hash display metadata, usage stats and save-merge all key on) no longer shifts on unrelated blank fields: an unset SSL mode now hashes the same as an explicit "prefer" (what a blank one actually connects as), and the SSH hop fields only count when the tunnel is on. A discovered connection with no PGSSLMODE set, most of them, previously got its test and connect stats recorded under a different identity than the one it is listed under, so they never showed up.
  • Grid columns keep their width: the measured widths freeze per statement, so a deep scroll (which could evict the sample block from the row cache) or a row delete no longer shifts them. Only a manual resize changes a column now.
  • Grid column widths actually hold: the table silently fell back to the auto layout (fixed needs a non-auto width), so long content blew a column out to its full width and resizing below the content width did nothing. Column widths are now measured from the real rendered text of the first rows, so short columns fit their content exactly; only columns past the default cap (64 characters' worth, and never more than three quarters of the grid's visible width) overflow with an ellipsis. Resizing works freely in both directions.
  • The sidebar tab strip renders correctly in the desktop app: WebKit ignores vertical writing on buttons, so on Linux (and macOS desktop) the labels stayed horizontal and the strip grew wide. The rotation moved onto the label itself.
  • Reopening a directory restores the last used sidebar tab again: the startup auto-connect no longer forces the sidebar onto Schema. Connecting by hand during a session still lands on Schema.

Removed ​

  • The unnamed "query" scratch buffer. A new pad is always a real script now: the tab strip's "+", the connection editor's New script and the row menus all create the next free scriptN.sql in the Scripts explorer (auto-named, no prompt; rename in the tree), open it pinned and autosave into it. There are only two kinds of things to edit: scripts (global dir, per workspace) and workspace files. Closing the last tab now shows a quiet empty pane with a New script button instead of conjuring another unnamed buffer. An old session's query tabs still restore and work; they just retire for good once closed.
  • Backup and restore from the saved connection menu; it is coming back later in a different shape.
  • The "PlumeSQL: Select connection (for this dir)" palette command, a leftover of the retired select-to-connect model. Connecting the schema browser lives in its header dropdown and the connection row menus.

Changed ​

  • Tooltips appear fast now, everywhere: a small themed tooltip layer replaces the native title bubble, whose show delay (about a second) browsers do not let apps tune. Every existing tooltip keeps working, multiline content included, and follows the theme; Monaco's own surfaces keep their built-in hovers. The delay is configurable (Settings, Appearance, "Tooltip delay"). Editor tab tooltips now lead with what the tab holds, the file path or the title, so a clipped tab reveals itself on hover.
  • The schema tree's filter is a floating always-on-top window now instead of a checkbox context menu, built around one hierarchical selection: check a schema to show all of it, or expand it and check individual object types to show exactly those, so "tables from public plus functions from sales" is just two checkmarks. Checked = shown, nothing checked = everything; only types actually present in a schema are listed (with object counts), the root extensions dir is an entry like any schema, and an "All schemas" group on top toggles one object type across every schema at once. The selection persists per connection; the window itself, its position and whether it is open, per workspace. The window never blocks the tree underneath (filtering applies live as you check), it can be dragged by its header and closed with Escape or its X. The filter button opens it, and so does Ctrl/Cmd+Shift+F while the tree has the keyboard; the rows are one roving Tab stop (arrows move, ArrowRight and ArrowLeft expand and collapse a schema, Space toggles) and a footer button shows everything again. While a selection is active the snapshot bar says what it leaves visible ("showing 10 of 1,450 · public: tables · sales: functions"), the freshness readout following; the tooltip always carries every line.
  • The schema snapshot bar tells the truth about freshness, ago style: "refreshed 14 hours ago", or "up to date, checked 2 minutes ago" once a later check verified the snapshot against the database. The build duration and the check duration are kept apart (the old line could show the cheap fingerprint check's milliseconds as if they were the build); the tooltip carries both. The Refresh button now checks for changes first and skips the rebuild when nothing changed, reporting the outcome as a toast; Shift+click still forces a full rebuild, and the unchanged outcome says so in the toast and the Log so the escape hatch is discoverable right where it is needed.
  • The schema browser's connection header is one compact line now, the same height as the snapshot bar under it: the name leads, the muted details follow (url, server version, and "idle" while the connection has no live session), and the tree gets the saved space. Widening the sidebar reveals more of the line; the tooltip always carries all of it.
  • The dark theme reads with more depth: the surfaces step up in clearer stages (the editor deepest, the sidebar and toolbars lifted above it, inputs and buttons lifted again) and the borders are brighter, so panels, rows, inputs and dividers stand apart instead of blending into one dark field. The accent and syntax colors are unchanged; the light theme is untouched.
  • A file that is not SQL (a .env or .json opened for viewing) never connects and cannot run: opening one no longer inherits the schema browser's connection or dials a session, and its toolbar keeps only the standard edit buttons (undo, redo, cut, copy, paste). Run, the connection dropdown, the selection and comment groups, find and replace, Ask AI and Explain all disappear. SQL (scripts, .sql files, restored scratch tabs) wears an "SQL" lettermark in the tab strip and both explorer trees; every other file the plain sheet icon.
  • The Files sidebar tab is now the Explorer (one tab, its two sections Scripts and Files inside), and the sidebar strip icons read at a glance: the PostgreSQL elephant for Connections (matching a connection's editor tab), the database cylinder for Schema (the database it browses), the "SQL" lettermark for the Explorer (matching the SQL scripts and files it holds).
  • Every sidebar panel now leads with the same header, its tab's icon and label, so a panel always matches the tab that opened it (the Schema panel had no header before). Any panel-specific menu (the Connections group toggles, the Explorer section toggles) sits on the right of that shared header.
  • The Connections sidebar is a pure connection editor now: rows carry no selected or connected status (that lives entirely in the schema header dropdown), clicking a row opens its editor tab instead of connecting, and the popup connection dialog is gone, New Connection and the startup reconnect prompt included. Share with team moved from the row menu into the editor tab as a button.
  • SQL keywords in the dark editor take VS Code Dark's blue, the pgcode look; the rest of the syntax palette and the editor surface stay on the PlumeSQL theme.
  • The sidebar tabs are a permanent vertical strip on the left edge, activity bar style: no more flipping between a horizontal row and the strip with the sidebar width. Clicking the already active tab hides the sidebar; clicking any tab while hidden reopens it there.
  • The cloud account moved out of Settings into its own Account page. A user button in the topbar, next to the AI and MCP buttons, shows the signed-in email (or "Sign in") and opens the page; it warns when the license is inactive. The page carries sign in, plan and trial status, Manage on the web and Sign out.
  • The UI chrome is no longer text selectable, app style: only the editor, the terminal, text inputs and the results grid (through its own selection and copy) keep selection.
  • The tab strip took its look from pgcode: flat flush tabs with a kind icon (file, query or settings), an ellipsized title, a close button that shows on hover or the active tab, and an accent stripe under the active tab. The per tab status, unsaved and running dots are gone for now.
  • Per directory UI state moved out of the workspace: the session now lives in the global plumesql dir, keyed by the workspace path, and an existing .plumesql/session.json migrates over on first use (the .plumesql dir is removed when that leaves it empty; a local settings.json stays put).

[0.19.0] 2026-07-17 ​

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • Integrated terminal: a VS Code style bottom panel that runs your shell in the workspace directory, toggled with Ctrl+` (configurable under Settings, Terminal). It follows the app theme in both dark and light, survives a page reload with its scrollback replayed, and its open state and height are remembered per directory. Turn it off entirely with the terminal.enabled setting, or point terminal.shell at a specific shell. macOS and Linux for now.

  • Editor settings: font size, tab size, indentation (spaces or tabs), whitespace rendering, word wrap, line numbers, minimap and smooth scrolling are now user settings, applied to the editor immediately. They live in the Settings tab under Editor, in both scopes, and the command palette carries a "PlumeSQL: Editor settings (for this dir)" entry whose picks save to the local scope, so the choice stays with the directory. Whitespace renders visibly by default, matching pgcode.

Changed ​

  • The schema browser header now shows the connection name on the first line and the connection url beneath it, matching the connection list.
  • New visual theme: the PlumeSQL brand palette (near-black surfaces, periwinkle accent, dark and light), Segoe UI style interface font with mono reserved for SQL, and the status bar as the accent strip it deserves to be. The connection segment sits on it in green, led by the PostgreSQL elephant, then the server version, "//" and the connection url. The elephant doubles as the online light: green-ish while the schema browser holds a live session, dimmed otherwise, and the segment dims with it.
  • The results grid took its look from pgcode: full grid lines, the header stacks the column name in bold over its type, bold row numbers on the panel background, NULL spelled out in the mark color, and a crosshair hover that lights up the hovered cell together with its row number and column header. The grid renders in the interface font.
  • SQL highlighting understands far more PostgreSQL: the editor grammar was replaced with the monaco-sql-languages PgSQL grammar (vendored, no new dependency) plus PostgreSQL fixes on top. Dollar-quoted strings ($tag$ ... $tag$), double-quoted identifiers, $1 positional parameters, MERGE, and distinct colors for types, scoped keywords and builtin variables now all tokenize correctly; the keyword list grew from about 100 to about 830 entries, which also enriches autocompletion.
  • Function and DO bodies highlight as code: a dollar-quoted literal after DO or AS keeps tokenizing as SQL, with the plpgsql control keywords (BEGIN, RAISE, LOOP, PERFORM, ...) recognized, instead of rendering as one flat string. A dollar-quoted literal anywhere else stays a string.
  • The grammar follows the server version: a tab connected to PostgreSQL 13 does not color MERGE as a keyword it cannot run, while 15+ does, and the completion lists match. Versions are per-major deltas over a base grammar, so a tab talking to 13 and a tab talking to 17 highlight correctly side by side; unknown (not yet connected) gets everything.
  • The PlumeSQL command palette carries color theme entries (Dark, Light): picking one switches the theme immediately and scopes the choice to the workspace (the local settings scope), leaving the global default alone.
  • Saving or promoting a discovered or environment connection no longer keeps the discovery label as its name. The candidate still shows where it came from ("db (.env)"), but the saved global connection, which has no workspace source, takes a clean name (the database, or a name you gave the candidate); the editor also prefills the clean name. Existing saved connections that already carried the label are cleaned on startup.
  • The PlumeSQL command palette has a Select connection entry that lists the available connections in a picker, saved first, then discovered, then the session connection; choosing one connects and remembers it for the directory. Clicking the connection segment in the status bar opens the same picker.
  • The command palette works like VS Code's: it drops from the top of the window with up to half a window of results, instead of being squeezed and clipped to a few rows when the results grid left the editor pane short. F1 now opens straight into the top level PlumeSQL commands; the editor commands stay one "> Editor commands" entry (or a typed ">") away.
  • The topbar carries a centered title with the workspace directory name, which doubles as the palette trigger, and the window title is the directory name too (a workspace at ~/dev/mathmodule2 titles the window "mathmodule2"). The desktop app's native window title follows along, and on macOS the app gets its own slim title bar row: the traffic lights and the workspace title share one line above the toolbar, VS Code style, and the row drags the window.
  • Connection rows lead with the display name, with the connection url as the smaller second line, in the sidebar, the startup picker and the tab's connection dropdown. A new setting, Connection display (connection.display), flips the url back on top; rows without a name always lead with the url.
  • The results grid was rebuilt around streaming and a server-side result spool. Rows render as soon as PostgreSQL produces them while the query keeps draining; the row counter and scrollbar grow live. The full result stays scrollable on the server (up to 1,000,000 rows / 512 MiB per statement by default, in memory up to 32 MiB then spilling to a temp file), served to the grid in windows, so the browser holds only the blocks it looks at. The connection itself is always drained to completion or cancel and never held open while results are read.
  • The grid virtualizes: any result size scrolls smoothly with exact row counts, placeholder shimmer while a block loads, and a proportional scrollbar for results beyond the browser height limit. Column widths freeze from the first rows so nothing reflows while scrolling.
  • The per-run row limit dropdown is gone; in its place Results settings cap what the server spools (row cap, byte cap, spill folder). Anything past a cap is still drained and counted exactly, marked truncated. Statement errors keep the rows that already arrived scrollable, and canceling mid-run keeps the partial result.
  • The Run button turns into Cancel while the tab's query runs.

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • Messages pane under the results, a chronological record of the run: RAISE NOTICE / WARNING lines in wire order between the statements that produced them, command tags with durations for non-SELECT statements, and errors. Warnings badge the pane toggle so they are never silently invisible. Notice floods cap at 1,000 per statement with the overflow counted.

  • Grid edits (update, delete) work on any spooled row and are mirrored server-side, so scrolling back to an edited block never shows stale data. CSV/JSON export reads the full spooled result, not just the visible rows, with a confirmation above 100,000 rows.

  • The grid has selections: click a cell, drag or shift-click a range, click a row number for the row, a column header for the column, the corner (or Ctrl/Cmd+A) for everything; Escape clears. Ctrl/Cmd+C copies the selection tab-separated (NULL as empty), fetching rows the browser has not scrolled to, capped at 10,000 rows.

  • The grid navigates by keyboard: arrows move the active cell, Shift extends the range, Ctrl/Cmd jumps to the edge, PageUp/PageDown move a viewport, Home/End walk the row (with Ctrl/Cmd, the whole grid), and the grid scrolls to keep the target in view at any result size. Enter or F2 edits the active cell when the result is editable.

  • The sidebar tabs (Connections, Schema, Files) flip to a vertical strip docked to the window's left edge when the sidebar is too narrow for the row, with labels reading downward, and flip back once there is room. No more clipped tab names on a narrow sidebar.

  • Hiding the sidebar keeps that vertical strip with no tab lit, so the panels stay one click away; picking a tab reopens the sidebar on it. Ctrl+B still collapses and restores as before.

  • The sidebar visibility toggle moved from the topbar into the tab row itself: rightmost next to Files while the sidebar is open, on top of the strip while it is hidden.

  • A Snippets tab joined the sidebar, a placeholder for the upcoming snippets library.

  • The editor has its context menu back: right-click offers Cut, Copy, Paste, Change All Occurrences and the Command Palette, with the default Monaco behavior and keybinding hints.

  • The file tree's context menu is now the same widget as the editor's (identical look, keyboard handling and dismissal; only one menu can be open at a time) and grew real file management: Cut, Copy and Paste move or duplicate files and folders (pasted copies get "name copy" style unique names, a pending cut renders dimmed), Copy Path and Copy Relative Path put the location on the clipboard, and folders keep New File, New Folder, Rename and Delete. Right-clicking the empty area offers New File, New Folder and Paste into the workspace root.

  • Dragging the sidebar edge well below its minimum width collapses it to the strip, and dragging back out in the same gesture restores it, instead of the resize just stopping at the minimum. The strip's edge stays draggable while the sidebar is hidden, so pulling it out reveals the sidebar again on its last tab, VS Code style.

Fixed ​

  • Double-clicking the desktop title bar row maximizes the window again; the custom title bar had the drag permission but not the maximize toggle it also needs.
  • The Files tab no longer breaks on a workspace with no files or folders at all; it shows the empty hint instead.

0.18.0 2026-07-16 ​

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • Cloud account and licensing. Sign in with a PlumeSQL account under Settings, Account: the app verifies your signed license (offline capable within a grace window) and mirrors your query history and usage to the cloud in the background, without blocking your work. When a signed in account's license has lapsed, running queries is locked with a prompt to renew. Accounts, billing and a 14 day trial live at plume.hercstack.com.

Changed ​

  • Connections are identified by their parameters: the id derives from host, port, database, user and the SSH hop. A rename never breaks per directory references, editing parameters moves the connection to a new identity, and saving the same parameters twice merges into one entry. Duplicate now opens the editor prefilled and unsaved; a copy becomes its own connection once its parameters differ.

  • Names and descriptions moved out of the encrypted vault into a plain connmeta.json in the global plumesql dir, keyed by the parameter identity. Both are edited inline in the sidebar (click the name line under a connection), and they attach to discovered and environment candidates too, which keep their labels without being saved.

  • Every connection row carries its own always-visible toolbar (edit, duplicate, backup, disconnect, delete for saved ones; save, duplicate, disconnect for candidates) instead of the tiny hover-only buttons. The pane header holds a search box, matching name, description, url and source file, and the new connection action. Discovered candidates show their source file on its own line.

  • The header connection chip is gone; the statusbar and the Schema tab header carry the connection info instead, including the read only and production badges, and the Schema header has its own disconnect action for the browser's session.

  • The sidebar now has three tabs: Connections, Schema and Files. The schema browser moved out of the Connections tab into its own Schema tab, headed by the connection's URL, a live status dot and a disconnect action that ends the browser's dedicated session. Every successful connect, the startup auto-connect included, lands on the Schema tab.

  • Selecting a connection is all it takes to make it the directory default; it reconnects on the next start there. The dedicated pin action is gone from the sidebar and the F1 palette. The connection.default local setting stays as an explicit team default, shareable through version control, and beats the remembered selection.

  • Connections identify by their PostgreSQL URL style form, user@host/dbname with the port shown when it is not 5432, across the sidebar, the startup picker and the statusbar; the display name became the secondary line. Connection rows show a live status dot (green connected, gray not) and keep the user-chosen color as a stripe on the row's left edge.

  • The statusbar reflects the schema browser's connection: status dot, the connection URL, the server's major version and the workspace path.

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • Connection description: an optional free note next to the name, edited inline or in the connection editor, searchable, and never encrypted.

  • Every connection label reports its PostgreSQL backend and its owner. Hovering a connection row shows one tooltip with the name, description, and the schema browser's pg_backend_pid when it holds that connection; the schema header and statusbar dots show the same. Each tab's dot shows its own session's pid (named after the file for file tabs) and doubles as the connect and disconnect toggle for that tab's dedicated session.

  • Connection discovery now also scans workspace subfolders for .env and appsettings*.json files. Which folders is a new setting, Discovery folders (discover.dirs, default config), a comma separated list next to the always scanned workspace root; clear it to scan only the root. Projects that keep environment files in config/ get their connections offered automatically.

  • Connection discovery covers many more project types: docker-compose and devcontainer files (a postgres image service becomes a localhost candidate on its published port, and app URLs pointing at a sibling service are remapped the same way), Prisma schemas, Rails database.yml (anchors, merge keys and the common ERB ENV patterns), Django settings modules, Spring application properties and YAML, Alembic ini, dotnet user secrets referenced from a csproj, Laravel database config, TypeORM and knex configs, cargo [env] tables and generic config files with a database section or a dsn key. Everything is parsed statically; project code is never executed.

  • Discovery understands more connection string formats: JDBC URLs, PHP PDO DSNs, SQLAlchemy driver qualified URIs, libpq key=value strings in .env values, the DB_* and POSTGRES_* split variable conventions and dotenv ${VAR} interpolation.

  • Environment placeholders resolve per ecosystem (${VAR:-default}, env("VAR"), ERB ENV[...], Python os.environ lookups, devcontainer ${localEnv:VAR}), reading the workspace .env chain before the process environment; what stays unresolved is kept visible and flagged as a warning on the candidate.

  • A discovered candidate that lacks a password gets it filled from the pgpass file when a line matches, called out in the candidate's warnings.

  • Discovered candidates list in precedence order (dev secret stores, environment specific variants, base files, container synthesized, best-effort literal scans) and show the project type they were inferred from, in the sidebar next to the source file and as a badge in the startup picker.

  • A failed startup auto-connect opens the connection editor prefilled with the failing connection and the error shown inside, instead of a passing toast; saving the fix reconnects immediately.

  • Environment and discovered connections are edited like saved ones. The editor notes where the connection came from; saving always stores an editable copy in the encrypted global connections, with untouched secrets carried over server side. The workspace file is never modified and the candidate stays listed beside the copy.

  • The Connections tab was redesigned around one primary action: the whole row (url, name, source line) is a single click target that connects. Each row keeps two visible buttons, Edit and a menu of labelled actions (Connect, Rename and describe, Duplicate, Save to encrypted connections, Backup and restore, Disconnect, Delete), replacing the five icon-only buttons and the hidden click-to-rename on the name line.

  • The Linux AppImage integrates itself on first launch: it writes a desktop entry and icon under the user's home, so it shows the PlumeSQL icon in the launcher and dock and appears in the applications menu, without needing AppImageLauncher or a manual step. deb and rpm already get this from their package.

Changed ​

  • The startup connection picker was redesigned: a wider dialog with two line rows showing the full connection parameters (user@host:port/database) under the name and the untruncated source file badge. The Saved section always shows, with a note when it is empty, and a New connection action opens the connection form and returns to the picker afterwards.
  • The vault key moved from a key file into the OS keyring: the macOS Keychain, Windows DPAPI, or the Linux Secret Service. Systems without a usable keyring keep using a key file in the OS config dir, and PLUMESQL_KEYRING=file forces that fallback. Vaults sealed with an earlier key file are not migrated: delete vault.json and save the connections again.
  • One state directory: query history and the per directory connection memory moved from the OS config dir into the global plumesql dir, next to the vault and the global settings. The -config flag is gone.

Fixed ​

  • Promote, disconnect and monitoring actions on discovered connections whose id contains # (candidates from postgres:// URL variables such as DATABASE_URL) failed with a not found error: connection ids are now escaped in API URLs.

0.17.0 2026-07-15 ​

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • Desktop auto-update. The app checks for a new signed release on startup and from PlumeSQL > Check for Updates, and installs it in place with one click on macOS, Windows and the Linux AppImage. Package manager installs (deb, rpm) are sent to the download page instead, since they update through the package manager. Updates are verified against a bundled public key, and the version manifest is served from plume.hercstack.com.

  • Per directory session restore: reopening a workspace brings it back exactly as it was left there. Open tabs and scripts, the active tab, the Monaco editor state (cursor, selection, scroll, folding) and the panel layout (sidebar width, collapsed state, active side tab, editor split) are saved to <workspace>/.plumesql/session.json and restored on the next visit. A directory opened for the first time starts from defaults. Panel layout is now per directory rather than global.

  • File management in the Files sidebar tab: New File and New Folder from the header, plus a right click menu on any item for New File, New Folder, Rename and Delete. Empty folders now show in the tree, and open editor tabs follow renames and detach from deleted files. All operations stay confined to the workspace.

  • Desktop File menu: Open File (Ctrl+Shift+O) picks any file through the native dialog, including outside the workspace, and opens it as an unsaved scratch tab; New Project (Ctrl+Shift+N) picks a folder and launches a fresh PlumeSQL window rooted there, leaving the current window open.

  • Dedicated sessions: every query tab owns its own PostgreSQL connection, so SET, temporary tables and interactive BEGIN now persist between runs in the same tab and stay invisible to other tabs. The statusbar shows when a tab sits inside an open or aborted transaction, and Cancel only ever stops that tab's statement. The schema sidebar holds its own dedicated connection too, created when a connection is picked and disposed on switch.

  • Encrypted connection store: saved connections and the Anthropic API key now live in vault.json, sealed with XChaCha20-Poly1305. The vault sits in the global plumesql dir (next to the binary; inside an app bundle or a read only bin dir it falls back to the OS config dir) while the key lives in the config dir, so copying either directory alone reveals nothing. Existing plaintext connections.json migrates automatically and is deleted after the vault verifies; older PlumeSQL versions will not see the migrated connections.

  • Connection per directory: pin a connection to the workspace directory (sidebar pin action, "PlumeSQL: Use this connection for this directory" in the F1 palette, or the connection.default setting in the local scope, shareable with the team). Independently of the pin, the connection last used in a directory is remembered and reconnects on the next start there. When nothing applies, a startup picker offers the saved and discovered connections.

  • Workspace connection discovery: on startup and refresh, the workspace root is scanned (non-recursively) for .env and .env.* files with PG* variables or postgres:// URLs, and appsettings*.json with Npgsql ConnectionStrings, including NpgsqlRest {!NAME:fallback} environment placeholders. Findings appear in a Discovered sidebar group with their source file, are connectable immediately and can be saved into the encrypted store with one action; their secrets never reach the browser.

  • psql style startup: plumesql -h host -p port -U user -d dbname (where -d also accepts a postgres:// URI or a key=value conninfo string), a positional postgres:// URL, the libpq PGHOST/PGPORT/PGDATABASE/PGUSER/ PGPASSWORD/PGSSL* environment variables and ~/.pgpass lookup. The resulting connection appears as "This session", auto-connects, and can be saved for later use.

  • Settings, VS Code style: a dedicated Settings tab (opened from the desktop app's native menu, PlumeSQL then Settings or Ctrl/Cmd+Comma, or with "PlumeSQL: Open settings" in the F1 palette), searchable and grouped by category. Settings live in two scopes: global in a .plumesql/settings.json next to the plumesql binary, so it travels when the binary is copied, and local in <workspace>/.plumesql/settings.json, meant to be shared with a team; local values override global ones and both override the built in defaults. Files may contain JSONC comments and trailing commas when hand edited; saving from the UI rewrites them as plain JSON. Every setting shows a modified indicator and can be reset to its default per scope.

  • Run keybinding setting (default F5): runs the query like the Run button, works with focus anywhere in the app and can be rebound by pressing the new combination in the settings capture field. Ctrl+Enter (Cmd+Enter on macOS) keeps working in the editor as a fixed secondary shortcut.

  • The statusbar shows the workspace directory, the place SQL files and the local settings scope live in.

  • The desktop app has a native menu bar on every platform. Edit and Selection mirror the VS Code menus: undo, redo, cut, copy, paste, find, replace, line and block comment toggles, select all, expand and shrink selection, copy, move and duplicate lines, multi cursor commands and occurrence selection. Editor commands reach Monaco directly, which the native menu selectors cannot.

  • The editor palette got a home for app commands: F1 opens editor command mode as before, deleting the ">" lists the PlumeSQL commands on their own (run query, open settings) instead of an empty picker.

Changed ​

  • Connection pooling is gone: every connection is a single isolated backend. One-shot features (activity, top queries, explain, backup, restore, AI context, MCP) open a throwaway connection per operation. Sessions the UI stops pinging (crashed tab, closed laptop) are reaped after five minutes; closing a tab or the app rolls back any transaction it left open. Expect one server backend per open tab instead of the old shared pool of four per connection.
  • EXPLAIN runs on its own connection, so it can no longer see temporary tables created in a tab's session.
  • The color theme is now a setting (dark or light, default dark) under the Appearance category. A theme previously toggled in the browser is migrated to the global settings file automatically.
  • The SQL editor is now Monaco, the editor from VS Code, replacing CodeMirror. Schema aware completion, Ctrl+Enter execution scopes and CSS variable driven theming behave as before; new are the F1 command palette (including a "PlumeSQL: Run query" action), Ctrl+G go to line, a persistent vertical scrollbar and bracket pair colorization. Monaco loads lazily so the app shell stays small; the binary grows by about 3 MB.
  • The whole UI uses the VS Code font stack (Menlo, Monaco, Consolas, Droid Sans Mono) instead of the system UI font.
  • The header is decluttered. Top Queries and Activity are now icon buttons beside the active connection (and mirrored in the native View menu), and Query history is a clock icon pinned to the right of the tab strip instead of a text button, since history reopens into query tabs.
  • The left sidebar now has Connections and Files tabs. Files is a VS Code style workspace tree with collapsible folders, a search box, SQL files highlighted, and the active file marked; files open in an editor tab show italic with an unsaved dot that mirrors the tab. This replaces the header Files button and the modal file picker; Ctrl+O switches to the Files tab.
  • The left sidebar is draggable to resize, and the width is remembered across restarts.
  • The left sidebar can be hidden and shown with a topbar toggle, Ctrl+B, or View > Toggle Sidebar; the choice is remembered across restarts.

Removed ​

  • The topbar theme toggle button; the theme changes through Settings.
  • The header Files button and its modal picker, folded into the sidebar Files tab.

0.16.1 2026-07-14 ​

Changed ​

  • Running plumesql from the command line now opens the native desktop app (rooted at the working directory) when it is installed, instead of a browser tab; without the desktop app it opens a chromeless app window, falling back to a browser tab only as a last resort. Use -browser to force a normal tab. The workspace directory is handed to the app so its Files browser is already pointed at your scripts.

0.16.0 2026-07-14 ​

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • SQL file workspace. Running plumesql in a directory (or plumesql <dir> / plumesql -dir <dir>) roots a file browser at that directory. The Files button (Ctrl+O) lists .sql scripts by a glob pattern (default **/*.sql, editable), hiding dotfiles, node_modules and vendor; opening a file loads it into a tab, and Ctrl+S saves edits back to disk. Tabs backed by a file show an unsaved indicator. File reads and writes are confined to the workspace directory.

0.15.0 2026-07-13 ​

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • Top queries panel visualizing pg_stat_statements: the heaviest queries for the current database with calls, total and mean execution time, rows and cache hit ratio, a bar for each query's share of total time, sortable by total/mean/calls/rows, with a reset button. When the extension is not enabled the panel shows how to turn it on instead of erroring.

  • top_queries MCP tool exposing the same statistics to agents.

0.14.0 2026-07-13 ​

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • Visual query plan. The Explain button now draws the EXPLAIN ANALYZE output as a tree: each node shows its share of total time as a bar, self time, estimated vs actual rows (misestimates flagged) and loops, with the heaviest nodes and large sequential scans highlighted. It runs in a rolled back transaction and no longer needs an AI key; AI interpretation is added on top when configured. Raw JSON is available behind a toggle.

  • Activity monitor. A new Activity panel lists the connected database's backends from pg_stat_activity (state, duration, wait event, user, query), auto refreshing, with idle-in-transaction highlighted and per backend cancel (pg_cancel_backend) and terminate (pg_terminate_backend), each confirmed.

0.13.0 2026-07-13 ​

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • Inline grid editing. When a query result comes from a single table whose primary key is fully present, the grid is marked editable: double-click a cell to change its value (with a NULL toggle), or hover a row to delete it. PlumeSQL generates a parameterized UPDATE or DELETE keyed on the primary key, with quoted identifiers and parameter placeholders so a cell value can never inject SQL, and refuses any edit that would match more than one row. Editing is off on read only connections and confirmed on production ones. Editability is detected from the wire protocol (table OID and attribute number per column), so joins, aliased columns and computed expressions are correctly treated as read only.

0.12.1 2026-07-13 ​

Fixed ​

  • CSV and JSON export did nothing in the desktop app: the browser blob download an <a download> triggers is silently ignored by the webview. Exports are now written server side to the Downloads folder (uniquely named, never overwriting) and the saved path is shown in a toast, working identically in the browser and the desktop app.

0.12.0 2026-07-13 ​

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • Run selection or statement under cursor: Ctrl+Enter (and the Run button) now execute the highlighted SQL, or the single statement the cursor is in when nothing is selected, instead of the whole editor. The splitter understands strings, dollar quoting and comments.

  • Per-connection safety, set in the connection form: "Read only" makes PostgreSQL refuse every write on that connection (enforced by the server, not just the UI), and "Production guard" asks for confirmation before a write or DDL statement, with a stronger warning for UPDATE/DELETE/TRUNCATE without a WHERE. Read only and production connections are badged in the header.

0.11.1 2026-07-13 ​

Changed ​

  • Added a visible "Ask AI" button next to Run so natural language to SQL is discoverable without knowing the Ctrl+K shortcut; when the assistant is not set up yet the button opens AI settings. The editor placeholder now mentions both Ctrl+Enter and Ctrl+K.

0.11.0 2026-07-13 ​

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • AI assistant, bring your own Anthropic key (AI button in the top bar, Claude Haiku by default with smarter models selectable): Ctrl+K turns a natural language description into SQL grounded in the connection's schema; failed queries get a one-click "Fix with AI" with an explanation and an apply button; the new Explain button runs EXPLAIN (ANALYZE, BUFFERS) in a rolled back transaction and interprets the plan in plain language with index suggestions. Only schema and query text are sent, never row data; the key is stored like connection passwords and never returned to the UI.

  • Three new MCP tools for agents: explain_query (rolled back EXPLAIN ANALYZE), sample_rows and table_stats, making performance diagnosis possible without extra setup.

0.10.0 2026-07-13 ​

Changed ​

  • Backup and restore no longer require external tools. Plain SQL export is now produced by a built-in engine that asks PostgreSQL itself for every definition and streams data over the COPY protocol, covering schemas, extensions, enum, composite and domain types, sequences, tables with identity, generated columns and partitioning, constraints, indexes, dependency ordered views and materialized views, functions and triggers. It works against any server version, ending the pg_dump client and server version matching problem.
  • Import of plain SQL dumps (PlumeSQL's or pg_dump's) now runs through a built-in importer in a single transaction, so a failed import leaves the database untouched. psql is no longer used.
  • pg_dump remains optional, only for the custom archive format; pg_restore only for restoring such archives.

0.9.0 2026-07-13 ​

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • Backup and restore per connection: a backup button in the sidebar opens export and import. Export runs pg_dump to a chosen path as plain SQL or a compressed custom archive, with schema only and data only options; import restores a dump through psql or pg_restore, detecting the format from the file. SSH tunneled connections are forwarded through a short lived local port for the duration of the run.

  • Tools detection endpoint reporting whether pg_dump, pg_restore and psql are installed, with a clear in-app note when they are missing.

Notes ​

  • Backup and restore require the PostgreSQL client tools on the machine and follow the standard rule that the client major version must be at least the server's.

0.8.1 2026-07-13 ​

Fixed ​

  • The MCP light in the top bar stayed gray after starting the HTTP endpoint; it only tracked stdio agent sessions. It now turns green whenever MCP is reachable (HTTP endpoint running or an agent session open) and refreshes immediately when the panel toggles the endpoint.

0.8.0 2026-07-13 ​

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • The MCP server can now be started and stopped from inside the app: a streamable HTTP transport at POST /mcp, controlled with a Start/Stop button in the MCP panel, persisted across restarts and off by default. Clients connect by URL, for example claude mcp add --transport http plumesql http://127.0.0.1:7433/mcp.

  • -port-fallback flag: retry on a random free port when the requested one is taken. The desktop app now prefers the default 7433 with fallback, so the HTTP MCP URL stays stable across restarts.

0.7.0 2026-07-13 ​

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • MCP visibility in the app: an MCP button in the top bar with a status light that turns green while an agent session is open. Its panel shows active sessions (with read only marking), copyable setup snippets for Claude Code and any MCP client using the real binary path, a tool overview and the latest agent issued queries.

  • Agent queries are tagged in history and shown with an "agent" badge in the History panel.

  • MCP processes announce themselves through heartbeat presence files in the config directory, so the UI detects sessions without any coupling between the processes; files left behind by killed processes expire on their own.

0.6.0 2026-07-13 ​

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • MCP server mode: plumesql mcp speaks the Model Context Protocol over stdio, exposing list_connections, get_schema and run_query tools to AI assistants. Saved connections, SSL settings and SSH tunnels are reused as is; secrets never appear in tool output and executed queries land in the shared history.

  • -read-only flag for MCP mode: sessions start with default_transaction_read_only = on, blocking accidental writes.

0.5.0 2026-07-13 ​

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • SSL certificate support: optional CA certificate, client certificate and client key paths per connection, passed as sslrootcert, sslcert and sslkey, alongside the existing sslmode choice, in a collapsible SSL section of the connection form.

  • SSH tunnel support per connection: host, port, user, with password, key file (encrypted keys supported via passphrase) or a running SSH agent for authentication. The database is dialed through the SSH server with no local ports opened, and the tunnel closes with the connection. SSH secrets are stored like the database password and never sent to the UI.

0.4.2 2026-07-13 ​

Fixed ​

  • Selects rendered a few pixels shorter than buttons in the same row. All buttons, selects and inputs now share one 30 px control height.

0.4.1 2026-07-13 ​

Changed ​

  • Confirmation prompts (deleting a connection, clearing history) now use a styled in-app dialog instead of the native browser confirm with its "JavaScript, http://127.0.0.1" chrome. Escape cancels, Enter confirms, destructive actions get a red confirm button. Clearing history previously had no confirmation at all.

0.4.0 2026-07-13 ​

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • Duplicate connection: a copy button next to edit and delete clones a saved connection server side, stored password included, under a derived name like "prod (copy)", and opens it for editing right away.

0.3.2 2026-07-13 ​

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • Branded splash screen in the desktop app: while the local server boots, the window shows the app icon, name and version (injected from the build, no manual upkeep), with the existing error hint when startup fails.

0.3.1 2026-07-13 ​

Fixed ​

  • Select inputs rendered with the native white widget style (oversized, unreadable light gray text) in the desktop app, because WebKitGTK ignores custom select backgrounds until the native appearance is disabled. Selects are now custom styled with a chevron and match button height everywhere, and native widgets follow the app theme via color-scheme.

0.3.0 2026-07-13 ​

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • Per tab connections: every query tab carries its own connection, chosen with a picker next to the Run button, so several databases can be worked on side by side and queries run in parallel across tabs. Tabs show a colored dot for their connection and the status bar reflects the active tab.

  • Schema and autocompletion are cached per connection, so switching tabs switches completion to the right database instantly.

Changed ​

  • Selecting a connection in the sidebar now only sets the browsing context (schema tree and default for new tabs) instead of retargeting existing tabs.
  • Running a query on a saved but disconnected connection connects it automatically.

0.2.0 2026-07-13 ​

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • Native desktop app: a Tauri 2 shell that bundles the server as a sidecar, opens it in a native window and shuts it down with the window. Packaged as deb, rpm and AppImage on Linux, dmg on macOS and msi plus setup exe on Windows.

  • -app flag on the CLI binary: opens PlumeSQL in a minimal app window (no address bar or tabs) using any installed Chromium family browser, falling back to a regular browser tab when none is found.

  • -watch-stdin flag tying the server lifetime to a parent process, used by the desktop shell so no orphaned server survives a killed window.

0.1.0 2026-07-10 ​

First working release.

Added ​

  • Markdown table as a copy and save format. Copy Format and Save Format (a result's dots menu, the Results settings) gained Markdown table: the header row is part of the shape, numbers right aligned by the column's type, NULL reads null, a pipe in a cell escaped; a save writes a .md. The console's copy() on cells follows the same setting.

  • Single binary distribution: a Go server with the web UI embedded, binding to 127.0.0.1 and opening the default browser on start.

  • Connection manager: create, edit, delete and color label PostgreSQL connections, with a test button that verifies credentials before saving.

  • Saved connections and query history persisted in the OS config directory with owner only file permissions; passwords are kept out of API responses.

  • SQL editor built on CodeMirror 6 with PostgreSQL syntax highlighting, schema aware autocompletion and Ctrl+Enter to run.

  • Multi statement execution over the simple query protocol, one result grid per statement, matching psql semantics.

  • Schema browser with schemas, tables, views, materialized views and foreign tables, column types, primary key markers, row estimates and table sizes, plus instant name filtering.

  • Result grid with sticky headers, row numbers, NULL styling and a configurable row limit (100 to 5000) with server side truncation.

  • CSV and JSON export of any result set.

  • Query history panel with timings and errors, reopening any entry in a new tab, and a clear action.

  • Cancellation of the running query.

  • Query tabs, dark and light themes, table preview on double click.

  • Host header validation on every request to block DNS rebinding.

  • Cross platform builds for Linux, macOS (amd64 and arm64) and Windows.

PlumeSQL is a hercstack product.