Chat View Refinement
Nucleic-Session: 4E56990A-96AE-4597-9140-753CB82E769D Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user