From bd46ca241f3daf8c2571d100a8ac325452d99db9 Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Sat, 27 Jun 2026 14:57:57 -0700 Subject: [PATCH 1/3] nvrsion: promote trunk to dev Nucleic-Promote: 1 --- NucleicRemote/NucleicRemote/HeartbeatReporter.swift | 2 +- NucleicRemote/NucleicRemote/NucleicRemoteApp.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/NucleicRemote/NucleicRemote/HeartbeatReporter.swift b/NucleicRemote/NucleicRemote/HeartbeatReporter.swift index a9a5701..182d8be 100644 --- a/NucleicRemote/NucleicRemote/HeartbeatReporter.swift +++ b/NucleicRemote/NucleicRemote/HeartbeatReporter.swift @@ -2,7 +2,7 @@ import Foundation import Security import NucleicProtocol -/// Sends the iPhone client's anonymous DAU heartbeat at most once per day +/// Sends the iPhone client's anonymous DAI (daily active installs) heartbeat at most once per day /// (`docs/CLOUD_INFRA.md` §4). Opt-out (default on) via `HeartbeatSettings`; debug builds never /// report. The install ID is a random UUID kept only in the Keychain, deliberately **separate** /// from the device's Noise identity (`IdentityStore`) so telemetry can't be correlated with it. diff --git a/NucleicRemote/NucleicRemote/NucleicRemoteApp.swift b/NucleicRemote/NucleicRemote/NucleicRemoteApp.swift index a41ede2..4d5fd12 100644 --- a/NucleicRemote/NucleicRemote/NucleicRemoteApp.swift +++ b/NucleicRemote/NucleicRemote/NucleicRemoteApp.swift @@ -15,7 +15,7 @@ struct NucleicRemoteApp: App { // Surface the notifications prompt + register for APNS. The token rides along // in the sync Hello; the relay uses it to wake the phone for approvals (§3). PushRegistrar.shared.requestAuthorizationAndRegister() - // Anonymous, opt-out, once-a-day DAU heartbeat (docs/CLOUD_INFRA.md §4). + // Anonymous, opt-out, once-a-day DAI heartbeat (docs/CLOUD_INFRA.md §4). Task { await HeartbeatReporter.reportIfDue() } } // Re-check on every foreground so a new active day is counted. From 9f9fb88672223f99a93d66374d1df50a21509189 Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Sat, 27 Jun 2026 15:24:50 -0700 Subject: [PATCH 2/3] add app icon to ios --- .../NucleicRemote.xcodeproj/project.pbxproj | 8 ++-- NucleicRemote/app-logo-file.icon/icon.json | 44 ++++++++++++++++--- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/NucleicRemote/NucleicRemote.xcodeproj/project.pbxproj b/NucleicRemote/NucleicRemote.xcodeproj/project.pbxproj index 26cd581..8af6cf0 100644 --- a/NucleicRemote/NucleicRemote.xcodeproj/project.pbxproj +++ b/NucleicRemote/NucleicRemote.xcodeproj/project.pbxproj @@ -8,11 +8,11 @@ /* Begin PBXBuildFile section */ BF00000000000000000001 /* NucleicProtocol in Frameworks */ = {isa = PBXBuildFile; productRef = PD00000000000000000001 /* NucleicProtocol */; }; - E7313E872FEF723300717105 /* app-logo-file.icon in Resources */ = {isa = PBXBuildFile; fileRef = E7313E862FEF723300717105 /* app-logo-file.icon */; }; + E7D43FF12FF0806800BF2407 /* app-logo-file.icon in Resources */ = {isa = PBXBuildFile; fileRef = E7D43FF02FF0806800BF2407 /* app-logo-file.icon */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - E7313E862FEF723300717105 /* app-logo-file.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = "app-logo-file.icon"; sourceTree = ""; }; + E7D43FF02FF0806800BF2407 /* app-logo-file.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = "app-logo-file.icon"; sourceTree = ""; }; FR00000000000000000001 /* NucleicRemote.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NucleicRemote.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -54,7 +54,7 @@ children = ( SG00000000000000000001 /* NucleicRemote */, GR00000000000000000002 /* Products */, - E7313E862FEF723300717105 /* app-logo-file.icon */, + E7D43FF02FF0806800BF2407 /* app-logo-file.icon */, ); sourceTree = ""; }; @@ -134,7 +134,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - E7313E872FEF723300717105 /* app-logo-file.icon in Resources */, + E7D43FF12FF0806800BF2407 /* app-logo-file.icon in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/NucleicRemote/app-logo-file.icon/icon.json b/NucleicRemote/app-logo-file.icon/icon.json index 8527fff..fb002b5 100644 --- a/NucleicRemote/app-logo-file.icon/icon.json +++ b/NucleicRemote/app-logo-file.icon/icon.json @@ -5,8 +5,40 @@ "hidden" : false, "layers" : [ { - "glass" : false, - "hidden" : false, + "blend-mode" : "normal", + "fill-specializations" : [ + { + "value" : "none" + }, + { + "appearance" : "tinted", + "value" : { + "solid" : "extended-gray:1.00000,1.00000" + } + } + ], + "glass-specializations" : [ + { + "value" : true + }, + { + "appearance" : "tinted", + "value" : true + } + ], + "hidden-specializations" : [ + { + "value" : false + }, + { + "idiom" : "iOS", + "value" : false + }, + { + "idiom" : "macOS", + "value" : false + } + ], "image-name" : "logo-v1-dark 2.png", "name" : "logo-v1-dark 2", "position" : { @@ -29,9 +61,9 @@ } ], "supported-platforms" : { - "circles" : [ - "watchOS" - ], - "squares" : "shared" + "squares" : [ + "iOS", + "macOS" + ] } } \ No newline at end of file From 0e74c7ceb5be34d06e2cea15363676fff487bbfb Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Sat, 27 Jun 2026 17:13:50 -0700 Subject: [PATCH 3/3] =?UTF-8?q?Promote=20nvrsion=20trunk=20=E2=86=92=20dev?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squash-promote the nucleic/trunk work (Sessions Auto-Switching, Play System Sounds Configuration) into dev. Resolved the two divergent conflicts where dev and trunk had independently implemented the chat status-sound feature, keeping trunk's canonical version: - SettingsView: the Sounds settings live under the General tab (trunk's placement); removed dev's duplicate Sounds section from the Chat tab. - AppStore: adopt trunk's centralized ChatCueState/announceChatCue edge-detection (fired from upsertSummary/upsertSummaries), and remove dev's superseded chatStatusSound(...) helper and its two hand-placed call sites. Also removed a silent merge-duplicated upsertSummaries. Nucleic-Promote: 1 Co-authored-by: Nucleic Co-Authored-By: Claude Opus 4.8 --- NucleicRemote/NucleicRemote/Views/HomeView.swift | 14 ++++---------- .../NucleicRemote/Views/SessionDetailView.swift | 5 ++++- NucleicRemote/NucleicRemote/Views/Theme.swift | 15 +++++++++++++++ 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/NucleicRemote/NucleicRemote/Views/HomeView.swift b/NucleicRemote/NucleicRemote/Views/HomeView.swift index 449e128..696e20b 100644 --- a/NucleicRemote/NucleicRemote/Views/HomeView.swift +++ b/NucleicRemote/NucleicRemote/Views/HomeView.swift @@ -192,6 +192,9 @@ struct ActivityGrid: View { let weekday = cal.component(.weekday, from: today) let startOfWeek = cal.date(byAdding: .day, value: -(weekday - 1), to: today)! let gridStart = cal.date(byAdding: .day, value: -7 * (weeks - 1), to: startOfWeek)! + // Brightness relative to *this user's* activity, outliers excluded so a few marathon + // days don't crush the everyday range. Computed once for the whole grid (mirrors Mac). + let scale = ActivityScale(activityByDay: activityByDay) return HStack(alignment: .top, spacing: gap) { ForEach(0.. Int { - switch count { - case 0: 0 - case 1...2: 1 - case 3...5: 2 - default: 3 - } - } } diff --git a/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift b/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift index c7ba332..0b95763 100644 --- a/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift +++ b/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift @@ -105,7 +105,10 @@ struct SessionDetailView: View { // No explicit model yet → use the session backend's default, so the menu // shows that backend's effort range rather than collapsing to one level. modelSKU: summary.model ?? store.modelCatalog.models(for: summary.backend).first?.sku, - controlled: controlled) + controlled: controlled, + // Orchestra is a one-way latch for an existing chat — the host won't turn it + // off, so disable the other levels here to match. + lockWhenOrchestra: true) Spacer(minLength: 4) if let percent = contextPercent { Label("\(percent)%", systemImage: "gauge.with.dots.needle.33percent") diff --git a/NucleicRemote/NucleicRemote/Views/Theme.swift b/NucleicRemote/NucleicRemote/Views/Theme.swift index 0efe699..596d822 100644 --- a/NucleicRemote/NucleicRemote/Views/Theme.swift +++ b/NucleicRemote/NucleicRemote/Views/Theme.swift @@ -44,6 +44,13 @@ enum Palette { default: success } } + + /// Activity-grid shade for a continuous brightness in `0…1` (relative to the user's own + /// activity, outliers excluded — see `ActivityScale`). `0` is an empty day's neutral gray. + /// Mirrors the Mac's `AppPalette.activity(intensity:)`. + static func activity(intensity: Double) -> Color { + intensity <= 0 ? Color.secondary.opacity(0.15) : success.opacity(intensity) + } } /// Status glyph + label, matching the Mac semantics (UX_MACOS §1). @@ -240,6 +247,10 @@ struct EffortMenu: View { var modelSKU: String? = nil /// Whether the target project is under Nucleic Control (gates Orchestra). var controlled: Bool = false + /// Latch Orchestra on once it's active: for an existing chat the host refuses to turn Orchestra + /// off (`SessionController.setEffort`), so the other levels are disabled to match. Left off for + /// new-chat composition, where the choice isn't committed yet and can still be changed. + var lockWhenOrchestra: Bool = false private var levels: [String] { catalog.offeredEfforts(forModel: modelSKU) } private var sentinel: String { catalog.orchestraSentinelOrFallback } @@ -258,6 +269,9 @@ struct EffortMenu: View { return levels.contains(effort) ? effort : (levels.last ?? MobileEfforts.fallback) } + /// Once Orchestra is latched on for an existing chat, the ordinary levels can't be chosen. + private var locked: Bool { lockWhenOrchestra && orchestraActive } + var body: some View { Menu { ForEach(levels, id: \.self) { level in @@ -265,6 +279,7 @@ struct EffortMenu: View { if effort == level { Label(display(level), systemImage: "checkmark") } else { Text(display(level)) } } + .disabled(locked) } Divider() Button { effort = sentinel } label: {