diff --git a/NucleicRemote/NucleicRemote/Views/Composer.swift b/NucleicRemote/NucleicRemote/Views/Composer.swift index e92f44c..309d5ab 100644 --- a/NucleicRemote/NucleicRemote/Views/Composer.swift +++ b/NucleicRemote/NucleicRemote/Views/Composer.swift @@ -28,7 +28,7 @@ struct StartChatComposer: View { .font(.subheadline) } Spacer() - EffortMenu(effort: $effort) + EffortMenu(effort: $effort, controlled: selected?.isNucleicControlled ?? false) Toggle(isOn: $auto) { Label("Auto", systemImage: auto ? "bolt.fill" : "bolt.slash") } @@ -40,7 +40,7 @@ struct StartChatComposer: View { TextField("Describe a task…", text: $draft, axis: .vertical) .textFieldStyle(.roundedBorder) .lineLimit(1...5) - .orchestraGlow(active: MobileEfforts.isOrchestra(effort)) + .orchestraGlow(active: MobileEfforts.isOrchestra(effort) && (selected?.isNucleicControlled ?? false)) Button { if let project = selected { store.startChat(in: project.id, message: draft, effort: effort, auto: auto) diff --git a/NucleicRemote/NucleicRemote/Views/ProjectsView.swift b/NucleicRemote/NucleicRemote/Views/ProjectsView.swift index 1200910..57daaa3 100644 --- a/NucleicRemote/NucleicRemote/Views/ProjectsView.swift +++ b/NucleicRemote/NucleicRemote/Views/ProjectsView.swift @@ -60,13 +60,13 @@ struct ProjectDetailView: View { Section { VStack(alignment: .leading, spacing: 8) { HStack { - EffortMenu(effort: $effort) + EffortMenu(effort: $effort, controlled: project.isNucleicControlled) Spacer() } HStack(alignment: .bottom, spacing: 8) { TextField("Start a chat in \(project.name)…", text: $draft, axis: .vertical) .textFieldStyle(.roundedBorder).lineLimit(1...4) - .orchestraGlow(active: MobileEfforts.isOrchestra(effort)) + .orchestraGlow(active: MobileEfforts.isOrchestra(effort) && project.isNucleicControlled) Button { store.startChat(in: project.id, message: draft, effort: effort) draft = "" diff --git a/NucleicRemote/NucleicRemote/Views/Theme.swift b/NucleicRemote/NucleicRemote/Views/Theme.swift index dd433e5..346a34f 100644 --- a/NucleicRemote/NucleicRemote/Views/Theme.swift +++ b/NucleicRemote/NucleicRemote/Views/Theme.swift @@ -138,9 +138,20 @@ enum MobileEfforts { } /// 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 { @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 { Menu { @@ -151,15 +162,18 @@ struct EffortMenu: View { } Divider() 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: { HStack(spacing: 4) { - Image(systemName: MobileEfforts.isOrchestra(effort) ? "sparkles" : "slider.horizontal.3") - Text(MobileEfforts.displayName(effort)) + Image(systemName: orchestraActive ? "sparkles" : "slider.horizontal.3") + Text(MobileEfforts.displayName(triggerEffort)) } .font(.caption) - .foregroundStyle(MobileEfforts.isOrchestra(effort) ? Palette.orchestra : Palette.accent) + .foregroundStyle(orchestraActive ? Palette.orchestra : Palette.accent) } } }