Merge nucleic/ivory-iris-heron-iipz into dev
This commit is contained in:
@@ -261,6 +261,24 @@ enum ComposerGlass {
|
||||
}
|
||||
}
|
||||
|
||||
/// Vertical motion shared by the in-session composer's multiline field and the user-input
|
||||
/// surfaces that attach to its top edge. Keeping this separate from panel motion gives the
|
||||
/// composer a calmer, more tightly damped response than large rearrangeable panes.
|
||||
enum ComposerMotion {
|
||||
static func layout(_ reduceMotion: Bool) -> Animation {
|
||||
reduceMotion
|
||||
? .easeInOut(duration: 0.14)
|
||||
: .spring(response: 0.36, dampingFraction: 0.86)
|
||||
}
|
||||
|
||||
static func inputTransition(_ reduceMotion: Bool) -> AnyTransition {
|
||||
guard !reduceMotion else { return .opacity }
|
||||
return .asymmetric(
|
||||
insertion: .offset(y: 12).combined(with: .opacity),
|
||||
removal: .offset(y: 8).combined(with: .opacity))
|
||||
}
|
||||
}
|
||||
|
||||
/// Safari's sidebar glass: the system `.sidebar` material blurring *behind-window* content only
|
||||
/// (the desktop — the host window is made non-opaque by `WindowTranslucencyConfigurator`). It
|
||||
/// deliberately never samples in-window content, the way `NavigationSplitView`'s built-in Liquid
|
||||
|
||||
@@ -2917,12 +2917,13 @@ struct SessionDetailView: View {
|
||||
return .black.opacity(0.42)
|
||||
}
|
||||
|
||||
/// The pane's floating bottom furniture: a pending approval card (when there is one) above the
|
||||
/// composer, both hovering over the transcript on glass. Measured as one block — the height the
|
||||
/// transcript reserves beneath its last message and anchors its fade to — so the two stay in
|
||||
/// step however the cluster grows.
|
||||
/// The pane's floating bottom furniture. A pending permission/question is rendered inside the
|
||||
/// composer's glass shell, so it reads as the composer growing an input section rather than a
|
||||
/// separate card appearing above it. The whole shell is measured as one block — the height the
|
||||
/// transcript reserves beneath its last message and anchors its fade to — so the transcript
|
||||
/// stays in step throughout the grow/collapse animation.
|
||||
private var floatingBottomCluster: some View {
|
||||
VStack(spacing: 8) {
|
||||
VStack(spacing: 0) {
|
||||
if let approval = store.openApprovals.first {
|
||||
Group {
|
||||
if approval.toolName == AskUserQuestion.toolName,
|
||||
@@ -2933,17 +2934,34 @@ struct SessionDetailView: View {
|
||||
ApprovalBar(request: approval)
|
||||
}
|
||||
}
|
||||
// Both bars are lightly tinted cards — legible over the pane, but not over live
|
||||
// prose — so floating them puts them on the same glass as the composer below,
|
||||
// with their own tint and border riding on top of it.
|
||||
.padding(8)
|
||||
.glassEffect(.regular, in: .rect(cornerRadius: 16))
|
||||
.contentShape(.rect(cornerRadius: 16))
|
||||
.shadow(color: .black.opacity(0.18), radius: 12, x: 0, y: 4)
|
||||
.chatColumn(maxWidth: contentMaxWidth, inset: transcriptInset, resizeWidth: columnWidth, settling: transcriptSettling)
|
||||
// Keep the attention-tinted request as a focused inset, but let the composer own
|
||||
// the only outer material, corners, and shadow. Its stable identity also resets a
|
||||
// question wizard cleanly when one pending request is replaced by another.
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.top, 8)
|
||||
.id(approval.id)
|
||||
.transition(ComposerMotion.inputTransition(reduceMotion))
|
||||
}
|
||||
composer
|
||||
}
|
||||
.background(inactiveGlassScrim, in: .rect(cornerRadius: 16))
|
||||
.glassEffect(.regular.tint(composerGlassTint), in: .rect(cornerRadius: 16))
|
||||
.animation(.easeInOut(duration: 0.15), value: controlActiveState)
|
||||
// The glass itself does not claim pointer events. The solid shape prevents clicks in the
|
||||
// request/composer breathing room from landing on the transcript behind the floating card.
|
||||
.contentShape(.rect(cornerRadius: 16))
|
||||
.shadow(color: .black.opacity(0.18), radius: 12, x: 0, y: 4)
|
||||
.chatColumn(
|
||||
maxWidth: contentMaxWidth, inset: transcriptInset,
|
||||
resizeWidth: columnWidth, settling: transcriptSettling)
|
||||
// Do not animate the inherited approval of a chat that is merely being opened; live
|
||||
// requests animate once the transcript has completed its first stable layout.
|
||||
.animation(
|
||||
transcriptSettling ? nil : ComposerMotion.layout(reduceMotion),
|
||||
value: store.openApprovals.first?.id)
|
||||
.animation(
|
||||
transcriptSettling ? nil : ComposerMotion.layout(reduceMotion),
|
||||
value: composerHeight)
|
||||
.padding(.bottom, 12)
|
||||
// The one measurement both the transcript's tail spacer and its fade are placed from.
|
||||
.onGeometryChange(for: CGFloat.self) { $0.size.height } action: { floatingComposerHeight = $0 }
|
||||
@@ -3159,27 +3177,10 @@ struct SessionDetailView: View {
|
||||
// row ends in a caption line ("GPT-5.6 Sol · Extra") whose descender space already reads
|
||||
// as padding, so matching the top's inset there left the card bottom-heavy.
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.top, 10)
|
||||
// An attached request already contributes the card's top inset; close the space between
|
||||
// the two sections so they read as one surface. The same layout spring eases this value.
|
||||
.padding(.top, store.openApprovals.first == nil ? 10 : 5)
|
||||
.padding(.bottom, 4)
|
||||
// Backed by the system Liquid Glass material, exactly like the floating new-chat
|
||||
// composer: the input area is a card hovering over the conversation, sampling and
|
||||
// refracting the transcript that scrolls behind it — no opaque backing, no divider.
|
||||
// Tinted a little darker than the bare material (see `composerGlassTint`), with a scrim
|
||||
// between the material and these contents for the window's unfocused state, where the
|
||||
// system swaps the glass for a flat bright slab (see `inactiveGlassScrim`).
|
||||
.background(inactiveGlassScrim, in: .rect(cornerRadius: 16))
|
||||
.glassEffect(.regular.tint(composerGlassTint), in: .rect(cornerRadius: 16))
|
||||
.animation(.easeInOut(duration: 0.15), value: controlActiveState)
|
||||
// The glass renders but doesn't hit-test, so give the card a solid hit shape. It now
|
||||
// floats over live transcript, so this is load-bearing: without it a click in the gap
|
||||
// between two controls would land on the message underneath.
|
||||
.contentShape(.rect(cornerRadius: 16))
|
||||
// A soft drop shadow lifts the card off the prose behind it, matching the floating
|
||||
// panel's treatment at a calmer weight — this one is always on screen.
|
||||
.shadow(color: .black.opacity(0.18), radius: 12, x: 0, y: 4)
|
||||
// Match the transcript's centered column so the input area and its
|
||||
// accessories never extend wider than the chat text above them.
|
||||
.chatColumn(maxWidth: contentMaxWidth, inset: transcriptInset, resizeWidth: columnWidth, settling: transcriptSettling)
|
||||
// Stream this composer's draft to the mesh (throttled in the store) so every other
|
||||
// device viewing this chat sees it live and locks its own composer; an emptied field —
|
||||
// including the clear in `send()` — ends the typing and unlocks them.
|
||||
@@ -4134,6 +4135,7 @@ private struct AnimatedEllipsis: View {
|
||||
struct ApprovalBar: View {
|
||||
@Environment(AppStore.self) private var store
|
||||
@Environment(\.appPalette) private var palette
|
||||
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||
let request: ApprovalRequest
|
||||
/// When set, the request belongs to a specific (off-screen) session — the ephemeral popup
|
||||
/// window — so answers route there rather than to the single open session. nil = open session.
|
||||
@@ -4444,6 +4446,7 @@ struct ApprovalBar: View {
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(palette.attention.opacity(0.08), in: .rect(cornerRadius: 10))
|
||||
.overlay(RoundedRectangle(cornerRadius: 10).strokeBorder(palette.attention.opacity(0.25), lineWidth: 1))
|
||||
.animation(ComposerMotion.layout(reduceMotion), value: revisingPlan)
|
||||
.onChange(of: request.id) { _, _ in
|
||||
revisingPlan = false
|
||||
planRevision = ""
|
||||
@@ -4514,6 +4517,7 @@ private struct ApprovalDetailHeightKey: PreferenceKey {
|
||||
struct AskUserQuestionBar: View {
|
||||
@Environment(AppStore.self) private var store
|
||||
@Environment(\.appPalette) private var palette
|
||||
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||
let request: ApprovalRequest
|
||||
let questions: [AskUserQuestion.Question]
|
||||
/// See `ApprovalBar.sessionID` — routes answers to the ephemeral popup's session. nil = open.
|
||||
@@ -4546,6 +4550,8 @@ struct AskUserQuestionBar: View {
|
||||
}
|
||||
otherRow(question)
|
||||
}
|
||||
.id(step)
|
||||
.transition(.opacity)
|
||||
}
|
||||
HStack {
|
||||
Button("Deny") { respond(.deny(reason: "Denied from Nucleic")) }
|
||||
@@ -4570,6 +4576,7 @@ struct AskUserQuestionBar: View {
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(palette.attention.opacity(0.08), in: .rect(cornerRadius: 10))
|
||||
.overlay(RoundedRectangle(cornerRadius: 10).strokeBorder(palette.attention.opacity(0.25), lineWidth: 1))
|
||||
.animation(ComposerMotion.layout(reduceMotion), value: step)
|
||||
}
|
||||
|
||||
/// The question for the active step, or `nil` if the index is somehow out of range.
|
||||
|
||||
Reference in New Issue
Block a user