Skip to content

refactor(running-work): ask once about running work and show one progress sheet - #4009

Merged
datlechin merged 11 commits into
mainfrom
refactor/running-work
Nov 3, 2026
Merged

datlechin merged 11 commits into
mainfrom
refactor/running-work

Conversation

@datlechin

@datlechin datlechin commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Summary

  • One registry of work in flight, RunningWorkRegistry, read by every closer:
    • pushed by the jobs: restore, backup, import, export, table transfer, copy objects;
    • pulled: a query run in an editor tab, a busy agent session, a comparison or sync apply.
    • A table tab loading rows is navigation and does not count.
  • Quit, Relaunch to Update, Close Tab, Close Other/All Tabs, closing a container, Close Connection, closing a window, Disconnect and the Compare window each ask once, in one alert that lists the work:
    • buttons "Quit Anyway", "Relaunch Anyway", "Close Anyway", "Disconnect Anyway", and Cancel; Escape cancels, Return is off the destructive button;
    • Quit lists unsaved tabs on the same alert; with nothing running, the old unsaved-tabs alert stays;
    • Close Connection and Disconnect list an open transaction on it;
    • when a close also has unsaved edits, the save question carries the work instead of a second alert;
    • the work stops only once the close is certain: Cancel, or a save that fails, keeps it running;
    • the alert sits on the window's frontmost sheet, so a progress sheet never holds it back.
  • Relaunch asks the theme editor and the running-work question before Sparkle or the quit takes over; the quit it starts does not ask again, and a Sparkle abort clears that approval.
  • A yes stops the listed work: a restore's or backup's child process ends, and agent sessions stop instead of being saved as failed.
  • Completion reporting (OperationCompletionReporter):
    • new kinds for restore, comparison and sync apply; a stopped sync reports cancelled;
    • a compare window owns its own notifications, visibility and click;
    • connection-owned work counts as on screen only when a visible window shows that connection;
    • a backup reads "Backed up to", not "Exported to".
  • AgentAttentionNotifier: a session that starts waiting on you or fails while out of sight posts a notification (Settings > Notifications switch honored, no threshold); the text is built from the state at delivery; answering removes it; the click opens the session.
  • The finished-tab mark is an SF Symbol per outcome: checkmark.circle.fill in the accent color, exclamationmark.triangle.fill in red. VoiceOver says "finished" or "failed".
  • One progress model and sheet, TransferProgress and TransferProgressSheet, for export, import (three hosts), backup, restore, table transfer and copy objects:
    • a job that leaves partial state says Stop, one that leaves nothing says Cancel; both confirm with the consequence the job really has, and Escape asks the same question;
    • restore shows a real bar from the read offset of the dump it feeds the tool;
    • export: no second spinner, Escape works, a Stop during the row count is kept, the sheet waits for its service, a Stop reports cancelled and never cancels the stream a format is still reading;
    • table transfer gets the sheet, a locked form, and a Stop that reports cancelled;
    • an import's Stop says "Anything already imported may stay." on every engine.
  • LoadingReveal reveals after its grace again (the gate never started on an empty Group); the results status bar shows one spinner while a query runs.
  • Plugins: one PluginInstallTracker with per-install generations and a cancel handle:
    • the install sheet shows downloading then installing, with Cancel and Escape; a cancel is not a failure;
    • Settings > Extensions shows the running install with Cancel, and a failed install or update on its row with Try Again and Dismiss;
    • auto-update feeds the tracker; a failed update of a loaded plugin reaches the row and a notification; a cancelled one waits for the next launch;
    • plugin notifications honor the Notifications switch and ask no permission when it is off;
    • the dead PluginInstallStatusRow and its form members are gone.
  • Cloud SQL Auth Proxy: a download task through the shared ProgressReportingDownload feeds a determinate bar with Cancel; HTTP status checked, checksum on the file, atomic replace; Connect shows "Downloading Cloud SQL Auth Proxy"; "Detected" only for a binary that passes its checksum.
  • Linked folders: each row says what the last scan found (connections, unreadable files, folder not found, can't read, not watched).
  • Team library: Settings > License > Team shows the last refresh with Refresh, and "Couldn't refresh the team library" with the reason; pulls are deduped per license and generation.
Defect on main Fixed by
Quit asked only about a sync apply and unsaved tabs, in up to three alerts; restore, import, export, backup and agent replies were cut off silently One running-work alert
Restore and backup tools kept running after quit, leaving a temporary credentials file The alert's yes cancels the job
Close Tab, Close Connection, closing a window and Close All cancelled a running query silently Running-work alert, or the save question naming the work
Disconnect warned on any tab execution, including a table loading rows Editor-tab queries only
Relaunch read "Quit Anyway" / "Stop and Quit" Relaunch wording, asked once
The compare window asked only during apply, app-modal, without a destructive button The same prompt, as a sheet, for every busy state
Quit's sync alert never brought a background app forward Activation before the modal
Restore and compare/sync never reported completion New kinds and a compare window owner
Connection-owned work counted as on screen while the window showed another connection Resolver checks the selected connection
A backup notification read "Exported to" "Backed up to"
Agent waiting on you or failed was never pushed AgentAttentionNotifier
The unseen mark was a colored dot, the same for success and failure A symbol per outcome
Six progress sheets disagreed on Stop vs Cancel, Escape, bars and spinners; restore's bar never moved; export drew two spinners TransferProgressSheet
A Stop mid-table in a transfer showed a red English "Import cancelled" and reported a failure Reported as cancelled
Export lost a Stop during the row count and opened its sheet before its service Fixed in ExportDialog and ExportService
LoadingReveal never revealed The gate runs on a view that always exists
Plugin auto-update showed no progress and only logged a failed update; failures vanished after 30 s; an older timer removed a newer failure; a duplicate install reset the running one; a cancel showed "Installation Failed" and left staging files PluginInstallTracker with generations and cancel
Plugin notifications ignored the Notifications switch Switch honored
The proxy download had no progress, no Cancel, no HTTP check, a non-atomic replace, and Connect never said it was downloading CloudSQLProxyDownload
Linked folder and team library failures were only logged Status per row, refresh line in Settings
A team library pull kept the tunnel command, startup SQL and refused SSH hosts in memory until relaunch The sanitized copy is published
A cancelled folder scan could land last and bring back a removed folder Stale scans dropped

Tests changed on purpose:

  • OperationCompletionReporterTests: the mark closure takes the outcome.
  • TableTransferSessionScopeTests "A stopped transfer still closes both scopes" now expects PluginImportCancellationError.
  • CloudSQLProxyBinaryManagerTests (all 7) and CloudSQLProxyManagerTests (2) moved to the download seam; checksum cases expect .checksumMismatch.
  • TeamLibrarySyncCoordinatorTests moved under GlobalStateSuites.
  • PluginRegistryUpdatePassTests injects a fresh tracker.

Decisions made without the maintainer:

  • The save decision stays a three-way alert; when work is also running it names the work instead of a second alert.
  • Close Connection still asks about a transaction before unsaved edits, since Save writes at once.
  • Agent and plugin notifications honor only the switch; "Only after" applies to finished work.
  • A failed plugin update shows on its row, not in a banner (the banners belong to another change).
  • Table transfer promises no rollback: the checkbox asks for a transaction, but an engine without one cannot roll back.
  • Routine debugging, live messages, row generation and structure apply are not on the running-work list.

Test plan

  • New suites: RunningWorkTests, AgentAttentionNotifierTests, TransferProgressTests, LoadingRevealTests, ExportServiceStopTests, ImportServiceRunningWorkTests, NativeDumpRestoreTrackingTests, PluginInstallTrackerTests, PluginNotificationServiceTests, CloudSQLProxyDownloadTests, LinkedFolderWatcherTests, plus new cases in the close, relaunch, completion, plugin and team library suites.
  • Every touched suite together: 472 passed, 0 failed (before review); review-fix suites re-run after each round.
  • Unit plan on the tree merged with main (refactor(license): one license and sync presentation on every surface #4005): 28,597 passed, 0 failed, 2 skipped. Each later fix ran its suites (72, 20, 19, 108, 39 passed); CI runs the plan on the final head.
  • Codex: an adversarial round and seven review rounds. Fixed: close ordering and repeated prompts, relaunch preflight, compare runs vanishing from the list while still writing, stopped syncs reported as success, team library pulls across a license change, cancelled plugin updates retried, pinned tabs losing their query, a Sparkle abort leaving the relaunch approved, alerts queued behind progress sheets, a cancelled export cutting a file, unsaved edits made while the alert was up, stale proxy download waiters, stale agent notification text. Round 5 found new defects in three additions, which are cut back to main (below). Round 6 found two wording and presentation gaps, both fixed: the container close now shows its window before asking, and the export Stop keeps main's "Partial files may remain on disk." since XLSX cannot stop its final write. Round 7 found two ordering gaps, both fixed: an agent notice answered while its post was in flight, and a batch close reading a cell edit as clean before committing it.
  • Not verified:
    • a real Sparkle install and relaunch (Debug builds disable updating);
    • a real plugin registry download and a real Cloud SQL proxy download (covered with fakes and a chunked URL protocol);
    • macOS 13 to 15.
  • Not done: the MCP disconnect tool still skips the confirmation (left to an MCP change under its own rules).

Deliberately not fixed here

  • Import rollback wording. A Stop that promised "Everything imported so far is rolled back." drew defects in two rounds: the user's own transaction, engines without transactions, then MyISAM tables. Knowing it needs per-table transactionality, as Compare & Sync has. The Stop now says "Anything already imported may stay." everywhere.
  • Stopping an export or a transfer when its dialog goes away. onDisappear also fires when a connection switch unparents the pane, which would stop work nobody cancelled. It needs a real teardown hook; the job keeps running as on main.
  • Agent mode and the result-on-screen check. Treating a selected tab as off screen in Agent mode left a mark nothing clears when Browse returns. It needs the mark cleared when the pane shows again.

Screenshots

The tab marks, the linked folder statuses and the plugin sheet are seeded through a temporary patch that is not in this PR. The alerts are a real long-running SQLite query, and the export is a real 3,000,000-row table.

Light Dark
Before: Disconnect with a running query Disconnect alert naming only a running query Disconnect alert, dark
After: Disconnect lists the work Disconnect while work is still running, with the query listed Disconnect while work is still running, dark
After: Close Tab with a running query (before, the tab closed without asking) Close while work is still running Close while work is still running, dark
Before: unseen tabs, one dot for both outcomes Two tabs with the same dot Two tabs with the same dot, dark
After: a checkmark for finished, a red triangle for failed Query 3 with a checkmark, Query 4 with a triangle Tab marks, dark
Before: export progress, Stop, footer spinner below Old export progress sheet Old export progress sheet, dark
After: one progress sheet, Stop, no footer spinner Exporting, 328,000 of 3,000,000 rows, Stop Exporting, dark
Before: plugin download, no Cancel Downloading the Oracle plugin, no button Plugin download, dark
After: plugin download with Cancel Downloading the Oracle plugin with Cancel Plugin download with Cancel, dark
Before: linked folders, no status Three linked folders with name and path only Linked folders, dark
After: a status line per folder 3 connections, 2 files couldn't be read, Folder not found Linked folder statuses, dark

# Conflicts:
#	apple/TablePro/Views/ConnectionForm/Panes/Transports/CloudSQLProxyTransportSections.swift
#	apple/TablePro/Views/Export/TableTransferSheet.swift
#	apple/TablePro/Views/Settings/Plugins/InstalledPluginsView.swift
@datlechin
datlechin enabled auto-merge (squash) November 3, 2026 08:20
@datlechin
datlechin merged commit ca87a1a into main Nov 3, 2026
9 of 10 checks passed
@datlechin
datlechin deleted the refactor/running-work branch November 3, 2026 11:03
datlechin added a commit that referenced this pull request Nov 3, 2026
Brings main's 27 commits, among them one license and sync presentation
(#4005), the Black Friday line (#3995), the update indicator and relaunch
(#3993, #3997), running work (#4009), native notices (#4007) and the
security merges for GHSA-3x6q-h487-r38w, GHSA-3xf7-86qj-4546,
GHSA-6g2g-65r6-v5wh, GHSA-q2hp-vxv3-87fw, GHSA-6p9q-hc7v-cxmx and
GHSA-vg8g-h6xp-6px5.

- #4005: ported onto FeatureAccess. LicensePresentation.gate reads
  AccountState: signed out names a paid plan and offers Sign In beside
  Purchase License, an account without the feature gets Upgrade Plan, the
  device limit opens Settings > Account, and a lapsed or failed check reads
  as Settings > Account words it, with Try Again, so a paying customer is
  never sent to the pricing page. The pane's notice text moved to
  AccountSettingsContent.Notice so both read it. LicenseGateRow replaces
  ProBadge in the Sync, Linked Folders, Data Rewind and connection export
  rows, LicenseGateAlert serves Compare & Sync and Data Rewind, and the
  overlay renders the same notice. Try Again shows a spinner and the
  failed check's reason; Refresh in Settings > Account shows the reason
  too. Welcome shows the pane's first notice as a link (a plan being
  canceled stays in the pane), else Sign In. Dropped: the .suspended
  access, since an entitlement carries no suspended state, and
  LicenseManager's coalesced checks and stale-answer guard, since
  AccountServices already runs one check at a time and drops a refresh
  that lands after sign-out.
- #3995: the Black Friday line shows when FeatureAccess.supportAudience is
  .prospect, the notion the day-30 ask reads.
- #3989: ported before this merge, in e39c309.
- #3993, #3997: merged as main has them; Welcome and the Sync pane keep
  UpdateCenter.
- #4007: a Team Library publish that goes through shows no info alert on
  Welcome or in Favorites; the section shows the items. The queued and
  failed messages stay.
- #4009: merged outside the Team Library. TeamLibraryStore's write
  generation, the shared in-flight pull, the refused second publish and
  the Team Library row in Settings > License are dropped: TeamLibrarySync
  runs syncs and publishes one at a time, bumps a generation on sign-out,
  and the Welcome and Favorites headers show the running sync, the last
  sync and a failure.
- 726a167 (security): LicenseAPIClient and LiveTeamLibraryAPIClient
  follow redirects only on their own origin. Ported by giving the account
  URLSession SameOriginRedirects as its delegate, which covers token
  requests, /v2 calls and the team library. The pulled library keeps a
  connection's sign-in choice for the consent prompt: ported to
  TeamLibraryChange.connection, and its test moved to TeamLibrarySyncTests.
  Publishing needs nothing, since portableSettings keeps the choice by
  default. EnvVarReferenceConsent reads FeatureAccess.
- DatabaseManager+Sessions: main's recovery guard and consent path, with
  Telemetry.connectionAttempted in place of MacAnalyticsProvider.
- project.yml: PostHog stays and zstd goes, as main moved Kafka's codecs
  into TableProCore; the app links main's new products, without
  TableProAnalytics.
- The storage rule, data-sync-storage.md: the paths main listed for the
  deleted TeamLibraryModels, TeamLibraryStore and MacAnalyticsProvider name
  TeamLibraryItem, TeamLibraryState and Telemetry, plus Account's stores.
- Localizable.xcstrings: merged key by key in this branch's order; no key
  changed on both sides. 271 keys come from main; the three SSH host keys
  main removed are gone, as nothing uses them.
- Tests deleted with the code this branch deleted: LicenseManagerDecisionTests,
  LicensePresentationTests and LicenseTierTests under Models,
  TeamLibraryModelsTests, TeamLibrarySyncCoordinatorTests, and the Macs
  link case in SupportLinksTests. New: LicensePresentationTests under
  Views/Settings, the sign-in choice case in TeamLibrarySyncTests and the
  redirect case in AccountConfigurationTests.
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