From 6174a5c1220da9ccff79dea35d58f11d4bd8f07a Mon Sep 17 00:00:00 2001 From: broisnischal Date: Fri, 3 Jul 2026 10:17:05 +0545 Subject: [PATCH 1/3] chore: v1.1 changeset + auto-release tag push via PAT Co-Authored-By: Claude Fable 5 --- .changeset/quality-pass-1-1.md | 40 ++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .changeset/quality-pass-1-1.md diff --git a/.changeset/quality-pass-1-1.md b/.changeset/quality-pass-1-1.md new file mode 100644 index 00000000..0862ef2a --- /dev/null +++ b/.changeset/quality-pass-1-1.md @@ -0,0 +1,40 @@ +### New Features + +#### Tabs +- Pin tabs: pinned tabs stay grouped at the front, show a pin badge, and survive "Close Others" / "Close All" + +#### Themes +- New **Graphite** theme, wired through the editor and diagram themes + +#### Licensing +- New in-app license page: "Activate Pro" now opens a dedicated tab with key entry, plan status, what Pro unlocks, and a purchase link — confetti included on activation +- License entry in Settings with a live plan badge (Pro / Trial / Free) + +#### SQL Editor +- Column sorting on query results — numbers sort numerically, NULLs last, and the sorted order carries into the JSON view, charts, and CSV/JSON exports + +### Bug Fixes + +#### Data Table +- Pinned columns now freeze flush to the left edge — no more empty gutter-wide gap after scrolling +- Query results grid is read-only: removed actions that can't apply to ad-hoc results (Filter/Exclude by value, Edit, Set NULL, Duplicate/Delete row, INSERT copy) +- Column stats no longer error with "Invalid identifier" on SQL editor results +- Sidebar rows no longer shift as row counts load in + +#### Window +- Double-clicking the titlebar now maximizes reliably (a duplicate handler was maximizing and instantly restoring) + +### Changes + +#### Performance +- Reconnect feels instant: the overlay drops as soon as the connection is live, and schemas/tables stream into the sidebar instead of blocking the screen +- Table row counts load lazily — the sidebar renders immediately with placeholders and exact counts fill in from a background pass; schema switches get the same speedup +- Faster Postgres connect: one fewer round trip per pooled connection, and the reachability check runs concurrently with the real handshake +- Sidebar stays virtualized while table rows are expanded (no more hitch when expanding columns on large schemas) +- AI streaming responses parse markdown at most once per frame instead of once per token + +#### Settings +- Settings dialog redesigned: grouped sections (Appearance / Behavior / More), clearer hierarchy, keyboard-shortcut chips + +#### Release pipeline +- Releases now publish to the public `stroke` repository — auto-update, Homebrew, and Scoop keep working with the private source repo From 134e65ebb138cf4550a89842a82976c6e940da30 Mon Sep 17 00:00:00 2001 From: broisnischal Date: Fri, 3 Jul 2026 10:20:57 +0545 Subject: [PATCH 2/3] chore(changelog): fold unreleased notes into 1.0.0, sync changelog to public repo on release Co-Authored-By: Claude Fable 5 --- .github/workflows/release.yml | 27 ++++++++++++++++++++++++--- CHANGELOG.md | 6 ------ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 55d720e0..1ac1370f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -405,9 +405,10 @@ jobs: TAG: ${{ env.RELEASE_TAG }} run: gh release upload "$TAG" --repo "$RELEASES_REPO" latest.json --clobber -# ── 4. Refresh the Scoop manifest in the public releases repo ──────────────── -# Scoop clones the bucket repo anonymously, so the manifest must live in -# RELEASES_REPO, not in this private source repo. +# ── 4. Refresh the Scoop manifest + changelog in the public releases repo ──── +# Scoop clones the bucket repo anonymously, and the website reads the +# changelog from the public repo — both must live in RELEASES_REPO, not +# in this private source repo. update-scoop: needs: build runs-on: ubuntu-latest @@ -468,6 +469,26 @@ jobs: git push origin HEAD fi + # Mirror the changelog into the public repo so the website (and anyone + # browsing github.com/broisnischal/stroke) sees release history without + # access to the private source repo. Fetched at the release tag so it + # includes the entry auto-release just folded in. + - name: Sync CHANGELOG.md + env: + GH_TOKEN: ${{ secrets.RELEASES_TOKEN }} + TAG: ${{ env.RELEASE_TAG }} + run: | + gh api "repos/${{ github.repository }}/contents/CHANGELOG.md?ref=${TAG}" \ + --jq .content | base64 -d > CHANGELOG.md + + git add CHANGELOG.md + if git diff --cached --quiet; then + echo "Changelog already current." + else + git commit -m "docs(changelog): stroke ${TAG}" + git push origin HEAD + fi + # Optional instant Homebrew tap refresh. Without a PAT this is skipped and # the tap updates itself on its own schedule instead (see homebrew-tap repo). # (secrets can't be used in an `if:`, so the check lives in the script.) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1821b051..fc96e856 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,6 @@ All notable changes to Stroke are listed here, newest first. ## [1.0.0] - 2026-07-01 -### Changes -- feat:: perf, ux, themes and optimizations (#13) - - -## [Unreleased] - ### Performance - Faster (re)connect: `onConnected` now loads query history + MCP autostart concurrently with the schema/table load instead of behind it, and no longer double-fetches the query stores on startup. - Fixed the reconnect "slow acquire" storm: per-table `COUNT(*)` on large Postgres schemas now runs with bounded concurrency (capped at the pool size) instead of firing every count at once and starving the 4-connection pool. From b90f41ea3d8f305e7ab7b8ed1c5080e3dd027559 Mon Sep 17 00:00:00 2001 From: broisnischal Date: Fri, 3 Jul 2026 11:07:53 +0545 Subject: [PATCH 3/3] =?UTF-8?q?hotfix(sidebar):=20remove=20inline=20column?= =?UTF-8?q?=20expansion=20(chevron)=20=E2=80=94=20restore=20plain=20virtua?= =?UTF-8?q?lized=20scrolling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- src/lib/components/Sidebar.svelte | 186 +----------------------------- 1 file changed, 5 insertions(+), 181 deletions(-) diff --git a/src/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte index a310275f..912b68b0 100644 --- a/src/lib/components/Sidebar.svelte +++ b/src/lib/components/Sidebar.svelte @@ -8,8 +8,6 @@ import Eye from "@lucide/svelte/icons/eye"; import Layers from "@lucide/svelte/icons/layers"; import ChevronDown from "@lucide/svelte/icons/chevron-down"; - import ChevronRight from "@lucide/svelte/icons/chevron-right"; - import Loader2 from "@lucide/svelte/icons/loader-2"; import Columns3 from "@lucide/svelte/icons/columns-3"; import ListFilter from "@lucide/svelte/icons/list-filter"; import Lock from "@lucide/svelte/icons/lock"; @@ -44,7 +42,6 @@ import ResizeHandle from "./ResizeHandle.svelte"; import { cn } from "$lib/utils.js"; import { formatTableRowCount } from "$lib/table-list.js"; - import { getTableColumnStructure } from "$lib/api.js"; import { clampNavSidebarWidth, loadLayout, @@ -92,76 +89,6 @@ onviewstructure = /** @type {(table: string) => void} */ (() => {}), } = $props(); - // ── Inline column expansion (chevron on table rows) ───────────────────────── - /** @type {Set} table names expanded in the current schema */ - let expandedTables = $state(new Set()); - /** @type {Map | 'loading' | 'error'>} keyed by "schema.table" */ - let expandedColumnsCache = $state(new Map()); - - /** @param {string} tableName */ - async function toggleExpand(tableName) { - const next = new Set(expandedTables); - if (next.has(tableName)) { - next.delete(tableName); - expandedTables = next; - return; - } - next.add(tableName); - expandedTables = next; - const key = `${activeSchema}.${tableName}`; - if (expandedColumnsCache.has(key) && expandedColumnsCache.get(key) !== 'error') return; - expandedColumnsCache = new Map(expandedColumnsCache).set(key, 'loading'); - try { - const cols = await getTableColumnStructure(activeSchema, tableName); - expandedColumnsCache = new Map(expandedColumnsCache).set( - key, - cols.map((c) => ({ name: c.name, dataType: c.dataType ?? '' })), - ); - } catch { - expandedColumnsCache = new Map(expandedColumnsCache).set(key, 'error'); - } - } - - // Collapse everything when the schema changes — names refer to other tables now - $effect(() => { - activeSchema; - untrack(() => { - expandedTables = new Set(); - expandedHeights = new Map(); - }); - }); - - /** - * Measured pixel height of each expanded row's inline column list, keyed by - * table name. Lets the virtual list keep working while rows are expanded: - * collapsed rows use the fixed ROW_H stride, expanded rows add their real - * rendered height. Measurements persist after a row scrolls out of the - * render window so the spacer math stays stable. - * @type {Map} - */ - let expandedHeights = $state(new Map()); - - /** @param {string} name @param {number} h */ - function setExpandedHeight(name, h) { - if (h <= 0 || expandedHeights.get(name) === h) return; - const next = new Map(expandedHeights); - next.set(name, h); - expandedHeights = next; - } - - /** - * Svelte action: keep the expanded column list's height in expandedHeights. - * offsetHeight is exact because the list uses padding (not margins) for its - * outer spacing. - * @param {HTMLElement} node @param {string} name - */ - function measureExpanded(node, name) { - setExpandedHeight(name, node.offsetHeight); - const ro = new ResizeObserver(() => setExpandedHeight(name, node.offsetHeight)); - ro.observe(node); - return { destroy: () => ro.disconnect() }; - } - const openTableSet = $derived(new Set(openTables)) // PostgreSQL is the only driver where "schema" is a CREATE SCHEMA namespace. @@ -514,78 +441,17 @@ const shouldVirtualize = $derived(filteredRegularTables.length > VIRT_THRESHOLD) - /** Fallback extra height for an expanded row that hasn't been measured yet - * (only reachable in exotic cases — expanding mounts the row, which measures - * immediately). Roughly one "Loading columns…" line. */ - const EXPANDED_FALLBACK_H = 26 - - /** - * Expanded rows within the filtered list, ascending by index, with the extra - * height their inline column list adds beyond the fixed ROW_H stride. - * Usually 0-3 entries, so the per-scroll window math below stays O(expanded) - * rather than O(tables). - */ - const expandedExtras = $derived.by(() => { - if (expandedTables.size === 0) return /** @type {{ i: number, extra: number }[]} */ ([]) - const out = [] - for (let i = 0; i < filteredRegularTables.length; i++) { - const name = filteredRegularTables[i].name - if (expandedTables.has(name)) { - out.push({ i, extra: expandedHeights.get(name) ?? EXPANDED_FALLBACK_H }) - } - } - return out - }) - const virtStart = $derived.by(() => { if (!shouldVirtualize) return 0 - const y = sidebarScrollTop - tableListOffsetTop - // Subtract the extra height of expanded blocks that sit fully above y. - // If y lands INSIDE an expanded block (its tall column list), cap the - // start at that row so it stays mounted while any part of it is visible. - let acc = 0 - let cap = Infinity - for (const { i, extra } of expandedExtras) { - const top = i * ROW_H + acc - if (top + ROW_H + extra <= y) { - acc += extra - } else { - if (top <= y) cap = i - break - } - } - const idx = Math.min(cap, Math.floor((y - acc) / ROW_H)) - return Math.max(0, idx - VIRT_BUFFER) + return Math.max(0, Math.floor((sidebarScrollTop - tableListOffsetTop) / ROW_H) - VIRT_BUFFER) }) const virtEnd = $derived.by(() => { if (!shouldVirtualize) return filteredRegularTables.length - const y2 = sidebarScrollTop + sidebarHeight - tableListOffsetTop - // Expanded blocks starting above the viewport bottom push later rows down, - // so fewer fixed-stride rows fit — fold their extra height out of y2. - // minEnd keeps a block that straddles the bottom edge mounted. - let acc = 0 - let minEnd = 0 - for (const { i, extra } of expandedExtras) { - if (i * ROW_H + acc <= y2) { - acc += extra - minEnd = i + 1 - } else break - } - const end = Math.max(minEnd, Math.ceil((y2 - acc) / ROW_H)) + VIRT_BUFFER + const end = Math.ceil((sidebarScrollTop + sidebarHeight - tableListOffsetTop) / ROW_H) + VIRT_BUFFER return Math.min(filteredRegularTables.length, end) }) - const virtTopPad = $derived.by(() => { - if (!shouldVirtualize) return 0 - let extra = 0 - for (const e of expandedExtras) { if (e.i < virtStart) extra += e.extra; else break } - return virtStart * ROW_H + extra - }) - const virtBotPad = $derived.by(() => { - if (!shouldVirtualize) return 0 - let extra = 0 - for (const e of expandedExtras) { if (e.i >= virtEnd) extra += e.extra } - return Math.max(0, (filteredRegularTables.length - virtEnd) * ROW_H + extra) - }) + const virtTopPad = $derived(shouldVirtualize ? virtStart * ROW_H : 0) + const virtBotPad = $derived(shouldVirtualize ? Math.max(0, (filteredRegularTables.length - virtEnd) * ROW_H) : 0) /** Shared field chrome for schema select + table filter (aligned in sidebar grid) */ const sidebarFieldClass = @@ -1145,14 +1011,13 @@ {#if virtTopPad > 0}{/if} {#each filteredRegularTables.slice(virtStart, virtEnd) as table (table.name)} {@const isSelected = selectedItems.has(table.name)} - {@const isExpanded = expandedTables.has(table.name)}
  • {/each} {#if virtBotPad > 0}{/if}