Merge nucleic/nimble-velvet-toad-86ur into dev

This commit is contained in:
2026-08-02 01:12:30 -07:00
parent 174a984fd2
commit aeac392d83
3 changed files with 55 additions and 16 deletions
+39 -3
View File
@@ -50,7 +50,12 @@ enum AppTheme {
/// Primary prose color for chat text. Deliberately *not* pure black/white:
/// a soft off-white on the deep dark background reads calmly at night instead
/// of glowing, and a near-black (not 0) keeps light mode from feeling harsh.
static let primaryText = dynamic(
static let primaryText = Color(nsColor: primaryTextNSColor)
/// The `NSColor` behind ``primaryText``, for AppKit-hosted text that has to match
/// SwiftUI-rendered prose exactly the composer's `NSTextView`, whose default `.labelColor`
/// is a full-strength black in light mode and so typed noticeably darker than the transcript
/// it sits under.
static let primaryTextNSColor = dynamicNSColor(
light: NSColor(srgbRed: 0.28, green: 0.28, blue: 0.31, alpha: 1),
dark: NSColor(srgbRed: 0.80, green: 0.81, blue: 0.83, alpha: 1),
contrastLight: NSColor(srgbRed: 0.06, green: 0.06, blue: 0.08, alpha: 1),
@@ -92,6 +97,25 @@ enum AppTheme {
dark: .labelColor,
contrastLight: NSColor(srgbRed: 0.10, green: 0.10, blue: 0.13, alpha: 1),
contrastDark: NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1))
/// Fill behind the in-session composer's chrome: the text field's well, the small bordered
/// controls around it (Model / Effort / Auto / Merge), and the queued-message and
/// remote-typing pills. Replaces the `.quaternary.opacity(0.5)` these each used to spell out
/// a hierarchical style derived from the window-wide `defaultText`, which in light mode
/// resolved to roughly a 10%-alpha near-black and left the whole cluster barely detached from
/// the pale detail background. A little more ink gives each control a visible edge without
/// making the composer a gray slab. Dark keeps the value it already resolved to.
static let composerFill = dynamic(
light: NSColor(srgbRed: 0.28, green: 0.28, blue: 0.31, alpha: 0.145),
dark: NSColor(white: 1, alpha: 0.10),
contrastLight: NSColor(srgbRed: 0.28, green: 0.28, blue: 0.31, alpha: 0.26),
contrastDark: NSColor(white: 1, alpha: 0.20))
/// The 1pt border drawn around those same composer controls the bare `.quaternary` beside
/// ``composerFill``. Darkened in step with it so the outline keeps leading the fill.
static let composerStroke = dynamic(
light: NSColor(srgbRed: 0.28, green: 0.28, blue: 0.31, alpha: 0.28),
dark: NSColor(white: 1, alpha: 0.20),
contrastLight: NSColor(srgbRed: 0.28, green: 0.28, blue: 0.31, alpha: 0.48),
contrastDark: NSColor(white: 1, alpha: 0.40))
/// The signature orchestra gold the orchestration mode's accent for the effort pill,
/// the composer's animated glow, and the multi-agent transcript cards. A rich amber-gold
/// in light mode (dark enough to read on white); a bright, warm gold in dark so it glows
@@ -112,13 +136,25 @@ enum AppTheme {
contrastLight: NSColor? = nil,
contrastDark: NSColor? = nil
) -> Color {
Color(nsColor: NSColor(name: nil) { appearance in
Color(nsColor: dynamicNSColor(
light: light, dark: dark, contrastLight: contrastLight, contrastDark: contrastDark))
}
/// ``dynamic`` before the SwiftUI wrapper for the AppKit views that need the `NSColor`
/// itself (see ``primaryTextNSColor``).
fileprivate static func dynamicNSColor(
light: NSColor,
dark: NSColor,
contrastLight: NSColor? = nil,
contrastDark: NSColor? = nil
) -> NSColor {
NSColor(name: nil) { appearance in
let isDark = AppearanceClassifier.isDark(appearance)
if HighContrast.isEnabled, let boosted = isDark ? contrastDark : contrastLight {
return boosted
}
return isDark ? dark : light
})
}
}
}
+5 -2
View File
@@ -129,8 +129,11 @@ struct ChatInputField: NSViewRepresentable {
textView.isRichText = false
textView.allowsUndo = true
textView.font = .systemFont(ofSize: NSFont.systemFontSize)
textView.textColor = .labelColor
textView.insertionPointColor = .labelColor
// Match the transcript's prose exactly rather than using `.labelColor`, which is a
// full-strength black in light mode noticeably darker than the messages above, so a
// draft read heavier than the conversation it was about to join.
textView.textColor = AppTheme.primaryTextNSColor
textView.insertionPointColor = AppTheme.primaryTextNSColor
textView.drawsBackground = false
textView.backgroundColor = .clear
textView.textContainerInset = NSSize(width: 0, height: 2)
+11 -11
View File
@@ -631,8 +631,8 @@ struct SessionDetailView: View {
}
.menuStyle(.button)
.buttonStyle(.plain)
.background(.quaternary.opacity(0.5), in: .rect(cornerRadius: 6))
.overlay(RoundedRectangle(cornerRadius: 6).strokeBorder(.quaternary, lineWidth: 1))
.background(AppTheme.composerFill, in: .rect(cornerRadius: 6))
.overlay(RoundedRectangle(cornerRadius: 6).strokeBorder(AppTheme.composerStroke, lineWidth: 1))
.fixedSize()
.help("Model for the next turn")
}
@@ -700,9 +700,9 @@ struct SessionDetailView: View {
}
.menuStyle(.button)
.buttonStyle(.plain)
.background(.quaternary.opacity(0.5), in: .rect(cornerRadius: 6))
.background(AppTheme.composerFill, in: .rect(cornerRadius: 6))
.overlay(RoundedRectangle(cornerRadius: 6).strokeBorder(
isOrchestra ? AnyShapeStyle(AppTheme.orchestra.opacity(0.6)) : AnyShapeStyle(.quaternary),
isOrchestra ? AnyShapeStyle(AppTheme.orchestra.opacity(0.6)) : AnyShapeStyle(AppTheme.composerStroke),
lineWidth: 1))
.fixedSize()
.help(isOrchestra ? ModelCatalog.orchestraBlurb : "Reasoning effort for the next turn")
@@ -1145,8 +1145,8 @@ struct SessionDetailView: View {
}
.buttonStyle(.plain)
.disabled(locked)
.background(.quaternary.opacity(0.5), in: .rect(cornerRadius: 6))
.overlay(RoundedRectangle(cornerRadius: 6).strokeBorder(.quaternary, lineWidth: 1))
.background(AppTheme.composerFill, in: .rect(cornerRadius: 6))
.overlay(RoundedRectangle(cornerRadius: 6).strokeBorder(AppTheme.composerStroke, lineWidth: 1))
.fixedSize()
.help(locked
? lockedHelp
@@ -1216,8 +1216,8 @@ struct SessionDetailView: View {
.transition(.move(edge: .leading).combined(with: .opacity))
}
}
.background(.quaternary.opacity(0.5), in: .rect(cornerRadius: 6))
.overlay(RoundedRectangle(cornerRadius: 6).strokeBorder(.quaternary, lineWidth: 1))
.background(AppTheme.composerFill, in: .rect(cornerRadius: 6))
.overlay(RoundedRectangle(cornerRadius: 6).strokeBorder(AppTheme.composerStroke, lineWidth: 1))
.fixedSize()
}
@@ -2802,7 +2802,7 @@ struct SessionDetailView: View {
? { stepSessionIntelligence($0) } : nil)
.frame(height: composerHeight)
.padding(8)
.background(.quaternary.opacity(0.5), in: .rect(cornerRadius: 8))
.background(AppTheme.composerFill, in: .rect(cornerRadius: 8))
// Light up the field's border while a file/image drag hovers it.
.overlay {
if composerDropTargeted {
@@ -3029,7 +3029,7 @@ struct SessionDetailView: View {
}
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(.quaternary.opacity(0.5), in: .rect(cornerRadius: 12))
.background(AppTheme.composerFill, in: .rect(cornerRadius: 12))
.help("Someone is composing a message for this chat on another device. The composer unlocks when they send or stop.")
}
@@ -3072,7 +3072,7 @@ struct SessionDetailView: View {
.font(.callout)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(.quaternary.opacity(0.5), in: .rect(cornerRadius: 12))
.background(AppTheme.composerFill, in: .rect(cornerRadius: 12))
.help("This message will send automatically when the agent finishes its current turn.")
}