Skip to content

fix: resolve reported data grid, editor, and filter UX issues#1773

Closed
datlechin wants to merge 6 commits into
mainfrom
fix/ux-batch-datagrid-editor-filters
Closed

fix: resolve reported data grid, editor, and filter UX issues#1773
datlechin wants to merge 6 commits into
mainfrom
fix/ux-batch-datagrid-editor-filters

Conversation

@datlechin

Copy link
Copy Markdown
Member

Fixes a batch of UX issues reported from real first-use feedback. Investigated with parallel codebase tracing across the data grid, SQL editor, filter bar, and the bottom toolbar.

Root cause: one bug behind three "stopped working mid-session" symptoms

The dead Columns / + Add buttons, the dead mouse in the SQL editor, and the general "unusable after a while" feeling were all the same bug. The SQL autocomplete popup (SuggestionController in the vendored CodeEditSourceEditor) is a borderless non-activating panel at pop-up-menu level, added as a child window ordered above the main window. Its positioning only flipped the panel above the cursor when it would run off the screen bottom, never the editor's bottom. With the cursor on a lower line and the schema loaded, the panel drew straight down over the status bar and lower editor lines. Being non-activating, clicking it never changed first responder, so it never auto-closed and just ate every click until Esc.

Fixes

Editor / autocomplete

  • Constrain the completion panel to the editor's bounds (flip above the cursor against the editor bottom, not just the screen), so it never overlaps the status bar or sibling chrome.
  • Dismiss the panel on any click outside it, matching native menu/popover behavior.
  • Fix a retain cycle in FindPanelHostingView that leaked an Escape key monitor every time a tab was closed with the Find bar open.
  • Defensive cancelHover() on the jump-to-definition failure path.

Data grid

  • Dragging a column edge to resize no longer triggers a sort (the header mouseDown now bails to pure resize when the click starts in the resize zone).
  • Hidden columns stay hidden after a resize or reorder. The width/order persistence path was replacing the whole layout struct and wiping the hidden-column set; it now merges geometry only.
  • Column widths no longer snap back when the grid reloads, sorts, or paginates right after a resize. Resizes update an in-memory cache immediately; the disk write stays debounced.

Filters

  • SQL errors from filters now appear in a selectable, copyable inline banner above the results instead of a small alert window. The opt-in "Fix with AI" prompt is preserved.
  • Autocomplete no longer pops up on empty space; it only appears while typing a word.
  • Pressing Escape to close the autocomplete no longer also closes the filter bar.

Not changed (by design)

Backspace deleting two characters is CodeEditSourceEditor's smart-edit behavior (delete a full indent unit in leading whitespace, or both halves of an auto-inserted bracket pair), the same as Xcode. Left as-is.

Tests

  • ColumnLayoutStateTests - geometry updates preserve the hidden-column set.
  • FileColumnLayoutPersisterTests - cacheLayout is in-memory only and is not written to disk.
  • FilterValueTextFieldTests - whitespace suppresses completion; Escape grace keeps the filter bar open.

The gesture/positioning fixes (header resize zone, panel geometry) are AppKit-window-bound and verified by hand rather than unit tested.

Notes

  • The autocomplete and Find-panel changes are in the vendored LocalPackages/CodeEditSourceEditor. They lint clean under that package's own config.

https://claude.ai/code/session_0198faM6VCrViRU4XwRoS1DC

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6ed9dc8f87

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

onDismiss: { tab.display.activeResultSet?.errorMessage = nil }
onDismiss: {
tab.display.activeResultSet?.errorMessage = nil
tab.execution.errorMessage = nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Clear execution errors through tab state

When the banner is showing tab.execution.errorMessage, this dismiss handler tries to assign through the QueryTab value captured by resultsSection(tab:). Unlike activeResultSet (a reference type), execution is a stored struct property on an immutable function parameter, so this breaks the build instead of clearing the error; route the dismissal through tabManager.mutate(tabId:) or another mutable tab-state path.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 73bd3840c3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}

func fixErrorWithAI(error: String) {
guard let query = lastQueryErrorSQL else { return }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use the tab's failed SQL for AI fixes

This reads a coordinator-global lastQueryErrorSQL, which is overwritten by the next failing query in any tab. Because the banner is stored per tab via tab.execution.errorMessage, a user can fail a query in tab A, fail a different query in tab B, then return to tab A and click Fix with AI; the AI receives tab B's SQL paired with tab A's error. Store or pass the failed SQL with the tab/result that owns the banner instead of using a single coordinator field.

Useful? React with 👍 / 👎.

private func executionErrorBanner(tab: QueryTab) -> some View {
if let error = tab.execution.errorMessage,
tab.display.activeResultSet?.errorMessage == nil,
tab.display.resultSets.count <= 1 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Show execution errors when results are pinned

When a tab has a pinned result (or otherwise has more than one result set), this condition suppresses the new execution-level banner. The single-statement error path only sets tab.execution.errorMessage and leaves the existing display.resultSets in place, so after a failed run with pinned results the UI keeps showing the old results and no error banner, while the modal sheet was removed in this commit. The result-set count should not hide execution errors that are not represented by an active result set.

Useful? React with 👍 / 👎.

Comment on lines +103 to +105
let liveWidths = currentColumnWidths()
guard !liveWidths.isEmpty else { return saved }
return (saved ?? ColumnLayoutState()).mergingWidths(liveWidths)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Skip live-width merging after schema changes

This merges current NSTableView widths even on updates where the result schema changed. In applyStructuralUpdate, rebuildColumnMetadataCache(from:) has already swapped identitySchema to the new columns before this runs, while the table still contains slot-based columns from the previous result (dataColumn-0, etc.), so old widths are attributed to unrelated new column names by position. After resizing a column and running a different SELECT, the new result can inherit stale widths instead of its stored/default layout; only merge live widths when the schema is unchanged, or capture them before replacing the schema.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

https://github.com/TableProApp/TablePro/blob/5043f1e2f10e5b52295a5d11130cc76abfb3a363/Core/Coordinators/QueryExecutionCoordinator%2BHelpers.swift#L486-L488
P2 Badge Expand results before showing inline execution errors

When the user has toggled the results pane closed, single-statement failures are now invisible: this path only stores tab.execution.errorMessage, but the new banner is rendered in QuerySplitView's bottom content, which is hidden while tab.display.isResultsCollapsed is true. Since the modal alert was removed from this error path and success paths explicitly uncollapse results, set the tab and toolbar collapsed state to false when recording the error so the inline banner can actually be seen.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

tab.execution.errorMessage = error.localizedDescription
tab.execution.isExecuting = false
tab.execution.lastExecutedAt = Date()

P2 Badge Uncollapse the results pane for execution errors

When the user has collapsed the results pane, this error is now only rendered by resultsSection inside the collapsed QuerySplitView bottom item; the modal sheet that previously made the failure visible was removed in this change. Unlike successful executions, this error path does not set tab.display.isResultsCollapsed = false or update toolbarState.isResultsCollapsed, so a syntax/runtime failure can appear to do nothing until the user manually expands results.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

InlineErrorBanner(
message: error,
onFixWithAI: AppSettingsManager.shared.ai.enabled && tab.tabType == .query
? { coordinator.fixErrorWithAI(query: tab.content.query, error: error) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use the failed statement for Fix with AI

Fresh evidence in this revision is that the new banner callback passes the whole editor buffer, while runQuery() can execute only the selected text or the statement at the cursor. In a tab containing multiple statements, Fix with AI now receives unrelated SQL instead of the exact statement that failed, regressing the previous error handler's queryCopy = sql behavior; store the executed SQL with the error state and pass that here.

Useful? React with 👍 / 👎.

if !latestRows.columns.isEmpty {
coordinator.isRebuildingColumns = true
let savedLayout = coordinator.savedColumnLayout(binding: columnLayout)
let savedLayout = coordinator.resolvedColumnLayout(binding: columnLayout, liveWidths: liveColumnWidths)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not merge widths from the previous grid

When updateNSView runs after switching to a different tab/table, liveColumnWidths still reflects the reused NSTableView's previous result, but this line merges those widths into the new grid's saved layout before reconciling columns. If the two results share common names such as id or name, the new tab/table ignores its persisted/default width and inherits the old tab's live width; merge live widths only when the prior schema/config is the same logical grid.

Useful? React with 👍 / 👎.

@datlechin datlechin force-pushed the fix/ux-batch-datagrid-editor-filters branch from 1642d69 to 93e772a Compare June 27, 2026 11:13

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 93e772a688

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if let error = tab.display.activeResultSet?.errorMessage ?? tab.execution.errorMessage {
InlineErrorBanner(
message: error,
onFixWithAI: AppSettingsManager.shared.ai.enabled && tab.tabType == .query

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve Fix with AI for table filter errors

When a table-tab filter/raw-filter query fails, the tab rendering this banner has tab.tabType == .table, so this condition makes onFixWithAI nil even when AI is enabled. The old execution-error flow offered the AI fix action for these failures, and this change says filter errors should keep that opt-in action; with a malformed filter the banner now only allows copy/dismiss. Allow table/filter execution errors to supply the action too, using the generated failed SQL/filter context.

Useful? React with 👍 / 👎.

let clamped = min(max(cursor, 0), nsText.length)
guard clamped > 0 else { return false }
guard let scalar = Unicode.Scalar(nsText.character(at: clamped - 1)) else { return true }
return !CharacterSet.whitespaces.contains(scalar)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Treat newlines as whitespace for filter completions

Raw SQL filter fields are multiline (allowsMultiLine: true), but this check only uses CharacterSet.whitespaces, which does not contain \n. When the cursor is at the start of a new/blank line, the function returns true and still asks the SQL-token provider for completions with an empty token, so the popup can appear on empty space despite this fix; use .whitespacesAndNewlines (or include .newlines) before offering token completion.

Useful? React with 👍 / 👎.

@datlechin datlechin closed this Jun 27, 2026
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