Merge nucleic/clever-lunar-newt-tfwt into dev

This commit is contained in:
2026-08-01 05:12:01 -07:00
parent a345664823
commit 6022e46ee6
6 changed files with 117 additions and 40 deletions
+69 -18
View File
@@ -22,7 +22,7 @@ enum AppTheme {
/// capsule drowns out the status dot and icons in the row; a gray lets that /// capsule drowns out the status dot and icons in the row; a gray lets that
/// per-row iconography read through while still marking the active chat. /// per-row iconography read through while still marking the active chat.
static let selection = dynamic( static let selection = dynamic(
light: NSColor(srgbRed: 155.0 / 255.0, green: 158.0 / 255.0, blue: 165.0 / 255.0, alpha: 1), light: NSColor(srgbRed: 0.86, green: 0.865, blue: 0.88, alpha: 1),
dark: NSColor(srgbRed: 0.34, green: 0.35, blue: 0.38, alpha: 1)) dark: NSColor(srgbRed: 0.34, green: 0.35, blue: 0.38, alpha: 1))
/// Subtle hairline for borders/dividers. /// Subtle hairline for borders/dividers.
static let hairline = dynamic( static let hairline = dynamic(
@@ -41,8 +41,14 @@ enum AppTheme {
/// a soft off-white on the deep dark background reads calmly at night instead /// 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. /// of glowing, and a near-black (not 0) keeps light mode from feeling harsh.
static let primaryText = dynamic( static let primaryText = dynamic(
light: NSColor(srgbRed: 0.24, green: 0.24, blue: 0.27, alpha: 1), 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)) dark: NSColor(srgbRed: 0.80, green: 0.81, blue: 0.83, alpha: 1))
/// Default label color for views that do not choose a semantic foreground explicitly.
/// Light mode uses the same softened near-black as prose; dark mode delegates to AppKit's
/// label color so applying this at the window root does not alter the existing dark theme.
static let defaultText = dynamic(
light: NSColor(srgbRed: 0.28, green: 0.28, blue: 0.31, alpha: 1),
dark: .labelColor)
/// The signature orchestra gold the orchestration mode's accent for the effort pill, /// 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 /// 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 /// in light mode (dark enough to read on white); a bright, warm gold in dark so it glows
@@ -259,6 +265,13 @@ struct AppPalette: Equatable {
/// the AI sidebar segment's snowflake/glow. /// the AI sidebar segment's snowflake/glow.
var frozen: Color var frozen: Color
var activityBase: Color var activityBase: Color
/// Higher-contrast text variants for the small categorical log lines in a transcript.
/// Fill/status colors can stay vivid in light mode, but those same bright values wash out
/// against the transcript background when used as text (amber is the clearest offender).
/// Their dark-mode side deliberately matches the existing night-softened palette.
var logAttention: Color
var logSuccess: Color
var logDanger: Color
static func make(_ mode: ColorVisionMode, controlled: Bool = false) -> AppPalette { static func make(_ mode: ColorVisionMode, controlled: Bool = false) -> AppPalette {
// Cached so the resolved (dark-softened) colors keep a stable identity // Cached so the resolved (dark-softened) colors keep a stable identity
@@ -296,40 +309,58 @@ struct AppPalette: Equatable {
private static func base(_ mode: ColorVisionMode) -> AppPalette { private static func base(_ mode: ColorVisionMode) -> AppPalette {
switch mode { switch mode {
case .standard: case .standard:
AppPalette( let attention = Color(red: 1.00, green: 0.65, blue: 0.15)
let success = Color(red: 0.30, green: 0.80, blue: 0.45)
let danger = Color(red: 0.92, green: 0.34, blue: 0.34)
return AppPalette(
accent: Color(red: 0.04, green: 0.52, blue: 0.50), // deep teal 4.5:1 on white text accent: Color(red: 0.04, green: 0.52, blue: 0.50), // deep teal 4.5:1 on white text
neutral: .gray, neutral: .gray,
active: Color(red: 0.20, green: 0.55, blue: 0.95), // blue active: Color(red: 0.20, green: 0.55, blue: 0.95), // blue
attention: Color(red: 1.00, green: 0.65, blue: 0.15), // amber attention: attention, // amber
success: Color(red: 0.30, green: 0.80, blue: 0.45), // green success: success, // green
paused: Color(red: 0.62, green: 0.49, blue: 0.93), // purple paused: Color(red: 0.62, green: 0.49, blue: 0.93), // purple
danger: Color(red: 0.92, green: 0.34, blue: 0.34), // red danger: danger, // red
frozen: Color(red: 0.26, green: 0.64, blue: 0.96), // cold "iced" blue frozen: Color(red: 0.26, green: 0.64, blue: 0.96), // cold "iced" blue
activityBase: Color(red: 0.30, green: 0.80, blue: 0.45)) activityBase: Color(red: 0.30, green: 0.80, blue: 0.45),
logAttention: textTone(light: Color(red: 0.61, green: 0.36, blue: 0.03), darkSource: attention),
logSuccess: textTone(light: Color(red: 0.12, green: 0.47, blue: 0.24), darkSource: success),
logDanger: textTone(light: Color(red: 0.70, green: 0.18, blue: 0.20), darkSource: danger))
case .deuteranopia, .protanopia: case .deuteranopia, .protanopia:
// OkabeIto-derived: avoids relying on redgreen separation. // OkabeIto-derived: avoids relying on redgreen separation.
AppPalette( let attention = Color(red: 0.90, green: 0.62, blue: 0.00)
let success = Color(red: 0.34, green: 0.71, blue: 0.91)
let danger = Color(red: 0.84, green: 0.37, blue: 0.00)
return AppPalette(
accent: Color(red: 0.00, green: 0.50, blue: 0.58), // deep teal 4.5:1 on white text accent: Color(red: 0.00, green: 0.50, blue: 0.58), // deep teal 4.5:1 on white text
neutral: .gray, neutral: .gray,
active: Color(red: 0.00, green: 0.45, blue: 0.70), // blue active: Color(red: 0.00, green: 0.45, blue: 0.70), // blue
attention: Color(red: 0.90, green: 0.62, blue: 0.00), // orange attention: attention, // orange
success: Color(red: 0.34, green: 0.71, blue: 0.91), // sky blue success: success, // sky blue
paused: Color(red: 0.80, green: 0.47, blue: 0.65), // reddish purple paused: Color(red: 0.80, green: 0.47, blue: 0.65), // reddish purple
danger: Color(red: 0.84, green: 0.37, blue: 0.00), // vermillion danger: danger, // vermillion
frozen: Color(red: 0.20, green: 0.58, blue: 0.86), // cool sky blue frozen: Color(red: 0.20, green: 0.58, blue: 0.86), // cool sky blue
activityBase: Color(red: 0.00, green: 0.45, blue: 0.70)) activityBase: Color(red: 0.00, green: 0.45, blue: 0.70),
logAttention: textTone(light: Color(red: 0.56, green: 0.35, blue: 0.00), darkSource: attention),
logSuccess: textTone(light: Color(red: 0.02, green: 0.39, blue: 0.59), darkSource: success),
logDanger: textTone(light: Color(red: 0.64, green: 0.25, blue: 0.00), darkSource: danger))
case .tritanopia: case .tritanopia:
// Blueyellow weak; redgreen is intact, so lean on those plus magenta. // Blueyellow weak; redgreen is intact, so lean on those plus magenta.
AppPalette( let attention = Color(red: 0.85, green: 0.20, blue: 0.46)
let success = Color(red: 0.00, green: 0.45, blue: 0.70)
let danger = Color(red: 0.79, green: 0.10, blue: 0.15)
return AppPalette(
accent: Color(red: 0.46, green: 0.33, blue: 0.78), // indigo-violet accent: Color(red: 0.46, green: 0.33, blue: 0.78), // indigo-violet
neutral: .gray, neutral: .gray,
active: Color(red: 0.00, green: 0.62, blue: 0.45), // green-teal active: Color(red: 0.00, green: 0.62, blue: 0.45), // green-teal
attention: Color(red: 0.85, green: 0.20, blue: 0.46), // magenta attention: attention, // magenta
success: Color(red: 0.00, green: 0.45, blue: 0.70), // blue success: success, // blue
paused: Color(red: 0.67, green: 0.36, blue: 0.66), // purple paused: Color(red: 0.67, green: 0.36, blue: 0.66), // purple
danger: Color(red: 0.79, green: 0.10, blue: 0.15), // red danger: danger, // red
frozen: Color(red: 0.30, green: 0.72, blue: 0.85), // light cyan (kept bright to stay legible) frozen: Color(red: 0.30, green: 0.72, blue: 0.85), // light cyan (kept bright to stay legible)
activityBase: Color(red: 0.00, green: 0.45, blue: 0.70)) activityBase: Color(red: 0.00, green: 0.45, blue: 0.70),
logAttention: textTone(light: Color(red: 0.63, green: 0.12, blue: 0.33), darkSource: attention),
logSuccess: textTone(light: Color(red: 0.00, green: 0.38, blue: 0.59), darkSource: success),
logDanger: textTone(light: Color(red: 0.66, green: 0.07, blue: 0.12), darkSource: danger))
} }
} }
@@ -347,7 +378,10 @@ struct AppPalette: Equatable {
paused: AppPalette.nightSoft(paused), paused: AppPalette.nightSoft(paused),
danger: AppPalette.nightSoft(danger), danger: AppPalette.nightSoft(danger),
frozen: AppPalette.nightSoft(frozen), frozen: AppPalette.nightSoft(frozen),
activityBase: AppPalette.nightSoft(activityBase)) activityBase: AppPalette.nightSoft(activityBase),
logAttention: logAttention,
logSuccess: logSuccess,
logDanger: logDanger)
} }
/// Light mode keeps `base`; dark mode gets a desaturated, dimmed variant. /// Light mode keeps `base`; dark mode gets a desaturated, dimmed variant.
@@ -364,6 +398,23 @@ struct AppPalette: Equatable {
}) })
} }
/// A text color that is deliberately darker in light mode while retaining the categorical
/// palette's existing softened color in dark mode.
private static func textTone(light: Color, darkSource: Color) -> Color {
guard let lightNS = NSColor(light).usingColorSpace(.sRGB),
let sourceNS = NSColor(darkSource).usingColorSpace(.sRGB)
else { return light }
var h: CGFloat = 0, s: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
sourceNS.getHue(&h, saturation: &s, brightness: &b, alpha: &a)
let dark = NSColor(hue: h,
saturation: s * 0.68,
brightness: min(b, 0.80) * 0.82,
alpha: a)
return Color(nsColor: NSColor(name: nil) { appearance in
AppearanceClassifier.isDark(appearance) ? dark : lightNS
})
}
/// The dot/accent color for a session's status, refined by the last turn's /// The dot/accent color for a session's status, refined by the last turn's
/// disposition: a finished-the-work turn reads as "done" (success) rather than the /// disposition: a finished-the-work turn reads as "done" (success) rather than the
/// calm "ready for you" accent used when the agent is genuinely waiting on you. /// calm "ready for you" accent used when the agent is genuinely waiting on you.
+15 -3
View File
@@ -70,10 +70,12 @@ struct BuildInfo {
/// locally-built / pre-prod copies, canary-yellow for canary, blue for beta, gold for a release candidate. The /// locally-built / pre-prod copies, canary-yellow for canary, blue for beta, gold for a release candidate. The
/// leading edge names the channel; the commit hash and an all-caps "EXPERIMENTAL" /// leading edge names the channel; the commit hash and an all-caps "EXPERIMENTAL"
/// badge sit on the trailing /// badge sit on the trailing
/// edge. The tint is kept translucent so it reads as a quiet marker rather than /// edge. Light mode uses a deeper, denser channel color so the strip does not wash out against
/// fighting the window background. Renders nothing for a shipping App Store release. /// the pale window; dark mode keeps the quieter translucent treatment. Renders nothing for a
/// shipping App Store release.
struct BuildBanner: View { struct BuildBanner: View {
var info: BuildInfo = .current var info: BuildInfo = .current
@Environment(\.colorScheme) private var colorScheme
var body: some View { var body: some View {
if let style = Style(channel: info.channel) { if let style = Style(channel: info.channel) {
@@ -102,7 +104,8 @@ struct BuildBanner: View {
// so the *toolbar* is the glassy header and this is just its colored // so the *toolbar* is the glassy header and this is just its colored
// backdrop. Giving the strip its own `glassEffect` drew a bright glass edge on // backdrop. Giving the strip its own `glassEffect` drew a bright glass edge on
// it; the toolbar's own glass is pinned visible in `RootView` instead. // it; the toolbar's own glass is pinned visible in `RootView` instead.
.background(style.tint.opacity(0.5)) .background(style.tint(for: colorScheme)
.opacity(colorScheme == .light ? 0.88 : 0.5))
.help(style.tooltip(version: info.version, build: info.buildLabel)) .help(style.tooltip(version: info.version, build: info.buildLabel))
} }
} }
@@ -110,6 +113,7 @@ struct BuildBanner: View {
/// Per-channel appearance. `nil` for `.release` so the banner disappears entirely. /// Per-channel appearance. `nil` for `.release` so the banner disappears entirely.
private struct Style { private struct Style {
var tint: Color var tint: Color
var lightTint: Color
var label: String var label: String
var icon: String var icon: String
// Most channels share the default glyph size; the canary bell-with-waves // Most channels share the default glyph size; the canary bell-with-waves
@@ -120,19 +124,23 @@ struct BuildBanner: View {
switch channel { switch channel {
case .local: case .local:
tint = Color(red: 0.78, green: 0.18, blue: 0.18) // red tint = Color(red: 0.78, green: 0.18, blue: 0.18) // red
lightTint = Color(red: 0.66, green: 0.12, blue: 0.12)
label = "Local Development Build" label = "Local Development Build"
icon = "hammer.fill" icon = "hammer.fill"
case .canary: case .canary:
tint = Color(red: 1.00, green: 0.87, blue: 0.00) // bright canary yellow tint = Color(red: 1.00, green: 0.87, blue: 0.00) // bright canary yellow
lightTint = Color(red: 0.48, green: 0.32, blue: 0.02)
label = "Canary Build" label = "Canary Build"
icon = "bell.and.waves.left.and.right.fill" icon = "bell.and.waves.left.and.right.fill"
iconSize = 13 iconSize = 13
case .beta: case .beta:
tint = Color(red: 0.13, green: 0.40, blue: 0.86) // blue tint = Color(red: 0.13, green: 0.40, blue: 0.86) // blue
lightTint = Color(red: 0.08, green: 0.29, blue: 0.68)
label = "Public Beta" label = "Public Beta"
icon = "person.3.fill" icon = "person.3.fill"
case .releaseCandidate: case .releaseCandidate:
tint = Color(red: 0.83, green: 0.65, blue: 0.12) // gold tint = Color(red: 0.83, green: 0.65, blue: 0.12) // gold
lightTint = Color(red: 0.48, green: 0.30, blue: 0.02)
label = "Release Candidate" label = "Release Candidate"
icon = "checkmark.seal.fill" icon = "checkmark.seal.fill"
case .release: case .release:
@@ -140,6 +148,10 @@ struct BuildBanner: View {
} }
} }
func tint(for colorScheme: ColorScheme) -> Color {
colorScheme == .light ? lightTint : tint
}
func tooltip(version: String?, build: String) -> String { func tooltip(version: String?, build: String) -> String {
let v = version.map { "\($0) " } ?? "" let v = version.map { "\($0) " } ?? ""
return "\(label)\(v)commit \(build). Not a release build." return "\(label)\(v)commit \(build). Not a release build."
@@ -323,7 +323,7 @@ enum SyntaxTheme {
/// Shared across every mode neutral, already-safe roles. /// Shared across every mode neutral, already-safe roles.
private static let plain = dynamic( private static let plain = dynamic(
light: NSColor(srgbRed: 0.13, green: 0.13, blue: 0.15, alpha: 1), 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)) dark: NSColor(srgbRed: 0.80, green: 0.81, blue: 0.83, alpha: 1))
private static let comment = dynamic( private static let comment = dynamic(
light: NSColor(srgbRed: 0.42, green: 0.48, blue: 0.52, alpha: 1), light: NSColor(srgbRed: 0.42, green: 0.48, blue: 0.52, alpha: 1),
+25 -12
View File
@@ -316,6 +316,9 @@ struct RootView: View {
// opaque fill, which is why its identical sidebar material reads as clear glass. // opaque fill, which is why its identical sidebar material reads as clear glass.
.background { WindowTranslucencyConfigurator() } .background { WindowTranslucencyConfigurator() }
.preferredColorScheme(appearance.colorScheme) .preferredColorScheme(appearance.colorScheme)
// Soften the default near-black used by unstyled labels and sidebar titles in light mode.
// Explicit semantic/categorical foreground styles below still take precedence.
.foregroundStyle(AppTheme.defaultText)
.dynamicTypeSize(textSize.dynamicTypeSize) .dynamicTypeSize(textSize.dynamicTypeSize)
.tint(palette.accent) .tint(palette.accent)
.environment(panels) .environment(panels)
@@ -1399,6 +1402,7 @@ private extension SessionSummary {
struct SessionRow: View { struct SessionRow: View {
@Environment(\.appPalette) private var palette @Environment(\.appPalette) private var palette
@Environment(\.colorScheme) private var colorScheme
let summary: SessionSummary let summary: SessionSummary
var isSelected: Bool = false var isSelected: Bool = false
/// Blocked in the `LockManager` queue the status dot becomes an hourglass so the /// Blocked in the `LockManager` queue the status dot becomes an hourglass so the
@@ -1427,6 +1431,16 @@ struct SessionRow: View {
/// Nil under a remote project header (the header's globe already says it) and for local rows. /// Nil under a remote project header (the header's globe already says it) and for local rows.
var remoteHostName: String? = nil var remoteHostName: String? = nil
/// The dark selection still needs light glyphs. The much softer light selection lets each
/// glyph keep its normal semantic color instead of turning into a low-contrast white mark.
private var selectionNeutral: Color {
isSelected && colorScheme == .dark ? .white : .secondary
}
private var selectionAccent: Color {
isSelected && colorScheme == .dark ? .white : palette.accent
}
var body: some View { var body: some View {
HStack(spacing: 6) { HStack(spacing: 6) {
if isWaitingForLock { if isWaitingForLock {
@@ -1483,18 +1497,16 @@ struct SessionRow: View {
// A chat running on another device a peer Mac or a Covalence Cloud runner // A chat running on another device a peer Mac or a Covalence Cloud runner
// folded under this Mac's section for the same project (every device holds // folded under this Mac's section for the same project (every device holds
// the same projects, so the header is shared and the row says where it runs). // the same projects, so the header is shared and the row says where it runs).
// White on the selection highlight so it stays legible.
Image(systemName: "globe").font(.system(size: 8)) Image(systemName: "globe").font(.system(size: 8))
.foregroundStyle(isSelected ? Color.white : .secondary) .foregroundStyle(selectionNeutral)
.help("Runs on \(foldedHost) — mirrored live over the mesh") .help("Runs on \(foldedHost) — mirrored live over the mesh")
.accessibilityLabel("On \(foldedHost)") .accessibilityLabel("On \(foldedHost)")
} }
if movedToName == nil, summary.auto { if movedToName == nil, summary.auto {
// Against the mid-gray selection highlight the saturated teal // The deep dark-mode selection needs white; the subtle light-mode
// accent nearly disappears; switch to high-contrast white when // selection leaves the semantic accent legible as-is.
// the row is selected so the bolt stays legible.
Image(systemName: "bolt.fill").font(.system(size: 8)) Image(systemName: "bolt.fill").font(.system(size: 8))
.foregroundStyle(isSelected ? Color.white : palette.accent) .foregroundStyle(selectionAccent)
} }
if movedToName == nil, isOrchestraActive { if movedToName == nil, isOrchestraActive {
// Orchestration mode marker, sat beside the Auto bolt. Always the // Orchestration mode marker, sat beside the Auto bolt. Always the
@@ -1511,11 +1523,11 @@ struct SessionRow: View {
// and nothing's happened since a glanceable marker, sat directly // and nothing's happened since a glanceable marker, sat directly
// beside the Auto bolt. Cleared once a new turn lands, or by a // beside the Auto bolt. Cleared once a new turn lands, or by a
// later failed/conflict autoship (which never lights this). White // later failed/conflict autoship (which never lights this). White
// on the selection highlight so it stays legible. Suppressed when // on the dark selection highlight so it stays legible. Suppressed when
// autoship failed or a conflict is pending (the warning below takes // autoship failed or a conflict is pending (the warning below takes
// precedence). // precedence).
Image(systemName: "shippingbox.fill").font(.system(size: 8)) Image(systemName: "shippingbox.fill").font(.system(size: 8))
.foregroundStyle(isSelected ? Color.white : palette.accent) .foregroundStyle(selectionAccent)
.accessibilityLabel("Autoship ran") .accessibilityLabel("Autoship ran")
} }
} }
@@ -1544,10 +1556,11 @@ struct SessionRow: View {
} }
if movedToName == nil, summary.unseenCompletion { if movedToName == nil, summary.unseenCompletion {
// Unread dot: the chat finished its work and the user hasn't opened it // Unread dot: the chat finished its work and the user hasn't opened it
// yet. Cleared the moment they do. White on the selection highlight so // yet. Cleared the moment they do. It keeps the semantic accent on the soft
// it stays legible (the row can't be both selected and unread for long, // light selection and switches to white on the deeper dark selection (the row
// but selection can land before the open-driven clear). // can't be both selected and unread for long, but selection can land before the
Circle().fill(isSelected ? Color.white : palette.accent) // open-driven clear).
Circle().fill(selectionAccent)
.frame(width: 7, height: 7) .frame(width: 7, height: 7)
.accessibilityLabel("Unread — completed") .accessibilityLabel("Unread — completed")
} }
+1
View File
@@ -72,6 +72,7 @@ struct SettingsView: View {
// flexible. // flexible.
.frame(minWidth: 840, idealWidth: 840, maxWidth: 840, minHeight: 560, maxHeight: .infinity) .frame(minWidth: 840, idealWidth: 840, maxWidth: 840, minHeight: 560, maxHeight: .infinity)
.preferredColorScheme(appearance.colorScheme) .preferredColorScheme(appearance.colorScheme)
.foregroundStyle(AppTheme.defaultText)
.dynamicTypeSize(textSize.dynamicTypeSize) .dynamicTypeSize(textSize.dynamicTypeSize)
.background(SettingsWindowConfigurator(reapplyToken: (selection ?? .general).rawValue)) .background(SettingsWindowConfigurator(reapplyToken: (selection ?? .general).rawValue))
.onAppear { if selection == nil { selection = SettingsPane(rawValue: storedPane) ?? .general } } .onAppear { if selection == nil { selection = SettingsPane(rawValue: storedPane) ?? .general } }
+6 -6
View File
@@ -737,7 +737,7 @@ struct TranscriptRow: View, Equatable {
finished: true, finished: true,
worktreeRoot: store.openSession?.worktreePath) worktreeRoot: store.openSession?.worktreePath)
case .approvalRequested(let request): case .approvalRequested(let request):
label("pause.circle", "Approval requested: \(request.title)", palette.attention) label("pause.circle", "Approval requested: \(request.title)", palette.logAttention)
case .approvalResolved(let resolved): case .approvalResolved(let resolved):
resolvedByLabel(resolved.decidedBy) resolvedByLabel(resolved.decidedBy)
case .usage(let usage): case .usage(let usage):
@@ -761,13 +761,13 @@ struct TranscriptRow: View, Equatable {
switch finished.outcome { switch finished.outcome {
case .interrupted: case .interrupted:
// A deliberate manual stop, not a failure: stop-sign octagon in amber. // A deliberate manual stop, not a failure: stop-sign octagon in amber.
label("octagon", "Run stopped", palette.attention) label("octagon", "Run stopped", palette.logAttention)
case .errored: case .errored:
label("exclamationmark.triangle", "Run errored", palette.danger) label("exclamationmark.triangle", "Run errored", palette.logDanger)
case .completed: case .completed:
label("flag.checkered", "Run completed", palette.success) label("flag.checkered", "Run completed", palette.logSuccess)
case .maxTurns: case .maxTurns:
label("flag.checkered", "Run maxTurns", palette.success) label("flag.checkered", "Run maxTurns", palette.logSuccess)
} }
} }
case .error(let error): case .error(let error):
@@ -777,7 +777,7 @@ struct TranscriptRow: View, Equatable {
if Self.isAuthError(error.message) { if Self.isAuthError(error.message) {
AuthErrorRow(rawMessage: error.message, onLogin: onLogin) AuthErrorRow(rawMessage: error.message, onLogin: onLogin)
} else { } else {
label("exclamationmark.triangle", error.message, palette.danger) label("exclamationmark.triangle", error.message, palette.logDanger)
} }
case .note(let note) where note.vmMonitor: case .note(let note) where note.vmMonitor:
// A "VM opened" note reads as a one-click prompt to surface the monitor panel for this // A "VM opened" note reads as a one-click prompt to surface the monitor panel for this