2026-07-02 04:42:59 -07:00
|
|
|
import ActivityKit
|
|
|
|
|
import SwiftUI
|
|
|
|
|
import WidgetKit
|
|
|
|
|
|
2026-07-05 00:28:21 -07:00
|
|
|
/// Lock-screen / Dynamic Island rendering of the aggregate session Activity (UX_IOS §5.3).
|
|
|
|
|
/// The glance answers "what is Nucleic doing right now?": headline counts (running / waiting /
|
|
|
|
|
/// approvals), the total churn in flight, and the most urgent sessions with their state and diff.
|
|
|
|
|
/// Tapping opens the app (straight to the approval, via the notification-style route).
|
|
|
|
|
///
|
|
|
|
|
/// Self-contained colors/glyphs — the extension doesn't link the app's `Theme`, so the small
|
|
|
|
|
/// palette + glyph maps it needs live here (the same teal/amber/blue/green as `Palette`).
|
2026-07-02 04:42:59 -07:00
|
|
|
struct SessionLiveActivity: Widget {
|
|
|
|
|
var body: some WidgetConfiguration {
|
|
|
|
|
ActivityConfiguration(for: NucleicSessionAttributes.self) { context in
|
|
|
|
|
LockScreenView(state: context.state, hostName: context.attributes.hostName)
|
2026-07-05 00:28:21 -07:00
|
|
|
.widgetURL(context.state.tapURL)
|
nvrsion: Add: Reconnect Icon Update, fix mesh iPhone discovery for new Macs, fix session order adjustment to pin sidebar sessions for remote projects, fix session status synchronization for remote viewer Macs, fix session order adjustment for iOS app session list, fix session status synchronization for remote sessions, fix iOS app live activity height by removing the “<n> files changed” line, fix Tool Call Card Mesh so AI bash summaries transit the mesh layer to other Macs, replace “Resolved by mac-ui” in transcripts with device ID, fix Chat: Print Wait Message, fix Chat: Reconnect Icon Update, fix Chat: Activity Theme Adjustment for “attention needed” live activity theme.
2026-07-05 04:24:25 -07:00
|
|
|
// When something needs the user, wash the whole card in the attention tint;
|
|
|
|
|
// otherwise the calm dark background (see the border/header cues in LockScreenView).
|
|
|
|
|
.activityBackgroundTint(context.state.needsAttention
|
|
|
|
|
? ActivityPalette.attentionTint(context.state).opacity(0.28)
|
|
|
|
|
: Color.black.opacity(0.55))
|
2026-07-02 04:42:59 -07:00
|
|
|
.activitySystemActionForegroundColor(.white)
|
|
|
|
|
} dynamicIsland: { context in
|
2026-07-05 00:28:21 -07:00
|
|
|
let state = context.state
|
|
|
|
|
return DynamicIsland {
|
2026-07-02 04:42:59 -07:00
|
|
|
DynamicIslandExpandedRegion(.leading) {
|
2026-07-05 00:28:21 -07:00
|
|
|
CountBadge(
|
|
|
|
|
systemImage: "circle.lefthalf.filled",
|
|
|
|
|
count: state.runningCount, tint: ActivityPalette.active)
|
2026-07-02 04:42:59 -07:00
|
|
|
}
|
|
|
|
|
DynamicIslandExpandedRegion(.trailing) {
|
2026-07-05 00:28:21 -07:00
|
|
|
if state.approvalCount > 0 {
|
|
|
|
|
CountBadge(
|
|
|
|
|
systemImage: "exclamationmark.shield.fill",
|
|
|
|
|
count: state.approvalCount, tint: ActivityPalette.attention)
|
|
|
|
|
} else if state.needsYouCount > 0 {
|
|
|
|
|
CountBadge(
|
|
|
|
|
systemImage: "bubble.left.fill",
|
|
|
|
|
count: state.needsYouCount, tint: ActivityPalette.accent)
|
2026-07-02 04:42:59 -07:00
|
|
|
} else {
|
|
|
|
|
Label("0", systemImage: "checkmark.circle")
|
2026-07-05 00:28:21 -07:00
|
|
|
.font(.headline).foregroundStyle(.secondary)
|
2026-07-02 04:42:59 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
DynamicIslandExpandedRegion(.bottom) {
|
2026-07-05 00:28:21 -07:00
|
|
|
VStack(alignment: .leading, spacing: 5) {
|
|
|
|
|
ForEach(state.lines.prefix(2)) { line in
|
|
|
|
|
SessionRow(line: line)
|
|
|
|
|
}
|
2026-07-02 04:42:59 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} compactLeading: {
|
2026-07-05 00:28:21 -07:00
|
|
|
Image(systemName: state.needsAttention
|
|
|
|
|
? ActivityPalette.glyph(.approval) : ActivityPalette.glyph(.running))
|
|
|
|
|
.foregroundStyle(state.needsAttention
|
|
|
|
|
? ActivityPalette.attention : ActivityPalette.active)
|
2026-07-02 04:42:59 -07:00
|
|
|
} compactTrailing: {
|
2026-07-05 00:28:21 -07:00
|
|
|
Text("\(state.needsAttention ? max(state.approvalCount, state.needsYouCount) : state.runningCount)")
|
|
|
|
|
.font(.caption2.bold())
|
|
|
|
|
.foregroundStyle(state.needsAttention ? ActivityPalette.attention : ActivityPalette.active)
|
2026-07-02 04:42:59 -07:00
|
|
|
} minimal: {
|
2026-07-05 00:28:21 -07:00
|
|
|
Image(systemName: state.needsAttention
|
|
|
|
|
? ActivityPalette.glyph(.approval) : ActivityPalette.glyph(.running))
|
|
|
|
|
.foregroundStyle(state.needsAttention
|
2026-07-02 04:42:59 -07:00
|
|
|
? ActivityPalette.attention : ActivityPalette.active)
|
|
|
|
|
}
|
2026-07-05 00:28:21 -07:00
|
|
|
// The compact/minimal presentations tap through to the same target as the lock
|
|
|
|
|
// screen: a waiting session opens itself; otherwise the sessions list.
|
|
|
|
|
.widgetURL(state.tapURL)
|
2026-07-02 04:42:59 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-05 00:28:21 -07:00
|
|
|
// MARK: - Lock screen
|
|
|
|
|
|
2026-07-02 04:42:59 -07:00
|
|
|
private struct LockScreenView: View {
|
|
|
|
|
let state: NucleicSessionAttributes.ContentState
|
|
|
|
|
let hostName: String
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
nvrsion: Add: Reconnect Icon Update, fix mesh iPhone discovery for new Macs, fix session order adjustment to pin sidebar sessions for remote projects, fix session status synchronization for remote viewer Macs, fix session order adjustment for iOS app session list, fix session status synchronization for remote sessions, fix iOS app live activity height by removing the “<n> files changed” line, fix Tool Call Card Mesh so AI bash summaries transit the mesh layer to other Macs, replace “Resolved by mac-ui” in transcripts with device ID, fix Chat: Print Wait Message, fix Chat: Reconnect Icon Update, fix Chat: Activity Theme Adjustment for “attention needed” live activity theme.
2026-07-05 04:24:25 -07:00
|
|
|
let attention = state.needsAttention
|
|
|
|
|
let tint = ActivityPalette.attentionTint(state)
|
|
|
|
|
return VStack(alignment: .leading, spacing: 8) {
|
|
|
|
|
// Header: brand + host, and the headline counts. When the user is needed the brand
|
|
|
|
|
// mark becomes the attention glyph in its tint, so the header itself reads "act now".
|
2026-07-05 00:28:21 -07:00
|
|
|
HStack(spacing: 8) {
|
nvrsion: Add: Reconnect Icon Update, fix mesh iPhone discovery for new Macs, fix session order adjustment to pin sidebar sessions for remote projects, fix session status synchronization for remote viewer Macs, fix session order adjustment for iOS app session list, fix session status synchronization for remote sessions, fix iOS app live activity height by removing the “<n> files changed” line, fix Tool Call Card Mesh so AI bash summaries transit the mesh layer to other Macs, replace “Resolved by mac-ui” in transcripts with device ID, fix Chat: Print Wait Message, fix Chat: Reconnect Icon Update, fix Chat: Activity Theme Adjustment for “attention needed” live activity theme.
2026-07-05 04:24:25 -07:00
|
|
|
Image(systemName: attention
|
|
|
|
|
? ActivityPalette.glyph(state.approvalCount > 0 ? .approval : .needsInput)
|
|
|
|
|
: "hexagon.fill")
|
2026-07-05 00:28:21 -07:00
|
|
|
.font(.footnote)
|
nvrsion: Add: Reconnect Icon Update, fix mesh iPhone discovery for new Macs, fix session order adjustment to pin sidebar sessions for remote projects, fix session status synchronization for remote viewer Macs, fix session order adjustment for iOS app session list, fix session status synchronization for remote sessions, fix iOS app live activity height by removing the “<n> files changed” line, fix Tool Call Card Mesh so AI bash summaries transit the mesh layer to other Macs, replace “Resolved by mac-ui” in transcripts with device ID, fix Chat: Print Wait Message, fix Chat: Reconnect Icon Update, fix Chat: Activity Theme Adjustment for “attention needed” live activity theme.
2026-07-05 04:24:25 -07:00
|
|
|
.foregroundStyle(attention ? tint : ActivityPalette.active)
|
2026-07-05 00:28:21 -07:00
|
|
|
Text(hostName)
|
|
|
|
|
.font(.caption.weight(.semibold))
|
|
|
|
|
.lineLimit(1)
|
|
|
|
|
Spacer(minLength: 8)
|
|
|
|
|
CountChips(state: state)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The most urgent sessions — what's actually happening.
|
|
|
|
|
ForEach(state.lines.prefix(3)) { line in
|
|
|
|
|
SessionRow(line: line)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.padding(14)
|
|
|
|
|
.foregroundStyle(.white)
|
nvrsion: Add: Reconnect Icon Update, fix mesh iPhone discovery for new Macs, fix session order adjustment to pin sidebar sessions for remote projects, fix session status synchronization for remote viewer Macs, fix session order adjustment for iOS app session list, fix session status synchronization for remote sessions, fix iOS app live activity height by removing the “<n> files changed” line, fix Tool Call Card Mesh so AI bash summaries transit the mesh layer to other Macs, replace “Resolved by mac-ui” in transcripts with device ID, fix Chat: Print Wait Message, fix Chat: Reconnect Icon Update, fix Chat: Activity Theme Adjustment for “attention needed” live activity theme.
2026-07-05 04:24:25 -07:00
|
|
|
// Attention theme: a soft tinted wash + a colored border around the whole glance, so a
|
|
|
|
|
// session waiting on the user is unmistakable at a glance even before reading the rows.
|
|
|
|
|
.background {
|
|
|
|
|
if attention {
|
|
|
|
|
RoundedRectangle(cornerRadius: 22, style: .continuous)
|
|
|
|
|
.fill(tint.opacity(0.12))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.overlay {
|
|
|
|
|
if attention {
|
|
|
|
|
RoundedRectangle(cornerRadius: 22, style: .continuous)
|
|
|
|
|
.strokeBorder(tint.opacity(0.9), lineWidth: 2)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-05 00:28:21 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Shared pieces
|
|
|
|
|
|
|
|
|
|
/// One session: state glyph + title + "project · Backend" + a right-aligned status/diff.
|
2026-07-05 02:42:46 -07:00
|
|
|
///
|
|
|
|
|
/// Each row is its own tap target: wrapped in a `Link` to `nucleic://session/<id>`, so tapping a
|
|
|
|
|
/// specific session on the lock screen or expanded Dynamic Island opens *that* session — overriding
|
|
|
|
|
/// the activity-wide `widgetURL` (which still governs the header, churn footer, and empty space, and
|
|
|
|
|
/// remains the only tap target the compact/minimal presentations can offer).
|
2026-07-05 00:28:21 -07:00
|
|
|
private struct SessionRow: View {
|
|
|
|
|
let line: NucleicSessionAttributes.SessionLine
|
|
|
|
|
|
2026-07-06 15:10:07 -07:00
|
|
|
/// An approval this row can resolve inline: it's blocked on an approval, carries that approval's
|
|
|
|
|
/// id, and isn't high-risk (§3.3 — high-risk keeps the plain tap-to-open row and is answered on
|
|
|
|
|
/// the app's guarded card). `nil` until the producer populates `approvalID` — an older host, or
|
|
|
|
|
/// today's summary that doesn't yet carry it, so the row stays a deep-link exactly as before.
|
|
|
|
|
private var inlineApprovalID: String? {
|
|
|
|
|
guard line.kind == .approval, (line.approvalIsHighRisk ?? false) == false else { return nil }
|
|
|
|
|
return line.approvalID
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-05 00:28:21 -07:00
|
|
|
var body: some View {
|
2026-07-06 15:10:07 -07:00
|
|
|
if let approvalID = inlineApprovalID {
|
|
|
|
|
actionableRow(approvalID: approvalID)
|
|
|
|
|
} else if let url = NucleicDeepLink.session(line.id) {
|
2026-07-05 02:42:46 -07:00
|
|
|
Link(destination: url) { rowContent }
|
|
|
|
|
} else {
|
|
|
|
|
rowContent
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 15:10:07 -07:00
|
|
|
/// An approval row with inline Allow/Deny. The identity still deep-links to the session; the
|
|
|
|
|
/// buttons fire `ApproveFromActivityIntent`, which the system performs in the app's background
|
|
|
|
|
/// process — allow/deny without unlocking (§4.1).
|
|
|
|
|
private func actionableRow(approvalID: String) -> some View {
|
|
|
|
|
HStack(spacing: 8) {
|
|
|
|
|
if let url = NucleicDeepLink.session(line.id) {
|
|
|
|
|
Link(destination: url) { rowIdentity }
|
|
|
|
|
} else {
|
|
|
|
|
rowIdentity
|
|
|
|
|
}
|
|
|
|
|
Spacer(minLength: 6)
|
|
|
|
|
ApprovalButtons(sessionID: line.id, approvalID: approvalID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The glyph + title + "project · Backend" — the row's identity, minus the trailing status.
|
|
|
|
|
private var rowIdentity: some View {
|
2026-07-05 00:28:21 -07:00
|
|
|
HStack(spacing: 8) {
|
|
|
|
|
Image(systemName: ActivityPalette.glyph(line.kind))
|
|
|
|
|
.font(.footnote)
|
|
|
|
|
.foregroundStyle(ActivityPalette.color(line.kind))
|
|
|
|
|
.frame(width: 16)
|
|
|
|
|
VStack(alignment: .leading, spacing: 1) {
|
|
|
|
|
Text(line.title)
|
|
|
|
|
.font(.subheadline.weight(.medium))
|
|
|
|
|
.lineLimit(1)
|
2026-07-02 04:42:59 -07:00
|
|
|
HStack(spacing: 4) {
|
2026-07-05 00:28:21 -07:00
|
|
|
Text(line.project)
|
|
|
|
|
Text("·").foregroundStyle(.secondary)
|
|
|
|
|
Text(line.backend.label)
|
|
|
|
|
.foregroundStyle(ActivityPalette.backendColor(line.backend))
|
2026-07-02 04:42:59 -07:00
|
|
|
}
|
2026-07-05 00:28:21 -07:00
|
|
|
.font(.caption2)
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
.lineLimit(1)
|
2026-07-02 04:42:59 -07:00
|
|
|
}
|
2026-07-06 15:10:07 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var rowContent: some View {
|
|
|
|
|
HStack(spacing: 8) {
|
|
|
|
|
rowIdentity
|
2026-07-05 00:28:21 -07:00
|
|
|
Spacer(minLength: 6)
|
|
|
|
|
Text(line.detail)
|
|
|
|
|
.font(.caption2.weight(.medium))
|
|
|
|
|
.monospacedDigit()
|
|
|
|
|
.foregroundStyle(ActivityPalette.color(line.kind))
|
2026-07-02 04:42:59 -07:00
|
|
|
.lineLimit(1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 15:10:07 -07:00
|
|
|
/// The compact inline Allow/Deny pair for an actionable approval row. Each button fires
|
|
|
|
|
/// `ApproveFromActivityIntent` (a low/medium-risk decision — high-risk never reaches here).
|
|
|
|
|
private struct ApprovalButtons: View {
|
|
|
|
|
let sessionID: String
|
|
|
|
|
let approvalID: String
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
HStack(spacing: 6) {
|
|
|
|
|
Button(intent: intent(allow: false)) {
|
|
|
|
|
Image(systemName: "xmark").font(.caption2.bold())
|
|
|
|
|
}
|
|
|
|
|
.tint(ActivityPalette.danger)
|
|
|
|
|
Button(intent: intent(allow: true)) {
|
|
|
|
|
Image(systemName: "checkmark").font(.caption2.bold())
|
|
|
|
|
}
|
|
|
|
|
.tint(ActivityPalette.success)
|
|
|
|
|
}
|
|
|
|
|
.buttonStyle(.borderedProminent)
|
|
|
|
|
.controlSize(.small)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func intent(allow: Bool) -> ApproveFromActivityIntent {
|
|
|
|
|
ApproveFromActivityIntent(
|
|
|
|
|
approvalID: approvalID, sessionID: sessionID, isHighRisk: false, allow: allow)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-05 00:28:21 -07:00
|
|
|
/// Headline count chips: running (blue), waiting (teal), approvals (amber). Only nonzero show.
|
|
|
|
|
private struct CountChips: View {
|
|
|
|
|
let state: NucleicSessionAttributes.ContentState
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
HStack(spacing: 10) {
|
|
|
|
|
if state.runningCount > 0 {
|
|
|
|
|
chip("circle.lefthalf.filled", state.runningCount, ActivityPalette.active)
|
|
|
|
|
}
|
|
|
|
|
if state.needsYouCount > 0 {
|
|
|
|
|
chip("bubble.left.fill", state.needsYouCount, ActivityPalette.accent)
|
|
|
|
|
}
|
|
|
|
|
if state.approvalCount > 0 {
|
|
|
|
|
chip("exclamationmark.shield.fill", state.approvalCount, ActivityPalette.attention)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func chip(_ systemImage: String, _ count: Int, _ tint: Color) -> some View {
|
|
|
|
|
HStack(spacing: 3) {
|
|
|
|
|
Image(systemName: systemImage).font(.caption2)
|
|
|
|
|
Text("\(count)").font(.caption.weight(.semibold)).monospacedDigit()
|
|
|
|
|
}
|
|
|
|
|
.foregroundStyle(tint)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Dynamic Island expanded leading/trailing count badge.
|
|
|
|
|
private struct CountBadge: View {
|
|
|
|
|
let systemImage: String
|
|
|
|
|
let count: Int
|
|
|
|
|
let tint: Color
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
Label("\(count)", systemImage: systemImage)
|
|
|
|
|
.font(.headline)
|
|
|
|
|
.monospacedDigit()
|
|
|
|
|
.foregroundStyle(tint)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The categorical colors + glyphs the Activity needs — the same semantics as the app's
|
|
|
|
|
/// `Palette.status` / `StatusStyle.symbol`, re-declared because the widget extension can't link
|
|
|
|
|
/// the app's `Theme`.
|
2026-07-02 04:42:59 -07:00
|
|
|
private enum ActivityPalette {
|
|
|
|
|
static let active = Color(red: 0.20, green: 0.55, blue: 0.95) // running blue
|
2026-07-05 00:28:21 -07:00
|
|
|
static let attention = Color(red: 1.00, green: 0.65, blue: 0.15) // approval amber
|
|
|
|
|
static let accent = Color(red: 0.16, green: 0.72, blue: 0.68) // needs-input teal
|
|
|
|
|
static let success = Color(red: 0.30, green: 0.78, blue: 0.45) // done green
|
|
|
|
|
static let danger = Color(red: 0.92, green: 0.34, blue: 0.34) // error red
|
|
|
|
|
static let neutral = Color.secondary
|
|
|
|
|
static let added = Color(red: 0.30, green: 0.78, blue: 0.45)
|
|
|
|
|
static let removed = Color(red: 0.92, green: 0.34, blue: 0.34)
|
|
|
|
|
|
nvrsion: Add: Reconnect Icon Update, fix mesh iPhone discovery for new Macs, fix session order adjustment to pin sidebar sessions for remote projects, fix session status synchronization for remote viewer Macs, fix session order adjustment for iOS app session list, fix session status synchronization for remote sessions, fix iOS app live activity height by removing the “<n> files changed” line, fix Tool Call Card Mesh so AI bash summaries transit the mesh layer to other Macs, replace “Resolved by mac-ui” in transcripts with device ID, fix Chat: Print Wait Message, fix Chat: Reconnect Icon Update, fix Chat: Activity Theme Adjustment for “attention needed” live activity theme.
2026-07-05 04:24:25 -07:00
|
|
|
/// The color the "attention needed" theme paints with: approval amber is loudest, else the
|
|
|
|
|
/// needs-input teal. Only meaningful when `state.needsAttention` — callers gate on that.
|
|
|
|
|
static func attentionTint(_ state: NucleicSessionAttributes.ContentState) -> Color {
|
|
|
|
|
state.approvalCount > 0 ? attention : accent
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-05 00:28:21 -07:00
|
|
|
static func glyph(_ kind: NucleicSessionAttributes.Kind) -> String {
|
|
|
|
|
switch kind {
|
|
|
|
|
case .running: "circle.lefthalf.filled"
|
|
|
|
|
case .provisioning: "circle.dotted"
|
|
|
|
|
case .approval: "exclamationmark.triangle.fill"
|
|
|
|
|
case .needsInput: "bubble.left.fill"
|
|
|
|
|
case .done: "checkmark.circle.fill"
|
|
|
|
|
case .error: "xmark.octagon.fill"
|
|
|
|
|
case .idle: "circle"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static func color(_ kind: NucleicSessionAttributes.Kind) -> Color {
|
|
|
|
|
switch kind {
|
|
|
|
|
case .running: active
|
|
|
|
|
case .provisioning: neutral
|
|
|
|
|
case .approval: attention
|
|
|
|
|
case .needsInput: accent
|
|
|
|
|
case .done: success
|
|
|
|
|
case .error: danger
|
|
|
|
|
case .idle: neutral
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static func backendColor(_ backend: NucleicSessionAttributes.Backend) -> Color {
|
|
|
|
|
switch backend {
|
|
|
|
|
case .claude: Color(red: 0.85, green: 0.52, blue: 0.30) // Anthropic clay
|
|
|
|
|
case .codex: Color(red: 0.45, green: 0.72, blue: 0.55) // OpenAI green
|
|
|
|
|
case .grok: Color(red: 0.55, green: 0.58, blue: 0.66) // xAI graphite
|
|
|
|
|
case .other: Color.secondary
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-02 04:42:59 -07:00
|
|
|
}
|