Skip to content
Merged
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
4 changes: 3 additions & 1 deletion Sources/MacOptimizer/App/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -354,12 +354,14 @@ public final class AppState: ObservableObject {
processNames: runningProcesses.prefix(4).map { "\($0.name) (PID: \($0.pid))" },
includeProcesses: UserDefaults.standard.bool(forKey: "MacOptimizer_IncludeRunningAppNames")
)

let localSnapshot = LocalHeuristicProvider.SystemSnapshot(hardware: hardwareInfo, memory: memoryStats, cpu: cpuStats, disk: diskStats)

Task {
let result = await AIAssistantService.shared.chatWithCopilot(
userMessage: trimmed,
history: self.chatMessages,
systemContext: context,
localSnapshot: localSnapshot,
config: self.nimConfig
)

Expand Down
34 changes: 28 additions & 6 deletions Sources/MacOptimizer/Core/Operations/CLICommandRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,13 @@ public struct CLICommandRunner {
let disk = await SystemMonitorService.shared.fetchDiskStats()
let batt = await SystemMonitorService.shared.fetchBatteryStats()
let hw = await SystemMonitorService.shared.fetchHardwareInfo()

print("""

print(statusReport(memory: mem, cpu: cpu, disk: disk, battery: batt, hardware: hw))
}

/// English-only status text. The CLI never uses `L10n`; model raw values are not display text.
static func statusReport(memory mem: MemoryStats, cpu: CPUStats, disk: DiskStats, battery batt: BatteryStats, hardware hw: HardwareInfo) -> String {
"""
========================================================
⚡ MacOptimizer Pro System Status
========================================================
Expand All @@ -65,16 +70,33 @@ public struct CLICommandRunner {
⏱️ Uptime: \(hw.uptimeString)

🧠 Memory Used: \(ByteFormatter.formatMemory(mem.actualUsedBytes)) / \(ByteFormatter.formatMemory(mem.totalBytes)) (\(Int(mem.usedPercentage * 100))%)
📊 Memory Pressure: \(mem.pressureLevel.rawValue)
📊 Memory Pressure: \(englishLabel(for: mem.pressureLevel))
💾 Swap Used: \(ByteFormatter.formatMemory(mem.swapUsedBytes)) / \(ByteFormatter.formatMemory(mem.swapTotalBytes))

🔥 CPU Usage: \(String(format: "%.1f", cpu.totalUsage))% (\(cpu.physicalCores) cores)
🌡️ Thermal State: \(cpu.thermalState.rawValue)
🌡️ Thermal State: \(englishLabel(for: cpu.thermalState))

💽 Disk: \(ByteFormatter.format(disk.usedBytes)) used / \(ByteFormatter.format(disk.freeBytes)) free
🔋 Battery: \(batt.percentage)% (\(batt.powerSource))
========================================================
""")
"""
}

static func englishLabel(for level: MemoryStats.MemoryPressureLevel) -> String {
switch level {
case .normal: return "Normal"
case .warning: return "Warning"
case .critical: return "Critical"
}
}

static func englishLabel(for state: CPUStats.ThermalState) -> String {
switch state {
case .nominal: return "Normal (Cool)"
case .fair: return "Slightly Warm"
case .serious: return "High Temperature (Throttling Risk)"
case .critical: return "Critical Temperature (Fans at Maximum)"
}
}

private static func runJunkClean(dryRun: Bool, includeTrash: Bool) async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,39 @@ public struct LocalHeuristicProvider: AIProvider {
public let providerId: String = "local_heuristics"
public let displayName: String = L10n.string("Local Rule Engine (100% Offline & Secure)", table: .ai)
public let requiresNetwork: Bool = false

public init() {}


/// Telemetry shown to the user in offline replies. The English cloud snapshot is model input only.
public struct SystemSnapshot: Sendable {
public let hardware: HardwareInfo
public let memory: MemoryStats
public let cpu: CPUStats
public let disk: DiskStats

public init(hardware: HardwareInfo, memory: MemoryStats, cpu: CPUStats, disk: DiskStats) {
self.hardware = hardware
self.memory = memory
self.cpu = cpu
self.disk = disk
}
}

private let snapshot: SystemSnapshot?

public init(snapshot: SystemSnapshot? = nil) {
self.snapshot = snapshot
}

/// Localized system summary for the user (never the English cloud snapshot text).
static func localizedSummary(_ snapshot: SystemSnapshot) -> String {
[
L10n.string("Mac model: %@ (%@), macOS version: %@", table: .ai,
snapshot.hardware.modelName, snapshot.hardware.chipName, snapshot.hardware.osVersion),
L10n.string("RAM: %lld%%", table: .ai, Int(snapshot.memory.usedPercentage * 100)),
L10n.string("CPU: %@%%, Free disk: %lld%%", table: .ai,
String(format: "%.1f", snapshot.cpu.totalUsage), Int(snapshot.disk.freePercentage * 100))
].joined(separator: "\n")
}

public func diagnose(
memory: MemoryStats,
cpu: CPUStats,
Expand Down Expand Up @@ -106,7 +136,11 @@ public struct LocalHeuristicProvider: AIProvider {

// Keyword lists cover both Turkish and English user input.
if lastUserMsg.contains("ram") || lastUserMsg.contains("bellek") || lastUserMsg.contains("memory") {
return L10n.string("I reviewed your Mac’s memory status. To reduce memory pressure, you can try closing the most demanding apps.\n\nSystem Info:\n%@", table: .ai, snapshotContext)
// `snapshotContext` is the English cloud model input; the user sees the localized summary instead.
guard let snapshot else {
return L10n.string("I reviewed your Mac’s memory status. To reduce memory pressure, you can try closing the most demanding apps.", table: .ai)
}
return L10n.string("I reviewed your Mac’s memory status. To reduce memory pressure, you can try closing the most demanding apps.\n\nSystem Info:\n%@", table: .ai, Self.localizedSummary(snapshot))
} else if lastUserMsg.contains("ısın") || lastUserMsg.contains("cpu") || lastUserMsg.contains("fan") || lastUserMsg.contains("heat") {
return L10n.string("Based on processor and hardware telemetry, you can check the most resource-hungry processes in Task Manager and safely terminate unresponsive user apps.", table: .ai)
} else if lastUserMsg.contains("temiz") || lastUserMsg.contains("disk") || lastUserMsg.contains("yer")
Expand Down
4 changes: 4 additions & 0 deletions Sources/MacOptimizer/Resources/en.lproj/ai.strings
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,7 @@
"NVIDIA NIM API key is missing." = "NVIDIA NIM API key is missing.";
"Invalid /chat/completions URL format." = "Invalid /chat/completions URL format.";
"Could not parse the API response." = "Could not parse the API response.";
"I reviewed your Mac’s memory status. To reduce memory pressure, you can try closing the most demanding apps." = "I reviewed your Mac’s memory status. To reduce memory pressure, you can try closing the most demanding apps.";
"Mac model: %@ (%@), macOS version: %@" = "Mac model: %@ (%@), macOS version: %@";
"RAM: %lld%%" = "RAM: %lld%%";
"CPU: %@%%, Free disk: %lld%%" = "CPU: %@%%, Free disk: %lld%%";
39 changes: 39 additions & 0 deletions Sources/MacOptimizer/Resources/en.lproj/cleanup.strings
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,42 @@
"Copy" = "Copy";
"Duplicate File Scan Not Run" = "Duplicate File Scan Not Run";
"Start a scan to detect files with identical content in your Downloads, Documents, and Pictures folders." = "Start a scan to detect files with identical content in your Downloads, Documents, and Pictures folders.";
"User" = "User";
"Apple Silicon" = "Apple Silicon";
"Universal" = "Universal";
"Intel (x86)" = "Intel (x86)";
"Updates" = "Updates";
"By Size" = "By Size";
"By Name" = "By Name";
"Installed Apps & Uninstaller" = "Installed Apps & Uninstaller";
"Review all apps installed on your Mac, their architectures, and sizes." = "Review all apps installed on your Mac, their architectures, and sizes.";
"Scan Apps" = "Scan Apps";
"Checking..." = "Checking...";
"Find Updates" = "Find Updates";
"Search by app name or Bundle ID..." = "Search by app name or Bundle ID...";
"Sort" = "Sort";
"Apps Not Scanned" = "Apps Not Scanned";
"Start a scan to list installed apps and check for updates." = "Start a scan to list installed apps and check for updates.";
"Update: v%@" = "Update: v%@";
"Update" = "Update";
"Uninstall" = "Uninstall";
"Uninstall Along with Leftovers" = "Uninstall Along with Leftovers";
"Uninstalling %@" = "Uninstalling %@";
"Close" = "Close";
"Searching for related files and caches..." = "Searching for related files and caches...";
"Space to Reclaim: %@" = "Space to Reclaim: %@";
"Uninstalling..." = "Uninstalling...";
"Delete All and Uninstall (%@)" = "Delete All and Uninstall (%@)";
"Uninstall %@" = "Uninstall %@";
"The app bundle will be moved to the Trash. Selected leftovers will also be moved to the Trash. Continue?" = "The app bundle will be moved to the Trash. Selected leftovers will also be moved to the Trash. Continue?";
"Uninstall with Leftovers" = "Uninstall with Leftovers";
"Software & App Updates" = "Software & App Updates";
"Check for the latest versions of your installed macOS apps." = "Check for the latest versions of your installed macOS apps.";
"Check for Updates" = "Check for Updates";
"Current: v%@" = "Current: v%@";
"New: v%@" = "New: v%@";
"Release Notes" = "Release Notes";
"Updating %@ via Homebrew." = "Updating %@ via Homebrew.";
"Get Update" = "Get Update";
"All Your Apps Are Up to Date" = "All Your Apps Are Up to Date";
"No pending software updates were found. Use the button above to check for new versions." = "No pending software updates were found. Use the button above to check for new versions.";
15 changes: 15 additions & 0 deletions Sources/MacOptimizer/Resources/en.lproj/cleanup.stringsdict
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,19 @@
<key>count</key><dict><key>NSStringFormatSpecTypeKey</key><string>NSStringPluralRuleType</string><key>NSStringFormatValueTypeKey</key><string>lld</string>
<key>one</key><string>%lld Duplicate File</string><key>other</key><string>%lld Duplicate Files</string></dict>
</dict>
<key>%lld apps detected • Total: %@</key><dict>
<key>NSStringLocalizedFormatKey</key><string>%#@count@ detected • Total: %@</string>
<key>count</key><dict><key>NSStringFormatSpecTypeKey</key><string>NSStringPluralRuleType</string><key>NSStringFormatValueTypeKey</key><string>lld</string>
<key>one</key><string>%lld app</string><key>other</key><string>%lld apps</string></dict>
</dict>
<key>Related Files Found (%lld items)</key><dict>
<key>NSStringLocalizedFormatKey</key><string>Related Files Found (%#@count@)</string>
<key>count</key><dict><key>NSStringFormatSpecTypeKey</key><string>NSStringPluralRuleType</string><key>NSStringFormatValueTypeKey</key><string>lld</string>
<key>one</key><string>%lld item</string><key>other</key><string>%lld items</string></dict>
</dict>
<key>New versions available for %lld apps!</key><dict>
<key>NSStringLocalizedFormatKey</key><string>New versions available for %#@count@!</string>
<key>count</key><dict><key>NSStringFormatSpecTypeKey</key><string>NSStringPluralRuleType</string><key>NSStringFormatValueTypeKey</key><string>lld</string>
<key>one</key><string>%lld app</string><key>other</key><string>%lld apps</string></dict>
</dict>
</dict></plist>
4 changes: 4 additions & 0 deletions Sources/MacOptimizer/Resources/tr.lproj/ai.strings
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,7 @@
"NVIDIA NIM API key is missing." = "NVIDIA NIM API Anahtarı eksik.";
"Invalid /chat/completions URL format." = "Geçersiz /chat/completions URL formatı.";
"Could not parse the API response." = "API yanıtı ayrıştırılamadı.";
"I reviewed your Mac’s memory status. To reduce memory pressure, you can try closing the most demanding apps." = "Mac'inizin bellek durumunu inceledim. Bellek baskısını azaltmak için en yoğun uygulamaları kapatmayı deneyebilirsiniz.";
"Mac model: %@ (%@), macOS version: %@" = "Mac modeli: %@ (%@), macOS sürümü: %@";
"RAM: %lld%%" = "RAM: %%%lld";
"CPU: %@%%, Free disk: %lld%%" = "CPU: %%%@, Boş disk: %%%lld";
39 changes: 39 additions & 0 deletions Sources/MacOptimizer/Resources/tr.lproj/cleanup.strings
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,42 @@
"Copy" = "Kopya";
"Duplicate File Scan Not Run" = "Yinelenen Dosya Taraması Yapılmadı";
"Start a scan to detect files with identical content in your Downloads, Documents, and Pictures folders." = "İndirilenler, Belgeler ve Resimler klasörlerindeki aynı içerikli dosyaları tespit etmek için taramayı başlatın.";
"User" = "Kullanıcı";
"Apple Silicon" = "Apple Silicon";
"Universal" = "Universal";
"Intel (x86)" = "Intel (x86)";
"Updates" = "Güncellemeler";
"By Size" = "Boyuta Göre";
"By Name" = "İsme Göre";
"Installed Apps & Uninstaller" = "Yüklü Uygulamalar & Kaldırıcı";
"Review all apps installed on your Mac, their architectures, and sizes." = "Mac'inizde kurulu tüm uygulamaları, mimarilerini ve boyutlarını inceleyin.";
"Scan Apps" = "Uygulamaları Tara";
"Checking..." = "Denetleniyor...";
"Find Updates" = "Güncellemeleri Bul";
"Search by app name or Bundle ID..." = "Uygulama adı veya Bundle ID ara...";
"Sort" = "Sırala";
"Apps Not Scanned" = "Uygulamalar Taranmadı";
"Start a scan to list installed apps and check for updates." = "Kurulu olan uygulamaları listelemek ve güncelleme kontrolü yapmak için taramayı başlatın.";
"Update: v%@" = "Güncelleme: v%@";
"Update" = "Güncelle";
"Uninstall" = "Kaldır";
"Uninstall Along with Leftovers" = "Kalıntılarıyla Birlikte Kaldır";
"Uninstalling %@" = "%@ Kaldırılıyor";
"Close" = "Kapat";
"Searching for related files and caches..." = "İlişkili dosyalar ve önbellekler aranıyor...";
"Space to Reclaim: %@" = "Kazanılacak Alan: %@";
"Uninstalling..." = "Kaldırılıyor...";
"Delete All and Uninstall (%@)" = "Tümünü Sil ve Kaldır (%@)";
"Uninstall %@" = "%@ Uygulamasını Kaldır";
"The app bundle will be moved to the Trash. Selected leftovers will also be moved to the Trash. Continue?" = "Uygulama paketi Çöp Sepetine taşınacak. Seçili kalıntılar da Çöp Sepetine taşınacaktır. Devam edilsin mi?";
"Uninstall with Leftovers" = "Kalıntılarıyla Kaldır";
"Software & App Updates" = "Yazılım & Uygulama Güncellemeleri";
"Check for the latest versions of your installed macOS apps." = "Yüklü macOS uygulamalarınızın en son sürümlerini kontrol edin.";
"Check for Updates" = "Güncellemeleri Denetle";
"Current: v%@" = "Mevcut: v%@";
"New: v%@" = "Yeni: v%@";
"Release Notes" = "Sürüm Notları";
"Updating %@ via Homebrew." = "%@ Homebrew üzerinden güncelleniyor.";
"Get Update" = "Güncellemeyi Al";
"All Your Apps Are Up to Date" = "Tüm Uygulamalarınız Güncel";
"No pending software updates were found. Use the button above to check for new versions." = "Herhangi bir bekleyen yazılım güncellemesi bulunamadı. Yeni sürümleri denetlemek için yukarıdaki butonu kullanabilirsiniz.";
15 changes: 15 additions & 0 deletions Sources/MacOptimizer/Resources/tr.lproj/cleanup.stringsdict
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,19 @@
<key>count</key><dict><key>NSStringFormatSpecTypeKey</key><string>NSStringPluralRuleType</string><key>NSStringFormatValueTypeKey</key><string>lld</string>
<key>one</key><string>%lld Kopya Dosya</string><key>other</key><string>%lld Kopya Dosya</string></dict>
</dict>
<key>%lld apps detected • Total: %@</key><dict>
<key>NSStringLocalizedFormatKey</key><string>%#@count@ tespit edildi • Toplam: %@</string>
<key>count</key><dict><key>NSStringFormatSpecTypeKey</key><string>NSStringPluralRuleType</string><key>NSStringFormatValueTypeKey</key><string>lld</string>
<key>one</key><string>%lld uygulama</string><key>other</key><string>%lld uygulama</string></dict>
</dict>
<key>Related Files Found (%lld items)</key><dict>
<key>NSStringLocalizedFormatKey</key><string>Bulunan İlişkili Dosyalar (%#@count@)</string>
<key>count</key><dict><key>NSStringFormatSpecTypeKey</key><string>NSStringPluralRuleType</string><key>NSStringFormatValueTypeKey</key><string>lld</string>
<key>one</key><string>%lld öğe</string><key>other</key><string>%lld öğe</string></dict>
</dict>
<key>New versions available for %lld apps!</key><dict>
<key>NSStringLocalizedFormatKey</key><string>%#@count@ için yeni sürüm mevcut!</string>
<key>count</key><dict><key>NSStringFormatSpecTypeKey</key><string>NSStringPluralRuleType</string><key>NSStringFormatValueTypeKey</key><string>lld</string>
<key>one</key><string>%lld uygulama</string><key>other</key><string>%lld uygulama</string></dict>
</dict>
</dict></plist>
7 changes: 4 additions & 3 deletions Sources/MacOptimizer/Services/AIAssistantService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ public actor AIAssistantService {
public static let shared = AIAssistantService()
public init() {}

private func provider(for config: NIMConfig) -> any AIProvider {
private func provider(for config: NIMConfig, localSnapshot: LocalHeuristicProvider.SystemSnapshot? = nil) -> any AIProvider {
guard config.isEnabled, config.providerType == .nvidiaNIM,
UserDefaults.standard.bool(forKey: "MacOptimizer_NIMDisclosureAccepted"),
!config.apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return LocalHeuristicProvider() }
!config.apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return LocalHeuristicProvider(snapshot: localSnapshot) }
return NvidiaNIMProvider(config: config)
}

Expand All @@ -19,6 +19,7 @@ public actor AIAssistantService {
}

public func chatWithCopilot(userMessage: String, history: [AIChatMessage], systemContext: String,
localSnapshot: LocalHeuristicProvider.SystemSnapshot? = nil,
config: NIMConfig) async -> (reply: String, actions: [AIAction]) {
let lower = userMessage.lowercased()
var actions: [AIAction] = []
Expand All @@ -28,7 +29,7 @@ public actor AIAssistantService {
if lower.contains("güncelle") || lower.contains("update") || lower.contains("yeni sürüm") || lower.contains("new version") { actions.append(AIAction(title: L10n.string("Check for Updates", table: .ai), type: .checkUpdates)) }
if lower.contains("dns") || lower.contains("ağ") || lower.contains("internet") || lower.contains("network") { actions.append(AIAction(title: L10n.string("Reset DNS Cache", table: .ai), type: .flushDNS)) }
do {
let reply = try await provider(for: config).queryCopilot(messages: Array(history.suffix(6)), snapshotContext: systemContext)
let reply = try await provider(for: config, localSnapshot: localSnapshot).queryCopilot(messages: Array(history.suffix(6)), snapshotContext: systemContext)
return (reply, actions)
} catch {
return (L10n.string("NVIDIA NIM connection error. Local suggestions are offered in the action buttons below.", table: .ai), actions)
Expand Down
2 changes: 1 addition & 1 deletion Sources/MacOptimizer/Services/AutonomousGuardService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public actor AutonomousGuardService {
lastThermalAlertTime = now
let alert = AutonomousAlert(
title: L10n.string("Thermal Throttling / Temperature Warning", table: .services),
message: L10n.string("CPU temperature has reached a critical threshold (%@). Close apps with high CPU usage to protect the hardware.", table: .services, cpu.thermalState.rawValue),
message: L10n.string("CPU temperature has reached a critical threshold (%@). Close apps with high CPU usage to protect the hardware.", table: .services, cpu.thermalState.localizedTitle),
type: .runawayProcess,
timestamp: now,
isResolved: false,
Expand Down
Loading
Loading