Skip to content

feat(editor): window controls and detachable results panel#369

Open
debba wants to merge 7 commits into
mainfrom
feat/detachable-query-results
Open

feat(editor): window controls and detachable results panel#369
debba wants to merge 7 commits into
mainfrom
feat/detachable-query-results

Conversation

@debba

@debba debba commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

What this does

The query results panel had a single chevron to collapse it and not much else. This PR turns the panel header into the familiar set of window controls and adds the ability to pop results out into their own window.

Manual drag-to-resize of the panel is untouched.

Window controls (right side of the results bar)

  • Minimize and Close collapse the panel — no data is lost, the existing "Show Results" button brings it back.
  • Maximize hides the editor so results take the full height; the same button restores the split.
  • Detach opens the results in a separate window.

Detachable results

Detach pops the active tab's results into their own OS window. While detached, the in-app panel shows a "results detached" placeholder with a Re-attach button.

Each window is bound to the tab it was opened from, so:

  • it keeps showing that tab's results even when you switch tabs in the main window;
  • you can detach several tabs into several windows at the same time;
  • closing a tab closes its detached window.

The main window stays the single source of truth. It pushes result state to each detached window over Tauri events keyed by tab id, and the windows send user actions back (pagination, page change, entry select / close / rename, load count) to be applied to the right tab. No query or DB logic is duplicated in the detached window — it renders the existing MultiResultPanel / ResultEntryContent and forwards everything else.

How it's built

  • src-tauri/src/results_window.rsopen_results_window / close_results_window commands; one window per tab (label results-window-{tabId}), remembered bounds, emits results-window:closed on close.
  • src/utils/resultsWindowSync.ts — the event protocol (sync payload, action envelope) plus pure helpers (buildSyncPayload, applyAction, singleResultToEntry), with unit tests.
  • src/pages/ResultsWindowPage.tsx — the detached window, reuses the existing result components.
  • src/pages/Editor.tsx — the header controls and the sync/action wiring.
  • New tooltip and placeholder strings added across all 8 locales.

Scope / not in this PR

In-place row editing (insert / delete / pending changes) is intentionally not available from inside the detached window for now — that machinery is tightly coupled to the main editor. In-grid scroll, sort, cell copy and pagination all work.

Testing

  • npx tsc --noEmit — clean
  • npm run build — clean
  • npm test (resultsWindowSync plus the full suite) — green
  • cargo check and cargo test results_window — green

Context

Framed as part of the UI consistency effort in #195. This isn't a design-system change in itself, but it brings the results panel in line with conventional window affordances and adds a feature that was awkward to do before. Relates to #195.

Replace the single collapse chevron in the results panel header with the
usual window-style controls — minimize, maximize, close — and add a
detach button that pops the results into their own window.

Manual drag-resize of the panel is unchanged.

- minimize / close collapse the panel (no data loss); the "Show Results"
  button brings it back
- maximize hides the editor so results take the full height; the same
  button restores it
- detach opens a separate window for the active tab's results; the
  in-app panel shows a "results detached" placeholder with a re-attach
  button

Each detached window is bound to the tab it was opened from, so it keeps
showing that tab's results even when you switch tabs, and you can detach
several tabs into several windows at once. The main window stays the
single source of truth: it pushes result state to each window over Tauri
events keyed by tab id, and the windows forward user actions (pagination,
page change, entry select/close/rename, load count) back to be applied to
the right tab. Closing a tab closes its detached window.
@debba debba requested a review from NewtTheWolf June 24, 2026 08:09
Comment thread src/pages/Editor.tsx Outdated
Comment thread src/pages/Editor.tsx
return tab && tab.results ? tab : null;
};
return {
onRunQueryPage: (query, page) => runQuery(query, page, tabId),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

WARNING: Detached-window query runs mutate the main window's global results-panel state

onRunQueryPage forwards page changes from the detached window to runQuery(query, page, tabId). Inside runQuery, setIsResultsCollapsed(false) is toggled unconditionally (line ~698). When the detached tab is not the active tab in the main window, this still expands the main window's results panel for whatever tab is currently active.

Scope the collapse state change to the target tab, or skip it when the action originates from a non-active detached tab.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread src/pages/Editor.tsx
onRunQueryPage: (query, page) => runQuery(query, page, tabId),
onPageChange: (entryId, page) => runResultEntryPage(entryId, page, tabId),
onRerunEntry: (entryId) => runResultEntryPage(entryId, 1, tabId),
onLoadCount: () => loadCount(tabId),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

WARNING: Detached-window row-count loading affects the active tab's UI

onLoadCount calls loadCount(tabId), which sets the global isCountLoading flag and later clears it in a finally block. If the detached tab is not the active tab in the main window, the spinner and disabled state are shown on the active tab's load-count button instead of the detached tab's.

Use a tab-scoped loading indicator for count operations triggered from detached windows.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

All 3 previous review warnings have been addressed in the latest commits.

Files Reviewed (1 file)
  • src/pages/Editor.tsx
Previous Review Summaries (3 snapshots, latest commit 28ce2e9)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 28ce2e9)

Status: No Issues Found | Recommendation: Merge

All 3 previous review warnings have been addressed in the latest commits.

Files Reviewed (1 file)
  • src/pages/Editor.tsx

Previous review (commit 11470e1)

Status: No Issues Found | Recommendation: Merge

All 3 previous review warnings have been addressed in the latest commits.

Files Reviewed (1 file)
  • src/pages/Editor.tsx

Previous review (commit e9196af)

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
src/pages/Editor.tsx 1292 Detached-window actions applied without verifying tab is detached
src/pages/Editor.tsx 1213 Detached query runs mutate main window's global results-panel state
src/pages/Editor.tsx 1216 Detached row-count loading affects active tab's UI
Files Reviewed (16 files)
  • src-tauri/capabilities/default.json
  • src-tauri/src/lib.rs
  • src-tauri/src/results_window.rs
  • src/App.tsx
  • src/i18n/locales/de.json
  • src/i18n/locales/en.json
  • src/i18n/locales/es.json
  • src/i18n/locales/fr.json
  • src/i18n/locales/it.json
  • src/i18n/locales/ja.json
  • src/i18n/locales/ru.json
  • src/i18n/locales/zh.json
  • src/pages/Editor.tsx - 3 issues
  • src/pages/ResultsWindowPage.tsx
  • src/utils/resultsWindowSync.ts
  • tests/utils/resultsWindowSync.test.ts

Fix these issues in Kilo Cloud


Reviewed by laguna-xs.2-20260421:free · Input: 534.4K · Output: 6.8K · Cached: 145.8K

NewtTheWolf
NewtTheWolf previously approved these changes Jun 24, 2026

@NewtTheWolf NewtTheWolf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed locally — really nice feature, and cleanly built: the action handlers are properly tab-keyed, the main window stays the source of truth, and no query/DB logic is duplicated. Unit tests pass (11/11), tsc clean, i18n complete across all 8 locales, and the new Tauri commands are registered. 🙏

I can confirm all three of @kilo-code-bot's warnings are real — they're shared main-window UI state leaking into actions a detached window triggers, so no data loss, but worth tidying:

  1. Editor.tsx:698runQuery calls setIsResultsCollapsed(false) (and would pop setQueryParamsModal for parameterised queries), so a re-run/page-change from a detached window re-expands the main panel — which slightly defeats the point of detaching. This is the one I'd fix before merge.
  2. Editor.tsx:1095loadCount flips the global setIsCountLoading, so a detached count-load shows a spinner on the active tab. Cosmetic.
  3. Editor.tsx:1292applyAction has no detachedTabIds.has(tabId) guard; low risk since events only come from app-owned windows, but cheap defense-in-depth.

#2 and #3 are polish and fine as follow-ups. Approving — just flagging #1 as worth a look first.

@debba

debba commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review! Fixed all three — they boiled down to detached-window actions leaking into main-window-only UI state, so I gated each on whether the target tab is actually detached.

To do that without re-creating the callbacks or reading stale closures, I added a detachedTabIdsRef kept in sync next to tabsRef/activeTabIdRef:

  1. runQuerysetIsResultsCollapsed(false) and the setQueryParamsModal pop now only fire when the target tab is not detached, so a re-run/page-change from a detached window no longer re-expands the main panel (and won't hijack the active tab with a params modal).
  2. loadCountsetIsCountLoading is skipped for detached counts, so the spinner no longer flashes on the active main-window tab.
  3. applyAction — the action listener now early-returns unless detachedTabIdsRef.current.has(tabId), as defense in depth against events for a reattached/unknown tab.

Build, tsc, and ESLint are clean, and the full test suite passes (2635/2635).

@debba debba requested a review from NewtTheWolf June 29, 2026 07:43
# Conflicts:
#	src/pages/Editor.tsx
@NewtTheWolf NewtTheWolf force-pushed the feat/detachable-query-results branch from 28ce2e9 to 3ed7ab6 Compare June 29, 2026 15:22
@NewtTheWolf

Copy link
Copy Markdown
Collaborator

Re-reviewed locally after merging main in. No blockers — mergeable as-is. A few minor nits I noticed; happy to leave them as follow-ups or fix in this PR, your call:

  1. react.md Not loading on macos. #2 — sync setState in effect (Editor.tsx, orphan-cleanup effect): setDetachedTabIds(...) is called synchronously inside the useEffect body. Could let the results-window:closed listener own the pruning so the effect stays side-effect-only.
  2. Detach init race (Editor.tsx): the ready/action/closed listeners are gated behind detachedTabIds.size > 0 and listen() registers async, so a freshly opened window's ready handshake can be missed (window stuck on "Loading…"). Registering them unconditionally (the action listener already self-guards via detachedTabIdsRef) removes the race.
  3. Window bounds are global (results_window.rs): last_bounds: Mutex<Option<WindowBounds>> is shared across all detached windows, so with multi-detach each new window opens on top of the last one. A HashMap<tab_id, _> would remember per tab.
  4. outer vs inner size drift (results_window.rs): bounds are saved from outer_size() but restored via .inner_size(...), so the window grows by the decoration height on each detach→close→reopen cycle.
  5. (tiny) the two results_window tests only exercise Mutex<Option<_>> get/set; window_label() is the actual pure helper and is untested.

Want me to push fixes for any of these, or merge and follow up separately?

debba added 2 commits June 29, 2026 17:36
…race

- Orphan-cleanup effect no longer prunes detachedTabIds synchronously;
  closing the window emits results-window:closed and the listener owns pruning,
  keeping the effect side-effect-only.
- Register the ready/action/closed listeners unconditionally instead of gating
  behind detachedTabIds.size > 0, so a freshly opened window's ready handshake
  can't be missed (was leaving the window stuck on "Loading…"). Each handler
  self-guards, and detachedTabIds is dropped from the deps to avoid needless
  listener re-registration.
- Remember window bounds per tab (HashMap keyed by tab_id) instead of a single
  global slot, so multiple simultaneously-detached windows no longer all reopen
  stacked on top of the last-closed one.
- Save inner_size() instead of outer_size() on close; bounds are restored via
  .inner_size(...), so saving the outer size grew the window by the decoration
  height on every detach->close->reopen cycle.
- Test the pure window_label() helper and per-tab bounds storage.
@debba

debba commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough re-review! I've addressed all five points in two commits (a95471e and e4f5c6c):

  1. Sync setState in effect — the orphan-cleanup effect no longer prunes detachedTabIds itself. Closing the window emits results-window:closed, and that listener now owns the pruning, so the effect stays side-effect-only.
  2. Detach init race — removed the detachedTabIds.size > 0 gate; the ready/action/closed listeners are now registered unconditionally at mount, so a freshly opened window's ready handshake can't be missed. Each handler self-guards (action via detachedTabIdsRef, ready via the tabsRef lookup, closed via prev.has), and detachedTabIds was dropped from the deps to avoid needless re-registration.
  3. Global window boundslast_bounds: Mutex<Option<WindowBounds>> is now bounds: Mutex<HashMap<String, WindowBounds>>, keyed by tab_id, so each detached window reopens at its own last position instead of stacking.
  4. outer vs inner size drift — bounds are now saved from inner_size() to match the .inner_size(...) restore, so the window no longer grows by the decoration height on each detach→close→reopen cycle.
  5. Tests — added coverage for the window_label() pure helper and for per-tab bounds storage.

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.

2 participants