Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
c1c428f
feat(connections): search the saved list, and make Cmd+R actually rescan
broisnischal Aug 9, 2026
862b4ab
fix(settings): move anonymous usage data out of Agent into General
broisnischal Aug 9, 2026
4c170d8
feat(shortcuts): Cmd+Shift+D disconnects, Enter confirms
broisnischal Aug 9, 2026
5c2db8b
feat(settings): default the agent chat and code fonts to 16px
broisnischal Aug 9, 2026
6893bd0
refactor(providers): one sign-in card instead of four shapes per prov…
broisnischal Aug 9, 2026
410ba01
fix(providers): make the sign-in a section, not a floating card
broisnischal Aug 9, 2026
f6691f5
chore(proc): a helper for spawning without a console window
broisnischal Aug 9, 2026
7d61849
fix(docker): no console window behind the container scan
broisnischal Aug 9, 2026
444ee77
fix(scan): no console window behind the local port scan
broisnischal Aug 9, 2026
f217f8d
fix(license): no console window behind the machine-id lookup
broisnischal Aug 9, 2026
2c918b1
fix(ssh): give the tunnel process no console of its own
broisnischal Aug 9, 2026
7391e2d
fix(omniroute): install into the app's own directory, not npm -g
broisnischal Aug 9, 2026
720e66d
fix(omniroute): stop the button promising a global install
broisnischal Aug 9, 2026
5b76bdd
fix(insights): bound the session and prepared-transaction queries
broisnischal Aug 9, 2026
52a80ba
perf(insights): stop the page paying for data it only reads
broisnischal Aug 9, 2026
e065009
perf(charts): merge live updates instead of rebuilding the chart
broisnischal Aug 9, 2026
8a532fb
perf(startup): load Shiki when a code block needs it
broisnischal Aug 9, 2026
ed68823
perf(startup): load confetti at the moment of celebration
broisnischal Aug 9, 2026
2cf34f1
perf(startup): keep Monaco out of the split-pane snapshot
broisnischal Aug 9, 2026
2aaed7f
feat(shortcuts): one place a keybinding is written down
broisnischal Aug 9, 2026
b59998a
refactor(shortcuts): render the help dialog from the index
broisnischal Aug 9, 2026
126b83b
fix(shortcuts): ⌘B toggles the sidebar again
broisnischal Aug 9, 2026
3b84c1a
refactor(sql): drop the ⌘B forwarding workaround
broisnischal Aug 9, 2026
4f51b96
refactor(orm): drop the ⌘B forwarding workaround
broisnischal Aug 9, 2026
25d94d6
refactor(sql): stop threading onmodb through the console
broisnischal Aug 9, 2026
3b25d55
fix(connect): drop the engine blurb from the form header
broisnischal Aug 9, 2026
a12b63e
feat(connections): a filter you can find, and one that keeps up
broisnischal Aug 9, 2026
4ef79f2
docs(changeset): what this release changes
broisnischal Aug 9, 2026
20dea38
fix(ask): the transcript stops moving, and follows the answer
broisnischal Aug 9, 2026
feef9dc
docs(changeset): cover the Ask AI fixes
broisnischal Aug 9, 2026
d031221
feat(table): mark required columns with an asterisk in the header
broisnischal Aug 9, 2026
d861d2a
docs(changeset): note the required-column marker
broisnischal Aug 9, 2026
6746e3c
feat(ui): tighten the select dropdown
broisnischal Aug 9, 2026
7b59ce9
fix(ui): stop call sites overriding the select item radius
broisnischal Aug 9, 2026
67bda8f
docs(changeset): note the dropdown restyle
broisnischal Aug 9, 2026
d880148
fix(codegen): a real tablist, at the app's own size
broisnischal Aug 9, 2026
fece8b9
perf(json): stop the JSON view materialising the whole result
broisnischal Aug 9, 2026
d60f9c0
feat(codegen): a SQL target, and the whole database in one script
broisnischal Aug 9, 2026
1998c4c
fix(sql): a running query survives leaving its tab
broisnischal Aug 9, 2026
aade516
docs(changeset): cover the SQL, JSON and codegen work
broisnischal Aug 9, 2026
16bb473
fix(codegen): make both tablists behave like tablists
broisnischal Aug 9, 2026
da87925
fix(json): give the JSON viewer its colours back
broisnischal Aug 9, 2026
f1cab45
feat(json): a wrap toggle for long values
broisnischal Aug 9, 2026
e02ecda
docs(changeset): note the JSON colouring and wrap toggle
broisnischal Aug 9, 2026
c9c3845
feat(json): one wrap preference, in Settings, honoured everywhere
broisnischal Aug 9, 2026
62cc104
feat(json): one JSONPath suggestion widget, and a better one
broisnischal Aug 9, 2026
5b1aa1d
feat(shortcuts): "/" focuses the search box for whatever you are on
broisnischal Aug 9, 2026
6d18b86
docs(changeset): cover the JSON suggestions, wrap setting and / shortcut
broisnischal Aug 9, 2026
30feeb5
fix(json): silence the suggestion wrapper's a11y warning correctly
broisnischal Aug 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .changeset/perf-startup-and-platform-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
### Changes

- **The app starts in a fraction of the time it used to.** Stroke was compiling **6.8 MB** of JavaScript and CSS before it could paint a single pixel, and about 3.9 MB of that was the Monaco editor — loaded at boot whether or not you ever opened a SQL tab. The cause was one static import four components deep: the split-pane snapshot imported the JSON view, which imported the Monaco text view, which imported Monaco. Being in a separate chunk file does not make code lazy; only being unreachable from a plain `import` does. Monaco, Shiki and the confetti library are now off that path entirely, along with eight tab pages that were shipped to every user at boot despite being behind "only if opened" guards. **The startup payload is 2.6 MB, down 61%.** Every one of those chunks is still warmed during idle time, so opening a tab is no slower than before — the work simply no longer happens between launching the app and seeing it.

- **Required columns are marked in the grid header.** A `NOT NULL` column carries a red asterisk before its name, the same mark a required form field carries. The constraint used to be discoverable only by trying — clearing a cell and being told "Cannot set NULL" — which is late to find out.

### Bug Fixes

#### Instance Insights
- **The Config tab no longer stalls when it opens.** Postgres reports 350-odd settings and MySQL over 600, and every one of them was built into the page at once. Rows are now constructed a screenful at a time and the rest fill in during idle, so the tab opens immediately and ⌘F still finds every setting a moment later.
- **The session and lock tables stopped rebuilding their column list once per row.** A 200-session table allocated 200 throwaway arrays on every render of the page, and read each cell twice over.
- **The server payloads stopped being deep-proxied.** The settings list, session list and replication stats are replaced whole every refresh and only ever read, so wrapping every row and every field in a reactive proxy cost more than the render it fed.
- **`pg_stat_activity` and `pg_prepared_xacts` are bounded.** They had no `LIMIT` while the lock query beside them had one, so a busy server could hand back an unbounded result — and the session query calls `pg_blocking_pids` once per row.
- **Live charts update instead of rebuilding.** Every five-second refresh tore down all three charts and built them again from nothing; they now merge the new numbers while the shape of the data holds, and rebuild only when it actually changes.

#### OmniRoute
- **Installing it works on macOS and Linux.** It installed with `npm install -g`, which writes to a prefix owned by root on a default nodejs.org or distribution install — so it failed with a permissions error for exactly the people who followed Stroke's own advice to install Node from nodejs.org, and all the app could do was tell them to go run npm themselves. Stroke now keeps its own copy under its data directory, which needs no elevation anywhere and leaves your global npm prefix alone.
- **No more console window on Windows.** Checking for Node, installing the package and starting the gateway each went through `cmd.exe`, and a windowed app on Windows gets a visible console for any console program it starts. Every process Stroke runs — npm, Docker, ssh tunnels, the local port scan, the licence check — is now started without one. The gateway itself is launched directly through Node, so on Windows there is no console to suppress in the first place, and stopping it actually stops it: under `cmd.exe` the Node process outlived the shell that was killed.
- **The install shows progress.** It pulls roughly 1200 packages and takes minutes, and npm prints nothing at all until it finishes, so the panel sat on "resolving package…" long enough to look hung.

- **JSONPath suggestions read like an editor's.** Each row used to print the same identifier twice with a generic badge and no sign of which characters matched what you typed — because the widget was throwing away the type, the detail and the value preview the completion engine had already worked out, then rebuilding a worse version from what was left. Suggestions now show what kind of value is behind a key, highlight the part you typed, preview the value on the highlighted row, arm the top match so Enter takes it immediately, and state their own keys along the bottom. There were three copies of this list, one of them good; there is one now.

#### Keyboard
- **`/` focuses the search box for whatever you are looking at.** The app has around thirty search and filter fields and this works on all of them, including ones added later, because the key finds its target from what is on screen rather than being wired to each field. It follows you into a dialog, ignores hidden fields belonging to tabs you left open, and selects the existing text so typing replaces a stale query.
- **`?` no longer interrupts you mid-sentence.** It opens the shortcuts panel, but only checked for a plain text field — so typing a question mark in the AI chat or certain editor contexts popped the panel open instead.
- **⌘B toggles the sidebar again.** The binding went through a listener on the document in the bubble phase, so anything between the focused element and the document that stopped the event swallowed it first — which is every Monaco editor, the grid canvas, and the dialog overlays. It is now bound in the capture phase, ahead of all of them, and matches ⌘ on macOS and Ctrl elsewhere rather than accepting either (Ctrl+B still moves the caret in a text field on a Mac, as it should).
- **Shortcuts are written down in one place.** `src/lib/shortcuts.js` holds every binding as a single combo string; the macOS and Windows spellings are derived from it rather than typed out twice, which is what let the help dialog drift from what the app actually bound. Adding a shortcut now gets both platforms for free.

#### Connections
- **The saved-connections filter is always there once you have more than one.** It only appeared past five connections, so it materialised out of nowhere as the list grew and the people most likely to want it had never seen it. It also gained Enter to take the top match, arrow keys into the list, and a clear button big enough to hit.
- **Filtering no longer replays the list's entrance animation on every keystroke.** Rows carried a staggered rise of up to half a second, which is a nice first impression and reads as lag when it fires on each character typed. Both the connections rail and the database picker opposite it were affected.
- **The engine blurb is gone from the connection form header.** "Local file-based database" under a heading that says SQLite is describing a decision you have already made.

#### SQL editor
- **A running query survives leaving its tab.** Two Query Editor tabs share one editor component, and switching between them swapped its state out from under an in-flight query — the spinner stopped, the results pane showed the other tab's, and whatever came back landed in whichever tab happened to be in front. The query itself never stopped; only the app lost track of which tab had asked. A run now belongs to the tab that started it.
- **JSON has its colours back.** The viewer was rendering correctly-indented monochrome — keys, strings and numbers all the same shade. Every editor theme in the app was written for SQL and named none of the scopes JSON actually produces, so nearly every token fell through to the plain foreground. All 28 themes now colour it, with property names distinct from values so a page of rows can be scanned.
- **Wrap long JSON values, everywhere, from one switch.** An embedding or a document chunk ran off the right edge with no way to read the rest of it. There is now a Wrap button on every JSON view and a matching setting in Settings → General, all driving the same preference — flip it anywhere and every open view reflows. The row detail panel used to keep its own separate switch, so the same preference existed twice and applied to two of six views. Off by default: unwrapped keeps the structure scannable, and one embedding wrapped can bury every row around it.
- **The JSON view no longer freezes on a large result.** It built one object per row, flattened all of them to a single string, then parsed that string straight back into objects — three passes over the whole result and two copies of it in memory, with no limit. `SELECT *` over a million rows spent its time assembling text far too large to read. It now renders the first thousand rows, says so, and Export still carries everything.

#### Codegen
- **A SQL target, beside Prisma and Drizzle.** The schema as the statements that would rebuild it: enum types, `CREATE TABLE` with nullability, defaults and keys, the indexes, and the foreign keys as `ALTER TABLE` after every table exists — inline `REFERENCES` only works if the parent is created first, which no ordering can promise once the schema has a cycle. It covers every engine, because it is the engine's own language.
- **The whole database in one script.** A scope switch on the SQL target emits every schema at once, schema-qualified, so two schemas owning a `users` table produce two statements rather than a collision. Read only when you ask for it.
- **The Prisma/Drizzle switch is reachable from the keyboard.** It was two plain buttons — no tablist, no arrow keys, invisible to a screen reader — for the primary control on the page. Its pills were also 24px next to 28px buttons; they match now.

#### Interface
- **Dropdown menus were rebuilt to fit their own panel.** Item corners were rounder than the space they sat in, leaving a crescent of gap at each one; rows were small enough to mis-click; the highlight jumped from row to row with no transition, so running the pointer down a list strobed; and the selected value was marked only by a tick at the far right edge, away from the label you were reading. Fixed in the shared component, so every menu in the app gets it.

#### Ask AI
- **You can select and copy an answer.** The command palette is rendered outside the app's DOM root, so the rule that makes dialog text selectable never applied to it — and a quick answer you cannot quote is most of the point of asking.
- **The panel stops moving while you type.** It had a maximum height but no height, so the box grew as the answer streamed and as query results appeared, shifting everything already on screen out from under the cursor. A transcript wants a window that scrolls, not one that resizes.
- **It follows the answer to the end.** Auto-scroll measured the page the instant new text arrived, but the markdown renders a moment later — so it scrolled to a bottom that did not exist yet, fell further behind with each token, and gave up entirely once it had drifted far enough, which is exactly when there was most left to read. It now waits for the text to actually land, and scrolling up to re-read something no longer fights the stream.
- **Typing during an answer is smoother.** Every token re-ran a walk up the DOM asking the browser to recompute styles, for the whole length of the answer.
11 changes: 7 additions & 4 deletions src-tauri/src/db/insights.rs
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,8 @@ pub async fn instance_state(state: State<'_, DbState>) -> Result<InstanceState,
wait_event,
pg_blocking_pids(pid) AS blocking_pids
FROM pg_stat_activity
ORDER BY pid",
ORDER BY pid
LIMIT 500",
)
.await;
let locks = pg_query_objects(
Expand All @@ -595,7 +596,7 @@ pub async fn instance_state(state: State<'_, DbState>) -> Result<InstanceState,
)
.await;
let prepared_transactions =
pg_query_objects(&pool, "SELECT * FROM pg_prepared_xacts").await;
pg_query_objects(&pool, "SELECT * FROM pg_prepared_xacts LIMIT 200").await;
Ok(InstanceState { engine: "postgres".into(), sessions, locks, prepared_transactions })
}
ActiveConnection::Mysql(pool) => {
Expand All @@ -604,7 +605,8 @@ pub async fn instance_state(state: State<'_, DbState>) -> Result<InstanceState,
"SELECT ID AS id, USER AS user, HOST AS host, DB AS db,
COMMAND AS command, STATE AS state, INFO AS info
FROM information_schema.PROCESSLIST
ORDER BY ID",
ORDER BY ID
LIMIT 500",
)
.await;
// performance_schema.data_locks may be disabled or unavailable → empty.
Expand Down Expand Up @@ -635,7 +637,8 @@ pub async fn instance_state(state: State<'_, DbState>) -> Result<InstanceState,
&cfg,
"SELECT query_id, user, toString(address) AS client, elapsed, \
read_rows, memory_usage, query \
FROM system.processes",
FROM system.processes \
LIMIT 500",
)
.await;
Ok(InstanceState {
Expand Down
7 changes: 6 additions & 1 deletion src-tauri/src/db/local_scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,12 @@ fn listening_ports(pid: u32) -> Vec<u16> {

#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn listening_ports(pid: u32) -> Vec<u16> {
let Ok(out) = std::process::Command::new("netstat").args(["-ano", "-p", "TCP"]).output() else {
// quiet_std: the local-database scan runs on the connection screen, so a bare
// spawn flashed a console window over the app on Windows.
let Ok(out) = crate::proc::quiet_std(
std::process::Command::new("netstat").args(["-ano", "-p", "TCP"]),
)
.output() else {
return Vec::new();
};
let pid = pid.to_string();
Expand Down
10 changes: 7 additions & 3 deletions src-tauri/src/db/ssh_tunnel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,15 @@ impl SshTunnel {

args.push(dest);

let child = Command::new("ssh")
.args(&args)
let mut cmd = Command::new("ssh");
cmd.args(&args)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
// quiet: the tunnel is a background process the user never interacts with
// (all three streams are already closed), so on Windows it must not get a
// console window of its own.
let child = crate::proc::quiet(&mut cmd)
.spawn()
.map_err(|e| {
format!(
Expand Down
19 changes: 14 additions & 5 deletions src-tauri/src/docker.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
use crate::proc::quiet;
use std::process::Stdio;
use tauri::Emitter;
use tokio::io::AsyncBufReadExt;
use tokio::process::Command as Cmd;

/// `docker …`, with no console window on Windows. The container scan runs on the
/// connection screen, so a bare spawn flashed a cmd.exe window at app start.
fn docker() -> Cmd {
let mut c = Cmd::new("docker");
quiet(&mut c);
c
}

// ── DB-level readiness checks ─────────────────────────────────────────────────
// TCP-open ≠ database-ready. MySQL/Postgres accept TCP connections seconds before
// they finish initialization. We retry actual SQL pings to confirm readiness.
Expand Down Expand Up @@ -76,7 +85,7 @@ fn emit_log(app: &tauri::AppHandle, event: &str, line: &str, kind: &str) {

#[tauri::command]
pub async fn docker_check() -> Result<String, String> {
let out = Cmd::new("docker")
let out = docker()
.args(["version", "--format", "{{.Server.Version}}"])
.output()
.await
Expand Down Expand Up @@ -135,7 +144,7 @@ pub async fn docker_run_db(
// ── Pull ──────────────────────────────────────────────────────────────────
emit_log(&app, &evt, &format!("Pulling {image}…"), "info");

let mut pull_child = Cmd::new("docker")
let mut pull_child = docker()
.args(["pull", image])
.stdout(Stdio::null())
.stderr(Stdio::piped())
Expand All @@ -162,7 +171,7 @@ pub async fn docker_run_db(
emit_log(&app, &evt, "Image ready. Starting container…", "info");

// ── Run ───────────────────────────────────────────────────────────────────
let mut run_cmd = Cmd::new("docker");
let mut run_cmd = docker();
run_cmd
.arg("run")
.arg("-d")
Expand Down Expand Up @@ -490,7 +499,7 @@ fn database_from_inspect(c: &Value) -> Option<DockerDatabase> {
/// there is nothing to offer, and the connection screen stays quiet about it.
#[tauri::command]
pub async fn scan_docker_databases() -> Result<Vec<DockerDatabase>, String> {
let Ok(ps) = Cmd::new("docker").args(["ps", "--format", "{{.ID}}"]).output().await else {
let Ok(ps) = docker().args(["ps", "--format", "{{.ID}}"]).output().await else {
return Ok(Vec::new());
};
if !ps.status.success() {
Expand All @@ -508,7 +517,7 @@ pub async fn scan_docker_databases() -> Result<Vec<DockerDatabase>, String> {

// One inspect for every container, line-delimited so a single malformed
// entry can't take the whole scan down with it.
let Ok(out) = Cmd::new("docker")
let Ok(out) = docker()
.arg("inspect")
.arg("--format")
.arg("{{json .}}")
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod license;
mod mcp;
mod omniroute;
mod metrics;
mod proc;
mod providers;
mod secrets;
mod web_search;
Expand Down
16 changes: 8 additions & 8 deletions src-tauri/src/license.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,14 +167,14 @@ fn machine_id_raw() -> String {
}
#[cfg(target_os = "windows")]
{
if let Ok(out) = std::process::Command::new("reg")
.args([
"query",
"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography",
"/v",
"MachineGuid",
])
.output()
// quiet_std: the machine-id lookup runs at startup for the licence check.
if let Ok(out) = crate::proc::quiet_std(std::process::Command::new("reg").args([
"query",
"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography",
"/v",
"MachineGuid",
]))
.output()
{
if let Ok(s) = String::from_utf8(out.stdout) {
for line in s.lines() {
Expand Down
Loading
Loading