feat(keyboard): add custom skills plus events and navigate Shortcuts
Ship user-defined Shortcut skills, companion Events/Navigate recipes, shared skill/style card chrome, and bump the build to 69.
This commit is contained in:
@@ -12,46 +12,65 @@ public final class AIAgentSkillLayoutStore: ObservableObject {
|
||||
public static let shared = AIAgentSkillLayoutStore()
|
||||
|
||||
@Published public private(set) var layout: AIAgentSkillLayout
|
||||
@Published public private(set) var userCatalog: AIUserSkillCatalog
|
||||
|
||||
private let defaults: UserDefaults?
|
||||
private let persist: (AIAgentSkillLayout) -> Void
|
||||
private let load: () -> AIAgentSkillLayout
|
||||
private let persistLayout: (AIAgentSkillLayout) -> Void
|
||||
private let persistUserCatalog: (AIUserSkillCatalog) -> Void
|
||||
private let loadLayout: () -> AIAgentSkillLayout
|
||||
private let loadUserCatalog: () -> AIUserSkillCatalog
|
||||
|
||||
public init(defaults: UserDefaults? = nil) {
|
||||
if let defaults {
|
||||
self.defaults = defaults
|
||||
self.load = { AppGroupStore(defaults: defaults).agentSkillLayout }
|
||||
self.persist = { AppGroupStore(defaults: defaults).setAgentSkillLayout($0) }
|
||||
self.loadUserCatalog = { AppGroupStore(defaults: defaults).agentUserSkillCatalog }
|
||||
self.persistUserCatalog = { AppGroupStore(defaults: defaults).setAgentUserSkillCatalog($0) }
|
||||
self.loadLayout = { AppGroupStore(defaults: defaults).agentSkillLayout }
|
||||
self.persistLayout = { AppGroupStore(defaults: defaults).setAgentSkillLayout($0) }
|
||||
} else {
|
||||
self.defaults = nil
|
||||
self.load = { AppGroupStore().agentSkillLayout }
|
||||
self.persist = { AppGroupStore().setAgentSkillLayout($0) }
|
||||
self.loadUserCatalog = { AppGroupStore().agentUserSkillCatalog }
|
||||
self.persistUserCatalog = { AppGroupStore().setAgentUserSkillCatalog($0) }
|
||||
self.loadLayout = { AppGroupStore().agentSkillLayout }
|
||||
self.persistLayout = { AppGroupStore().setAgentSkillLayout($0) }
|
||||
}
|
||||
self.layout = self.load()
|
||||
self.userCatalog = self.loadUserCatalog()
|
||||
self.layout = self.loadLayout()
|
||||
}
|
||||
|
||||
public func reload() {
|
||||
layout = load()
|
||||
userCatalog = loadUserCatalog()
|
||||
layout = loadLayout()
|
||||
}
|
||||
|
||||
public var mergedCatalog: [AIClipboardSkill] {
|
||||
AIClipboardSkillCatalog.all(userCatalog: userCatalog)
|
||||
}
|
||||
|
||||
public var enabledSkills: [AIClipboardSkill] {
|
||||
AIClipboardSkillCatalog.visible(enabledIDs: layout.enabledIDs)
|
||||
AIClipboardSkillCatalog.visible(
|
||||
enabledIDs: layout.enabledIDs,
|
||||
userCatalog: userCatalog
|
||||
)
|
||||
}
|
||||
|
||||
public var availableSkills: [AIClipboardSkill] {
|
||||
AIClipboardSkillCatalog.catalog.filter { !layout.isEnabled($0.id) }
|
||||
mergedCatalog.filter { !layout.isEnabled($0.id) }
|
||||
}
|
||||
|
||||
public func userSkill(id: String) -> AIUserSkill? {
|
||||
userCatalog.skill(id: id)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func enable(_ id: String) -> AIAgentSkillEnableResult {
|
||||
let current = layout.sanitized()
|
||||
guard let skill = AIClipboardSkillCatalog.skill(id: id) else { return .unknown }
|
||||
let current = layout.sanitized(catalog: mergedCatalog)
|
||||
guard let skill = AIClipboardSkillCatalog.skill(id: id, userCatalog: userCatalog) else {
|
||||
return .unknown
|
||||
}
|
||||
if current.isEnabled(id) { return .alreadyEnabled }
|
||||
if skill.requiresShortcut, !current.hasConfirmedShortcut(id) {
|
||||
return .needsShortcut
|
||||
}
|
||||
if current.isFull { return .full }
|
||||
commit(
|
||||
commitLayout(
|
||||
AIAgentSkillLayout(
|
||||
enabledIDs: current.enabledIDs + [id],
|
||||
confirmedShortcutIDs: current.confirmedShortcutIDs
|
||||
@@ -63,8 +82,8 @@ public final class AIAgentSkillLayoutStore: ObservableObject {
|
||||
/// 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(
|
||||
let current = layout.sanitized(catalog: mergedCatalog)
|
||||
commitLayout(
|
||||
AIAgentSkillLayout(
|
||||
enabledIDs: current.enabledIDs.filter { $0 != id },
|
||||
confirmedShortcutIDs: current.confirmedShortcutIDs
|
||||
@@ -75,27 +94,34 @@ public final class AIAgentSkillLayoutStore: ObservableObject {
|
||||
/// 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 {
|
||||
guard let skill = AIClipboardSkillCatalog.skill(id: id, userCatalog: userCatalog),
|
||||
skill.requiresShortcut else {
|
||||
return .unknown
|
||||
}
|
||||
var current = layout.sanitized()
|
||||
var current = layout.sanitized(catalog: mergedCatalog)
|
||||
if !current.confirmedShortcutIDs.contains(id) {
|
||||
current.confirmedShortcutIDs.append(id)
|
||||
}
|
||||
commit(current)
|
||||
commitLayout(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 }
|
||||
let ids = layout.sanitized(catalog: mergedCatalog).enabledIDs
|
||||
guard let to = ids.firstIndex(of: targetID) else { return }
|
||||
moveEnabled(id: draggedID, toIndex: to)
|
||||
}
|
||||
|
||||
public func moveEnabled(id draggedID: String, toIndex: Int) {
|
||||
var ids = layout.sanitized(catalog: mergedCatalog).enabledIDs
|
||||
guard let from = ids.firstIndex(of: draggedID), !ids.isEmpty else { return }
|
||||
let to = min(max(toIndex, 0), ids.count - 1)
|
||||
guard from != to else { return }
|
||||
ids.move(
|
||||
fromOffsets: IndexSet(integer: from),
|
||||
toOffset: to > from ? to + 1 : to
|
||||
)
|
||||
commit(
|
||||
commitLayout(
|
||||
AIAgentSkillLayout(
|
||||
enabledIDs: ids,
|
||||
confirmedShortcutIDs: layout.confirmedShortcutIDs
|
||||
@@ -103,8 +129,47 @@ public final class AIAgentSkillLayoutStore: ObservableObject {
|
||||
)
|
||||
}
|
||||
|
||||
private func commit(_ layout: AIAgentSkillLayout) {
|
||||
persist(layout)
|
||||
self.layout = load()
|
||||
public func saveUserSkill(_ skill: AIUserSkill) throws {
|
||||
let previousURL = userCatalog.skill(id: skill.id)?.shortcutICloudURL
|
||||
var catalog = userCatalog
|
||||
try catalog.upsert(skill)
|
||||
commitUserCatalog(catalog)
|
||||
if previousURL != nil, previousURL != skill.shortcutICloudURL {
|
||||
dropShortcutConfirmation(for: skill.id)
|
||||
}
|
||||
}
|
||||
|
||||
public func deleteUserSkill(id: String) {
|
||||
var catalog = userCatalog
|
||||
catalog.remove(id: id)
|
||||
commitUserCatalog(catalog)
|
||||
let current = layout.sanitized(catalog: mergedCatalog)
|
||||
commitLayout(
|
||||
AIAgentSkillLayout(
|
||||
enabledIDs: current.enabledIDs.filter { $0 != id },
|
||||
confirmedShortcutIDs: current.confirmedShortcutIDs.filter { $0 != id }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private func dropShortcutConfirmation(for id: String) {
|
||||
let current = layout.sanitized(catalog: mergedCatalog)
|
||||
commitLayout(
|
||||
AIAgentSkillLayout(
|
||||
enabledIDs: current.enabledIDs.filter { $0 != id },
|
||||
confirmedShortcutIDs: current.confirmedShortcutIDs.filter { $0 != id }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private func commitLayout(_ layout: AIAgentSkillLayout) {
|
||||
persistLayout(layout)
|
||||
self.layout = loadLayout()
|
||||
}
|
||||
|
||||
private func commitUserCatalog(_ catalog: AIUserSkillCatalog) {
|
||||
persistUserCatalog(catalog)
|
||||
userCatalog = loadUserCatalog()
|
||||
layout = loadLayout()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user