diff --git a/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift b/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift index 09f7aa9..57c5b5e 100644 --- a/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift +++ b/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift @@ -192,58 +192,70 @@ struct SessionDetailView: View { } } + /// The floating action layer: one Liquid Glass bar carrying the queued pills, the working + /// indicator, the session controls, and the composer — or the approval card when a gate is + /// pending. Content scrolls beneath it; nothing renders when there's nothing to act on. @ViewBuilder private var actionArea: some View { - Divider() - if let approval = store.openApprovals.first { - ApprovalCardView(approval: approval) - .padding() - } else if let status = summary?.status { - let running = status == .running - let canCompose = running || status == .awaitingInput || status.isTerminal - VStack(spacing: 8) { - // Follow-ups submitted mid-turn, held by the host to send when the current turn - // finishes. Each is shown as its own pill (with its attachments) and — at control - // scope — can be cancelled individually, matching the Mac composer. (They still - // send as one combined turn.) - if let queued = summary?.queuedMessages, !queued.isEmpty { - VStack(spacing: 6) { - ForEach(queued) { message in - queuedMessageRow(message) + Group { + if let approval = store.openApprovals.first { + ApprovalCardView(approval: approval) + } else if let status = summary?.status { + let running = status == .running + let canCompose = running || status == .awaitingInput || status.isTerminal + let queued = summary?.queuedMessages ?? [] + if canCompose || store.canControl || !queued.isEmpty { + VStack(spacing: 10) { + // Follow-ups submitted mid-turn, held by the host to send when the current + // turn finishes. Each is shown as its own pill (with its attachments) and — + // at control scope — can be cancelled individually, matching the Mac + // composer. (They still send as one combined turn.) + if !queued.isEmpty { + VStack(spacing: 6) { + ForEach(queued) { message in + queuedMessageRow(message) + } + } + } + if running { + HStack(spacing: 8) { + ProgressView().controlSize(.small) + Text("Agent is working…").font(.footnote).foregroundStyle(.secondary) + Spacer() + } + .frame(maxWidth: .infinity, alignment: .leading) + } + if store.canControl, let summary { controlRow(summary) } + if canCompose { + HStack(alignment: .bottom, spacing: 8) { + TextField(running ? "Queue a follow-up…" : "Send a follow-up…", + text: $draft, axis: .vertical) + .textFieldStyle(.plain) + .lineLimit(1...4) + .keyboardDismissable() + .padding(.vertical, 3) + Button { + store.sendInput(draft, to: sessionID) + draft = "" + } label: { + Image(systemName: running ? "clock.arrow.circlepath" : "arrow.up.circle.fill") + .font(.title2) + } + .disabled(draft.trimmingCharacters(in: .whitespaces).isEmpty || !store.connectivity.isLive) + // Hardware-keyboard send (Magic Keyboard on iPad), mirroring the + // Mac — plain Return stays newline in the multiline field. + .keyboardShortcut(.return, modifiers: .command) + } } } - } - if running { - HStack(spacing: 8) { - ProgressView().controlSize(.small) - Text("Agent is working…").font(.footnote).foregroundStyle(.secondary) - Spacer() - } - .frame(maxWidth: .infinity, alignment: .leading) - } - if canCompose { - HStack(spacing: 8) { - TextField(running ? "Queue a follow-up…" : "Send a follow-up…", - text: $draft, axis: .vertical) - .textFieldStyle(.roundedBorder) - .lineLimit(1...4) - .keyboardDismissable() - Button { - store.sendInput(draft, to: sessionID) - draft = "" - } label: { - Image(systemName: running ? "clock.arrow.circlepath" : "arrow.up.circle.fill") - .font(.title2) - } - .disabled(draft.trimmingCharacters(in: .whitespaces).isEmpty || !store.connectivity.isLive) - // Hardware-keyboard send (Magic Keyboard on iPad), mirroring the Mac — - // plain Return stays newline in the multiline field. - .keyboardShortcut(.return, modifiers: .command) - } + .padding(.horizontal, 14) + .padding(.vertical, 12) + .glassSurface(cornerRadius: 26) } } - .padding() } + .padding(.horizontal, 12) + .padding(.bottom, 8) } /// A queued follow-up shown above the composer: its text (or attachment names) with the