Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions app/AgentHub/AgentHubApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ struct AgentHubApp: App {

Settings {
SettingsView()
.agentHub(appDelegate.provider)
}
}
}
11 changes: 10 additions & 1 deletion app/modules/AgentHubCore/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions app/modules/AgentHubCore/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ let package = Package(
.package(url: "https://github.com/gonzalezreal/swift-markdown-ui", from: "2.0.0"),
.package(url: "https://github.com/groue/GRDB.swift", from: "6.24.0"),
.package(url: "https://github.com/appstefan/HighlightSwift", from: "1.1.0"),
.package(url: "https://github.com/jpsim/Yams", from: "5.0.0"),
],
targets: [
.target(
Expand All @@ -31,8 +32,12 @@ let package = Package(
.product(name: "MarkdownUI", package: "swift-markdown-ui"),
.product(name: "GRDB", package: "GRDB.swift"),
.product(name: "HighlightSwift", package: "HighlightSwift"),
.product(name: "Yams", package: "Yams"),
],
path: "Sources/AgentHub",
resources: [
.copy("Design/Theme/BundledThemes")
],
swiftSettings: [
.swiftLanguageMode(.v5)
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,25 @@ public enum AgentHubDefaults {
/// Type: String (default: "#64748B")
public static let customTertiaryHex = "\(keyPrefix)theme.customTertiaryHex"

/// Active YAML theme primary color hex value cache
/// Type: String (default: unset)
public static let yamlPrimaryHex = "\(keyPrefix)theme.yamlPrimaryHex"

/// Active YAML theme secondary color hex value cache
/// Type: String (default: unset)
public static let yamlSecondaryHex = "\(keyPrefix)theme.yamlSecondaryHex"

/// Active YAML theme tertiary color hex value cache
/// Type: String (default: unset)
public static let yamlTertiaryHex = "\(keyPrefix)theme.yamlTertiaryHex"

/// Installed bundled theme version for a given theme name
/// Type: String (default: unset)
/// Usage: `UserDefaults.standard.string(forKey: AgentHubDefaults.installedBundledThemeVersion(for: "sentry"))`
public static func installedBundledThemeVersion(for themeName: String) -> String {
"\(keyPrefix)theme.installedVersion.\(themeName)"
}

// MARK: - Migration

/// Legacy keys mapping for migration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,20 @@ extension EnvironmentValues {
/// View modifier that injects AgentHub provider into the environment
private struct AgentHubModifier: ViewModifier {
let provider: AgentHubProvider
let themeManager: ThemeManager

init(provider: AgentHubProvider) {
self.provider = provider
self.themeManager = provider.themeManager
}

func body(content: Content) -> some View {
content
.environment(\.agentHub, provider)
.environment(provider.statsService)
.environment(provider.displaySettings)
.environment(themeManager)
.environment(\.runtimeTheme, themeManager.currentTheme)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,13 @@ public final class AgentHubProvider {
}
}()

// MARK: - Theme Management

/// Theme manager for YAML and built-in themes
public private(set) lazy var themeManager: ThemeManager = {
ThemeManager()
}()

// MARK: - View Models

/// Claude sessions view model - created lazily and cached
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ public enum AgentHubLayout {

private struct AgentHubPanelModifier: ViewModifier {
@Environment(\.colorScheme) private var colorScheme
@Environment(\.runtimeTheme) private var runtimeTheme

func body(content: Content) -> some View {
// Simple black/white background based on color scheme
let backgroundColor = colorScheme == .dark ? Color.black : Color.white
let defaultBackground = colorScheme == .dark ? Color.black : Color.white
let backgroundColor = runtimeTheme?.hasCustomBackgrounds == true
? Color.adaptiveBackground(for: colorScheme, theme: runtimeTheme)
: defaultBackground

return content
.background(
Expand Down
141 changes: 103 additions & 38 deletions app/modules/AgentHubCore/Sources/AgentHub/Design/Color+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,28 @@ extension Color {
getCurrentThemeColors().brandTertiary
}

// MARK: - Runtime Theme Support

/// Get brand colors from runtime theme or fallback to UserDefaults
public static func brandPrimary(from theme: RuntimeTheme?) -> Color {
theme?.brandPrimary ?? brandPrimary
}

public static func brandSecondary(from theme: RuntimeTheme?) -> Color {
theme?.brandSecondary ?? brandSecondary
}

public static func brandTertiary(from theme: RuntimeTheme?) -> Color {
theme?.brandTertiary ?? brandTertiary
}

// MARK: - Provider-Aware Colors

public static func brandPrimary(for provider: SessionProviderKind) -> Color {
if let yamlProviderColor = yamlProviderPrimaryColor(for: provider) {
return yamlProviderColor
}

switch provider {
case .claude:
return Color(hex: "#CC785C") // bookCloth
Expand Down Expand Up @@ -162,49 +181,65 @@ extension Color {
// MARK: - Theme Colors Helper

private static func getCurrentThemeColors() -> ThemeColors {
let selectedTheme = UserDefaults.standard.string(forKey: "selectedTheme") ?? "claude"
let theme = AppTheme(rawValue: selectedTheme) ?? .claude

switch theme {
case .claude:
let selectedTheme = UserDefaults.standard.string(forKey: AgentHubDefaults.selectedTheme) ?? "claude"
guard let theme = AppTheme(rawValue: selectedTheme) else {
// YAML theme — read cached hex values
let yamlPrimary = UserDefaults.standard.string(forKey: AgentHubDefaults.yamlPrimaryHex) ?? "#CC785C"
let yamlSecondary = UserDefaults.standard.string(forKey: AgentHubDefaults.yamlSecondaryHex) ?? "#D4A27F"
let yamlTertiary = UserDefaults.standard.string(forKey: AgentHubDefaults.yamlTertiaryHex) ?? "#EBDBBC"
return ThemeColors(
brandPrimary: Color(hex: "#CC785C"), // bookCloth
brandSecondary: Color(hex: "#D4A27F"), // kraft
brandTertiary: Color(hex: "#EBDBBC") // manilla
brandPrimary: Color(hex: yamlPrimary),
brandSecondary: Color(hex: yamlSecondary),
brandTertiary: Color(hex: yamlTertiary)
)
}

// Delegate to ThemeManager as single source of truth for built-in themes
return ThemeManager.getThemeColors(for: theme)
}

private static func yamlProviderPrimaryColor(for provider: SessionProviderKind) -> Color? {
guard isYAMLThemeSelected else { return nil }

let claudePrimary = UserDefaults.standard.string(forKey: AgentHubDefaults.yamlPrimaryHex)
let sentryLightCodex = UserDefaults.standard.string(forKey: AgentHubDefaults.yamlSecondaryHex) ?? "#362D59"
let sentryDarkCodex = UserDefaults.standard.string(forKey: AgentHubDefaults.yamlTertiaryHex) ?? "#584774"
let codexPrimary: String? = {
if isSentryYAMLSelected {
return sentryDarkCodex
}
return UserDefaults.standard.string(forKey: AgentHubDefaults.yamlSecondaryHex)
}()

switch provider {
case .claude:
guard let claudePrimary else { return nil }
return Color(hex: claudePrimary)
case .codex:
return ThemeColors(
brandPrimary: Color(hex: "#00A5B2"), // teal
brandSecondary: Color(hex: "#00A5B2"), // teal (same as primary)
brandTertiary: Color(hex: "#00A5B2") // teal (same as primary)
)
case .bat:
// Bat: purple primary, real mustard secondary, slate tertiary
return ThemeColors(
brandPrimary: Color(hex: "#7C3AED"), // deep purple
brandSecondary: Color(hex: "#FFB000"), // mustard
brandTertiary: Color(hex: "#64748B") // slate gray
)
case .xcode:
// Xcode: dynamic system colors inspired by Xcode syntax highlights
// Use system variants to adapt to light/dark automatically
return ThemeColors(
brandPrimary: Color(nsColor: .systemBlue),
brandSecondary: Color(nsColor: .systemIndigo),
brandTertiary: Color(nsColor: .systemTeal)
)
case .custom:
// Read user-defined custom palette from UserDefaults (hex strings)
let primary = UserDefaults.standard.string(forKey: "customPrimaryHex") ?? "#7C3AED"
let secondary = UserDefaults.standard.string(forKey: "customSecondaryHex") ?? "#FFB000"
let tertiary = UserDefaults.standard.string(forKey: "customTertiaryHex") ?? "#64748B"
return ThemeColors(
brandPrimary: Color(hex: primary),
brandSecondary: Color(hex: secondary),
brandTertiary: Color(hex: tertiary)
)
if isSentryYAMLSelected {
let dynamicCodex = NSColor(name: nil) { appearance in
let isDark = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
return NSColor.fromHex(isDark ? sentryDarkCodex : sentryLightCodex)
}
return Color(nsColor: dynamicCodex)
}
guard let codexPrimary else { return nil }
return Color(hex: codexPrimary)
}
}

private static var isYAMLThemeSelected: Bool {
let selectedTheme = UserDefaults.standard.string(forKey: AgentHubDefaults.selectedTheme) ?? "claude"
return AppTheme(rawValue: selectedTheme) == nil
}

private static var isSentryYAMLSelected: Bool {
let selectedTheme = (UserDefaults.standard.string(forKey: AgentHubDefaults.selectedTheme) ?? "")
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
return selectedTheme == "sentry.yaml" || selectedTheme == "sentry.yml"
}

static let backgroundDark = Color(hex: "#262624")
static let backgroundLight = Color(hex: "#FAF9F5")
static let expandedContentBackgroundDark = Color(hex: "#1F2421")
Expand Down Expand Up @@ -262,6 +297,10 @@ extension Color {
)
}

public static func backgroundGradient(from theme: RuntimeTheme?) -> LinearGradient {
theme?.backgroundGradient ?? backgroundGradient
}

// MARK: - Adaptive Colors

static func adaptiveBackground(for colorScheme: ColorScheme) -> Color {
Expand All @@ -272,6 +311,32 @@ extension Color {
colorScheme == .dark ? expandedContentBackgroundDark : expandedContentBackgroundLight
}

public static func adaptiveBackground(for colorScheme: ColorScheme, theme: RuntimeTheme?) -> Color {
if let theme = theme {
if colorScheme == .dark, let dark = theme.backgroundDark {
return dark
} else if colorScheme == .light, let light = theme.backgroundLight {
return light
}
}
return adaptiveBackground(for: colorScheme)
}

public static func adaptiveExpandedContentBackground(for colorScheme: ColorScheme, theme: RuntimeTheme?) -> Color {
if let theme = theme {
if colorScheme == .dark, let dark = theme.expandedContentBackgroundDark {
return dark
} else if colorScheme == .light, let light = theme.expandedContentBackgroundLight {
return light
}
}
return adaptiveExpandedContentBackground(for: colorScheme)
}

public static var isSentryThemeSelectedStrict: Bool {
isSentryYAMLSelected
}

}

// MARK: - Hex <-> NSColor Bridging
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: "Sentry"
version: "1.0"
author: "Sentry"
description: "Official Sentry-inspired color theme"

colors:
brand:
primary: "#E1567C"
secondary: "#362D59"
tertiary: "#584774"

backgrounds:
dark: "#1A1625"
light: "#FAF9FB"
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// ThemeEnvironment.swift
// AgentHub
//
// SwiftUI environment integration for runtime themes
//

import SwiftUI

private struct ThemeEnvironmentKey: EnvironmentKey {
static let defaultValue: RuntimeTheme? = nil
}

extension EnvironmentValues {
public var runtimeTheme: RuntimeTheme? {
get { self[ThemeEnvironmentKey.self] }
set { self[ThemeEnvironmentKey.self] = newValue }
}
}

extension View {
public func runtimeTheme(_ theme: RuntimeTheme) -> some View {
environment(\.runtimeTheme, theme)
}
}
Loading