feat(keyboard): add Skills tab and extract-todos Shortcut export
Ship a Skills catalog with drag-to-reorder chips and a companion Shortcut that receives extracted titles and writes them to Reminders.
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user