krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

283fbf00fdffd64c924b6cb22c84e803b718f078

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2026-08-07T22:33:46Z

Adopt Swift 6 language mode

Flip SWIFT_VERSION to 6.0 (keeping SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor)
and resolve every resulting diagnostic. Mechanical, no behaviour change:

- Models: pure value types in Models/ and utility extensions (Date, DateFormatter,
  SRHTWebURL) marked nonisolated so the nonisolated networking layer can use them.
- App Intents: stored 'static var' (title/description/openAppWhenRun/
  typeDisplayRepresentation/defaultQuery/caseDisplayRepresentations) -> 'static let';
  drop the invalid @retroactive on the in-module LookupType: AppEnum.
- Sendable dicts: 'nil as String? as Any' -> 'nil as String? as any Sendable'.
- UserDefaults: retroactive @unchecked Sendable (documented thread-safe).
- WidgetKit completion handlers rebound nonisolated(unsafe) to cross into Task {};
  TipStoreViewModel task handle is @ObservationIgnored nonisolated(unsafe) for
  its nonisolated deinit.
- Tests run on @MainActor; a few constant fixtures marked nonisolated for use in
  @Sendable stub closures.

Builds and the full suite are clean in Swift 6 mode. Roadmap updated.
 Hutch.xcodeproj/project.pbxproj                    | 16 ++---
 Hutch/App/HutchIntents.swift                       | 76 +++++++++++-----------
 Hutch/Extensions/Date+Relative.swift               |  2 +-
 Hutch/Extensions/DateFormatter+SRHT.swift          |  2 +-
 Hutch/Extensions/Foundation+Sendable.swift         |  7 ++
 Hutch/Extensions/SRHTWebURL.swift                  |  2 +-
 Hutch/Models/AccountEntry.swift                    |  2 +-
 Hutch/Models/ArtifactInfo.swift                    |  6 +-
 Hutch/Models/Builds.swift                          | 18 ++---
 Hutch/Models/CommitDetail.swift                    | 16 ++---
 Hutch/Models/CommitSummary.swift                   |  4 +-
 Hutch/Models/ContributionCalendar.swift            | 28 ++++----
 Hutch/Models/Git.swift                             | 40 ++++++------
 Hutch/Models/Inbox.swift                           | 20 +++---
 Hutch/Models/Meta.swift                            | 16 ++---
 Hutch/Models/Paste.swift                           |  6 +-
 Hutch/Models/Patchset.swift                        | 12 ++--
 Hutch/Models/Project.swift                         | 24 +++----
 Hutch/Models/RepositoryACL.swift                   |  4 +-
 Hutch/Models/RepositorySummary.swift               |  8 +--
 Hutch/Models/SystemStatusModels.swift              | 10 +--
 Hutch/Models/TicketBulkAction.swift                |  6 +-
 Hutch/Models/Todo.swift                            | 30 ++++-----
 Hutch/Models/User.swift                            |  2 +-
 Hutch/Views/Lists/MailingListListView.swift        |  2 +-
 Hutch/Views/More/TipStoreViewModel.swift           |  4 +-
 .../Repositories/RepositoryDetailViewModel.swift   |  2 +-
 Hutch/Views/Repositories/SyntaxHighlighter.swift   |  2 +-
 Hutch/Views/Settings/SettingsViewModel.swift       | 10 +--
 HutchTests/APICacheTests.swift                     |  1 +
 HutchTests/AppStateTests.swift                     |  1 +
 HutchTests/BuildListViewModelTests.swift           |  1 +
 HutchTests/BuildTaskLogSearchTests.swift           |  1 +
 HutchTests/BuildTaskTests.swift                    |  1 +
 HutchTests/BundleUserAgentTests.swift              |  1 +
 HutchTests/ContributionCalendarTests.swift         |  1 +
 HutchTests/DeepLinkTests.swift                     |  1 +
 HutchTests/GitObjectTests.swift                    |  1 +
 HutchTests/HomeViewModelTests.swift                |  1 +
 HutchTests/HutchTests.swift                        |  1 +
 HutchTests/InboxViewModelTests.swift               |  1 +
 HutchTests/LookupHistoryStoreTests.swift           |  1 +
 HutchTests/MailingListActivityTests.swift          |  1 +
 HutchTests/ManPageCatalogTests.swift               |  1 +
 HutchTests/PatchsetTests.swift                     |  2 +
 HutchTests/ProjectFormTests.swift                  |  1 +
 HutchTests/ProjectPinStoreTests.swift              |  1 +
 HutchTests/ProjectTests.swift                      |  1 +
 HutchTests/ReadmeViewTests.swift                   |  5 ++
 HutchTests/RecentActivityStoreTests.swift          |  1 +
 HutchTests/RepositoryACLViewModelTests.swift       |  1 +
 HutchTests/RepositoryDeployKeyTests.swift          |  1 +
 HutchTests/RepositoryListViewModelTests.swift      |  1 +
 HutchTests/RepositorySettingsViewModelTests.swift  |  1 +
 HutchTests/SRHTClientTests.swift                   |  1 +
 HutchTests/SRHTWebURLTests.swift                   |  1 +
 HutchTests/ScopedSearchHistoryStoreTests.swift     |  1 +
 HutchTests/SettingsViewModelTests.swift            |  1 +
 HutchTests/SettingsViewTests.swift                 |  1 +
 HutchTests/SourceHutWebDeepLinkMapperTests.swift   |  1 +
 HutchTests/SyntaxHighlighterTests.swift            |  9 ++-
 HutchTests/SystemStatusRepositoryTests.swift       |  9 +--
 HutchTests/SystemStatusServiceTests.swift          |  1 +
 HutchTests/TicketBulkActionTests.swift             |  1 +
 HutchTests/TicketDetailViewModelTests.swift        |  1 +
 HutchTests/TicketSavedFilterStoreTests.swift       |  1 +
 HutchTests/TipStoreViewModelTests.swift            |  1 +
 HutchTests/TrackerListViewModelTests.swift         |  1 +
 HutchTests/TrackerManagementViewModelTests.swift   |  1 +
 HutchWidgetExtension/ContributionGraphWidget.swift |  4 ++
 ROADMAP.txt                                        | 40 +++++++-----
 71 files changed, 273 insertions(+), 209 deletions(-)

diff --git a/Hutch.xcodeproj/project.pbxproj b/Hutch.xcodeproj/project.pbxproj
index dfa2b62..355157b 100644
--- a/Hutch.xcodeproj/project.pbxproj
+++ b/Hutch.xcodeproj/project.pbxproj
@@ -622,7 +622,7 @@
 				SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
 				SWIFT_EMIT_LOC_STRINGS = YES;
 				SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
-				SWIFT_VERSION = 5.0;
+				SWIFT_VERSION = 6.0;
 				TARGETED_DEVICE_FAMILY = "1,2";
 			};
 			name = Debug;
@@ -659,7 +659,7 @@
 				SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
 				SWIFT_EMIT_LOC_STRINGS = YES;
 				SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
-				SWIFT_VERSION = 5.0;
+				SWIFT_VERSION = 6.0;
 				TARGETED_DEVICE_FAMILY = "1,2";
 			};
 			name = Release;
@@ -680,7 +680,7 @@
 				SWIFT_APPROACHABLE_CONCURRENCY = YES;
 				SWIFT_EMIT_LOC_STRINGS = NO;
 				SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
-				SWIFT_VERSION = 5.0;
+				SWIFT_VERSION = 6.0;
 				TARGETED_DEVICE_FAMILY = "1,2";
 				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Hutch.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Hutch";
 			};
@@ -702,7 +702,7 @@
 				SWIFT_APPROACHABLE_CONCURRENCY = YES;
 				SWIFT_EMIT_LOC_STRINGS = NO;
 				SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
-				SWIFT_VERSION = 5.0;
+				SWIFT_VERSION = 6.0;
 				TARGETED_DEVICE_FAMILY = "1,2";
 				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Hutch.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Hutch";
 			};
@@ -732,7 +732,7 @@
 				SWIFT_APPROACHABLE_CONCURRENCY = YES;
 				SWIFT_EMIT_LOC_STRINGS = YES;
 				SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
-				SWIFT_VERSION = 5.0;
+				SWIFT_VERSION = 6.0;
 				TARGETED_DEVICE_FAMILY = "1,2";
 			};
 			name = Debug;
@@ -761,7 +761,7 @@
 				SWIFT_APPROACHABLE_CONCURRENCY = YES;
 				SWIFT_EMIT_LOC_STRINGS = YES;
 				SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
-				SWIFT_VERSION = 5.0;
+				SWIFT_VERSION = 6.0;
 				TARGETED_DEVICE_FAMILY = "1,2";
 			};
 			name = Release;
@@ -790,7 +790,7 @@
 				SWIFT_APPROACHABLE_CONCURRENCY = YES;
 				SWIFT_EMIT_LOC_STRINGS = YES;
 				SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
-				SWIFT_VERSION = 5.0;
+				SWIFT_VERSION = 6.0;
 				TARGETED_DEVICE_FAMILY = "1,2";
 			};
 			name = Debug;
@@ -819,7 +819,7 @@
 				SWIFT_APPROACHABLE_CONCURRENCY = YES;
 				SWIFT_EMIT_LOC_STRINGS = YES;
 				SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
-				SWIFT_VERSION = 5.0;
+				SWIFT_VERSION = 6.0;
 				TARGETED_DEVICE_FAMILY = "1,2";
 			};
 			name = Release;
diff --git a/Hutch/App/HutchIntents.swift b/Hutch/App/HutchIntents.swift
index ff8fec8..3f4cd83 100644
--- a/Hutch/App/HutchIntents.swift
+++ b/Hutch/App/HutchIntents.swift
@@ -4,9 +4,9 @@ import Foundation
 // MARK: - Navigation Intents
 
 struct OpenWorkQueueIntent: AppIntent {
-    static var title: LocalizedStringResource = "Open Work Queue"
-    static var description = IntentDescription("Opens Hutch to your Work Queue.")
-    static var openAppWhenRun = true
+    static let title: LocalizedStringResource = "Open Work Queue"
+    static let description = IntentDescription("Opens Hutch to your Work Queue.")
+    static let openAppWhenRun = true
 
     var route: HutchRoute { .workQueue(scope: .all) }
 
@@ -18,9 +18,9 @@ struct OpenWorkQueueIntent: AppIntent {
 }
 
 struct OpenRecentActivityIntent: AppIntent {
-    static var title: LocalizedStringResource = "Open Recent Activity"
-    static var description = IntentDescription("Opens Hutch to recent activity.")
-    static var openAppWhenRun = true
+    static let title: LocalizedStringResource = "Open Recent Activity"
+    static let description = IntentDescription("Opens Hutch to recent activity.")
+    static let openAppWhenRun = true
 
     var route: HutchRoute { .recentActivity }
 
@@ -32,9 +32,9 @@ struct OpenRecentActivityIntent: AppIntent {
 }
 
 struct OpenSystemStatusIntent: AppIntent {
-    static var title: LocalizedStringResource = "Open System Status"
-    static var description = IntentDescription("Opens Hutch to SourceHut system status.")
-    static var openAppWhenRun = true
+    static let title: LocalizedStringResource = "Open System Status"
+    static let description = IntentDescription("Opens Hutch to SourceHut system status.")
+    static let openAppWhenRun = true
 
     var route: HutchRoute { .systemStatus }
 
@@ -46,9 +46,9 @@ struct OpenSystemStatusIntent: AppIntent {
 }
 
 struct OpenPinnedResourceIntent: AppIntent {
-    static var title: LocalizedStringResource = "Open Pinned Resource"
-    static var description = IntentDescription("Opens a pinned Hutch resource.")
-    static var openAppWhenRun = true
+    static let title: LocalizedStringResource = "Open Pinned Resource"
+    static let description = IntentDescription("Opens a pinned Hutch resource.")
+    static let openAppWhenRun = true
 
     @Parameter(title: "Pinned Resource")
     var pinnedResource: PinnedResourceEntity
@@ -63,9 +63,9 @@ struct OpenPinnedResourceIntent: AppIntent {
 }
 
 struct OpenProjectDashboardIntent: AppIntent {
-    static var title: LocalizedStringResource = "Open Project Dashboard"
-    static var description = IntentDescription("Opens a pinned project dashboard in Hutch.")
-    static var openAppWhenRun = true
+    static let title: LocalizedStringResource = "Open Project Dashboard"
+    static let description = IntentDescription("Opens a pinned project dashboard in Hutch.")
+    static let openAppWhenRun = true
 
     @Parameter(title: "Project")
     var project: ProjectEntity
@@ -84,16 +84,16 @@ struct OpenProjectDashboardIntent: AppIntent {
 enum HutchShortcutScope: String, AppEnum {
     case all
 
-    static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Scope")
-    static var caseDisplayRepresentations: [HutchShortcutScope: DisplayRepresentation] = [
+    static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Scope")
+    static let caseDisplayRepresentations: [HutchShortcutScope: DisplayRepresentation] = [
         .all: "All"
     ]
 }
 
 struct OpenFailedBuildsIntent: AppIntent {
-    static var title: LocalizedStringResource = "Open Failed Builds"
-    static var description = IntentDescription("Opens Hutch to failed builds.")
-    static var openAppWhenRun = true
+    static let title: LocalizedStringResource = "Open Failed Builds"
+    static let description = IntentDescription("Opens Hutch to failed builds.")
+    static let openAppWhenRun = true
 
     @Parameter(title: "Scope", default: .all)
     var scope: HutchShortcutScope
@@ -108,9 +108,9 @@ struct OpenFailedBuildsIntent: AppIntent {
 }
 
 struct OpenAssignedTicketsIntent: AppIntent {
-    static var title: LocalizedStringResource = "Open Assigned Tickets"
-    static var description = IntentDescription("Opens Hutch to tickets assigned to you.")
-    static var openAppWhenRun = true
+    static let title: LocalizedStringResource = "Open Assigned Tickets"
+    static let description = IntentDescription("Opens Hutch to tickets assigned to you.")
+    static let openAppWhenRun = true
 
     @Parameter(title: "Scope", default: .all)
     var scope: HutchShortcutScope
@@ -125,9 +125,9 @@ struct OpenAssignedTicketsIntent: AppIntent {
 }
 
 struct SearchHutchIntent: AppIntent {
-    static var title: LocalizedStringResource = "Search Hutch"
-    static var description = IntentDescription("Opens Hutch lookup with a search query.")
-    static var openAppWhenRun = true
+    static let title: LocalizedStringResource = "Search Hutch"
+    static let description = IntentDescription("Opens Hutch lookup with a search query.")
+    static let openAppWhenRun = true
 
     @Parameter(title: "Query")
     var query: String
@@ -155,8 +155,8 @@ struct SearchHutchIntent: AppIntent {
 // MARK: - App Entities
 
 struct PinnedResourceEntity: AppEntity, Identifiable {
-    static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Pinned Resource")
-    static var defaultQuery = PinnedResourceQuery()
+    static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Pinned Resource")
+    static let defaultQuery = PinnedResourceQuery()
 
     let id: String
     let name: String
@@ -181,8 +181,8 @@ struct PinnedResourceQuery: EntityQuery {
 }
 
 struct ProjectEntity: AppEntity, Identifiable {
-    static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Project")
-    static var defaultQuery = ProjectEntityQuery()
+    static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Project")
+    static let defaultQuery = ProjectEntityQuery()
 
     let id: String
     let name: String
@@ -278,8 +278,8 @@ enum HutchIntentEntityStore {
 // MARK: - Existing Read-Only Summary Intents
 
 struct CheckSystemStatusIntent: AppIntent {
-    static var title: LocalizedStringResource = "Check SourceHut Status"
-    static var description = IntentDescription("Returns the current SourceHut system status.")
+    static let title: LocalizedStringResource = "Check SourceHut Status"
+    static let description = IntentDescription("Returns the current SourceHut system status.")
 
     @MainActor
     func perform() async throws -> some IntentResult & ReturnsValue<String> & ProvidesDialog {
@@ -302,8 +302,8 @@ struct CheckSystemStatusIntent: AppIntent {
 }
 
 struct CheckBuildsIntent: AppIntent {
-    static var title: LocalizedStringResource = "Check Hutch Builds"
-    static var description = IntentDescription("Returns a summary of your recent build status.")
+    static let title: LocalizedStringResource = "Check Hutch Builds"
+    static let description = IntentDescription("Returns a summary of your recent build status.")
 
     @MainActor
     func perform() async throws -> some IntentResult & ReturnsValue<String> & ProvidesDialog {
@@ -357,8 +357,8 @@ extension LookupType: AppEnum {
 // MARK: - Mutating Intents
 
 struct ClearRecentActivityIntent: AppIntent {
-    static var title: LocalizedStringResource = "Clear Recent Activity"
-    static var description = IntentDescription("Clears the Recent list on the Hutch Home tab.")
+    static let title: LocalizedStringResource = "Clear Recent Activity"
+    static let description = IntentDescription("Clears the Recent list on the Hutch Home tab.")
 
     @MainActor
     func perform() async throws -> some IntentResult & ProvidesDialog {
@@ -368,8 +368,8 @@ struct ClearRecentActivityIntent: AppIntent {
 }
 
 struct UnpinResourceIntent: AppIntent {
-    static var title: LocalizedStringResource = "Unpin Resource"
-    static var description = IntentDescription("Removes a pinned resource from the Hutch Home tab.")
+    static let title: LocalizedStringResource = "Unpin Resource"
+    static let description = IntentDescription("Removes a pinned resource from the Hutch Home tab.")
 
     @Parameter(title: "Pinned Resource")
     var pinnedResource: PinnedResourceEntity
diff --git a/Hutch/Extensions/Date+Relative.swift b/Hutch/Extensions/Date+Relative.swift
index f06ba59..b6c28ed 100644
--- a/Hutch/Extensions/Date+Relative.swift
+++ b/Hutch/Extensions/Date+Relative.swift
@@ -1,6 +1,6 @@
 import Foundation
 
-extension Date {
+nonisolated extension Date {
     /// A short relative description like "2h ago", "3d ago", or "Jan 5, 2025".
     var relativeDescription: String {
         let formatter = RelativeDateTimeFormatter()
diff --git a/Hutch/Extensions/DateFormatter+SRHT.swift b/Hutch/Extensions/DateFormatter+SRHT.swift
index c017b11..a06b58a 100644
--- a/Hutch/Extensions/DateFormatter+SRHT.swift
+++ b/Hutch/Extensions/DateFormatter+SRHT.swift
@@ -1,6 +1,6 @@
 import Foundation
 
-extension DateFormatter {
+nonisolated extension DateFormatter {
     /// Formatter for the sr.ht `Time` scalar: `%Y-%m-%dT%H:%M:%SZ` (UTC).
     static let srht: DateFormatter = {
         let formatter = DateFormatter()
diff --git a/Hutch/Extensions/Foundation+Sendable.swift b/Hutch/Extensions/Foundation+Sendable.swift
new file mode 100644
index 0000000..9a12aa6
--- /dev/null
+++ b/Hutch/Extensions/Foundation+Sendable.swift
@@ -0,0 +1,7 @@
+import Foundation
+
+// `UserDefaults` is documented as thread-safe but is not marked `Sendable` by
+// Foundation. Hutch passes it into account sessions and the various per-account
+// stores, all of which need to cross concurrency boundaries. The retroactive
+// unchecked conformance reflects the documented thread-safety.
+extension UserDefaults: @retroactive @unchecked Sendable {}
diff --git a/Hutch/Extensions/SRHTWebURL.swift b/Hutch/Extensions/SRHTWebURL.swift
index fc5a1d5..5715ce4 100644
--- a/Hutch/Extensions/SRHTWebURL.swift
+++ b/Hutch/Extensions/SRHTWebURL.swift
@@ -1,6 +1,6 @@
 import Foundation
 
-enum SRHTWebURL {
+nonisolated enum SRHTWebURL {
     static let chat = URL(string: "https://chat.sr.ht")!
     static let status = URL(string: "https://status.sr.ht")!
     /// RSS/Atom feed for status incidents (HTML snapshot uses `status`).
diff --git a/Hutch/Models/AccountEntry.swift b/Hutch/Models/AccountEntry.swift
index 551cd19..cfc2f62 100644
--- a/Hutch/Models/AccountEntry.swift
+++ b/Hutch/Models/AccountEntry.swift
@@ -1,7 +1,7 @@
 import Foundation
 
 /// A stored sr.ht account (token + resolved username).
-struct AccountEntry: Codable, Identifiable, Equatable, Sendable {
+nonisolated struct AccountEntry: Codable, Identifiable, Equatable, Sendable {
     let id: String
     var username: String
     var token: String
diff --git a/Hutch/Models/ArtifactInfo.swift b/Hutch/Models/ArtifactInfo.swift
index dbe9d96..6418ba8 100644
--- a/Hutch/Models/ArtifactInfo.swift
+++ b/Hutch/Models/ArtifactInfo.swift
@@ -1,7 +1,7 @@
 import Foundation
 
 /// An artifact with its parent reference name, used in the artifacts tab.
-struct ArtifactInfo: Codable, Sendable, Identifiable {
+nonisolated struct ArtifactInfo: Codable, Sendable, Identifiable {
     let id: Int
     let filename: String
     let checksum: String
@@ -9,13 +9,13 @@ struct ArtifactInfo: Codable, Sendable, Identifiable {
     let url: URL
 }
 
-struct ArtifactPage: Codable, Sendable {
+nonisolated struct ArtifactPage: Codable, Sendable {
     let results: [ArtifactInfo]
     let cursor: String?
 }
 
 /// A reference (tag) that has associated artifacts.
-struct ReferenceWithArtifacts: Codable, Sendable, Identifiable {
+nonisolated struct ReferenceWithArtifacts: Codable, Sendable, Identifiable {
     var id: String { name }
     let name: String
     let artifacts: [ArtifactInfo]
diff --git a/Hutch/Models/Builds.swift b/Hutch/Models/Builds.swift
index 3926888..48a9c91 100644
--- a/Hutch/Models/Builds.swift
+++ b/Hutch/Models/Builds.swift
@@ -3,7 +3,7 @@ import Foundation
 // MARK: - Enums
 
 /// Status of a build job.
-enum JobStatus: String, Codable, Sendable {
+nonisolated enum JobStatus: String, Codable, Sendable {
     case pending = "PENDING"
     case queued = "QUEUED"
     case running = "RUNNING"
@@ -30,7 +30,7 @@ enum JobStatus: String, Codable, Sendable {
 }
 
 /// Status of a single build task within a job.
-enum TaskStatus: String, Codable, Sendable {
+nonisolated enum TaskStatus: String, Codable, Sendable {
     case pending = "PENDING"
     case running = "RUNNING"
     case success = "SUCCESS"
@@ -41,7 +41,7 @@ enum TaskStatus: String, Codable, Sendable {
 // MARK: - Build Task
 
 /// A single task within a build job.
-struct BuildTask: Codable, Sendable, Identifiable, Equatable {
+nonisolated struct BuildTask: Codable, Sendable, Identifiable, Equatable {
     private(set) var ordinal: Int?
     let name: String
     let status: TaskStatus
@@ -74,7 +74,7 @@ struct BuildTask: Codable, Sendable, Identifiable, Equatable {
 // MARK: - Job Summary (for list view)
 
 /// Lightweight job model matching the fields returned by the jobs list query.
-struct JobSummary: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct JobSummary: Codable, Sendable, Identifiable, Hashable {
     let id: Int
     let created: Date
     let updated: Date
@@ -103,7 +103,7 @@ struct JobSummary: Codable, Sendable, Identifiable, Hashable {
 }
 
 /// Minimal task info for the list query.
-struct JobTaskSummary: Codable, Sendable, Hashable {
+nonisolated struct JobTaskSummary: Codable, Sendable, Hashable {
     let name: String
     let status: TaskStatus
 }
@@ -111,7 +111,7 @@ struct JobTaskSummary: Codable, Sendable, Hashable {
 // MARK: - Job Detail (for detail view)
 
 /// Full job model with all fields for the detail view.
-struct JobDetail: Codable, Sendable, Equatable {
+nonisolated struct JobDetail: Codable, Sendable, Equatable {
     let id: Int
     let created: Date
     let updated: Date
@@ -128,7 +128,7 @@ struct JobDetail: Codable, Sendable, Equatable {
 }
 
 /// A downloadable artifact emitted by a build job.
-struct BuildArtifact: Codable, Sendable, Identifiable, Equatable {
+nonisolated struct BuildArtifact: Codable, Sendable, Identifiable, Equatable {
     let id: Int
     let created: Date
     let path: String
@@ -145,14 +145,14 @@ struct BuildArtifact: Codable, Sendable, Identifiable, Equatable {
 }
 
 /// The log associated with a build job.
-struct BuildLog: Codable, Sendable, Equatable {
+nonisolated struct BuildLog: Codable, Sendable, Equatable {
     let fullURL: String
 }
 
 // MARK: - Job Group
 
 /// A group of related build jobs.
-struct JobGroup: Codable, Sendable, Identifiable {
+nonisolated struct JobGroup: Codable, Sendable, Identifiable {
     let id: Int
     let created: Date
     let note: String?
diff --git a/Hutch/Models/CommitDetail.swift b/Hutch/Models/CommitDetail.swift
index c95199d..962d2a4 100644
--- a/Hutch/Models/CommitDetail.swift
+++ b/Hutch/Models/CommitDetail.swift
@@ -1,7 +1,7 @@
 import Foundation
 
 /// Full commit detail returned by the revparse_single query.
-struct CommitDetail: Codable, Sendable, Identifiable {
+nonisolated struct CommitDetail: Codable, Sendable, Identifiable {
     let id: String
     let shortId: String
     let author: CommitAuthor
@@ -26,39 +26,39 @@ struct CommitDetail: Codable, Sendable, Identifiable {
     }
 }
 
-struct CommitTrailer: Codable, Sendable, Identifiable {
+nonisolated struct CommitTrailer: Codable, Sendable, Identifiable {
     var id: String { "\(name):\(value)" }
     let name: String
     let value: String
 }
 
-struct ParentCommit: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct ParentCommit: Codable, Sendable, Identifiable, Hashable {
     let id: String
     let shortId: String
     let author: ParentAuthor
 }
 
-struct ParentAuthor: Codable, Sendable, Hashable {
+nonisolated struct ParentAuthor: Codable, Sendable, Hashable {
     let name: String
 }
 
-struct CommitTree: Codable, Sendable {
+nonisolated struct CommitTree: Codable, Sendable {
     let entries: CommitTreeEntries
 }
 
-struct CommitTreeEntries: Codable, Sendable {
+nonisolated struct CommitTreeEntries: Codable, Sendable {
     let results: [CommitTreeEntry]
     let cursor: String?
 }
 
-struct CommitTreeEntry: Codable, Sendable, Identifiable {
+nonisolated struct CommitTreeEntry: Codable, Sendable, Identifiable {
     let id: String
     let name: String
     let mode: Int
     let object: CommitTreeObject?
 }
 
-struct CommitTreeObject: Codable, Sendable {
+nonisolated struct CommitTreeObject: Codable, Sendable {
     let type: String?
     let id: String?
     let shortId: String?
diff --git a/Hutch/Models/CommitSummary.swift b/Hutch/Models/CommitSummary.swift
index 88c3e7d..99ed233 100644
--- a/Hutch/Models/CommitSummary.swift
+++ b/Hutch/Models/CommitSummary.swift
@@ -2,7 +2,7 @@ import Foundation
 
 /// Lightweight commit model for list views. Matches the subset of fields
 /// returned by the repository log query.
-struct CommitSummary: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct CommitSummary: Codable, Sendable, Identifiable, Hashable {
     let id: String
     let shortId: String
     let author: CommitAuthor
@@ -15,7 +15,7 @@ struct CommitSummary: Codable, Sendable, Identifiable, Hashable {
 }
 
 /// A compact author representation used in commit list responses.
-struct CommitAuthor: Codable, Sendable, Hashable {
+nonisolated struct CommitAuthor: Codable, Sendable, Hashable {
     let name: String
     let email: String?
     let time: Date
diff --git a/Hutch/Models/ContributionCalendar.swift b/Hutch/Models/ContributionCalendar.swift
index a429fa2..cf87ca7 100644
--- a/Hutch/Models/ContributionCalendar.swift
+++ b/Hutch/Models/ContributionCalendar.swift
@@ -1,6 +1,6 @@
 import Foundation
 
-struct ContributionCalendarResponse: Decodable, Sendable, Hashable {
+nonisolated struct ContributionCalendarResponse: Decodable, Sendable, Hashable {
     let actor: String
     let from: Date
     let to: Date
@@ -9,7 +9,7 @@ struct ContributionCalendarResponse: Decodable, Sendable, Hashable {
     let indexingState: ContributionIndexingState
     let days: [ContributionDay]
 
-    enum CodingKeys: String, CodingKey {
+    nonisolated enum CodingKeys: String, CodingKey {
         case actor
         case from
         case to
@@ -59,14 +59,14 @@ struct ContributionCalendarResponse: Decodable, Sendable, Hashable {
     }
 }
 
-struct ContributionDay: Decodable, Sendable, Hashable, Identifiable {
+nonisolated struct ContributionDay: Decodable, Sendable, Hashable, Identifiable {
     var id: Date { date }
 
     let date: Date
     let count: Int
     let score: Double
 
-    enum CodingKeys: String, CodingKey {
+    nonisolated enum CodingKeys: String, CodingKey {
         case date
         case count
         case score
@@ -90,7 +90,7 @@ struct ContributionDay: Decodable, Sendable, Hashable, Identifiable {
     }
 }
 
-struct ContributionStatsResponse: Decodable, Sendable, Hashable {
+nonisolated struct ContributionStatsResponse: Decodable, Sendable, Hashable {
     let actor: String
     let from: Date
     let to: Date
@@ -103,7 +103,7 @@ struct ContributionStatsResponse: Decodable, Sendable, Hashable {
     let longestStreak: Int
     let currentStreak: Int
 
-    enum CodingKeys: String, CodingKey {
+    nonisolated enum CodingKeys: String, CodingKey {
         case actor
         case from
         case to
@@ -117,7 +117,7 @@ struct ContributionStatsResponse: Decodable, Sendable, Hashable {
         case currentStreak = "current_streak"
     }
 
-    struct StatsWindow: Sendable, Hashable {
+    nonisolated struct StatsWindow: Sendable, Hashable {
         let actor: String
         let from: Date
         let to: Date
@@ -126,7 +126,7 @@ struct ContributionStatsResponse: Decodable, Sendable, Hashable {
         let indexingState: ContributionIndexingState
     }
 
-    struct StatsTotals: Sendable, Hashable {
+    nonisolated struct StatsTotals: Sendable, Hashable {
         let totalEvents: Int
         let totalScore: Double
         let activeDays: Int
@@ -181,13 +181,13 @@ struct ContributionStatsResponse: Decodable, Sendable, Hashable {
     }
 }
 
-enum ContributionIndexingState: String, Codable, Sendable, Hashable {
+nonisolated enum ContributionIndexingState: String, Codable, Sendable, Hashable {
     case pending
     case indexed
     case error
 }
 
-enum ContributionIntensity: Int, Sendable, CaseIterable {
+nonisolated enum ContributionIntensity: Int, Sendable, CaseIterable {
     case empty = 0
     case level1 = 1
     case level2 = 2
@@ -210,12 +210,12 @@ enum ContributionIntensity: Int, Sendable, CaseIterable {
     }
 }
 
-struct ContributionWeek: Sendable, Hashable {
+nonisolated struct ContributionWeek: Sendable, Hashable {
     let startDate: Date
     let days: [ContributionDay]
 }
 
-enum ContributionCalendarLayout {
+nonisolated enum ContributionCalendarLayout {
     static func weekColumns(
         from days: [ContributionDay],
         calendar: Calendar = .contributionCalendar
@@ -241,7 +241,7 @@ enum ContributionCalendarLayout {
     }
 }
 
-enum ContributionDateParser {
+nonisolated enum ContributionDateParser {
     static func parse(_ rawValue: String) -> Date? {
         let parts = rawValue.split(separator: "-", omittingEmptySubsequences: false)
         guard
@@ -348,7 +348,7 @@ enum ContributionDateParser {
     }
 }
 
-extension Calendar {
+nonisolated extension Calendar {
     static var contributionCalendar: Calendar {
         var calendar = Calendar(identifier: .gregorian)
         calendar.firstWeekday = 1
diff --git a/Hutch/Models/Git.swift b/Hutch/Models/Git.swift
index 26a5e2d..ae74f19 100644
--- a/Hutch/Models/Git.swift
+++ b/Hutch/Models/Git.swift
@@ -3,14 +3,14 @@ import Foundation
 // MARK: - Enums
 
 /// Repository visibility level.
-enum Visibility: String, Codable, Sendable {
+nonisolated enum Visibility: String, Codable, Sendable {
     case publicVisibility = "PUBLIC"
     case unlisted = "UNLISTED"
     case privateVisibility = "PRIVATE"
 }
 
 /// Repository access mode.
-enum AccessMode: String, Codable, Sendable, CaseIterable {
+nonisolated enum AccessMode: String, Codable, Sendable, CaseIterable {
     case ro = "RO"
     case rw = "RW"
 }
@@ -19,14 +19,14 @@ enum AccessMode: String, Codable, Sendable, CaseIterable {
 
 /// The `Entity` GraphQL interface from git.sr.ht. Represents the owner of a
 /// resource (typically a user).
-struct Entity: Codable, Sendable, Hashable {
+nonisolated struct Entity: Codable, Sendable, Hashable {
     let canonicalName: String
 }
 
 // MARK: - Repository
 
 /// A git repository from git.sr.ht.
-struct Repository: Codable, Sendable, Identifiable {
+nonisolated struct Repository: Codable, Sendable, Identifiable {
     let id: Int
     let created: Date
     let updated: Date
@@ -37,7 +37,7 @@ struct Repository: Codable, Sendable, Identifiable {
     let accessMode: AccessMode
     let owner: Entity
 
-    enum CodingKeys: String, CodingKey {
+    nonisolated enum CodingKeys: String, CodingKey {
         case id, created, updated, name, description, visibility, readme
         case accessMode = "access"
         case owner
@@ -47,7 +47,7 @@ struct Repository: Codable, Sendable, Identifiable {
 // MARK: - Signature
 
 /// A Git commit/tag signature (author or committer).
-struct Signature: Codable, Sendable {
+nonisolated struct Signature: Codable, Sendable {
     let name: String
     let email: String
     let time: Date
@@ -56,7 +56,7 @@ struct Signature: Codable, Sendable {
 // MARK: - Trailer
 
 /// A Git commit trailer (e.g. "Signed-off-by", "Co-authored-by").
-struct Trailer: Codable, Sendable {
+nonisolated struct Trailer: Codable, Sendable {
     let name: String
     let value: String
 }
@@ -64,7 +64,7 @@ struct Trailer: Codable, Sendable {
 // MARK: - Commit
 
 /// A Git commit from git.sr.ht.
-struct Commit: Codable, Sendable, Identifiable {
+nonisolated struct Commit: Codable, Sendable, Identifiable {
     let id: String
     let shortId: String
     let author: Signature
@@ -77,13 +77,13 @@ struct Commit: Codable, Sendable, Identifiable {
 // MARK: - Reference
 
 /// A Git reference (branch or tag name).
-struct Reference: Codable, Sendable, Hashable {
+nonisolated struct Reference: Codable, Sendable, Hashable {
     let name: String
     let target: String?
 }
 
 /// A Git reference enriched with the date of its tip commit or tag, for display in the Refs tab.
-struct ReferenceDetail: Sendable, Hashable {
+nonisolated struct ReferenceDetail: Sendable, Hashable {
     let name: String
     let target: String?
     let date: Date?
@@ -92,7 +92,7 @@ struct ReferenceDetail: Sendable, Hashable {
 // MARK: - TreeEntry
 
 /// An entry in a Git tree (file or directory).
-struct TreeEntry: Codable, Sendable, Identifiable {
+nonisolated struct TreeEntry: Codable, Sendable, Identifiable {
     let id: String
     let name: String
     let mode: Int?
@@ -104,41 +104,41 @@ struct TreeEntry: Codable, Sendable, Identifiable {
 /// Both TextBlob and BinaryBlob have type == "BLOB"; they are
 /// distinguished by the presence of the "text" key (TextBlob) vs
 /// the "content" key (BinaryBlob).
-enum GitObject: Sendable {
+nonisolated enum GitObject: Sendable {
     case tree(GitTree)
     case textBlob(GitTextBlob)
     case binaryBlob(GitBinaryBlob)
     case unknown
 }
 
-struct GitTree: Codable, Sendable {
+nonisolated struct GitTree: Codable, Sendable {
     let id: String?
     let shortId: String?
     let entries: GitTreeEntryPage?
 }
 
-struct GitTextBlob: Codable, Sendable {
+nonisolated struct GitTextBlob: Codable, Sendable {
     let id: String?
     let shortId: String?
     let text: String?
     let size: Int?
 }
 
-struct GitBinaryBlob: Codable, Sendable {
+nonisolated struct GitBinaryBlob: Codable, Sendable {
     let id: String?
     let shortId: String?
     let size: Int?
     let content: String?
 }
 
-struct GitTreeEntryPage: Codable, Sendable {
+nonisolated struct GitTreeEntryPage: Codable, Sendable {
     let results: [TreeEntry]
     let cursor: String?
 }
 
 // MARK: - GitObject Codable
 
-extension GitObject: Codable {
+nonisolated extension GitObject: Codable {
     private enum CodingKeys: String, CodingKey {
         case type, id, shortId, entries, text, size, content
         case typename = "__typename"
@@ -211,7 +211,7 @@ extension GitObject: Codable {
 }
 
 /// Convenience helpers for checking object type.
-extension GitObject {
+nonisolated extension GitObject {
     var isTree: Bool {
         if case .tree = self { return true }
         return false
@@ -230,7 +230,7 @@ extension GitObject {
 // MARK: - Tag
 
 /// An annotated Git tag from git.sr.ht.
-struct Tag: Codable, Sendable, Identifiable {
+nonisolated struct Tag: Codable, Sendable, Identifiable {
     let id: String
     let shortId: String
     let name: String
@@ -241,7 +241,7 @@ struct Tag: Codable, Sendable, Identifiable {
 // MARK: - Artifact
 
 /// A release artifact attached to a Git tag.
-struct Artifact: Codable, Sendable, Identifiable {
+nonisolated struct Artifact: Codable, Sendable, Identifiable {
     let id: Int
     let created: Date
     let filename: String
diff --git a/Hutch/Models/Inbox.swift b/Hutch/Models/Inbox.swift
index c8e41bb..dfed719 100644
--- a/Hutch/Models/Inbox.swift
+++ b/Hutch/Models/Inbox.swift
@@ -1,6 +1,6 @@
 import Foundation
 
-struct InboxThreadSummary: Identifiable, Hashable, Sendable {
+nonisolated struct InboxThreadSummary: Identifiable, Hashable, Sendable {
     let rootEmailID: Int
     let rootMessageID: String
     let threadRootEmailIDs: [Int]
@@ -76,7 +76,7 @@ struct InboxThreadSummary: Identifiable, Hashable, Sendable {
     }
 }
 
-struct InboxMessage: Identifiable, Hashable, Sendable {
+nonisolated struct InboxMessage: Identifiable, Hashable, Sendable {
     let id: Int
     let author: Entity
     let date: Date
@@ -89,12 +89,12 @@ struct InboxMessage: Identifiable, Hashable, Sendable {
     let rawMessageURL: URL?
 }
 
-enum InboxMessageContentBlock: Hashable, Sendable {
+nonisolated enum InboxMessageContentBlock: Hashable, Sendable {
     case plainText(String)
     case diff(String)
 }
 
-struct InboxThreadDetail: Sendable {
+nonisolated struct InboxThreadDetail: Sendable {
     let id: String
     let rootEmailID: Int
     let rootMessageID: String
@@ -114,7 +114,7 @@ struct InboxThreadDetail: Sendable {
     }
 }
 
-extension InboxThreadDetail {
+nonisolated extension InboxThreadDetail {
     var replyRecipient: String {
         "\(listOwner.canonicalName)/\(listName)@lists.sr.ht"
     }
@@ -144,7 +144,7 @@ extension InboxThreadDetail {
     }
 }
 
-struct MailComposeDraft: Sendable {
+nonisolated struct MailComposeDraft: Sendable {
     let recipients: [String]
     let ccRecipients: [String]
     let subject: String
@@ -155,20 +155,20 @@ struct MailComposeDraft: Sendable {
     }
 }
 
-extension MailComposeDraft: Identifiable {}
+nonisolated extension MailComposeDraft: Identifiable {}
 
-struct InboxMailingListReference: Decodable, Sendable, Hashable, Identifiable {
+nonisolated struct InboxMailingListReference: Decodable, Sendable, Hashable, Identifiable {
     let id: Int
     let rid: String
     let name: String
     let owner: Entity
 }
 
-struct InboxPatchPreview: Decodable, Sendable, Hashable {
+nonisolated struct InboxPatchPreview: Decodable, Sendable, Hashable {
     let subject: String?
 }
 
-enum InboxReadStateStore {
+nonisolated enum InboxReadStateStore {
     private static let key = "InboxThreadLastViewed"
     private static let baselineKey = "InboxUnreadBaseline"
 
diff --git a/Hutch/Models/Meta.swift b/Hutch/Models/Meta.swift
index f52fc76..e0a09de 100644
--- a/Hutch/Models/Meta.swift
+++ b/Hutch/Models/Meta.swift
@@ -3,7 +3,7 @@ import Foundation
 // MARK: - User Profile (full)
 
 /// Extended user profile from meta.sr.ht with all fields from the `me` query.
-struct UserProfile: Codable, Sendable {
+nonisolated struct UserProfile: Codable, Sendable {
     let username: String
     let canonicalName: String
     let email: String
@@ -20,7 +20,7 @@ struct UserProfile: Codable, Sendable {
 
 // MARK: - SSH Key
 
-struct SSHKey: Codable, Sendable, Identifiable {
+nonisolated struct SSHKey: Codable, Sendable, Identifiable {
     let id: Int
     let comment: String?
     let created: Date
@@ -34,27 +34,27 @@ struct SSHKey: Codable, Sendable, Identifiable {
     }
 }
 
-struct SSHKeyPage: Codable, Sendable {
+nonisolated struct SSHKeyPage: Codable, Sendable {
     let results: [SSHKey]
     let cursor: String?
 }
 
 // MARK: - PGP Key
 
-struct PGPKey: Codable, Sendable, Identifiable {
+nonisolated struct PGPKey: Codable, Sendable, Identifiable {
     let id: Int
     let fingerprint: String
     let created: Date
 }
 
-struct PGPKeyPage: Codable, Sendable {
+nonisolated struct PGPKeyPage: Codable, Sendable {
     let results: [PGPKey]
     let cursor: String?
 }
 
 // MARK: - Subscription
 
-struct Subscription: Codable, Sendable {
+nonisolated struct Subscription: Codable, Sendable {
     let status: String?
     let autorenew: Bool?
     let interval: String?
@@ -62,7 +62,7 @@ struct Subscription: Codable, Sendable {
 
 // MARK: - Personal Access Token
 
-struct PersonalAccessToken: Codable, Sendable, Identifiable {
+nonisolated struct PersonalAccessToken: Codable, Sendable, Identifiable {
     let id: Int
     let issued: Date
     let expires: Date?
@@ -72,7 +72,7 @@ struct PersonalAccessToken: Codable, Sendable, Identifiable {
 
 /// One entry in meta.sr.ht's audit log: a security-relevant action on the
 /// account, with the address it came from.
-struct AuditLogEntry: Codable, Sendable, Identifiable {
+nonisolated struct AuditLogEntry: Codable, Sendable, Identifiable {
     let id: Int
     let created: Date
     let ipAddress: String
diff --git a/Hutch/Models/Paste.swift b/Hutch/Models/Paste.swift
index b4c8f18..a8bf926 100644
--- a/Hutch/Models/Paste.swift
+++ b/Hutch/Models/Paste.swift
@@ -1,6 +1,6 @@
 import Foundation
 
-struct PasteFile: Codable, Sendable, Hashable, Identifiable {
+nonisolated struct PasteFile: Codable, Sendable, Hashable, Identifiable {
     let filename: String?
     let hash: String
     let contents: URL?
@@ -8,7 +8,7 @@ struct PasteFile: Codable, Sendable, Hashable, Identifiable {
     var id: String { hash }
 }
 
-struct Paste: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct Paste: Codable, Sendable, Identifiable, Hashable {
     let id: String
     let created: Date
     let visibility: Visibility
@@ -16,7 +16,7 @@ struct Paste: Codable, Sendable, Identifiable, Hashable {
     let user: Entity
 }
 
-struct PasteUploadDraft: Identifiable, Equatable, Sendable {
+nonisolated struct PasteUploadDraft: Identifiable, Equatable, Sendable {
     let id: UUID
     var filename: String
     var contents: String
diff --git a/Hutch/Models/Patchset.swift b/Hutch/Models/Patchset.swift
index 330bbbc..cc10b35 100644
--- a/Hutch/Models/Patchset.swift
+++ b/Hutch/Models/Patchset.swift
@@ -1,7 +1,7 @@
 import Foundation
 
 /// Review state of a patchset on lists.sr.ht.
-enum PatchsetStatus: String, Codable, Sendable, CaseIterable {
+nonisolated enum PatchsetStatus: String, Codable, Sendable, CaseIterable {
     case unknown = "UNKNOWN"
     case proposed = "PROPOSED"
     case needsRevision = "NEEDS_REVISION"
@@ -55,7 +55,7 @@ enum PatchsetStatus: String, Codable, Sendable, CaseIterable {
 /// A patchset as it appears in a mailing list listing, derived from the thread's
 /// root email rather than a dedicated patchsets query — `MailingList` exposes no
 /// such field.
-struct PatchsetSummary: Identifiable, Hashable, Sendable {
+nonisolated struct PatchsetSummary: Identifiable, Hashable, Sendable {
     let id: Int
     let subject: String
     let version: Int
@@ -70,7 +70,7 @@ struct PatchsetSummary: Identifiable, Hashable, Sendable {
 }
 
 /// One email within a patchset: either the cover letter or a single patch.
-struct PatchsetEmail: Identifiable, Hashable, Sendable {
+nonisolated struct PatchsetEmail: Identifiable, Hashable, Sendable {
     let id: Int
     let subject: String
     let date: Date?
@@ -88,13 +88,13 @@ struct PatchsetEmail: Identifiable, Hashable, Sendable {
 }
 
 /// A build or check reported against a patchset.
-struct PatchsetToolResult: Identifiable, Hashable, Sendable {
+nonisolated struct PatchsetToolResult: Identifiable, Hashable, Sendable {
     let id: Int
     let icon: PatchsetToolIcon
     let details: String
 }
 
-enum PatchsetToolIcon: String, Codable, Sendable {
+nonisolated enum PatchsetToolIcon: String, Codable, Sendable {
     case pending = "PENDING"
     case waiting = "WAITING"
     case success = "SUCCESS"
@@ -112,7 +112,7 @@ enum PatchsetToolIcon: String, Codable, Sendable {
 }
 
 /// A patchset with its cover letter, patches, and review context.
-struct PatchsetDetail: Sendable {
+nonisolated struct PatchsetDetail: Sendable {
     let id: Int
     let created: Date
     let updated: Date
diff --git a/Hutch/Models/Project.swift b/Hutch/Models/Project.swift
index 8f5aa68..bd1a688 100644
--- a/Hutch/Models/Project.swift
+++ b/Hutch/Models/Project.swift
@@ -1,7 +1,7 @@
 import Foundation
 
-struct Project: Identifiable, Hashable, Sendable {
-    struct MailingList: Identifiable, Hashable, Sendable {
+nonisolated struct Project: Identifiable, Hashable, Sendable {
+    nonisolated struct MailingList: Identifiable, Hashable, Sendable {
         let id: String
         let name: String
         let description: String?
@@ -22,8 +22,8 @@ struct Project: Identifiable, Hashable, Sendable {
         }
     }
 
-    struct SourceRepo: Identifiable, Hashable, Sendable {
-        enum RepoType: String, Decodable, Sendable {
+    nonisolated struct SourceRepo: Identifiable, Hashable, Sendable {
+        nonisolated enum RepoType: String, Decodable, Sendable {
             case git = "GIT"
             case hg = "HG"
 
@@ -47,7 +47,7 @@ struct Project: Identifiable, Hashable, Sendable {
         }
     }
 
-    struct Tracker: Identifiable, Hashable, Sendable {
+    nonisolated struct Tracker: Identifiable, Hashable, Sendable {
         let id: String
         let name: String
         let description: String?
@@ -72,7 +72,7 @@ struct Project: Identifiable, Hashable, Sendable {
     let isFullyLoaded: Bool
 
     /// Identity and display fields for a project.
-    struct Metadata: Sendable, Hashable {
+    nonisolated struct Metadata: Sendable, Hashable {
         let id: String
         let name: String
         let description: String?
@@ -82,7 +82,7 @@ struct Project: Identifiable, Hashable, Sendable {
         let updated: Date
     }
 
-    struct Resources: Sendable, Hashable {
+    nonisolated struct Resources: Sendable, Hashable {
         let mailingLists: [MailingList]
         let sources: [SourceRepo]
         let trackers: [Tracker]
@@ -171,7 +171,7 @@ struct Project: Identifiable, Hashable, Sendable {
     }
 }
 
-extension Project.MailingList {
+nonisolated extension Project.MailingList {
     var displayName: String {
         Project.normalizedText(name) ?? "Untitled Mailing List"
     }
@@ -185,7 +185,7 @@ extension Project.MailingList {
     }
 }
 
-extension Project.SourceRepo {
+nonisolated extension Project.SourceRepo {
     var displayName: String {
         Project.normalizedText(name) ?? "Untitled Repository"
     }
@@ -203,7 +203,7 @@ extension Project.SourceRepo {
     }
 }
 
-extension Project.Tracker {
+nonisolated extension Project.Tracker {
     var displayName: String {
         Project.normalizedText(name) ?? "Untitled Tracker"
     }
@@ -221,13 +221,13 @@ extension Project.Tracker {
     }
 }
 
-extension String {
+nonisolated extension String {
     var srhtUsername: String {
         hasPrefix("~") ? String(dropFirst()) : self
     }
 }
 
-extension Visibility {
+nonisolated extension Visibility {
     var displayName: String {
         switch self {
         case .publicVisibility:
diff --git a/Hutch/Models/RepositoryACL.swift b/Hutch/Models/RepositoryACL.swift
index e5f5eb1..e5d93f6 100644
--- a/Hutch/Models/RepositoryACL.swift
+++ b/Hutch/Models/RepositoryACL.swift
@@ -1,12 +1,12 @@
 import Foundation
 
-struct RepositoryACLEntry: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct RepositoryACLEntry: Codable, Sendable, Identifiable, Hashable {
     let id: Int
     let mode: AccessMode
     let entity: Entity
 }
 
-extension AccessMode {
+nonisolated extension AccessMode {
     var shortLabel: String { rawValue }
 
     var displayName: String {
diff --git a/Hutch/Models/RepositorySummary.swift b/Hutch/Models/RepositorySummary.swift
index b41589b..40ec7a4 100644
--- a/Hutch/Models/RepositorySummary.swift
+++ b/Hutch/Models/RepositorySummary.swift
@@ -3,7 +3,7 @@ import Foundation
 /// Lightweight repository model matching the fields returned by the
 /// repositories list query. Avoids optionalizing all fields on the full
 /// `Repository` model.
-struct RepositorySummary: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct RepositorySummary: Codable, Sendable, Identifiable, Hashable {
     let id: Int
     /// GraphQL resource identifier used by `repository(rid:)` queries.
     let rid: String
@@ -15,13 +15,13 @@ struct RepositorySummary: Codable, Sendable, Identifiable, Hashable {
     let owner: Entity
     let head: Reference?
 
-    enum CodingKeys: String, CodingKey {
+    nonisolated enum CodingKeys: String, CodingKey {
         case id, rid, service, name, description, visibility, updated, owner
         case head = "HEAD"
     }
 
     /// Grouped initializer fields (single parameter keeps APIs explicit without exceeding parameter-count limits).
-    struct Fields: Sendable, Hashable {
+    nonisolated struct Fields: Sendable, Hashable {
         let id: Int
         let rid: String
         let service: SRHTService
@@ -59,7 +59,7 @@ struct RepositorySummary: Codable, Sendable, Identifiable, Hashable {
     }
 }
 
-extension RepositorySummary {
+nonisolated extension RepositorySummary {
     var defaultBranchName: String? {
         head?.name.replacingOccurrences(of: "refs/heads/", with: "")
     }
diff --git a/Hutch/Models/SystemStatusModels.swift b/Hutch/Models/SystemStatusModels.swift
index 9a664b0..ddd659c 100644
--- a/Hutch/Models/SystemStatusModels.swift
+++ b/Hutch/Models/SystemStatusModels.swift
@@ -1,6 +1,6 @@
 import Foundation
 
-enum StatusLevel: String, Codable, Sendable {
+nonisolated enum StatusLevel: String, Codable, Sendable {
     case operational
     case degraded
     case majorOutage
@@ -32,7 +32,7 @@ enum StatusLevel: String, Codable, Sendable {
     }
 }
 
-struct StatusServiceState: Identifiable, Hashable, Codable, Sendable {
+nonisolated struct StatusServiceState: Identifiable, Hashable, Codable, Sendable {
     let id: String
     let name: String
     let slug: String?
@@ -40,7 +40,7 @@ struct StatusServiceState: Identifiable, Hashable, Codable, Sendable {
     let description: String?
 }
 
-struct StatusIncident: Identifiable, Hashable, Codable, Sendable {
+nonisolated struct StatusIncident: Identifiable, Hashable, Codable, Sendable {
     let id: String
     let title: String
     let summary: String?
@@ -50,7 +50,7 @@ struct StatusIncident: Identifiable, Hashable, Codable, Sendable {
     let isActive: Bool?
 }
 
-struct SystemStatusSnapshot: Hashable, Codable, Sendable {
+nonisolated struct SystemStatusSnapshot: Hashable, Codable, Sendable {
     let services: [StatusServiceState]
     let activeIncidents: [StatusIncident]
     let lastUpdated: Date
@@ -78,7 +78,7 @@ struct SystemStatusSnapshot: Hashable, Codable, Sendable {
     }
 }
 
-struct SystemStatusPageData: Sendable {
+nonisolated struct SystemStatusPageData: Sendable {
     let snapshot: SystemStatusSnapshot
     let recentIncidents: [StatusIncident]
 }
diff --git a/Hutch/Models/TicketBulkAction.swift b/Hutch/Models/TicketBulkAction.swift
index dcf7676..ea9cc01 100644
--- a/Hutch/Models/TicketBulkAction.swift
+++ b/Hutch/Models/TicketBulkAction.swift
@@ -1,6 +1,6 @@
 import Foundation
 
-enum TicketBulkActionKind: String, Sendable {
+nonisolated enum TicketBulkActionKind: String, Sendable {
     case close
     case assign
 
@@ -23,7 +23,7 @@ enum TicketBulkActionKind: String, Sendable {
     }
 }
 
-struct TicketBulkActionResult: Identifiable, Sendable {
+nonisolated struct TicketBulkActionResult: Identifiable, Sendable {
     let id = UUID()
     let action: TicketBulkActionKind
     let totalCount: Int
@@ -82,7 +82,7 @@ struct TicketBulkActionResult: Identifiable, Sendable {
     }
 }
 
-struct TicketBulkActionFailure: Sendable {
+nonisolated struct TicketBulkActionFailure: Sendable {
     let ticketID: Int
     let message: String
 }
diff --git a/Hutch/Models/Todo.swift b/Hutch/Models/Todo.swift
index 129d4bf..d2ae889 100644
--- a/Hutch/Models/Todo.swift
+++ b/Hutch/Models/Todo.swift
@@ -3,7 +3,7 @@ import Foundation
 // MARK: - Enums
 
 /// Status of a ticket.
-enum TicketStatus: String, Codable, Sendable, CaseIterable {
+nonisolated enum TicketStatus: String, Codable, Sendable, CaseIterable {
     case reported = "REPORTED"
     case confirmed = "CONFIRMED"
     case inProgress = "IN_PROGRESS"
@@ -30,7 +30,7 @@ enum TicketStatus: String, Codable, Sendable, CaseIterable {
 }
 
 /// Resolution of a ticket.
-enum TicketResolution: String, Codable, Sendable {
+nonisolated enum TicketResolution: String, Codable, Sendable {
     case unresolved = "UNRESOLVED"
     case fixed = "FIXED"
     case implemented = "IMPLEMENTED"
@@ -64,7 +64,7 @@ enum TicketResolution: String, Codable, Sendable {
 }
 
 /// Authenticity of a ticket or comment.
-enum Authenticity: String, Codable, Sendable {
+nonisolated enum Authenticity: String, Codable, Sendable {
     case authentic = "AUTHENTIC"
     case tampered = "TAMPERED"
     case unauthenticated = "UNAUTHENTICATED"
@@ -74,7 +74,7 @@ enum Authenticity: String, Codable, Sendable {
 
 /// A label that can be applied to tickets.
 /// Named `TicketLabel` to avoid collision with `SwiftUI.Label`.
-struct TicketLabel: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct TicketLabel: Codable, Sendable, Identifiable, Hashable {
     let id: Int
     let name: String
     let backgroundColor: String
@@ -83,7 +83,7 @@ struct TicketLabel: Codable, Sendable, Identifiable, Hashable {
 
 // MARK: - Tracker ACL
 
-struct TrackerACLPermissions: Codable, Sendable, Hashable {
+nonisolated struct TrackerACLPermissions: Codable, Sendable, Hashable {
     let browse: Bool
     let submit: Bool
     let comment: Bool
@@ -91,7 +91,7 @@ struct TrackerACLPermissions: Codable, Sendable, Hashable {
     let triage: Bool
 }
 
-struct TrackerACL: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct TrackerACL: Codable, Sendable, Identifiable, Hashable {
     let id: Int
     let created: Date
     let entity: Entity
@@ -112,7 +112,7 @@ struct TrackerACL: Codable, Sendable, Identifiable, Hashable {
     }
 }
 
-struct DefaultTrackerACL: Codable, Sendable, Hashable {
+nonisolated struct DefaultTrackerACL: Codable, Sendable, Hashable {
     let browse: Bool
     let submit: Bool
     let comment: Bool
@@ -133,7 +133,7 @@ struct DefaultTrackerACL: Codable, Sendable, Hashable {
 // MARK: - Tracker
 
 /// A bug tracker from todo.sr.ht.
-struct Tracker: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct Tracker: Codable, Sendable, Identifiable, Hashable {
     let id: Int
     let created: Date
     let updated: Date
@@ -154,7 +154,7 @@ struct Tracker: Codable, Sendable, Identifiable, Hashable {
 // MARK: - Tracker Summary (for list view)
 
 /// Lightweight tracker model matching the fields returned by the trackers list query.
-struct TrackerSummary: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct TrackerSummary: Codable, Sendable, Identifiable, Hashable {
     let id: Int
     /// GraphQL resource identifier used by `tracker(id:)` queries.
     let rid: String
@@ -168,7 +168,7 @@ struct TrackerSummary: Codable, Sendable, Identifiable, Hashable {
 // MARK: - Ticket Summary (for list view)
 
 /// Lightweight ticket model for the list query.
-struct TicketSummary: Codable, Sendable, Identifiable, Hashable {
+nonisolated struct TicketSummary: Codable, Sendable, Identifiable, Hashable {
     let id: Int
     let title: String
     let status: TicketStatus
@@ -190,7 +190,7 @@ struct TicketSummary: Codable, Sendable, Identifiable, Hashable {
 // MARK: - Ticket Detail
 
 /// Full ticket model for the detail view.
-struct TicketDetail: Codable, Sendable {
+nonisolated struct TicketDetail: Codable, Sendable {
     let id: Int
     let created: Date
     let updated: Date
@@ -207,7 +207,7 @@ struct TicketDetail: Codable, Sendable {
 // MARK: - Event
 
 /// A timeline event on a ticket. Each event contains one or more changes.
-struct TicketEvent: Codable, Sendable, Identifiable {
+nonisolated struct TicketEvent: Codable, Sendable, Identifiable {
     let id: Int
     let created: Date
     var changes: [EventChange]
@@ -215,7 +215,7 @@ struct TicketEvent: Codable, Sendable, Identifiable {
 
 /// A single change within an event, decoded from the polymorphic EventDetail
 /// interface using inline fragments.
-struct EventChange: Codable, Sendable, Identifiable {
+nonisolated struct EventChange: Codable, Sendable, Identifiable {
     let id: UUID
 
     let eventType: String
@@ -272,12 +272,12 @@ struct EventChange: Codable, Sendable, Identifiable {
 
 /// Decoded from the `mentioned` field which may be a Ticket (with `id`) or
 /// an Entity (with `canonicalName`) depending on the event type.
-struct MentionTarget: Codable, Sendable {
+nonisolated struct MentionTarget: Codable, Sendable {
     let id: Int?
     let canonicalName: String?
 }
 
 /// Label info as returned within a LabelUpdate event change.
-struct EventLabel: Codable, Sendable {
+nonisolated struct EventLabel: Codable, Sendable {
     let name: String
 }
diff --git a/Hutch/Models/User.swift b/Hutch/Models/User.swift
index 4ab7315..10576ad 100644
--- a/Hutch/Models/User.swift
+++ b/Hutch/Models/User.swift
@@ -1,7 +1,7 @@
 import Foundation
 
 /// A Sourcehut user from meta.sr.ht.
-struct User: Decodable, Sendable, Hashable {
+nonisolated struct User: Decodable, Sendable, Hashable {
     let id: Int
     let created: String?
     let updated: String?
diff --git a/Hutch/Views/Lists/MailingListListView.swift b/Hutch/Views/Lists/MailingListListView.swift
index 1b159bf..b6ed2ff 100644
--- a/Hutch/Views/Lists/MailingListListView.swift
+++ b/Hutch/Views/Lists/MailingListListView.swift
@@ -100,7 +100,7 @@ final class MailingListListViewModel {
                 query: Self.createMailingListMutation,
                 variables: [
                     "name": trimmedName,
-                    "description": trimmedDescription.isEmpty ? nil as String? as Any : trimmedDescription,
+                    "description": trimmedDescription.isEmpty ? nil as String? as any Sendable : trimmedDescription,
                     "visibility": visibility.rawValue
                 ],
                 responseType: Response.self
diff --git a/Hutch/Views/More/TipStoreViewModel.swift b/Hutch/Views/More/TipStoreViewModel.swift
index bc909c1..043b218 100644
--- a/Hutch/Views/More/TipStoreViewModel.swift
+++ b/Hutch/Views/More/TipStoreViewModel.swift
@@ -32,7 +32,9 @@ final class TipStoreViewModel {
     var errorMessage: String?
     var statusMessage: String?
 
-    private var transactionUpdatesTask: Task<Void, Never>?
+    // Internal task handle, not observable state; assigned only on the main
+    // actor and read once from the nonisolated deinit.
+    @ObservationIgnored nonisolated(unsafe) private var transactionUpdatesTask: Task<Void, Never>?
 
     init() {
         transactionUpdatesTask = Task.detached(priority: .background) {
diff --git a/Hutch/Views/Repositories/RepositoryDetailViewModel.swift b/Hutch/Views/Repositories/RepositoryDetailViewModel.swift
index 82e1592..62a8b12 100644
--- a/Hutch/Views/Repositories/RepositoryDetailViewModel.swift
+++ b/Hutch/Views/Repositories/RepositoryDetailViewModel.swift
@@ -557,7 +557,7 @@ final class RepositoryDetailViewModel {
                 variables: [
                     "repoId": repository.id,
                     "revspec": revspec,
-                    "file": nil as String? as Any
+                    "file": nil as String? as any Sendable
                 ],
                 file: MultipartUploadFile(
                     variablePath: "file",
diff --git a/Hutch/Views/Repositories/SyntaxHighlighter.swift b/Hutch/Views/Repositories/SyntaxHighlighter.swift
index f8b2cbb..dfab443 100644
--- a/Hutch/Views/Repositories/SyntaxHighlighter.swift
+++ b/Hutch/Views/Repositories/SyntaxHighlighter.swift
@@ -208,7 +208,7 @@ nonisolated final class SyntaxHighlighter {
     ]
 }
 
-private extension UIColor {
+nonisolated private extension UIColor {
     /// `#rrggbb` for HTML inline styles, or `nil` if the color isn't RGB-convertible.
     var hexRGBString: String? {
         var red: CGFloat = 0
diff --git a/Hutch/Views/Settings/SettingsViewModel.swift b/Hutch/Views/Settings/SettingsViewModel.swift
index 8536e60..2d57ba5 100644
--- a/Hutch/Views/Settings/SettingsViewModel.swift
+++ b/Hutch/Views/Settings/SettingsViewModel.swift
@@ -198,9 +198,9 @@ final class SettingsViewModel {
         do {
             let input: [String: any Sendable] = [
                 "email": email,
-                "url": url.isEmpty ? nil as String? as Any : url,
-                "location": location.isEmpty ? nil as String? as Any : location,
-                "bio": bio.isEmpty ? nil as String? as Any : bio
+                "url": url.isEmpty ? nil as String? as any Sendable : url,
+                "location": location.isEmpty ? nil as String? as any Sendable : location,
+                "bio": bio.isEmpty ? nil as String? as any Sendable : bio
             ]
             let result = try await client.execute(
                 service: .meta,
@@ -243,7 +243,7 @@ final class SettingsViewModel {
         do {
             // The input variable has avatar set to null; the actual file
             // is sent as a separate multipart part per graphql-multipart-request-spec.
-            let input: [String: any Sendable] = ["avatar": nil as String? as Any]
+            let input: [String: any Sendable] = ["avatar": nil as String? as any Sendable]
             let result = try await client.executeMultipart(
                 service: .meta,
                 query: Self.updateUserMutation,
@@ -286,7 +286,7 @@ final class SettingsViewModel {
         error = nil
 
         do {
-            let input: [String: any Sendable] = ["avatar": nil as String? as Any]
+            let input: [String: any Sendable] = ["avatar": nil as String? as any Sendable]
             let result = try await client.execute(
                 service: .meta,
                 query: Self.updateUserMutation,
diff --git a/HutchTests/APICacheTests.swift b/HutchTests/APICacheTests.swift
index 24db870..9a93783 100644
--- a/HutchTests/APICacheTests.swift
+++ b/HutchTests/APICacheTests.swift
@@ -3,6 +3,7 @@ import Testing
 @testable import Hutch
 
 @Suite(.serialized)
+@MainActor
 struct APICacheTests {
     private struct Payload: Codable, Sendable, Equatable {
         let value: String
diff --git a/HutchTests/AppStateTests.swift b/HutchTests/AppStateTests.swift
index b7e1755..49baa17 100644
--- a/HutchTests/AppStateTests.swift
+++ b/HutchTests/AppStateTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct AppStateTests {
 
     @Test
diff --git a/HutchTests/BuildListViewModelTests.swift b/HutchTests/BuildListViewModelTests.swift
index 8ee5b81..9b64621 100644
--- a/HutchTests/BuildListViewModelTests.swift
+++ b/HutchTests/BuildListViewModelTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct BuildListViewModelTests {
 
     @Test
diff --git a/HutchTests/BuildTaskLogSearchTests.swift b/HutchTests/BuildTaskLogSearchTests.swift
index 4cb9984..8c7ddc2 100644
--- a/HutchTests/BuildTaskLogSearchTests.swift
+++ b/HutchTests/BuildTaskLogSearchTests.swift
@@ -1,6 +1,7 @@
 import Testing
 @testable import Hutch
 
+@MainActor
 struct BuildTaskLogSearchTests {
 
     @Test
diff --git a/HutchTests/BuildTaskTests.swift b/HutchTests/BuildTaskTests.swift
index 2e2a1e0..c7baae7 100644
--- a/HutchTests/BuildTaskTests.swift
+++ b/HutchTests/BuildTaskTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct BuildTaskTests {
 
     @Test
diff --git a/HutchTests/BundleUserAgentTests.swift b/HutchTests/BundleUserAgentTests.swift
index c1a20b8..a4bda55 100644
--- a/HutchTests/BundleUserAgentTests.swift
+++ b/HutchTests/BundleUserAgentTests.swift
@@ -40,6 +40,7 @@ private final class CapturingURLProtocol: URLProtocol, @unchecked Sendable {
 
 /// Tests are serialized because CapturingURLProtocol uses shared static state.
 @Suite(.serialized)
+@MainActor
 struct BundleUserAgentTests {
 
     // MARK: Bundle extension
diff --git a/HutchTests/ContributionCalendarTests.swift b/HutchTests/ContributionCalendarTests.swift
index 0328ad5..e5d625a 100644
--- a/HutchTests/ContributionCalendarTests.swift
+++ b/HutchTests/ContributionCalendarTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct ContributionCalendarTests {
 
     @Test
diff --git a/HutchTests/DeepLinkTests.swift b/HutchTests/DeepLinkTests.swift
index cdc5bab..dc4f605 100644
--- a/HutchTests/DeepLinkTests.swift
+++ b/HutchTests/DeepLinkTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct DeepLinkTests {
 
     @Test
diff --git a/HutchTests/GitObjectTests.swift b/HutchTests/GitObjectTests.swift
index 547c6f3..3546d11 100644
--- a/HutchTests/GitObjectTests.swift
+++ b/HutchTests/GitObjectTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct GitObjectTests {
 
     @Test
diff --git a/HutchTests/HomeViewModelTests.swift b/HutchTests/HomeViewModelTests.swift
index 851b520..a050931 100644
--- a/HutchTests/HomeViewModelTests.swift
+++ b/HutchTests/HomeViewModelTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct HomeViewModelTests {
 
     @Test
diff --git a/HutchTests/HutchTests.swift b/HutchTests/HutchTests.swift
index c681efa..98c68d3 100644
--- a/HutchTests/HutchTests.swift
+++ b/HutchTests/HutchTests.swift
@@ -7,6 +7,7 @@
 
 import Testing
 
+@MainActor
 struct HutchTests {
 
     @Test func example() async throws {
diff --git a/HutchTests/InboxViewModelTests.swift b/HutchTests/InboxViewModelTests.swift
index 5780b6a..9c35001 100644
--- a/HutchTests/InboxViewModelTests.swift
+++ b/HutchTests/InboxViewModelTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct InboxViewModelTests {
 
     @Test
diff --git a/HutchTests/LookupHistoryStoreTests.swift b/HutchTests/LookupHistoryStoreTests.swift
index 414d016..af7c69b 100644
--- a/HutchTests/LookupHistoryStoreTests.swift
+++ b/HutchTests/LookupHistoryStoreTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct LookupHistoryStoreTests {
 
     @Test
diff --git a/HutchTests/MailingListActivityTests.swift b/HutchTests/MailingListActivityTests.swift
index 46208cb..61a7e77 100644
--- a/HutchTests/MailingListActivityTests.swift
+++ b/HutchTests/MailingListActivityTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct MailingListActivityTests {
 
     @Test
diff --git a/HutchTests/ManPageCatalogTests.swift b/HutchTests/ManPageCatalogTests.swift
index df995f8..e6bd2fc 100644
--- a/HutchTests/ManPageCatalogTests.swift
+++ b/HutchTests/ManPageCatalogTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct ManPageCatalogTests {
     @Test
     func loadsBundledCatalog() {
diff --git a/HutchTests/PatchsetTests.swift b/HutchTests/PatchsetTests.swift
index 945131f..e181f14 100644
--- a/HutchTests/PatchsetTests.swift
+++ b/HutchTests/PatchsetTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct PatchsetStatusTests {
 
     @Test
@@ -46,6 +47,7 @@ struct PatchsetStatusTests {
     }
 }
 
+@MainActor
 struct PatchsetSummaryTests {
 
     @Test
diff --git a/HutchTests/ProjectFormTests.swift b/HutchTests/ProjectFormTests.swift
index a4b4dfa..3e1fffc 100644
--- a/HutchTests/ProjectFormTests.swift
+++ b/HutchTests/ProjectFormTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct ProjectFormTests {
     @Test
     func parsesTagsSplittingAndTrimming() {
diff --git a/HutchTests/ProjectPinStoreTests.swift b/HutchTests/ProjectPinStoreTests.swift
index 13df539..c040868 100644
--- a/HutchTests/ProjectPinStoreTests.swift
+++ b/HutchTests/ProjectPinStoreTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct ProjectPinStoreTests {
     @Test
     func storesPinnedProjectsPerUser() {
diff --git a/HutchTests/ProjectTests.swift b/HutchTests/ProjectTests.swift
index 23faa0d..785c98f 100644
--- a/HutchTests/ProjectTests.swift
+++ b/HutchTests/ProjectTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct ProjectTests {
     private enum Fixture {
         static let exampleWebsite = "https://example.com"
diff --git a/HutchTests/ReadmeViewTests.swift b/HutchTests/ReadmeViewTests.swift
index 9e053b8..96eb8c0 100644
--- a/HutchTests/ReadmeViewTests.swift
+++ b/HutchTests/ReadmeViewTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct ReadmeViewTests {
 
     @Test
@@ -28,6 +29,7 @@ struct ReadmeViewTests {
     }
 }
 
+@MainActor
 struct MarkdownRenderingTests {
 
     @Test
@@ -219,6 +221,7 @@ struct MarkdownRenderingTests {
     }
 }
 
+@MainActor
 struct OrgRenderingTests {
 
     @Test
@@ -376,6 +379,7 @@ struct OrgRenderingTests {
     }
 }
 
+@MainActor
 struct RepositoryAssetURLTests {
 
     @Test
@@ -391,6 +395,7 @@ struct RepositoryAssetURLTests {
     }
 }
 
+@MainActor
 struct RepositoryLinkURLTests {
 
     @Test
diff --git a/HutchTests/RecentActivityStoreTests.swift b/HutchTests/RecentActivityStoreTests.swift
index e6fcc04..7713e6d 100644
--- a/HutchTests/RecentActivityStoreTests.swift
+++ b/HutchTests/RecentActivityStoreTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct RecentActivityStoreTests {
     @Test
     func removeDropsMatchingEntryKeepingOthers() {
diff --git a/HutchTests/RepositoryACLViewModelTests.swift b/HutchTests/RepositoryACLViewModelTests.swift
index 9640b93..501074c 100644
--- a/HutchTests/RepositoryACLViewModelTests.swift
+++ b/HutchTests/RepositoryACLViewModelTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct RepositoryACLViewModelTests {
 
     @Test
diff --git a/HutchTests/RepositoryDeployKeyTests.swift b/HutchTests/RepositoryDeployKeyTests.swift
index c6ff667..d7a0dad 100644
--- a/HutchTests/RepositoryDeployKeyTests.swift
+++ b/HutchTests/RepositoryDeployKeyTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct RepositoryDeployKeyTests {
     @Test
     func decodesDeployKey() throws {
diff --git a/HutchTests/RepositoryListViewModelTests.swift b/HutchTests/RepositoryListViewModelTests.swift
index 5fe3974..7787fca 100644
--- a/HutchTests/RepositoryListViewModelTests.swift
+++ b/HutchTests/RepositoryListViewModelTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct RepositoryListViewModelTests {
 
     @Test
diff --git a/HutchTests/RepositorySettingsViewModelTests.swift b/HutchTests/RepositorySettingsViewModelTests.swift
index 13d0b21..25e46c9 100644
--- a/HutchTests/RepositorySettingsViewModelTests.swift
+++ b/HutchTests/RepositorySettingsViewModelTests.swift
@@ -10,6 +10,7 @@ private struct UpdateRepositoryInfoPayload: Decodable {
     let id: Int
 }
 
+@MainActor
 struct RepositorySettingsViewModelTests {
 
     @Test
diff --git a/HutchTests/SRHTClientTests.swift b/HutchTests/SRHTClientTests.swift
index 12d6d0f..51b6a68 100644
--- a/HutchTests/SRHTClientTests.swift
+++ b/HutchTests/SRHTClientTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct SRHTClientTests {
 
     @Test
diff --git a/HutchTests/SRHTWebURLTests.swift b/HutchTests/SRHTWebURLTests.swift
index d1053e6..f20f964 100644
--- a/HutchTests/SRHTWebURLTests.swift
+++ b/HutchTests/SRHTWebURLTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct SRHTWebURLTests {
     private enum Expected {
         static let chatOrigin = "https://chat.sr.ht"
diff --git a/HutchTests/ScopedSearchHistoryStoreTests.swift b/HutchTests/ScopedSearchHistoryStoreTests.swift
index 856d69f..8effa06 100644
--- a/HutchTests/ScopedSearchHistoryStoreTests.swift
+++ b/HutchTests/ScopedSearchHistoryStoreTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct ScopedSearchHistoryStoreTests {
 
     @Test
diff --git a/HutchTests/SettingsViewModelTests.swift b/HutchTests/SettingsViewModelTests.swift
index 906f33f..e88d064 100644
--- a/HutchTests/SettingsViewModelTests.swift
+++ b/HutchTests/SettingsViewModelTests.swift
@@ -64,6 +64,7 @@ private struct DeleteResultPayload: Decodable {
 }
 
 @Suite(.serialized)
+@MainActor
 struct SettingsViewModelTests {
 
     @Test
diff --git a/HutchTests/SettingsViewTests.swift b/HutchTests/SettingsViewTests.swift
index 950c3dc..f0f7fc1 100644
--- a/HutchTests/SettingsViewTests.swift
+++ b/HutchTests/SettingsViewTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct SettingsViewTests {
 
     @Test
diff --git a/HutchTests/SourceHutWebDeepLinkMapperTests.swift b/HutchTests/SourceHutWebDeepLinkMapperTests.swift
index 0fa3aaa..2c638c2 100644
--- a/HutchTests/SourceHutWebDeepLinkMapperTests.swift
+++ b/HutchTests/SourceHutWebDeepLinkMapperTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct SourceHutWebDeepLinkMapperTests {
     @Test
     func mapsRepositoryURLs() {
diff --git a/HutchTests/SyntaxHighlighterTests.swift b/HutchTests/SyntaxHighlighterTests.swift
index 258e81e..3150814 100644
--- a/HutchTests/SyntaxHighlighterTests.swift
+++ b/HutchTests/SyntaxHighlighterTests.swift
@@ -3,6 +3,7 @@ import Testing
 import UIKit
 @testable import Hutch
 
+@MainActor
 struct SyntaxHighlighterTests {
     @Test
     func mapsFileNamesToLanguages() {
@@ -24,9 +25,8 @@ struct SyntaxHighlighterTests {
         let highlighter = SyntaxHighlighter(theme: .light)
         let html = highlighter.highlightedHTML(for: "let answer = 42", language: "swift")
 
-        let unwrapped = try? #require(html)
-        #expect(unwrapped?.contains("<span style=\"color:") == true)
-        #expect(unwrapped?.contains("answer") == true)
+        #expect(html?.contains("<span style=\"color:") == true)
+        #expect(html?.contains("answer") == true)
     }
 
     @Test
@@ -42,7 +42,6 @@ struct SyntaxHighlighterTests {
         let font = UIFont.monospacedSystemFont(ofSize: 12, weight: .regular)
         let attributed = highlighter.attributedText(for: "print(\"hi\")", language: "python", font: font)
 
-        let unwrapped = try? #require(attributed)
-        #expect(unwrapped?.string.contains("print") == true)
+        #expect(attributed?.string.contains("print") == true)
     }
 }
diff --git a/HutchTests/SystemStatusRepositoryTests.swift b/HutchTests/SystemStatusRepositoryTests.swift
index 56fa7c0..45d32b9 100644
--- a/HutchTests/SystemStatusRepositoryTests.swift
+++ b/HutchTests/SystemStatusRepositoryTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct SystemStatusRepositoryTests {
 
     @Test
@@ -111,21 +112,21 @@ private struct TestSystemStatusService: SystemStatusServing {
 }
 
 private extension SystemStatusRepositoryTests {
-    static let cachedSnapshotHTML = #"""
+    nonisolated static let cachedSnapshotHTML = #"""
     <div class="component" data-status="disrupted">
       <a href="/affected/git.sr.ht/">git.sr.ht</a>
       <span class="component-status">Disrupted</span>
     </div>
     """#
 
-    static let cachedOperationalHTML = #"""
+    nonisolated static let cachedOperationalHTML = #"""
     <div class="component" data-status="ok">
       <a href="/affected/meta.sr.ht/">meta.sr.ht</a>
       <span class="component-status">Operational</span>
     </div>
     """#
 
-    static let cachedIncidentRSS = #"""
+    nonisolated static let cachedIncidentRSS = #"""
     <rss version="2.0">
       <channel>
         <item>
@@ -139,7 +140,7 @@ private extension SystemStatusRepositoryTests {
     </rss>
     """#
 
-    static let emptyRSS = #"""
+    nonisolated static let emptyRSS = #"""
     <rss version="2.0">
       <channel></channel>
     </rss>
diff --git a/HutchTests/SystemStatusServiceTests.swift b/HutchTests/SystemStatusServiceTests.swift
index d963fa4..9a7af1f 100644
--- a/HutchTests/SystemStatusServiceTests.swift
+++ b/HutchTests/SystemStatusServiceTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct SystemStatusServiceTests {
     private enum Fixture {
         static let ddosIssueURL = "https://status.sr.ht/issues/2026-04-06-ddos-attack/"
diff --git a/HutchTests/TicketBulkActionTests.swift b/HutchTests/TicketBulkActionTests.swift
index 2f4d1fd..2c1d738 100644
--- a/HutchTests/TicketBulkActionTests.swift
+++ b/HutchTests/TicketBulkActionTests.swift
@@ -1,6 +1,7 @@
 import Testing
 @testable import Hutch
 
+@MainActor
 struct TicketBulkActionTests {
 
     @Test
diff --git a/HutchTests/TicketDetailViewModelTests.swift b/HutchTests/TicketDetailViewModelTests.swift
index ba4a476..d2c0c8e 100644
--- a/HutchTests/TicketDetailViewModelTests.swift
+++ b/HutchTests/TicketDetailViewModelTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct TicketDetailViewModelTests {
 
     @Test
diff --git a/HutchTests/TicketSavedFilterStoreTests.swift b/HutchTests/TicketSavedFilterStoreTests.swift
index 2ff64e6..cfdcba6 100644
--- a/HutchTests/TicketSavedFilterStoreTests.swift
+++ b/HutchTests/TicketSavedFilterStoreTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct TicketSavedFilterStoreTests {
 
     @Test
diff --git a/HutchTests/TipStoreViewModelTests.swift b/HutchTests/TipStoreViewModelTests.swift
index ed58d1e..734a7cc 100644
--- a/HutchTests/TipStoreViewModelTests.swift
+++ b/HutchTests/TipStoreViewModelTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct TipStoreViewModelTests {
 
     @Test
diff --git a/HutchTests/TrackerListViewModelTests.swift b/HutchTests/TrackerListViewModelTests.swift
index 5c57e04..85b051f 100644
--- a/HutchTests/TrackerListViewModelTests.swift
+++ b/HutchTests/TrackerListViewModelTests.swift
@@ -2,6 +2,7 @@ import Foundation
 import Testing
 @testable import Hutch
 
+@MainActor
 struct TrackerListViewModelTests {
 
     @Test
diff --git a/HutchTests/TrackerManagementViewModelTests.swift b/HutchTests/TrackerManagementViewModelTests.swift
index 531608c..211c838 100644
--- a/HutchTests/TrackerManagementViewModelTests.swift
+++ b/HutchTests/TrackerManagementViewModelTests.swift
@@ -1,6 +1,7 @@
 import Testing
 @testable import Hutch
 
+@MainActor
 struct TrackerManagementViewModelTests {
 
     @Test
diff --git a/HutchWidgetExtension/ContributionGraphWidget.swift b/HutchWidgetExtension/ContributionGraphWidget.swift
index 2fc07f2..bc56a03 100644
--- a/HutchWidgetExtension/ContributionGraphWidget.swift
+++ b/HutchWidgetExtension/ContributionGraphWidget.swift
@@ -28,12 +28,16 @@ struct ContributionGraphTimelineProvider: TimelineProvider {
     }
 
     func getSnapshot(in _: Context, completion: @escaping (ContributionGraphEntry) -> Void) {
+        // WidgetKit's completion is safe to call from the task; rebind as
+        // nonisolated(unsafe) so it can cross into the `sending` Task closure.
+        nonisolated(unsafe) let completion = completion
         Task {
             completion(await loadEntry())
         }
     }
 
     func getTimeline(in _: Context, completion: @escaping (Timeline<ContributionGraphEntry>) -> Void) {
+        nonisolated(unsafe) let completion = completion
         Task {
             let entry = await loadEntry()
             let refreshDate = Calendar.contributionCalendar.date(byAdding: .hour, value: 1, to: Date()) ?? Date().addingTimeInterval(3600)
diff --git a/ROADMAP.txt b/ROADMAP.txt
index 5dbca20..4352b9e 100644
--- a/ROADMAP.txt
+++ b/ROADMAP.txt
@@ -158,7 +158,7 @@ so "breaking change" does not apply. These buckets track *user-visible scale*.
 | v3.11.0 | ~~Mailing list subscribe/unsubscribe toggle~~ (shipped) | Ingest-surfaced; state via the `subscriptions` query (the `subscription` field is a trap) |
 | v3.12.0 | Accessibility | Independent, device-verified |
 | v4.0.0 | Localization *with* translations | The only true re-presentation |
-| — | Swift 6 language mode; cache reads | Internal; ride along, no tag |
+| — | ~~Swift 6 language mode~~ (done); cache reads | Internal; ride along, no tag |
 
 3.9.0 was cut this session, bundling the hub.sr.ht writes with the other
 features listed. That reorders the original plan: the "What's cooking" ingest
@@ -384,21 +384,29 @@ rather than a bare `TODO`.)
   saved-search store for an intent to open. Add the intent once global
   saved-search persistence exists.
 
-### Swift 6 language mode — no release of its own
-
-The project builds in Swift 5 language mode with
-`SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`. Moving to Swift 6 is blocked on
-concurrency diagnostics that are warnings today and errors there:
-
-- `APICacheTests` and `BundleUserAgentTests` call main-actor-isolated
-  initialisers and properties from nonisolated contexts, and `await` a few
-  expressions without marking them. Roughly 20 warnings, all in tests.
-- Response types are implicitly `@MainActor` under the default isolation, so
-  their `Decodable` conformances are too. Decoding one from a nonisolated
-  context — an `async let` over a raw `client.execute`, say — warns now and
-  fails then. The pattern that avoids it is `async let` over `@MainActor`
-  methods, as in `HomeViewModel.loadDashboard` and
-  `NotificationPreferencesViewModel.load`.
+### Swift 6 language mode — done
+
+`SWIFT_VERSION` is now `6.0` (keeping `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`).
+The migration was mostly mechanical, in a few buckets:
+
+- **Models are `nonisolated`.** Under MainActor-default, every value-type model
+  was implicitly `@MainActor`; the pure data types in `Models/` (and utility
+  extensions like `Date.relativeDescription`, `DateFormatter+SRHT`, `SRHTWebURL`)
+  are now `nonisolated`, so the nonisolated networking layer can use them.
+- **App Intents statics.** `AppIntent`/`AppEntity`/`AppEnum` `static var`s were
+  "global shared mutable state"; the stored ones are now `static let`.
+- **Sendable dictionaries.** `nil as String? as Any` in `[String: any Sendable]`
+  GraphQL variable dicts became `nil as String? as any Sendable`.
+- **`UserDefaults`** gets a retroactive `@unchecked Sendable` (documented
+  thread-safe) since it threads through account sessions and stores.
+- **WidgetKit** completion handlers are rebound `nonisolated(unsafe)` to cross
+  into their `Task {}`; the `@Observable` `TipStoreViewModel` task handle is
+  `@ObservationIgnored nonisolated(unsafe)` for its nonisolated `deinit`.
+- **Tests** run on `@MainActor` (they exercise MainActor app code), with a few
+  constant fixtures marked `nonisolated` for use inside `@Sendable` stub
+  closures.
+
+Builds and the full suite are clean in Swift 6 mode with no behaviour change.
 
 ### Cache reads that bypass the client — no release of its own