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 @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 let queued = summary?.queuedMessages ?? []
VStack(spacing: 8) { if canCompose || store.canControl || !queued.isEmpty {
// Follow-ups submitted mid-turn, held by the host to send when the current turn VStack(spacing: 10) {
// finishes. Each is shown as its own pill (with its attachments) and — at control // Follow-ups submitted mid-turn, held by the host to send when the current
// scope — can be cancelled individually, matching the Mac composer. (They still // turn finishes. Each is shown as its own pill (with its attachments) and —
// send as one combined turn.) // at control scope — can be cancelled individually, matching the Mac
if let queued = summary?.queuedMessages, !queued.isEmpty { // composer. (They still send as one combined turn.)
VStack(spacing: 6) { if !queued.isEmpty {
ForEach(queued) { message in VStack(spacing: 6) {
queuedMessageRow(message) 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)
}
} }
} }
} .padding(.horizontal, 14)
if running { .padding(.vertical, 12)
HStack(spacing: 8) { .glassSurface(cornerRadius: 26)
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()
} }
.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