Chat View Refinement
Nucleic-Session: 4E56990A-96AE-4597-9140-753CB82E769D Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
@@ -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
|
||||
private var actionArea: some View {
|
||||
Divider()
|
||||
Group {
|
||||
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 {
|
||||
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)
|
||||
@@ -221,13 +225,15 @@ struct SessionDetailView: View {
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
if store.canControl, let summary { controlRow(summary) }
|
||||
if canCompose {
|
||||
HStack(spacing: 8) {
|
||||
HStack(alignment: .bottom, spacing: 8) {
|
||||
TextField(running ? "Queue a follow-up…" : "Send a follow-up…",
|
||||
text: $draft, axis: .vertical)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.textFieldStyle(.plain)
|
||||
.lineLimit(1...4)
|
||||
.keyboardDismissable()
|
||||
.padding(.vertical, 3)
|
||||
Button {
|
||||
store.sendInput(draft, to: sessionID)
|
||||
draft = ""
|
||||
@@ -236,15 +242,21 @@ struct SessionDetailView: View {
|
||||
.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.
|
||||
// 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, 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
|
||||
/// attachments as chips, and — at control scope — a ✕ that cancels just this message.
|
||||
|
||||
Reference in New Issue
Block a user