Chat View Refinement

Nucleic-Session: 4E56990A-96AE-4597-9140-753CB82E769D
Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
2026-07-05 00:53:47 -07:00
co-authored by Nucleic
parent 374f9855e5
commit c9cd2b1c3e
@@ -192,21 +192,25 @@ 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 @ViewBuilder
private var actionArea: some View { private var actionArea: some View {
Divider() Group {
if let approval = store.openApprovals.first { if let approval = store.openApprovals.first {
ApprovalCardView(approval: approval) ApprovalCardView(approval: approval)
.padding()
} else if let status = summary?.status { } else if let status = summary?.status {
let running = status == .running let running = status == .running
let canCompose = running || status == .awaitingInput || status.isTerminal let canCompose = running || status == .awaitingInput || status.isTerminal
VStack(spacing: 8) { let queued = summary?.queuedMessages ?? []
// Follow-ups submitted mid-turn, held by the host to send when the current turn if canCompose || store.canControl || !queued.isEmpty {
// finishes. Each is shown as its own pill (with its attachments) and — at control VStack(spacing: 10) {
// scope — can be cancelled individually, matching the Mac composer. (They still // Follow-ups submitted mid-turn, held by the host to send when the current
// send as one combined turn.) // turn finishes. Each is shown as its own pill (with its attachments) and —
if let queued = summary?.queuedMessages, !queued.isEmpty { // at control scope — can be cancelled individually, matching the Mac
// composer. (They still send as one combined turn.)
if !queued.isEmpty {
VStack(spacing: 6) { VStack(spacing: 6) {
ForEach(queued) { message in ForEach(queued) { message in
queuedMessageRow(message) queuedMessageRow(message)
@@ -221,13 +225,15 @@ struct SessionDetailView: View {
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
} }
if store.canControl, let summary { controlRow(summary) }
if canCompose { if canCompose {
HStack(spacing: 8) { HStack(alignment: .bottom, spacing: 8) {
TextField(running ? "Queue a follow-up…" : "Send a follow-up…", TextField(running ? "Queue a follow-up…" : "Send a follow-up…",
text: $draft, axis: .vertical) text: $draft, axis: .vertical)
.textFieldStyle(.roundedBorder) .textFieldStyle(.plain)
.lineLimit(1...4) .lineLimit(1...4)
.keyboardDismissable() .keyboardDismissable()
.padding(.vertical, 3)
Button { Button {
store.sendInput(draft, to: sessionID) store.sendInput(draft, to: sessionID)
draft = "" draft = ""
@@ -236,15 +242,21 @@ struct SessionDetailView: View {
.font(.title2) .font(.title2)
} }
.disabled(draft.trimmingCharacters(in: .whitespaces).isEmpty || !store.connectivity.isLive) .disabled(draft.trimmingCharacters(in: .whitespaces).isEmpty || !store.connectivity.isLive)
// Hardware-keyboard send (Magic Keyboard on iPad), mirroring the Mac — // Hardware-keyboard send (Magic Keyboard on iPad), mirroring the
// plain Return stays newline in the multiline field. // Mac — plain Return stays newline in the multiline field.
.keyboardShortcut(.return, modifiers: .command) .keyboardShortcut(.return, modifiers: .command)
} }
} }
} }
.padding() .padding(.horizontal, 14)
.padding(.vertical, 12)
.glassSurface(cornerRadius: 26)
} }
} }
}
.padding(.horizontal, 12)
.padding(.bottom, 8)
}
/// A queued follow-up shown above the composer: its text (or attachment names) with the /// A queued follow-up shown above the composer: its text (or attachment names) with the
/// attachments as chips, and — at control scope — a ✕ that cancels just this message. /// attachments as chips, and — at control scope — a ✕ that cancels just this message.