native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335import Foundationimport ProwlCLIShared
nonisolated struct UserRepositorySettings: Codable, Equatable, Sendable { var customCommands: [UserCustomCommand] var disabledGlobalCommandIDs: Set<UserCustomCommand.ID> /// Explicit per-repo Default Agent Profile designation (docs-ai 053). var defaultAgentProfileID: UUID? /// Per-repo launch memory: the last explicitly launched profile. One tier /// below the designation in the Recommended resolution. Dangling references /// are skipped at resolution time, not scrubbed here: this store cannot see /// the global profile list. var lastLaunchedAgentProfileID: UUID?
static let `default` = UserRepositorySettings(customCommands: [])
private enum CodingKeys: String, CodingKey { case customCommands case disabledGlobalCommandIDs case defaultAgentProfileID case lastLaunchedAgentProfileID }
init( customCommands: [UserCustomCommand], disabledGlobalCommandIDs: Set<UserCustomCommand.ID> = [], defaultAgentProfileID: UUID? = nil, lastLaunchedAgentProfileID: UUID? = nil ) { self.customCommands = Self.normalizedCommands(customCommands) self.disabledGlobalCommandIDs = disabledGlobalCommandIDs self.defaultAgentProfileID = defaultAgentProfileID self.lastLaunchedAgentProfileID = lastLaunchedAgentProfileID }
init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let commands = try container.decodeIfPresent([UserCustomCommand].self, forKey: .customCommands) ?? [] customCommands = Self.normalizedCommands(commands) disabledGlobalCommandIDs = try container.decodeIfPresent(Set<UserCustomCommand.ID>.self, forKey: .disabledGlobalCommandIDs) ?? [] defaultAgentProfileID = try container.decodeIfPresent(UUID.self, forKey: .defaultAgentProfileID) lastLaunchedAgentProfileID = try container.decodeIfPresent(UUID.self, forKey: .lastLaunchedAgentProfileID) }
func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(customCommands, forKey: .customCommands) try container.encode(disabledGlobalCommandIDs.sorted(), forKey: .disabledGlobalCommandIDs) try container.encodeIfPresent(defaultAgentProfileID, forKey: .defaultAgentProfileID) try container.encodeIfPresent(lastLaunchedAgentProfileID, forKey: .lastLaunchedAgentProfileID) }
func normalized() -> UserRepositorySettings { UserRepositorySettings( customCommands: customCommands, disabledGlobalCommandIDs: disabledGlobalCommandIDs, defaultAgentProfileID: defaultAgentProfileID, lastLaunchedAgentProfileID: lastLaunchedAgentProfileID ) }
static func normalizedCommands(_ commands: [UserCustomCommand]) -> [UserCustomCommand] { UserCustomCommand.normalizedCommands(commands) }
func isGlobalCommandEnabled(_ id: UserCustomCommand.ID) -> Bool { !disabledGlobalCommandIDs.contains(id) }
mutating func setGlobalCommandEnabled(_ isEnabled: Bool, id: UserCustomCommand.ID) { if isEnabled { disabledGlobalCommandIDs.remove(id) } else { disabledGlobalCommandIDs.insert(id) } }}
nonisolated struct UserCustomCommand: Codable, Equatable, Sendable, Identifiable { var id: String var title: String var systemImage: String var command: String var execution: UserCustomCommandExecution var splitDirection: UserCustomSplitDirection var closeOnSuccess: Bool var shortcut: UserCustomShortcut? var isEnabled: Bool
init( id: String = UUID().uuidString, title: String, systemImage: String, command: String, execution: UserCustomCommandExecution, splitDirection: UserCustomSplitDirection = .right, closeOnSuccess: Bool = false, shortcut: UserCustomShortcut?, isEnabled: Bool = true ) { self.id = id self.title = title self.systemImage = systemImage self.command = command self.execution = execution self.splitDirection = splitDirection self.closeOnSuccess = closeOnSuccess self.shortcut = shortcut?.normalized() self.isEnabled = isEnabled } private enum CodingKeys: String, CodingKey { case id, title, systemImage, command, execution, splitDirection, closeOnSuccess, shortcut, isEnabled } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try container.decode(String.self, forKey: .id) self.title = try container.decode(String.self, forKey: .title) self.systemImage = try container.decode(String.self, forKey: .systemImage) self.command = try container.decode(String.self, forKey: .command) self.execution = try container.decode(UserCustomCommandExecution.self, forKey: .execution) self.splitDirection = try container.decodeIfPresent(UserCustomSplitDirection.self, forKey: .splitDirection) ?? .right self.closeOnSuccess = try container.decodeIfPresent(Bool.self, forKey: .closeOnSuccess) ?? false // Preserves raw shortcut so downstream migration/validation can inspect the original key. self.shortcut = try container.decodeIfPresent(UserCustomShortcut.self, forKey: .shortcut) self.isEnabled = try container.decodeIfPresent(Bool.self, forKey: .isEnabled) ?? true }
static func `default`(index: Int) -> UserCustomCommand { UserCustomCommand( title: String(localized: "Command \(index + 1)"), systemImage: "terminal", command: "", execution: .shellScript, shortcut: nil ) }
func normalized() -> UserCustomCommand { UserCustomCommand( id: id, title: title, systemImage: systemImage, command: command, execution: execution, splitDirection: splitDirection, closeOnSuccess: closeOnSuccess, shortcut: shortcut?.normalized(), isEnabled: isEnabled ) } static func normalizedCommands(_ commands: [UserCustomCommand]) -> [UserCustomCommand] { commands.map { $0.normalized() } }
var resolvedTitle: String { let trimmed = title.trimmingCharacters(in: .whitespacesAndNewlines) if trimmed.isEmpty { return "Command" } return trimmed }
var resolvedSystemImage: String { let trimmed = systemImage.trimmingCharacters(in: .whitespacesAndNewlines) if trimmed.isEmpty { return "terminal" } return trimmed }
var hasRunnableCommand: Bool { !command.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }}
nonisolated enum CustomCommandSource: String, Codable, CaseIterable, Equatable, Hashable, Sendable { case repository case global
var displayTitle: String { switch self { case .repository: String(localized: "Local") case .global: String(localized: "Global") } }
/// Extra sentence appended to hover tooltips; global commands are only /// distinguished there so toolbar buttons stay visually uniform. var tooltipNote: String? { switch self { case .repository: nil case .global: String(localized: "Defined as a global command") } }}
nonisolated struct EffectiveCustomCommand: Equatable, Sendable, Identifiable { nonisolated struct Identifier: Hashable, Sendable { let source: CustomCommandSource let commandID: UserCustomCommand.ID }
let source: CustomCommandSource let command: UserCustomCommand
var id: Identifier { Identifier(source: source, commandID: command.id) }
var keybindingID: String { LegacyCustomCommandShortcutMigration.customCommandBindingID(for: command.id, source: source) }
/// Repository commands keep the pre-global `custom-command.<id>` form so /// persisted palette recency survives; globals get their own namespace, /// mirroring the keybinding ID scheme. var paletteID: String { switch source { case .repository: "custom-command.\(command.id)" case .global: "custom-command.global.\(command.id)" } }
static func resolve( repositoryCommands: [UserCustomCommand], globalCommands: [UserCustomCommand], disabledGlobalCommandIDs: Set<UserCustomCommand.ID> = [] ) -> [EffectiveCustomCommand] { UserCustomCommand.normalizedCommands(repositoryCommands) .filter(\.isEnabled) .map { .init(source: .repository, command: $0) } + UserCustomCommand.normalizedCommands(globalCommands) .filter { $0.isEnabled && !disabledGlobalCommandIDs.contains($0.id) } .map { .init(source: .global, command: $0) } }}
nonisolated enum UserCustomCommandExecution: String, Codable, CaseIterable, Identifiable, Sendable { case shellScript case terminalInput case split
var id: String { rawValue }
var title: String { switch self { case .shellScript: return String(localized: "New Tab") case .terminalInput: return String(localized: "In Place") case .split: return String(localized: "New Split") } }
var supportsCloseOnSuccess: Bool { switch self { case .shellScript, .split: return true case .terminalInput: return false } }}
nonisolated enum UserCustomSplitDirection: String, Codable, CaseIterable, Identifiable, Sendable { case right case left case down case top
var id: String { rawValue }
var title: String { switch self { case .right: return String(localized: "Right") case .left: return String(localized: "Left") case .down: return String(localized: "Down") case .top: return String(localized: "Up") } }}
nonisolated struct UserCustomShortcut: Codable, Equatable, Sendable { var key: String var modifiers: UserCustomShortcutModifiers
init(key: String, modifiers: UserCustomShortcutModifiers) { self.key = key self.modifiers = modifiers }
func normalized() -> UserCustomShortcut { let scalar = key.trimmingCharacters(in: .whitespacesAndNewlines).first return UserCustomShortcut( key: scalar.map { String($0).lowercased() } ?? "", modifiers: modifiers ) }
var isValid: Bool { let normalizedKey = key.trimmingCharacters(in: .whitespacesAndNewlines) return normalizedKey.count == 1 }
var display: String { var parts: [String] = [] if modifiers.command { parts.append("⌘") } if modifiers.shift { parts.append("⇧") } if modifiers.option { parts.append("⌥") } if modifiers.control { parts.append("⌃") } parts.append(key.uppercased()) return parts.joined() }}
nonisolated struct UserCustomShortcutModifiers: Codable, Equatable, Sendable { var command: Bool var shift: Bool var option: Bool var control: Bool
init(command: Bool = true, shift: Bool = false, option: Bool = false, control: Bool = false) { self.command = command self.shift = shift self.option = option self.control = control }
var isEmpty: Bool { !command && !shift && !option && !control }}