Context
Split out from GH #12. The substantive half of #12 — a maxTokens cap so a runaway cleanup decode self-terminates — shipped (CleanupTokenBudget + MLXLanguageModel.clean sets parameters.maxTokens). This issue tracks #12's optional second checkbox: a Cancel affordance.
Want
A Cancel button on NoteDetailView's "Cleaning up…" spinner state, mirroring the download Cancel in CleanupModelSection, so the user can stop an in-flight cleanup in place. Today the only escape is navigating away, which triggers cleaner.evict() on onDisappear — there's no stop-in-place affordance.
Why it was deferred (not just dropped)
- The cap is the load-bearing safety belt; Cancel is a UX nicety on top.
- A real cancel must interrupt the in-flight generation inside
MLXLanguageModel.clean (ChatSession.respond). That needs cooperative Task cancellation to actually stop the GPU decode — which can't be validated on the simulator (MLX-gating convention). Shipping a button that may not truly interrupt the decode is worse than not shipping it.
Implementation notes
- Verify
MLXLMCommon.ChatSession.respond(to:) honors Task.isCancelled mid-decode. If it doesn't stop promptly, the Cancel path may need to additionally evict() to free the model and break the loop.
runCleanup already wraps the work in a Task; hold a handle so a Cancel button can call .cancel().
- Map a cancelled run to no error UI (silent return to the pre-cleanup state), not the "Couldn't clean up" alert.
- Related:
ReTranscriber shares the same no-explicit-bound shape, but over fixed audio (lower risk) — could grow the same Cancel treatment as a stretch.
References
Relay Notes/Views/NoteDetailView.swift — runCleanup / cleanUpControl (the spinner state).
Relay Notes/Enrichment/MLXLanguageModel.swift — clean(_:).
CleanupModelSection — existing download Cancel pattern to mirror.
Extracted from GH #12 while shipping the cleanup token cap, 2026-06-15.
Context
Split out from GH #12. The substantive half of #12 — a
maxTokenscap so a runaway cleanup decode self-terminates — shipped (CleanupTokenBudget+MLXLanguageModel.cleansetsparameters.maxTokens). This issue tracks #12's optional second checkbox: a Cancel affordance.Want
A Cancel button on
NoteDetailView's "Cleaning up…" spinner state, mirroring the download Cancel inCleanupModelSection, so the user can stop an in-flight cleanup in place. Today the only escape is navigating away, which triggerscleaner.evict()ononDisappear— there's no stop-in-place affordance.Why it was deferred (not just dropped)
MLXLanguageModel.clean(ChatSession.respond). That needs cooperativeTaskcancellation to actually stop the GPU decode — which can't be validated on the simulator (MLX-gating convention). Shipping a button that may not truly interrupt the decode is worse than not shipping it.Implementation notes
MLXLMCommon.ChatSession.respond(to:)honorsTask.isCancelledmid-decode. If it doesn't stop promptly, the Cancel path may need to additionallyevict()to free the model and break the loop.runCleanupalready wraps the work in aTask; hold a handle so a Cancel button can call.cancel().ReTranscribershares the same no-explicit-bound shape, but over fixed audio (lower risk) — could grow the same Cancel treatment as a stretch.References
Relay Notes/Views/NoteDetailView.swift—runCleanup/cleanUpControl(the spinner state).Relay Notes/Enrichment/MLXLanguageModel.swift—clean(_:).CleanupModelSection— existing download Cancel pattern to mirror.Extracted from GH #12 while shipping the cleanup token cap, 2026-06-15.