From 0e74c7ceb5be34d06e2cea15363676fff487bbfb Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Sat, 27 Jun 2026 17:13:50 -0700 Subject: [PATCH] =?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: {