148ff807f2
Ship a Skills catalog with drag-to-reorder chips and a companion Shortcut that receives extracted titles and writes them to Reminders.
111 lines
3.7 KiB
Swift
111 lines
3.7 KiB
Swift
// AIAgentSkillLayoutStore.swift
|
|
// OSGKeyboard · Shared
|
|
//
|
|
// Observable facade over the persisted skill layout. The Skills tab mutates
|
|
// this; the keyboard reads the same App Group snapshot on each config poll.
|
|
|
|
import Foundation
|
|
import Combine
|
|
|
|
@MainActor
|
|
public final class AIAgentSkillLayoutStore: ObservableObject {
|
|
public static let shared = AIAgentSkillLayoutStore()
|
|
|
|
@Published public private(set) var layout: AIAgentSkillLayout
|
|
|
|
private let defaults: UserDefaults?
|
|
private let persist: (AIAgentSkillLayout) -> Void
|
|
private let load: () -> AIAgentSkillLayout
|
|
|
|
public init(defaults: UserDefaults? = nil) {
|
|
if let defaults {
|
|
self.defaults = defaults
|
|
self.load = { AppGroupStore(defaults: defaults).agentSkillLayout }
|
|
self.persist = { AppGroupStore(defaults: defaults).setAgentSkillLayout($0) }
|
|
} else {
|
|
self.defaults = nil
|
|
self.load = { AppGroupStore().agentSkillLayout }
|
|
self.persist = { AppGroupStore().setAgentSkillLayout($0) }
|
|
}
|
|
self.layout = self.load()
|
|
}
|
|
|
|
public func reload() {
|
|
layout = load()
|
|
}
|
|
|
|
public var enabledSkills: [AIClipboardSkill] {
|
|
AIClipboardSkillCatalog.visible(enabledIDs: layout.enabledIDs)
|
|
}
|
|
|
|
public var availableSkills: [AIClipboardSkill] {
|
|
AIClipboardSkillCatalog.catalog.filter { !layout.isEnabled($0.id) }
|
|
}
|
|
|
|
@discardableResult
|
|
public func enable(_ id: String) -> AIAgentSkillEnableResult {
|
|
let current = layout.sanitized()
|
|
guard let skill = AIClipboardSkillCatalog.skill(id: id) else { return .unknown }
|
|
if current.isEnabled(id) { return .alreadyEnabled }
|
|
if skill.requiresShortcut, !current.hasConfirmedShortcut(id) {
|
|
return .needsShortcut
|
|
}
|
|
if current.isFull { return .full }
|
|
commit(
|
|
AIAgentSkillLayout(
|
|
enabledIDs: current.enabledIDs + [id],
|
|
confirmedShortcutIDs: current.confirmedShortcutIDs
|
|
)
|
|
)
|
|
return .enabled
|
|
}
|
|
|
|
/// Drops the keyboard slot only. Companion Shortcuts stay installed;
|
|
/// the user deletes them in the Shortcuts app if they want them gone.
|
|
public func disable(_ id: String) {
|
|
let current = layout.sanitized()
|
|
commit(
|
|
AIAgentSkillLayout(
|
|
enabledIDs: current.enabledIDs.filter { $0 != id },
|
|
confirmedShortcutIDs: current.confirmedShortcutIDs
|
|
)
|
|
)
|
|
}
|
|
|
|
/// Marks the companion Shortcut as added, then tries to occupy a slot.
|
|
@discardableResult
|
|
public func confirmShortcutAndEnable(_ id: String) -> AIAgentSkillEnableResult {
|
|
guard let skill = AIClipboardSkillCatalog.skill(id: id), skill.requiresShortcut else {
|
|
return .unknown
|
|
}
|
|
var current = layout.sanitized()
|
|
if !current.confirmedShortcutIDs.contains(id) {
|
|
current.confirmedShortcutIDs.append(id)
|
|
}
|
|
commit(current)
|
|
return enable(id)
|
|
}
|
|
|
|
public func moveEnabled(id draggedID: String, onto targetID: String) {
|
|
var ids = layout.sanitized().enabledIDs
|
|
guard let from = ids.firstIndex(of: draggedID),
|
|
let to = ids.firstIndex(of: targetID),
|
|
from != to else { return }
|
|
ids.move(
|
|
fromOffsets: IndexSet(integer: from),
|
|
toOffset: to > from ? to + 1 : to
|
|
)
|
|
commit(
|
|
AIAgentSkillLayout(
|
|
enabledIDs: ids,
|
|
confirmedShortcutIDs: layout.confirmedShortcutIDs
|
|
)
|
|
)
|
|
}
|
|
|
|
private func commit(_ layout: AIAgentSkillLayout) {
|
|
persist(layout)
|
|
self.layout = load()
|
|
}
|
|
}
|