diff --git a/NucleicRemote/NucleicRemote/Views/Composer.swift b/NucleicRemote/NucleicRemote/Views/Composer.swift index 38d6e13..d896f0a 100644 --- a/NucleicRemote/NucleicRemote/Views/Composer.swift +++ b/NucleicRemote/NucleicRemote/Views/Composer.swift @@ -18,7 +18,7 @@ struct StartChatComposer: View { /// project page's "+", so the chat always lands in that project. var lockedProject: WireProject? = nil @State private var projectID: ProjectID? - @State private var auto = false + @State private var autoOverride: Bool? @State private var model: String? @State private var effort = MobileEfforts.fallback // Advanced options (the Mac new-session sheet's base branch + worktree fields). @@ -31,6 +31,11 @@ struct StartChatComposer: View { lockedProject ?? (projects.first { $0.id == projectID } ?? projects.first) } private var controlled: Bool { selected?.isNucleicControlled ?? false } + /// Auto-approve for the chat about to start: the user's choice off-control, but forced on for a + /// Nucleic Control project. The host locks Auto on for those chats regardless of what the phone + /// sends (they run autonomously — nvrsion needs it: see `AppStore.createSession`), so the + /// composer shows it locked on to match the Mac rather than offering a toggle that wouldn't take. + private var effectiveAuto: Bool { controlled || (autoOverride ?? false) } var body: some View { VStack(alignment: .leading, spacing: 10) { @@ -51,12 +56,17 @@ struct StartChatComposer: View { .font(.subheadline).foregroundStyle(.secondary) } Spacer() - Toggle(isOn: $auto) { - Label("Auto", systemImage: auto ? "bolt.fill" : "bolt.slash") + Toggle(isOn: Binding(get: { effectiveAuto }, set: { autoOverride = $0 })) { + Label("Auto", systemImage: effectiveAuto ? "bolt.fill" : "bolt.slash") } .toggleStyle(.button) .tint(Palette.accent) .font(.caption) + // Locked on for a Nucleic Control project — those chats always run autonomously. + .disabled(controlled) + .help(controlled + ? "Auto-approve is always on for Nucleic Control chats — they run autonomously." + : "Auto-approve safe actions; destructive ones still ask.") } HStack { ModelMenu(model: $model, catalog: store.modelCatalog, backend: nil) @@ -104,7 +114,7 @@ struct StartChatComposer: View { store.startChat( in: project.id, message: draft, model: model, effort: effort, baseBranch: branch.isEmpty ? nil : branch, - useWorktree: useWorktree, auto: auto) + useWorktree: useWorktree, auto: effectiveAuto) draft = "" onStart?() } @@ -123,6 +133,10 @@ struct StartChatComposer: View { .padding(.horizontal, 14) .padding(.vertical, 12) .glassSurface(cornerRadius: 26) + // Prefill the base branch with the project's default so it actually appears (mirroring the + // Mac composer's `effectiveBranch`), rather than sitting as an empty placeholder. + .onAppear(perform: syncToProject) + .onChange(of: projectID) { _, _ in syncToProject() } // Switching to a model with a lower effort cap can't leave an unsupported level selected. .onChange(of: model) { _, newModel in let levels = store.modelCatalog.offeredEfforts(forModel: newModel) @@ -132,6 +146,16 @@ struct StartChatComposer: View { } } } + + /// Sync the composer to the freshly-selected project: show its default branch as the base, and + /// resolve a carried-over Orchestra selection down to a real level when the new project isn't + /// under Nucleic Control (Orchestra can't run there — mirrors the Mac's `effectiveEffort`). + private func syncToProject() { + baseBranch = selected?.defaultBranch ?? "" + if MobileEfforts.isOrchestra(effort), !controlled { + effort = store.modelCatalog.offeredEfforts(forModel: model).last ?? MobileEfforts.fallback + } + } } /// The floating "+" start-chat affordance: a Liquid Glass circle in the lower-right that reveals