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
|
/// 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
|
/// (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
|
/// 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)
|
return .black.opacity(0.42)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The pane's floating bottom furniture: a pending approval card (when there is one) above the
|
/// The pane's floating bottom furniture. A pending permission/question is rendered inside the
|
||||||
/// composer, both hovering over the transcript on glass. Measured as one block — the height the
|
/// composer's glass shell, so it reads as the composer growing an input section rather than a
|
||||||
/// transcript reserves beneath its last message and anchors its fade to — so the two stay in
|
/// separate card appearing above it. The whole shell is measured as one block — the height the
|
||||||
/// step however the cluster grows.
|
/// 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 {
|
private var floatingBottomCluster: some View {
|
||||||
VStack(spacing: 8) {
|
VStack(spacing: 0) {
|
||||||
if let approval = store.openApprovals.first {
|
if let approval = store.openApprovals.first {
|
||||||
Group {
|
Group {
|
||||||
if approval.toolName == AskUserQuestion.toolName,
|
if approval.toolName == AskUserQuestion.toolName,
|
||||||
@@ -2933,17 +2934,34 @@ struct SessionDetailView: View {
|
|||||||
ApprovalBar(request: approval)
|
ApprovalBar(request: approval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Both bars are lightly tinted cards — legible over the pane, but not over live
|
// Keep the attention-tinted request as a focused inset, but let the composer own
|
||||||
// prose — so floating them puts them on the same glass as the composer below,
|
// the only outer material, corners, and shadow. Its stable identity also resets a
|
||||||
// with their own tint and border riding on top of it.
|
// question wizard cleanly when one pending request is replaced by another.
|
||||||
.padding(8)
|
.padding(.horizontal, 8)
|
||||||
.glassEffect(.regular, in: .rect(cornerRadius: 16))
|
.padding(.top, 8)
|
||||||
.contentShape(.rect(cornerRadius: 16))
|
.id(approval.id)
|
||||||
.shadow(color: .black.opacity(0.18), radius: 12, x: 0, y: 4)
|
.transition(ComposerMotion.inputTransition(reduceMotion))
|
||||||
.chatColumn(maxWidth: contentMaxWidth, inset: transcriptInset, resizeWidth: columnWidth, settling: transcriptSettling)
|
|
||||||
}
|
}
|
||||||
composer
|
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)
|
.padding(.bottom, 12)
|
||||||
// The one measurement both the transcript's tail spacer and its fade are placed from.
|
// 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 }
|
.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
|
// 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.
|
// as padding, so matching the top's inset there left the card bottom-heavy.
|
||||||
.padding(.horizontal, 14)
|
.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)
|
.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
|
// 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 —
|
// 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.
|
// including the clear in `send()` — ends the typing and unlocks them.
|
||||||
@@ -4134,6 +4135,7 @@ private struct AnimatedEllipsis: View {
|
|||||||
struct ApprovalBar: View {
|
struct ApprovalBar: View {
|
||||||
@Environment(AppStore.self) private var store
|
@Environment(AppStore.self) private var store
|
||||||
@Environment(\.appPalette) private var palette
|
@Environment(\.appPalette) private var palette
|
||||||
|
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||||
let request: ApprovalRequest
|
let request: ApprovalRequest
|
||||||
/// When set, the request belongs to a specific (off-screen) session — the ephemeral popup
|
/// 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.
|
/// 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)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.background(palette.attention.opacity(0.08), in: .rect(cornerRadius: 10))
|
.background(palette.attention.opacity(0.08), in: .rect(cornerRadius: 10))
|
||||||
.overlay(RoundedRectangle(cornerRadius: 10).strokeBorder(palette.attention.opacity(0.25), lineWidth: 1))
|
.overlay(RoundedRectangle(cornerRadius: 10).strokeBorder(palette.attention.opacity(0.25), lineWidth: 1))
|
||||||
|
.animation(ComposerMotion.layout(reduceMotion), value: revisingPlan)
|
||||||
.onChange(of: request.id) { _, _ in
|
.onChange(of: request.id) { _, _ in
|
||||||
revisingPlan = false
|
revisingPlan = false
|
||||||
planRevision = ""
|
planRevision = ""
|
||||||
@@ -4514,6 +4517,7 @@ private struct ApprovalDetailHeightKey: PreferenceKey {
|
|||||||
struct AskUserQuestionBar: View {
|
struct AskUserQuestionBar: View {
|
||||||
@Environment(AppStore.self) private var store
|
@Environment(AppStore.self) private var store
|
||||||
@Environment(\.appPalette) private var palette
|
@Environment(\.appPalette) private var palette
|
||||||
|
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||||
let request: ApprovalRequest
|
let request: ApprovalRequest
|
||||||
let questions: [AskUserQuestion.Question]
|
let questions: [AskUserQuestion.Question]
|
||||||
/// See `ApprovalBar.sessionID` — routes answers to the ephemeral popup's session. nil = open.
|
/// See `ApprovalBar.sessionID` — routes answers to the ephemeral popup's session. nil = open.
|
||||||
@@ -4546,6 +4550,8 @@ struct AskUserQuestionBar: View {
|
|||||||
}
|
}
|
||||||
otherRow(question)
|
otherRow(question)
|
||||||
}
|
}
|
||||||
|
.id(step)
|
||||||
|
.transition(.opacity)
|
||||||
}
|
}
|
||||||
HStack {
|
HStack {
|
||||||
Button("Deny") { respond(.deny(reason: "Denied from Nucleic")) }
|
Button("Deny") { respond(.deny(reason: "Denied from Nucleic")) }
|
||||||
@@ -4570,6 +4576,7 @@ struct AskUserQuestionBar: View {
|
|||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.background(palette.attention.opacity(0.08), in: .rect(cornerRadius: 10))
|
.background(palette.attention.opacity(0.08), in: .rect(cornerRadius: 10))
|
||||||
.overlay(RoundedRectangle(cornerRadius: 10).strokeBorder(palette.attention.opacity(0.25), lineWidth: 1))
|
.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.
|
/// The question for the active step, or `nil` if the index is somehow out of range.
|
||||||
|
|||||||
Reference in New Issue
Block a user