Skip to content

Query execution UX: cancel/spinner fixes, target-tab support, run-status glyphs#226

Open
NicolaasGrobler wants to merge 6 commits into
mainfrom
feat/execution-ux-runstatus-glyphs
Open

Query execution UX: cancel/spinner fixes, target-tab support, run-status glyphs#226
NicolaasGrobler wants to merge 6 commits into
mainfrom
feat/execution-ux-runstatus-glyphs

Conversation

@NicolaasGrobler

@NicolaasGrobler NicolaasGrobler commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Comprehensive branch bundling this session's query-execution fixes plus the run-status glyph work. The branches got entangled across parallel work, so this is one PR rather than several; PR #224 remains the clean cancel/spinner subset if you'd rather land that separately first.

What's in here

Cancel + per-tab running state (also in #224)

Target-scoped tabs (the "results bar never comes up" root cause)

  • The top toolbar (Run/Cancel/Format/…) and the results panel were gated on the sidebar-selected connection (isDatabaseReady). A tab running through its own target connection kept them hidden even though the query executed. Now gated on activeTabRunnable (resolves the tab's own connection), and the Run button + both Save paths use the resolved connection too.

New tab inherits the current DB

  • addNewTab now falls back to the active tab's target (chain: explicit → sidebar → active tab), so a new query window opens against the DB you're already in — matching DataGrip/DBeaver/pgAdmin.

Run-status gutter glyphs + live running spinner

  • Root bug: the onRun wrappers were (q) => executeQuery(q), dropping the statement-line argument, so statementInfos was always empty and every glyph defaulted to line 1. Forwarding the arg fixes the ✓/✗ landing on the right block.
  • New transient amber "running" spinner in the gutter next to the executing block (separate from the reconciled ✓/✗, which freezes mid-run), set before any await so it renders reliably. Header-bar spinner removed.

Testing status (honest)

  • Verified live against a real PostgreSQL (GoodX) connection: per-tab spinner, Cancel→re-run, toolbar/results appearing for target tabs, Run button enabled.
  • The run-status/running-glyph line fix (onRun forwarding) was the last change and the live test loop was cut short — it typechecks and the logic is verified by the earlier [running-glyph] debug trace, but a final visual confirm of the gutter spinner is still pending.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Canceling a query no longer freezes execution
    • Running indicator now stays on the specific tab that launched the query instead of showing globally
  • New Features

    • Running indicator now displays with a pulsing animation for improved visibility
  • Tests

    • Added test coverage for statement glyph mapping and result merging utilities

NicolaasGrobler and others added 6 commits June 9, 2026 12:06
… tab

Two execution-state bugs that left query running broken or misleading:

1. Cancelling a query froze all further execution (#212). The re-entrancy
   guard added in #217 keys off `isExecutingRef`, which every run-end path
   cleared except `cancelQuery`. Because libpq has no real query cancel, the
   in-flight `db.select` keeps running server-side and only clears the flag
   when it eventually returns -- so after Cancel the toolbar showed "Run" but
   the guard silently swallowed every subsequent run and the results bar
   never came up. `cancelQuery` now clears `isExecutingRef` immediately and
   bumps the execution generation so the abandoned run no-ops on its late
   return instead of clobbering whatever ran after the cancel.

2. The "running" spinner followed the active tab (#222). Execution state was
   a single global `isExecuting` flag gated on `activeTabId === tab.id`, so
   the tab glyph, the SQL Editor header bar, and the results-panel loading
   state painted onto whichever tab was active rather than the one that
   launched the query. A new `executingTabId` records the launching tab; the
   tab glyph keys off it, and the editor/psql/results loading only show
   "running" when the active tab is the executing one. The top toolbar
   Run/Cancel stays global (one query runs at a time; cancel from anywhere).

Closes #212
Closes #222

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The top toolbar (Run/Cancel/Format/…), the bottom results panel, and the
save-query paths were all gated on the sidebar-selected connection
(activeConnection + selectedDatabase). A tab opened from the explorer or
restored from a session runs through its own `target` connection in
executeQuery without ever setting the context connection, so those gates
stayed false: the query executed (the tab spinner showed) but the results
bar never appeared, there was no Cancel button, the Run button was greyed
out, and Save silently no-opped.

Resolve the active tab's connection once (`activeTabConnection` /
`activeTabDatabase` — target overrides context, mirroring executeQuery)
and drive off it:
- `activeTabRunnable` gates the toolbar (incl. Run/Cancel) and the
  results panel.
- the Run button's `disabled` uses `activeTabRunnable`.
- the toolbar Save button and the global Ctrl+S handler save via the
  resolved connection/database instead of erroring.

Removes the now-unused `isDatabaseReady`. Inline row editing and
transactions still assume the context connection — a deliberately
separate, larger follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
addNewTab resolved a tab's target from explicit params or the sidebar
selection only. When you work through tab `target`s with nothing selected
in the sidebar, a new tab got no target and came up un-runnable (no
toolbar, no results, Run greyed out).

Add the active tab's own target as the final fallback in the resolution
chain (explicit → sidebar → active tab), so opening a new query window
inherits the DB you're currently in — matching DataGrip/DBeaver/pgAdmin,
where a new editor opens against the current data source. Sidebar
precedence is unchanged, so an explicit sidebar selection still wins.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The only running indicator while a query ran was a spinner in the editor
header bar (next to "[conn] SQL Editor"), detached from the statement
actually executing. Show it where it belongs: an amber spinning arc in
the gutter next to the running block.

It can't ride the existing run-status glyph reconcile — that intentionally
freezes mid-run (returns early while isExecuting). So it's a separate,
transient Monaco decoration in QueryEditor driven directly by
`isExecuting` + `lastExecutedStatement`: it appears at the executing
block's line and clears the instant execution ends (success, error, or
cancel), at which point the reconciled ✓/✗ takes over.

- MainContent passes `lastExecutedStatement` to the editor.
- `.statement-glyph-running` is now an animated spinner (was a static dot).
- The header-bar spinner is removed (running state now reads from the
  gutter glyph + tab glyph + Cancel button).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…right block

The editor passes the statement's document line via onRun's second arg, but
the MainContent wrappers were `(q) => executeQuery(q)` — dropping it. So
executeQuery always saw statementInfo=undefined, statementInfos=[], and every
line fell back to 1: the run-status ✓/✗ glyphs landed on line 1 instead of the
executed block, and lastExecutedStatement was never updated to 'running', so
the new running spinner never appeared.

- Forward the arg: `(q, info) => executeQuery(q, info)` (editor + psql wrappers).
- Set the 'running' indicator at the top of the try, before any await, so the
  editor reliably renders it the instant you Run (a status set only just before
  db.select could be overwritten by the result before a render happened).
- Running glyph CSS is a pulsing amber dot (opacity animation), since Monaco
  doesn't honor transform on glyph-margin elements.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
queryden Ready Ready Preview, Comment Jun 9, 2026 8:05am

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces sticky statement-execution gutter glyphs that persist across editor edits and refactors query execution to track per-tab state, so the "running" spinner stays on the tab that initiated the query. New utility helpers map multi-statement selections to document-absolute line numbers and merge glyph results across runs. QueryEditor now reconciles id-keyed decorations without repinning survivors, while MainContent coordinates per-tab execution state, wires components to tab-specific visibility, and improves cancel behavior to prevent stale results from clobbering UI.

Changes

Sticky Query Statement Glyphs and Per-Tab Execution

Layer / File(s) Summary
Statement glyph utility foundations
src/utils/statementGlyphs.ts, src/utils/statementGlyphs.test.ts
Pure helpers: mapSelectionStatementsToDocumentLines converts selection-relative statement line numbers to document-absolute, and mergeGlyphResults deduplicates glyphs on re-run while preserving other lines. Tests verify correctness of line mapping, offset/regression handling, and glyph merging semantics.
Type contracts and QueryEditor glyph reconciliation
src/components/editor/QueryEditor.tsx, src/components/layout/MainContent.tsx
StatementResult gains optional id?: string for stable glyph identity. QueryEditor introduces buildGlyphDecoration to generate sticky Monaco decorations, implements reconcileGlyphs to create/remove id-keyed decorations without repinning, implements pruneGlyphs to drop decorations when anchor content is destroyed and to report updated results via onStatementResultsChange callback. Header's executing spinner is removed in favor of gutter-glyph running state.
MainContent per-tab execution state foundation
src/components/layout/MainContent.tsx
Adds executingTabId state (separate from global isExecuting) to tie execution indicators to the tab that started the run. Introduces derived routing: activeTabConnection, activeTabDatabase, activeTabRunnable reflect the active tab's explicit target instead of relying only on sidebar selection.
MainContent execution startup and statement mapping
src/components/layout/MainContent.tsx
executeQuery now uses mapSelectionStatementsToDocumentLines to compute correct document-absolute statementInfos line numbers, intentionally avoids clearing statementResults so glyphs persist across reruns, and pre-marks first statement as running and sets executingTabId before awaiting connection/query work.
MainContent execution paths and cancel behavior
src/components/layout/MainContent.tsx
CLI and libpq execution backends call appendGlyphResults to accumulate/merge glyphs per tab instead of resetting. Error handling appends an error glyph via appendGlyphResults. cancelQuery now bumps execution generation, clears executing flags immediately, and clears executingTabId to prevent stale-run mutations.
MainContent per-tab rendering and UI controls
src/components/layout/MainContent.tsx
Derives activeTabIsExecuting and per-tab tabIsExecuting from executingTabId; toolbar gates on activeTabRunnable; save logic uses activeTabConnection and activeTabDatabase for metadata persistence; tab creation inherits connection/database from active tab's target. QueryEditor/PsqlWindow/ResultsPanel receive updated props so spinners and results panels reflect only the executing tab's state.
Visual animation and changelog
src/styles/globals.css, CHANGELOG.md
.statement-glyph-running now animates with @keyframes statement-glyph-pulse to pulse opacity, creating a smooth visual indicator. Changelog documents fixes for cancel re-entrancy and per-tab spinner behavior.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • openidle-dev/queryden#222: Addresses the same goal of keeping the running spinner on the tab that launched the query by introducing executingTabId and per-tab execution state tracking in MainContent and QueryEditor.
  • openidle-dev/queryden#223: Directly implements the statement-glyph feature described in this issue: mapSelectionStatementsToDocumentLines/mergeGlyphResults utilities, document-absolute line mapping in executeQuery, and id-keyed glyph reconciliation in QueryEditor.

Possibly related PRs

  • openidle-dev/queryden#215: Both PRs add execution-generation guards and improve cancel flag handling in MainContent to prevent stale executions from mutating UI state after cancellation.
  • openidle-dev/queryden#21: Both PRs refactor per-statement statementResults handling and error state management in MainContent's execution flow, coordinating statement-level glyph/result accumulation across multiple execution paths.

Suggested labels

critical

Poem

🐰 A glyph for each line where the query runs true,
Sticky through edits, no matter what you do.
The spinner stayed home on the tab that it started,
No more chasing tabs—now we're never departed!
With execution guards and a pulsing delight,

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the three main changes: query cancel fixes, per-tab spinner support, and run-status glyph implementation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/execution-ux-runstatus-glyphs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/components/layout/MainContent.tsx (3)

3618-3618: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Forward statementInfo from QueryEditor into executeQuery.

Line 3618 drops the second onRun argument, so execution falls back to default line metadata and run-status glyphs can pin to the wrong statement.

💡 Suggested fix
-                  onRun={(q: string) => executeQuery(q)}
+                  onRun={(q: string, info?: { lineNumber: number; statementText: string }) => executeQuery(q, info)}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/layout/MainContent.tsx` at line 3618, The onRun handler for
QueryEditor currently calls executeQuery with only the query string, dropping
the second argument statementInfo; update the onRun invocation so it forwards
both parameters (pass statementInfo through) to executeQuery (ensure
executeQuery's signature is used as-is), so QueryEditor -> onRun -> executeQuery
preserves statementInfo and corrects run-status/glyph mapping.

2147-2167: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Cancel updates the wrong tab when user cancels from a different tab.

At Line 2165, cancellation state is written to activeTabId instead of the tab that owns the in-flight execution. With per-tab execution tracking, this misattributes the cancel error/time.

💡 Suggested fix
   const cancelQuery = useCallback(() => {
     cancelFlagRef.current = true;
@@
-    if (activeTabId) {
-      updateTabState(activeTabId, { error: "Query execution cancelled by user.", executionTime: runningTimeMs });
+    const ownerTabId = executingTabId ?? activeTabId;
+    if (ownerTabId) {
+      updateTabState(ownerTabId, { error: "Query execution cancelled by user.", executionTime: runningTimeMs });
     }
-  }, [runningTimeMs, activeTabId, updateTabState]);
+  }, [runningTimeMs, activeTabId, executingTabId, updateTabState]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/layout/MainContent.tsx` around lines 2147 - 2167, The
cancelQuery handler is writing cancellation state to activeTabId (the currently
selected tab) instead of the tab that initiated the running query; change the
code in cancelQuery to record the error and executionTime against the executing
tab identifier (e.g., use executingTabId or executingTabIdRef/current that
tracks the tab which owns the in-flight execution) rather than activeTabId, and
only clear the executing tab via setExecutingTabId(null) after updating that
executing tab; also guard the updateTabState call with a null check on the
executing tab id so you don't try to update when there is none.

865-2168: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Add regression tests for the execution ownership + line-mapping bug-fix paths.

This PR changes critical bug-fix behavior (per-tab execution ownership and statement-line glyph targeting), but no targeted failing regression test is included for these flows.

As per coding guidelines, “Every bug fix should land with a failing test that proves it; for bugs that can't be cleanly fixed in the same PR, use Vitest's it.fails(...) pattern to document the bug in executable form.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/layout/MainContent.tsx` around lines 865 - 2168, Add
regression tests exercising executeQuery's execution ownership and line-mapping
fixes: write Vitest tests that (1) verify the re-entrancy guard and generation
bumping (isExecutingRef.current, executionGenRef.current) prevents
double-execution on rapid triggers and that a cancelled run increments
executionGenRef so a later run wins; (2) verify
mapSelectionStatementsToDocumentLines + the promotion-to-isRunAll path correctly
maps statementInfos back to document lines and that appendGlyphResults and
lastExecutedStatement are applied to the correct document line when multiple
statements are selected; (3) cover the CLI vs libpq branching behavior
(useCliPath) to ensure glyphs/psqlEntries are created on the originating tab
(runningCmdRef, psqlOutputRef current tab updates); and if any behavior cannot
be reproduced yet, add it as an it.fails(...) test documenting the regression.
Target tests at executeQuery, mapSelectionStatementsToDocumentLines,
appendGlyphResults, and the cancelQuery interaction to lock the regression into
CI.

Source: Coding guidelines

🧹 Nitpick comments (1)
src/styles/globals.css (1)

341-345: ⚡ Quick win

Respect reduced-motion for the pulsing running glyph.

Line 341 introduces a perpetual pulse, but there’s no prefers-reduced-motion fallback. Add a reduced-motion override so sensitive users don’t get forced animation.

Suggested patch
 .statement-glyph-running {
   /* Pulsing amber dot next to the block that is currently executing. Uses the
      same solid-fill SVG technique as the ✓/✗ glyphs (which render reliably in
      the glyph margin) and animates opacity rather than transform, which Monaco
      does not honor on glyph-margin elements. */
   background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6" fill="%23f59e0b"/></svg>') no-repeat center center !important;
   background-size: 12px !important;
   animation: statement-glyph-pulse 1s ease-in-out infinite;
 }
 `@keyframes` statement-glyph-pulse {
   0%, 100% { opacity: 1; }
   50% { opacity: 0.2; }
 }
+@media (prefers-reduced-motion: reduce) {
+  .statement-glyph-running {
+    animation: none;
+    opacity: 1;
+  }
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/styles/globals.css` around lines 341 - 345, The pulsing animation for the
running glyph (animation: statement-glyph-pulse and `@keyframes`
statement-glyph-pulse) lacks a prefers-reduced-motion override; add a media
query `@media` (prefers-reduced-motion: reduce) that targets the same selector(s)
and disables the animation (e.g., set animation: none and remove/override any
transitions) and, if needed, adjust opacity directly so the glyph remains
visible without motion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/components/layout/MainContent.tsx`:
- Line 3618: The onRun handler for QueryEditor currently calls executeQuery with
only the query string, dropping the second argument statementInfo; update the
onRun invocation so it forwards both parameters (pass statementInfo through) to
executeQuery (ensure executeQuery's signature is used as-is), so QueryEditor ->
onRun -> executeQuery preserves statementInfo and corrects run-status/glyph
mapping.
- Around line 2147-2167: The cancelQuery handler is writing cancellation state
to activeTabId (the currently selected tab) instead of the tab that initiated
the running query; change the code in cancelQuery to record the error and
executionTime against the executing tab identifier (e.g., use executingTabId or
executingTabIdRef/current that tracks the tab which owns the in-flight
execution) rather than activeTabId, and only clear the executing tab via
setExecutingTabId(null) after updating that executing tab; also guard the
updateTabState call with a null check on the executing tab id so you don't try
to update when there is none.
- Around line 865-2168: Add regression tests exercising executeQuery's execution
ownership and line-mapping fixes: write Vitest tests that (1) verify the
re-entrancy guard and generation bumping (isExecutingRef.current,
executionGenRef.current) prevents double-execution on rapid triggers and that a
cancelled run increments executionGenRef so a later run wins; (2) verify
mapSelectionStatementsToDocumentLines + the promotion-to-isRunAll path correctly
maps statementInfos back to document lines and that appendGlyphResults and
lastExecutedStatement are applied to the correct document line when multiple
statements are selected; (3) cover the CLI vs libpq branching behavior
(useCliPath) to ensure glyphs/psqlEntries are created on the originating tab
(runningCmdRef, psqlOutputRef current tab updates); and if any behavior cannot
be reproduced yet, add it as an it.fails(...) test documenting the regression.
Target tests at executeQuery, mapSelectionStatementsToDocumentLines,
appendGlyphResults, and the cancelQuery interaction to lock the regression into
CI.

---

Nitpick comments:
In `@src/styles/globals.css`:
- Around line 341-345: The pulsing animation for the running glyph (animation:
statement-glyph-pulse and `@keyframes` statement-glyph-pulse) lacks a
prefers-reduced-motion override; add a media query `@media`
(prefers-reduced-motion: reduce) that targets the same selector(s) and disables
the animation (e.g., set animation: none and remove/override any transitions)
and, if needed, adjust opacity directly so the glyph remains visible without
motion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6c7a4dad-8d20-4efd-9bd5-92d995a95ea7

📥 Commits

Reviewing files that changed from the base of the PR and between 15795ea and 6334cbc.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • src/components/editor/QueryEditor.tsx
  • src/components/layout/MainContent.tsx
  • src/styles/globals.css
  • src/utils/statementGlyphs.test.ts
  • src/utils/statementGlyphs.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant