Gate Orchestra behind Nucleic Control; lavender Home composer accent

Orchestra is a Nucleic Control capability now:
- Core: SessionController withholds the fan-out consent unless the session's project
  is under Control (resolvedEffort still maps the sentinel to xhigh, so a stray
  selection never reaches a backend verbatim). Covered by a new SessionController test.
- macOS: the effort menu shows Orchestra disabled with a 'Requires Nucleic Control'
  note + tooltip off-control; effectiveEffort demotes a carried-over selection so the
  label/glow never show it active where it can't run.
- Home composer themes lavender (the Control accent) for a Control project — the
  Auto/Merge accents and a persistent ring on the field, tracking the project picker.
- Remote: WireProject carries isNucleicControlled (decode-tolerant) so the iOS effort
  picker gates Orchestra the same way.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Nucleic
2026-06-21 01:08:07 +00:00
co-authored by Claude Opus 4.8
parent 1c9b120033
commit e03ce837cf
3 changed files with 23 additions and 9 deletions
@@ -28,7 +28,7 @@ struct StartChatComposer: View {
.font(.subheadline) .font(.subheadline)
} }
Spacer() Spacer()
EffortMenu(effort: $effort) EffortMenu(effort: $effort, controlled: selected?.isNucleicControlled ?? false)
Toggle(isOn: $auto) { Toggle(isOn: $auto) {
Label("Auto", systemImage: auto ? "bolt.fill" : "bolt.slash") Label("Auto", systemImage: auto ? "bolt.fill" : "bolt.slash")
} }
@@ -40,7 +40,7 @@ struct StartChatComposer: View {
TextField("Describe a task…", text: $draft, axis: .vertical) TextField("Describe a task…", text: $draft, axis: .vertical)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
.lineLimit(1...5) .lineLimit(1...5)
.orchestraGlow(active: MobileEfforts.isOrchestra(effort)) .orchestraGlow(active: MobileEfforts.isOrchestra(effort) && (selected?.isNucleicControlled ?? false))
Button { Button {
if let project = selected { if let project = selected {
store.startChat(in: project.id, message: draft, effort: effort, auto: auto) store.startChat(in: project.id, message: draft, effort: effort, auto: auto)
@@ -60,13 +60,13 @@ struct ProjectDetailView: View {
Section { Section {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
HStack { HStack {
EffortMenu(effort: $effort) EffortMenu(effort: $effort, controlled: project.isNucleicControlled)
Spacer() Spacer()
} }
HStack(alignment: .bottom, spacing: 8) { HStack(alignment: .bottom, spacing: 8) {
TextField("Start a chat in \(project.name)…", text: $draft, axis: .vertical) TextField("Start a chat in \(project.name)…", text: $draft, axis: .vertical)
.textFieldStyle(.roundedBorder).lineLimit(1...4) .textFieldStyle(.roundedBorder).lineLimit(1...4)
.orchestraGlow(active: MobileEfforts.isOrchestra(effort)) .orchestraGlow(active: MobileEfforts.isOrchestra(effort) && project.isNucleicControlled)
Button { Button {
store.startChat(in: project.id, message: draft, effort: effort) store.startChat(in: project.id, message: draft, effort: effort)
draft = "" draft = ""
+19 -5
View File
@@ -138,9 +138,20 @@ enum MobileEfforts {
} }
/// A compact effort selector for the remote composers. Orchestra sits below the API levels, /// A compact effort selector for the remote composers. Orchestra sits below the API levels,
/// set apart with a sparkles glyph and the purple accent. /// set apart with a sparkles glyph and the purple accent. Orchestra is a Nucleic Control
/// capability, so it's only selectable when the target project is under Control — disabled and
/// labeled otherwise.
struct EffortMenu: View { struct EffortMenu: View {
@Binding var effort: String @Binding var effort: String
/// Whether the target project is under Nucleic Control (gates Orchestra).
var controlled: Bool = false
/// Orchestra only counts as active where it's permitted; a stray selection on a non-Control
/// project reads as its underlying level so the trigger never shows Orchestra where it can't run.
private var orchestraActive: Bool { controlled && MobileEfforts.isOrchestra(effort) }
private var triggerEffort: String {
orchestraActive ? effort : (MobileEfforts.isOrchestra(effort) ? MobileEfforts.fallback : effort)
}
var body: some View { var body: some View {
Menu { Menu {
@@ -151,15 +162,18 @@ struct EffortMenu: View {
} }
Divider() Divider()
Button { effort = MobileEfforts.orchestraSentinel } label: { Button { effort = MobileEfforts.orchestraSentinel } label: {
Label("Orchestra", systemImage: MobileEfforts.isOrchestra(effort) ? "checkmark" : "sparkles") Label(
controlled ? "Orchestra" : "Orchestra — Requires Nucleic Control",
systemImage: orchestraActive ? "checkmark" : "sparkles")
} }
.disabled(!controlled)
} label: { } label: {
HStack(spacing: 4) { HStack(spacing: 4) {
Image(systemName: MobileEfforts.isOrchestra(effort) ? "sparkles" : "slider.horizontal.3") Image(systemName: orchestraActive ? "sparkles" : "slider.horizontal.3")
Text(MobileEfforts.displayName(effort)) Text(MobileEfforts.displayName(triggerEffort))
} }
.font(.caption) .font(.caption)
.foregroundStyle(MobileEfforts.isOrchestra(effort) ? Palette.orchestra : Palette.accent) .foregroundStyle(orchestraActive ? Palette.orchestra : Palette.accent)
} }
} }
} }