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,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