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:
co-authored by
Claude Opus 4.8
parent
1c9b120033
commit
e03ce837cf
@@ -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 = ""
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user