Skip to content

fix(files): always finish the delete dialog after the server result - #498

Open
veryCrunchy wants to merge 6 commits into
mainfrom
fix/windows-delete-dialog-completion
Open

veryCrunchy wants to merge 6 commits into
mainfrom
fix/windows-delete-dialog-completion

Conversation

@veryCrunchy

@veryCrunchy veryCrunchy commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Outcome

Advances #477.

On Windows with virtual files enabled, the delete dialog could stay on "Deleting..." forever even though the server had deleted the file. After the server accepted the DELETE, executeFileMutation ran local cleanup inline (refreshRetainedFoldersAfterMutation), which waits on synchronized(virtualFileProviderLock). That wait cannot be cancelled, and Windows Cloud Files can hold the lock for a long time: storage summaries, corrupt-root recovery, and provider activation with startup recovery of up to 120 s. The unknown-result path ran the same refresh synchronously. main still has this structure.

Desktop local follow-up:

  • Local cleanup after a file change now runs on a background worker (DesktopFileMutationFollowUps.kt). After a server-confirmed change the caller waits at most 3 s, then gets the server result. The local outcome is reported separately as localFollowUp (Completed, StillRunning, Failed); it defaults to Completed, so Android is unchanged. Slow or failed cleanup records a files.mutation-local-follow-up diagnostic.
  • Pending follow-ups are coalesced per account and path, keeping only the newest refresh, so a stalled refresh cannot build an unbounded queue. At most 256 paths can wait behind a stall; beyond that the follow-up is reported as a local failure with a "dropped" diagnostic. Callers that time out remove themselves, and a shut-down service reports Failed.
  • refreshRetainedFoldersAfterMutation now returns the first failure it recovered from instead of swallowing it. Other callers ignore the return value.

Shared delete flow (FileDeleteFlow.kt, FileDeleteCoordinator.kt):

  • The DELETE is sent exactly once and never resent automatically.
  • A definitive rejection such as 401, 403, 423 or 507 keeps the dialog open and allows a safe retry.
  • A precondition conflict (405, 409, 412) blocks retry and reloads the folder, because resending a stale ETag cannot succeed.
  • 429 is a typed throttled outcome: no verification read, no reload, and retry stays blocked with a wait-then-refresh message. Retry-After is not honored because file mutations do not expose it; no delay is invented.
  • A 5xx or unknown result is resolved by re-reading the parent folder from the network. The same version present means safe to retry; a changed item, failed read or cached listing blocks retry until refresh.
  • Path absence is never reported as a delete. After an interrupted response or a 404 the outcome is "no longer in this folder" (it may have been deleted, moved or renamed). If the listing shows the same file ID under a new name, the outcome is a rename and retry is blocked.
  • Each delete and its verification are owned by a FileDeleteCoordinator scoped to the account session, not by the Files screen. Leaving the screen does not cancel the request or release the item; finished outcomes wait until a Files screen picks them up, which shows the notice and reloads the folder. Session teardown cancels unfinished work.
  • While a delete is unverified, every write to that item, its parent folders and its children is blocked for that account (name-prefix matches such as Documents2 vs Documents are not parents). In the file menu the write actions (edit, favorite, version history, rename, move, copy, share, delete) are disabled with the visible reason "Wait until the delete of this item or its folder finishes."; open, preview, details, download, send copy and offline stay available. The action handler rejects the same writes.
  • A dialog only takes a finished outcome if it started the request or shows the same item at the same version.
  • The dialog moved into FileDeleteDialog.kt.
  • Size baselines lowered: NextcloudNativeApp.kt to 11026, DesktopNextcloudServices.kt to 6164.

Verification

  • Every check relevant to the changed scope passes, or each unrun check is listed below with a reason
  • bash tools/check-repository.sh passes (the Debian package check was skipped locally because dpkg-deb is missing; CI runs it)
  • A new changes/unreleased/ fragment records the change
  • No credentials, private server data, machine-local paths, or generated output are included

Run on Windows with JDK 21:

  • Focused :ui:desktopTest (FileDeleteFlowTest 21, DesktopFileMutationFollowUpsTest 7, FilesRequestStateTest 5, FileOperationsTest 17, FileActionPlanningTest 18, FilesListInteractionTest 1, FilesSelectionSemanticsTest 1): pass.
  • Full :ui:desktopTest: 3799 tests, 0 failures, 69 skipped.
  • :androidApp:testDebugUnitTest: 1167 tests, 0 failures.
  • bash tools/check-kotlin-architecture.sh and node tools/changelog-fragments.mjs validate: pass.

FileDeleteFlowTest (21) covers success, local cleanup still running or failed, definitive rejection and safe retry, 412 blocking retry, 429 without verification or immediate retry, dialog dismissal keeping the item blocked, leaving the screen during verification and during desktop local cleanup (the outcome is delivered when a screen returns), session teardown cancelling and releasing without an outcome, late completion after teardown publishing nothing, write blocking for the item, parents and children (not name-prefix siblings) scoped per account, path absence not reported as a delete, a same-ID rename, cancellation, stale results, missing ETag, double submit and folder preconditions.

DesktopFileMutationFollowUpsTest (7) covers completion, failures, cleanup stuck on an uninterruptible lock returning StillRunning within the bound, 1,020 mutations behind a stall leaving only 10 coalesced entries that each run once with the newest refresh, the path cap reporting dropped follow-ups, caller cancellation and a closed service scope. FileActionPlanningTest checks that write actions are disabled with the reason while details stays enabled.

No Windows native or JNA code changed, so no Windows packaging build was run.

Compatibility and risk

  • Not reproduced on a live Windows Cloud Files session; the trigger is inferred from code. Which lock holder stalled in the reporter's run is not confirmed.
  • No end-to-end test drives executeFileMutation against a mock server while the provider lock is held; that boundary is covered through DesktopFileMutationFollowUps.
  • No Compose UI test of the disabled menu items and no Android device run.
  • Confirming a delete by stable file ID would need a new server lookup on both platforms; this PR reports "no longer in this folder" instead of claiming a delete.
  • The media viewer has its own delete dialog that still uses its screen scope and does not consult the delete coordinator.
  • Known follow-ups, not in this PR: the same inline post-mutation refresh remains in create-folder, save, create-file and upload paths; and refreshRetainedFoldersAfterMutation and scheduleVirtualFolderHydration take virtualFileProviderLock and resourceActivationMonitor in opposite orders.

Visual changes

The dialog layout is unchanged; it now always reaches a result or error state. Write actions in the file menu are disabled with a visible reason while a delete of that item or a parent folder is unverified. Screenshots not yet captured.

A server-confirmed WebDAV delete ran its local cache and virtual-file
bookkeeping inline before returning. That bookkeeping blocks on the
desktop virtual-file provider monitor, which Windows Cloud Files code
holds across long provider operations, so the dialog could stay on
"Deleting..." indefinitely although the server had already deleted the
file. A monitor wait cannot be cancelled or interrupted.

Desktop file mutations now run that bookkeeping on one ordered
background worker, wait at most three seconds, and report the local
result separately from the server result. Unknown results queue the
bookkeeping without blocking failure or cancellation.

The Files delete dialog now uses a fenced state holder: a definitive
rejection allows a safe retry, an unknown or server-failure result is
resolved by reading the parent folder instead of resending the delete,
and the dialog can be closed while the delete finishes without a later
completion changing a newer dialog.
@obiente-cloud
obiente-cloud Bot temporarily deployed to Obiente Preview / PR #498 / NC Native October 8, 2026 00:33 Destroyed
@obiente-cloud

obiente-cloud Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Obiente preview

NC Native · 04ddf928807e · Ready

Open preview

View in Obiente

Obiente updates this comment as the preview changes.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-08T18:52:34.722027Z b7a6555 New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@obiente-cloud
obiente-cloud Bot temporarily deployed to Obiente Preview / PR #498 / NC Native October 8, 2026 00:37 Destroyed

@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: e8129163ad

ℹ️ 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".

Comment thread ui/src/commonMain/kotlin/dev/obiente/nextcloudnative/app/FileDeleteFlow.kt Outdated
Comment thread ui/src/commonMain/kotlin/dev/obiente/nextcloudnative/app/FileDeleteFlow.kt Outdated
A 412 from an ETag-guarded delete now requires a folder refresh before
another attempt, since resending the same precondition cannot succeed.

HTTP 429 is a typed throttled file error. A throttled delete is not
treated as an unknown result, starts no verification read or folder
reload, and keeps retry blocked. File mutations do not expose
Retry-After, so no delay is invented.

Unresolved deletes are tracked per account and remote path outside the
dialog. Closing and reopening Delete no longer allows a second request
for the same item until the first reaches a verified outcome, and a
reopened dialog for the same version receives that outcome.
@obiente-cloud
obiente-cloud Bot temporarily deployed to Obiente Preview / PR #498 / NC Native October 8, 2026 17:33 Destroyed

@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: 374104f0dc

ℹ️ 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".

Comment thread ui/src/commonMain/kotlin/dev/obiente/nextcloudnative/app/NextcloudNativeApp.kt Outdated
Comment thread ui/src/commonMain/kotlin/dev/obiente/nextcloudnative/app/FileDeleteFlow.kt Outdated
Comment thread ui/src/commonMain/kotlin/dev/obiente/nextcloudnative/app/FileDeleteFlow.kt Outdated
@obiente-cloud
obiente-cloud Bot temporarily deployed to Obiente Preview / PR #498 / NC Native October 8, 2026 17:41 Destroyed
Files deletes now run in an account-session coordinator instead of the
Files screen's coroutine scope. Leaving the dialog or the screen no
longer cancels a delete or releases its resource before a verified
outcome; a returning Files screen applies the finished outcome, and
only account session teardown cancels unfinished work.

While a delete is unverified, every remote write to that item, its
folders, and its children is disabled in the file menu with a reason
and rejected by the Files action handler.

A parent listing that no longer contains the path after an unknown
result is reported as no longer in this folder, not as deleted. When
the same file ID appears under another name in that listing, the
result requires a refresh.

Desktop local follow-ups are coalesced per account and path behind one
drain worker, and at most 256 paths wait behind a stalled refresh.
Paths beyond that bound are reported as a local failure instead of
growing an unbounded queue.
@obiente-cloud
obiente-cloud Bot temporarily deployed to Obiente Preview / PR #498 / NC Native October 8, 2026 18:42 Destroyed

@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: b7a6555f5d

ℹ️ 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".

Comment on lines +6689 to +6691
if (action.changesRemoteItem() && fileDeletes.blocksWrites(file)) {
mutationError = "Wait until the delete of ${file.name} or its folder finishes."
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.

P1 Badge Block direct child writes during folder deletion

When an unverified delete targets a folder, this guard covers only per-item FileMenuActions. After closing the delete dialog, the user can still open that folder and use the workspace's unguarded Create action; if the creation reaches the server before the recursive DELETE finishes, the newly created content can be deleted without warning. Fresh evidence in the final tree is that NativeFilesWorkspace.onCreate remains outside this coordinator check, so current-path creation and other direct write entry points must also consult the overlapping delete state.

AGENTS.md reference: AGENTS.md:L401-L402

Useful? React with 👍 / 👎.

Comment on lines +174 to +178
private fun reset(file: NextcloudFile?) {
attachedToken = null
target = file
error = null
retryBlocked = false

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 throttling across dialog reopening

After a 429 completion sets retryBlocked, dismissing and reopening the same item's dialog calls this reset and clears the block; the coordinator has also released its in-flight claim, so Delete can immediately send the same request again without waiting or refreshing. Fresh evidence in the final tree is this reset path, which defeats the newly typed throttled outcome even though the in-dialog test passes. Persist the throttling state per resource until an explicit refresh or an appropriate delay has occurred.

AGENTS.md reference: AGENTS.md:L294-L296

Useful? React with 👍 / 👎.

Comment on lines +70 to +72
withTimeoutOrNull(completionWaitMillis) { waiters.awaitAll() }
?.let { results -> results.firstOrNull { it != FileMutationLocalFollowUp.Completed } }
?: if (waiters.all { it.isCompleted }) FileMutationLocalFollowUp.Completed else FileMutationLocalFollowUp.StillRunning

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 Retain dropped failures when another follow-up times out

When a multi-path mutation is submitted with only one queue slot left, one waiter can be accepted while another is immediately completed as Failed; if the accepted refresh remains blocked until this timeout, the Elvis branch returns StillRunning and discards the known failure. The caller then tells the user that local state is still updating even though one affected path was permanently dropped and will never be refreshed. Track any failed waiter separately and return Failed for this partial result.

AGENTS.md reference: AGENTS.md:L305-L306

Useful? React with 👍 / 👎.

Comment on lines +609 to +610
"ui/src/commonMain/kotlin/dev/obiente/nextcloudnative/app/FileDeleteDialog.kt": "b70e3b4fa9754b6af6bdae92e70f6d5392560e9e70c99794463b126cc33b307b",
"ui/src/commonMain/kotlin/dev/obiente/nextcloudnative/app/FileDeleteFlow.kt": "cffbc353d49c20342046b598e2eaead8cbf5b3d810739826dd120b2019e608eb",

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 Regenerate the capture manifest from the final sources

The committed capture inventory is stale: the hashes recorded here for FileDeleteDialog.kt and FileDeleteFlow.kt do not match their final contents, several other modified capture sources also mismatch, and the newly added FileDeleteCoordinator.kt is absent from both captureSources and captureSourceHashes even though discovery includes the entire ui/src/commonMain/kotlin directory. The capture freshness verifier therefore rejects this manifest, and the screenshots are not reproducibly tied to the reviewed source until they are regenerated.

AGENTS.md reference: AGENTS.md:L107-L109

Useful? React with 👍 / 👎.

Comment on lines +60 to +64
NextcloudFileOperationError.AuthenticationRequired,
NextcloudFileOperationError.PermissionDenied,
NextcloudFileOperationError.Locked,
NextcloudFileOperationError.InsufficientStorage,
-> return FileDeleteOutcome.NotDeleted(failure.message ?: "Could not delete ${target.name}.")

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 Block retries after authentication and permission failures

When DELETE returns 401 or 403, this branch converts the response to NotDeleted; FileDeleteDialogState.complete consequently leaves retryBlocked false, so the Delete button remains enabled even though the credentials or permission state must be corrected before another write is valid. Pressing it simply repeats the unauthorized request with the same stale authorization context. Return a blocked recovery outcome for these errors and require reauthentication or a permission refresh before enabling another write.

AGENTS.md reference: AGENTS.md:L401-L402

Useful? React with 👍 / 👎.

Comment on lines +126 to +127
private fun drain() {
while (true) {

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 Stop draining follow-ups after service cancellation

If DesktopNextcloudServices.close() cancels serviceScope while entry.refresh() is blocked, the coroutine is marked cancelled but this non-suspending loop never observes that state. Once the blocking refresh returns, it continues executing every queued cache/provider refresh, and the invokeOnCompletion cleanup cannot clear the queue until the loop exits; up to the full pending bound can therefore run after teardown, racing provider closure and extending shutdown. Check cancellation between entries and abandon the remaining queue as soon as the current non-cancellable refresh returns.

AGENTS.md reference: AGENTS.md:L353-L355

Useful? React with 👍 / 👎.

This branch was successfully deployed

1 active deployment
Obiente Preview / PR #498 / NC Native — 04ddf928 Deployed Oct 8, 2026 by obiente-cloud[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

1 participant