Merge branch 'canary' into dev

This commit is contained in:
2026-07-05 19:47:51 -07:00
@@ -18,7 +18,7 @@ struct StartChatComposer: View {
/// project page's "+", so the chat always lands in that project. /// project page's "+", so the chat always lands in that project.
var lockedProject: WireProject? = nil var lockedProject: WireProject? = nil
@State private var projectID: ProjectID? @State private var projectID: ProjectID?
@State private var auto = false @State private var autoOverride: Bool?
@State private var model: String? @State private var model: String?
@State private var effort = MobileEfforts.fallback @State private var effort = MobileEfforts.fallback
// Advanced options (the Mac new-session sheet's base branch + worktree fields). // 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) lockedProject ?? (projects.first { $0.id == projectID } ?? projects.first)
} }
private var controlled: Bool { selected?.isNucleicControlled ?? false } 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 { var body: some View {
VStack(alignment: .leading, spacing: 10) { VStack(alignment: .leading, spacing: 10) {
@@ -51,12 +56,17 @@ struct StartChatComposer: View {
.font(.subheadline).foregroundStyle(.secondary) .font(.subheadline).foregroundStyle(.secondary)
} }
Spacer() Spacer()
Toggle(isOn: $auto) { Toggle(isOn: Binding(get: { effectiveAuto }, set: { autoOverride = $0 })) {
Label("Auto", systemImage: auto ? "bolt.fill" : "bolt.slash") Label("Auto", systemImage: effectiveAuto ? "bolt.fill" : "bolt.slash")
} }
.toggleStyle(.button) .toggleStyle(.button)
.tint(Palette.accent) .tint(Palette.accent)
.font(.caption) .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 { HStack {
ModelMenu(model: $model, catalog: store.modelCatalog, backend: nil) ModelMenu(model: $model, catalog: store.modelCatalog, backend: nil)
@@ -104,7 +114,7 @@ struct StartChatComposer: View {
store.startChat( store.startChat(
in: project.id, message: draft, model: model, effort: effort, in: project.id, message: draft, model: model, effort: effort,
baseBranch: branch.isEmpty ? nil : branch, baseBranch: branch.isEmpty ? nil : branch,
useWorktree: useWorktree, auto: auto) useWorktree: useWorktree, auto: effectiveAuto)
draft = "" draft = ""
onStart?() onStart?()
} }
@@ -123,6 +133,10 @@ struct StartChatComposer: View {
.padding(.horizontal, 14) .padding(.horizontal, 14)
.padding(.vertical, 12) .padding(.vertical, 12)
.glassSurface(cornerRadius: 26) .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. // Switching to a model with a lower effort cap can't leave an unsupported level selected.
.onChange(of: model) { _, newModel in .onChange(of: model) { _, newModel in
let levels = store.modelCatalog.offeredEfforts(forModel: newModel) 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 /// The floating "+" start-chat affordance: a Liquid Glass circle in the lower-right that reveals