mirror of
https://github.com/BigMarc/grammargem.git
synced 2026-10-13 17:01:55 +02:00
Wave 1 (correctness/trust): - Fix paid large-model dead path: engine reads the persisted selected repo via a thread-safe ModelManager.activeRepo(), so download/load/isReady now agree. - Lemon Squeezy store/product check fails closed in release builds — the placeholder-ID 'any key unlocks paid' bypass can no longer ship. - Replace force-unwrapped Hugging Face URLs with safe URLComponents construction. - Never read/replace/scan secure (password) fields via AX.isSecureTextField. Wave 2 (compounding seam): - Extract AIPromptKit (prompts/temperature/maxTokens/cleanup) out of MLXEngine so every backend produces identical prompts and output. - AIEngine gains protectedTerms (personal dictionary now actually injected into the LLM), warmup(), and runStreaming(); add makeAIEngine() factory. - Honor WritingMode.lengthCap for maxTokens; warm the model on launch/after download. - Guard custom modes against empty prompts and duplicate names. Wave 3: OllamaEngine — a URLSession-only AIEngine backend (cross-platform, opt-in via GRAMMARGEM_AI_BACKEND=ollama). Wave 4: LocalEngineServer — loopback-only, token-authed HTTP bridge (Network.framework, no new deps), off by default, exposing Harper + the LLM + the entitlement gate to a browser extension. Tests: 6 new unit tests for the AIPrompts seam and custom-mode guard (19 pass). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
80 lines
2.8 KiB
Swift
80 lines
2.8 KiB
Swift
import Foundation
|
|
|
|
/// User-created Writing Modes (paid) plus per-app Mode overrides for App-Aware
|
|
/// switching. Persisted locally; the built-in modes live in `ModeRegistry`.
|
|
@MainActor
|
|
final class CustomModesStore: ObservableObject {
|
|
@Published private(set) var customModes: [WritingMode]
|
|
/// bundleID -> mode id (overrides the default App-Aware mapping).
|
|
@Published private(set) var appOverrides: [String: String]
|
|
|
|
private let modesKey = "GrammarGem.customModes"
|
|
private let mapKey = "GrammarGem.appModeOverrides"
|
|
private let d = UserDefaults.standard
|
|
|
|
init() {
|
|
if let raw = d.data(forKey: modesKey),
|
|
let decoded = try? JSONDecoder().decode([WritingMode].self, from: raw) {
|
|
customModes = decoded
|
|
} else {
|
|
customModes = []
|
|
}
|
|
appOverrides = (d.dictionary(forKey: mapKey) as? [String: String]) ?? [:]
|
|
}
|
|
|
|
/// Built-in + custom modes (custom ones are always paid).
|
|
func allModes() -> [WritingMode] { ModeRegistry.all + customModes }
|
|
|
|
/// Returns false (no-op) if the name or prompt is empty, or the name
|
|
/// collides with an existing built-in/custom mode — so an empty-prompt mode
|
|
/// can't slip through to the LLM as an empty system prompt.
|
|
@discardableResult
|
|
func addCustom(name: String, prompt: String) -> Bool {
|
|
let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let trimmedPrompt = prompt.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !trimmed.isEmpty, !trimmedPrompt.isEmpty else { return false }
|
|
guard !allModes().contains(where: { $0.name.caseInsensitiveCompare(trimmed) == .orderedSame })
|
|
else { return false }
|
|
let mode = WritingMode(
|
|
id: "custom-\(UUID().uuidString.prefix(8))",
|
|
name: trimmed,
|
|
systemPrompt: trimmedPrompt,
|
|
isPaid: true,
|
|
lengthCap: nil,
|
|
autoFormat: "Custom"
|
|
)
|
|
customModes.append(mode)
|
|
persistModes()
|
|
return true
|
|
}
|
|
|
|
func removeCustom(id: String) {
|
|
customModes.removeAll { $0.id == id }
|
|
appOverrides = appOverrides.filter { $0.value != id }
|
|
persistModes()
|
|
persistMap()
|
|
}
|
|
|
|
func setOverride(bundleID: String, modeID: String) {
|
|
let b = bundleID.trimmingCharacters(in: .whitespaces)
|
|
guard !b.isEmpty else { return }
|
|
appOverrides[b] = modeID
|
|
persistMap()
|
|
}
|
|
|
|
func clearOverride(bundleID: String) {
|
|
appOverrides.removeValue(forKey: bundleID)
|
|
persistMap()
|
|
}
|
|
|
|
private func persistModes() {
|
|
if let raw = try? JSONEncoder().encode(customModes) {
|
|
d.set(raw, forKey: modesKey)
|
|
}
|
|
}
|
|
|
|
private func persistMap() {
|
|
d.set(appOverrides, forKey: mapKey)
|
|
}
|
|
}
|