Skip to content

Keep iOS mic button retryable after denial - #6462

Closed
lawrencecchen wants to merge 1 commit into
mainfrom
task-ios-mic-permission-retry
Closed

lawrencecchen wants to merge 1 commit into
mainfrom
task-ios-mic-permission-retry

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • keep the iOS composer mic button enabled after speech or microphone permission denial
  • route denied authorization back to idle so the next tap invokes the permission APIs again
  • keep unavailable reserved for recognizer/audio setup failures and cover the retryable state in the dictation state tests

Testing

  • swift test --filter ComposerDictationTests (fails before compilation: fresh worktree is missing a built GhosttyKit.xcframework)
  • ./scripts/reload-cloud.sh --tag dog (passed, GitHub Actions run https://github.com/manaflow-ai/cmux/actions/runs/27853578184)
  • web warmup: / 200, /handler/sign-in 200, /handler/after-sign-in 200 after redirect
  • ./ios/scripts/reload.sh --tag dog (blocked by local build guard for simulator reload)
  • ./ios/scripts/reload-cloud.sh --tag dog --device-id E4058DA9-F4C7-52DD-951D-0354061B8E89 --wait (archive/sign/install reported success, launch failed because CoreDevice lost the iPhone connection; follow-up devicectl reported the device unavailable)

Issues

  • Task: iOS mic button should never become disabled after the user taps Don't Allow; pressing it again should request permission again.

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Note

Low Risk
Localized iOS composer dictation permission UX; authorization still goes through system APIs, with no change to recognition or send paths.

Overview
Fixes the iOS composer mic staying disabled after the user taps Don’t Allow on speech or microphone permission.

Dictation controller: Denied or restricted authorization no longer moves to unavailable. The flow clears the captured callback and returns to idle (via retryablePermissionDenied), including when permissions were already denied synchronously—those taps now fall through to the async permission APIs instead of stopping early. unavailable is reserved for a missing recognizer or failed audio/recognition setup.

UI: Removes isAvailable and the mic button’s .disabled(!dictation.isAvailable) so the control stays tappable after denial.

Tests: Asserts that retryablePermissionDenied is idle and canStart is true.

Reviewed by Cursor Bugbot for commit c5f9002. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Keep the iOS composer mic button tappable after speech/microphone permission denial so the next tap re-requests authorization. Only unsupported locale or audio setup failures now disable dictation. Addresses task: iOS mic button should not be disabled after “Don’t Allow”; tapping again should request permission again.

  • Bug Fixes
    • Route denied authorization back to idle via ComposerDictationState.retryablePermissionDenied; drop pending callback and reset baseText.
    • Keep the mic button enabled by removing UI disablement in TerminalComposerView.
    • Reserve .unavailable for nil SFSpeechRecognizer or audio setup failures; added a unit test to cover the retryable denial state.

Written for commit c5f9002. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Microphone permission denials are now retryable—users can request authorization again without restarting the app.
    • Mic button remains enabled after permission denial, allowing users to tap and retry the permission request.
  • Tests

    • Added test coverage for dictation permission denial retry behavior.

@vercel

vercel Bot commented Jun 19, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 20, 2026 12:04am
cmux-staging Building Building Preview, Comment Jun 20, 2026 12:04am

@coderabbitai

coderabbitai Bot commented Jun 19, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The dictation permission-denial path is changed from a terminal .unavailable state to a retryable flow. A new retryablePermissionDenied state alias resolves to .idle, the controller's .denied branch now proceeds to the async authorization request, and the mic button is no longer disabled on denial.

Changes

Retryable Dictation Permission Denial

Layer / File(s) Summary
retryablePermissionDenied state definition and docs
...ComposerDictationTextMerge.swift
Introduces ComposerDictationState.retryablePermissionDenied as a static var resolving to .idle. Updates state-machine documentation to describe denied permission as retryable back to idle, and narrows the unavailable comment to hardware/setup failures.
Authorization denial handling in ComposerDictationController
...ComposerDictationController.swift
Changes the .denied branch in start(...) from a terminal early-return to a break that enters the async requestAuthorization path. In the authorization completion block, the not-granted case now clears baseText and transitions to .retryablePermissionDenied instead of .unavailable. The recognizer == nil init path retains .unavailable with updated commentary.
Mic button enable state and state-machine test
...TerminalComposerView.swift, ...ComposerDictationTests.swift
Removes .disabled(!dictation.isAvailable) from micButton so it stays tappable after denial. Adds a test asserting retryablePermissionDenied == .idle and that the resulting state has canStart == true.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • manaflow-ai/cmux#6197: Original dictation implementation introducing ComposerDictationController, ComposerDictationTextMerge, and the state machine that this PR modifies for retryable permission denial.
  • manaflow-ai/cmux#6238: Also modifies ComposerDictationController.start(existingText:onText:)'s authorization callback flow, restructuring the MainActor hop and adding recognition guards in the same code path changed here.

Poem

🐰 Hop hop, the mic stays alive,
No more dead ends when permissions don't jive!
A gentle .idle, a retryable dream,
Tap once more — things aren't as bleak as they seem.
The rabbit keeps tapping 'til dictation takes flight~ 🎤


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (2 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error PR introduces Task.sleep at line 225 of ComposerDictationController.swift for a watchdog timeout, which violates the rule: Task.sleep is not allowed in production code for readiness waits without a... Replace the Task.sleep watchdog timeout with a proper timer abstraction or rely on existing callback/state-transition mechanisms instead of sleep-based timing.
Cmux Swift File And Package Boundaries ❌ Error TerminalComposerView.swift (824 lines) exceeds the 800-line threshold for new production files per swift-file-package-boundaries.md, violating file size limits for new files without demonstrated ex... Reduce TerminalComposerView.swift to ≤800 lines by extracting UI components (e.g., AttachmentChipRow, SendButton, MicButton) into separate smaller files, or document the extraction path in comments showing how UI subcomponents could be i...
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (20 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: keeping the iOS mic button retryable after denial.
Description check ✅ Passed The description covers the key aspects but lacks some elements from the template, particularly a demo video link and incomplete checklist items.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed All four modified files maintain proper Swift 6 actor isolation. ComposerDictationController is @MainActor-isolated with nonisolated off-main methods. ComposerDictationTextMerge defines pure value...
Cmux Expensive Synchronous Load ✅ Passed No expensive synchronous loaders (like RestorableAgentSessionIndex.load()) are added to main actor or interactive paths. Changes only add lightweight OS permission status reads for dictation retry...
Cmux Cache Substitution Correctness ✅ Passed PR modifies only transient UI state (dictation controller state machine, enum cases, button UI modifiers) with no persistence, caching, or fresh-to-cached read swaps.
Cmux No Hacky Sleeps ✅ Passed PR contains only Swift file changes. The "cmux no hacky sleeps" check applies only to TypeScript, JavaScript, shell, and non-Swift build/runtime scripts. Swift code is explicitly out of scope per ....
Cmux Algorithmic Complexity ✅ Passed PR modifies iOS mic button permission denial handling only—state machine transitions, text merge logic, and UI adjustments with no collection scanning, batch rescans, or algorithmic bloat on scalab...
Cmux Swift Concurrency ✅ Passed PR uses modern Swift concurrency (@MainActor, @Observable) and crosses OS/third-party boundaries (Speech, AVFoundation) with proper Task { @MainActor in } patterns. No legacy async patterns (Dispat...
Cmux Swift @Concurrent ✅ Passed Only one async function in PR: TerminalComposerView.prepare() is nonisolated async with explicit background task group hop, correctly handling CPU-heavy ImageIO work called from @MainActor context.
Cmux Swiftpm Lockfiles ✅ Passed PR modifies only Swift source code (dictation feature), not SwiftPM packages, Xcode projects, .gitignore, workflows, or dependencies. Custom check does not apply.
Cmux Swift Logging ✅ Passed No violations of Swift logging rules detected: no print/debugPrint/dump/NSLog in production code, no ad hoc file/stdout logging, no MainActor Logger issues, no sensitive data exposure in any of the...
Cmux User-Facing Error Privacy ✅ Passed PR contains production changes but introduces no user-facing error messages, vendor names, credentials, or sensitive implementation details—only safe state machine logic and generic UI labels.
Cmux Full Internationalization ✅ Passed No new user-facing strings added; existing L10n calls use localized keys with translations for all supported locales (en, ja). Test and documentation changes are exempt.
Cmux Swiftui State Layout ✅ Passed PR uses @Observable (modern) not @ObservableObject; no GeometryReader; ForEach passes value snapshots+closures not store refs; Task scheduling only in event handlers/callbacks, not render-time; @St...
Cmux Architecture Rethink ✅ Passed PR makes a small correctness fix to dictation permission handling with a single clear owner (@MainActor controller), named invariants, and no timing/state-ownership antipatterns beyond the document...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR modifies iOS dictation and composer UI logic only; no NSWindow, NSPanel, NSWindowController, SwiftUI Window, or WindowGroup code added or changed, so auxiliary window close shortcuts rule does n...
Cmux Source Artifacts ✅ Passed All changed files are legitimate source code, tests, configs, and assets; no forbidden artifacts (logs, DerivedData, caches, build output, temp dirs) were added to source control.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No test/debug seams found. The #if DEBUG block in TerminalComposerView gates real product diagnostic logging behavior, not test-observability. All new members (ComposerDictationState.retryablePermi...
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task-ios-mic-permission-retry

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c5f9002. Configure here.

.buttonStyle(.plain)
.foregroundStyle(listening ? AnyShapeStyle(Color.red) : AnyShapeStyle(TerminalPalette.foreground.opacity(0.7)))
.mobileGlassCircle()
.disabled(!dictation.isAvailable)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Mic enabled when unavailable

Medium Severity

Removing dictation.isAvailable and the mic’s .disabled modifier leaves the control tappable whenever ComposerDictationState is .unavailable (unsupported locale, nil recognizer, or failStart). start() still no-ops because canStart is false, so the UI suggests dictation works when it cannot.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c5f9002. Configure here.

@greptile-apps

greptile-apps Bot commented Jun 20, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes the iOS composer mic button becoming permanently disabled after the user taps "Don't Allow" on the speech or microphone permission prompt, by routing denied authorization back to .idle instead of .unavailable and removing the isAvailable button guard.

  • ComposerDictationController: the synchronous .denied branch now falls through to the async requestAuthorization path instead of hard-coding .unavailable; the async callback settles to retryablePermissionDenied (an alias for .idle) so the button stays tappable.
  • ComposerDictationTextMerge: unavailable is redefined to cover only locale/audio-setup failures; a new retryablePermissionDenied static alias expresses the denial-to-idle transition at the call site.
  • TerminalComposerView: the .disabled(!dictation.isAvailable) modifier and isAvailable property were removed entirely — but this also inadvertently un-disables the button for the unavailable state (unsupported locale, failed audio setup), where the button now appears enabled but silently does nothing on tap.

Confidence Score: 3/5

The denial-to-idle fix is correct, but removing the .disabled modifier entirely also silently breaks the mic button for users whose device doesn't support speech recognition at all.

The view change dropped the .disabled(!dictation.isAvailable) guard without a narrower replacement, so the unavailable state (unsupported locale, failed audio session) now presents an always-enabled button that does nothing on tap. The controller and state-machine changes are sound, but the view regression makes the button misleading for a real user-visible case.

TerminalComposerView.swift — the missing .disabled(dictation.state == .unavailable) guard is the only change needed to restore correct behavior for the unavailable case without reintroducing the denial regression.

Important Files Changed

Filename Overview
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalComposerView.swift Removed .disabled(!dictation.isAvailable) entirely; the button is now always enabled, including when state is .unavailable (unsupported locale / audio failure), causing a silent no-op on tap for those cases.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/ComposerDictationController.swift Denial path changed from .unavailable to retryablePermissionDenied (.idle); removed isAvailable property; async auth callback now clears baseText on denial. Logic is sound but falls through to requestAuthorization when already denied, producing a silent idle cycle with no user feedback.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/ComposerDictationTextMerge.swift Added retryablePermissionDenied static computed property (aliases .idle) and updated unavailable docs to exclude denial; purely semantic, no logic change.
Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/ComposerDictationTests.swift Added deniedPermissionSettlesToStartableIdle test asserting retryablePermissionDenied == .idle and canStart; clean addition to the existing state-machine test suite.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    idle([idle])
    requesting[requestingPermission]
    listening[listening]
    stopping[stopping]
    unavailable([unavailable])

    idle -->|"tap (canStart)"| requesting
    requesting -->|"second tap (cancel)"| idle
    requesting -->|"granted"| listening
    requesting -->|"denied/restricted\n(new: retryablePermissionDenied)"| idle
    listening -->|"tap stop / graceful stop"| stopping
    stopping -->|"final result or watchdog"| idle
    listening -->|"cancel / nav away"| idle
    idle -->|"recognizer nil\nor failStart"| unavailable

    style unavailable fill:#f99,stroke:#c33
    style idle fill:#9f9,stroke:#3a3
    style requesting fill:#ff9,stroke:#aa0
    style listening fill:#9cf,stroke:#39c
    style stopping fill:#fcf,stroke:#c3c
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    idle([idle])
    requesting[requestingPermission]
    listening[listening]
    stopping[stopping]
    unavailable([unavailable])

    idle -->|"tap (canStart)"| requesting
    requesting -->|"second tap (cancel)"| idle
    requesting -->|"granted"| listening
    requesting -->|"denied/restricted\n(new: retryablePermissionDenied)"| idle
    listening -->|"tap stop / graceful stop"| stopping
    stopping -->|"final result or watchdog"| idle
    listening -->|"cancel / nav away"| idle
    idle -->|"recognizer nil\nor failStart"| unavailable

    style unavailable fill:#f99,stroke:#c33
    style idle fill:#9f9,stroke:#3a3
    style requesting fill:#ff9,stroke:#aa0
    style listening fill:#9cf,stroke:#39c
    style stopping fill:#fcf,stroke:#c3c
Loading

Comments Outside Diff (1)

  1. Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalComposerView.swift, line 377-396 (link)

    P1 Mic button also lost its disabled state for genuine unavailable

    .disabled(!dictation.isAvailable) was removed entirely, so the button is now tappable even when state == .unavailable (unsupported locale, audio-setup failure). In that state start() exits early at guard state.canStart (canStart is self == .idle), leaving the user with a tappable control that silently does nothing. The old isAvailable was state != .unavailable; the fix only needed to narrow the condition to exclude denied rather than drop it altogether. Users with an unsupported locale will see an enabled mic button that produces no response on every tap.

Reviews (1): Last reviewed commit: "Keep iOS mic button retryable after deni..." | Re-trigger Greptile

Comment on lines 387 to 390
.buttonStyle(.plain)
.foregroundStyle(listening ? AnyShapeStyle(Color.red) : AnyShapeStyle(TerminalPalette.foreground.opacity(0.7)))
.mobileGlassCircle()
.disabled(!dictation.isAvailable)
.accessibilityIdentifier("MobileComposerMic")

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.

P1 When the denial case was moved from unavailable to idle, the entire .disabled guard was dropped rather than narrowed. unavailable is still the resting state for an unsupported locale or a failed audio setup, so those cases should still visually disable the button.

Suggested change
.buttonStyle(.plain)
.foregroundStyle(listening ? AnyShapeStyle(Color.red) : AnyShapeStyle(TerminalPalette.foreground.opacity(0.7)))
.mobileGlassCircle()
.disabled(!dictation.isAvailable)
.accessibilityIdentifier("MobileComposerMic")
.buttonStyle(.plain)
.foregroundStyle(listening ? AnyShapeStyle(Color.red) : AnyShapeStyle(TerminalPalette.foreground.opacity(0.7)))
.mobileGlassCircle()
.disabled(dictation.state == .unavailable)
.accessibilityIdentifier("MobileComposerMic")

Comment on lines 156 to 163
case .denied:
self.onText = nil
state = .unavailable
return
// Keep denial retryable. The OS may return `false` immediately after
// a prior denial, but the user's next tap should still invoke the
// permission APIs instead of short-circuiting into a disabled state.
break
case .undetermined:
// First-ever request: fall through to the async prompt below.
break

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.

P2 Silent failure when permission is already denied — no "Open Settings" feedback

When resolvedAuthorization() returns .denied, the break falls through to requestAuthorization, which calls SFSpeechRecognizer.requestAuthorization. iOS will immediately invoke the callback with .denied without showing any alert or dialog (the system only prompts once per permission). The state cycles idle → requestingPermission → idle with no user-visible indicator. Users who previously tapped "Don't Allow" will see the mic button appear to do nothing, with no path to understand they must visit Settings → Privacy. Consider adding a "Go to Settings" alert from the guard granted else branch to make the denied path actionable.

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalComposerView.swift (1)

389-390: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Disable the mic only in terminal .unavailable state to avoid inert taps.

Line 389 keeps the mic visually/actionably enabled even when dictation is .unavailable, but that tap path is a guaranteed no-op (start rejects non-.idle). Keep denial retryable, but prevent dead-end interaction for true unavailable conditions.

Suggested fix
         .buttonStyle(.plain)
         .foregroundStyle(listening ? AnyShapeStyle(Color.red) : AnyShapeStyle(TerminalPalette.foreground.opacity(0.7)))
         .mobileGlassCircle()
+        .disabled(dictation.state == .unavailable)
         .accessibilityIdentifier("MobileComposerMic")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalComposerView.swift`
around lines 389 - 390, The mic button with accessibilityIdentifier
"MobileComposerMic" remains enabled even when dictation is in an `.unavailable`
state, causing taps to be dead-end interactions since the start action rejects
non-idle states. Add a conditional disabled modifier to the mic button that
disables it specifically when the dictation state equals `.unavailable`,
preventing inert taps while keeping it interactive for other states like
transient ones where retry should remain possible.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalComposerView.swift`:
- Around line 389-390: The mic button with accessibilityIdentifier
"MobileComposerMic" remains enabled even when dictation is in an `.unavailable`
state, causing taps to be dead-end interactions since the start action rejects
non-idle states. Add a conditional disabled modifier to the mic button that
disables it specifically when the dictation state equals `.unavailable`,
preventing inert taps while keeping it interactive for other states like
transient ones where retry should remain possible.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f6b561bc-54f4-41bb-98da-8c29cd863361

📥 Commits

Reviewing files that changed from the base of the PR and between d886638 and c5f9002.

📒 Files selected for processing (4)
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/ComposerDictationController.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/ComposerDictationTextMerge.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalComposerView.swift
  • Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/ComposerDictationTests.swift

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026

This branch was successfully deployed

1 active deployment
Preview – cmux — c5f90024 Deployed Jun 20, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants