native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
6.0 kB · 189 lines
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190// App/Sources/CLIService/FocusCommandHandler.swift// Handles `prowl focus` by resolving, focusing, and returning final pane context.
import Foundationimport ProwlCLIShared
/// Resolved target metadata for focus payload construction.struct FocusResolvedTarget: Sendable { let worktreeID: String let worktreeName: String let worktreePath: String let worktreeRootPath: String let worktreeKind: ListCommandWorktree.Kind let tabID: UUID let tabTitle: String let tabSelected: Bool let paneID: UUID let paneTitle: String let paneCWD: String? let paneFocused: Bool}
extension FocusResolvedTarget { init(from resolved: ResolvedTarget) { self.worktreeID = resolved.worktreeID self.worktreeName = resolved.worktreeName self.worktreePath = resolved.worktreePath self.worktreeRootPath = resolved.worktreeRootPath self.worktreeKind = resolved.worktreeKind self.tabID = resolved.tabID self.tabTitle = resolved.tabTitle self.tabSelected = resolved.tabSelected self.paneID = resolved.paneID self.paneTitle = resolved.paneTitle self.paneCWD = resolved.paneCWD self.paneFocused = resolved.paneFocused }}
@MainActorfinal class FocusCommandHandler: CommandHandler { typealias ResolveProvider = @MainActor (TargetSelector) -> Result<FocusResolvedTarget, TargetResolverError> typealias FocusPerformer = @MainActor (FocusResolvedTarget) -> Bool typealias BringToFront = @MainActor () -> Bool
private let resolveProvider: ResolveProvider private let focusPerformer: FocusPerformer private let bringToFront: BringToFront
init( resolveProvider: @escaping ResolveProvider, focusPerformer: @escaping FocusPerformer, bringToFront: @escaping BringToFront ) { self.resolveProvider = resolveProvider self.focusPerformer = focusPerformer self.bringToFront = bringToFront }
func handle(envelope: CommandEnvelope) -> CommandResponse { guard case .focus(let input) = envelope.command else { return errorResponse(code: CLIErrorCode.focusFailed, message: "Invalid command.") }
let requested = makeRequestedTarget(from: input.selector)
// Resolve requested selector. let requestedTarget: FocusResolvedTarget switch resolveProvider(input.selector) { case .success(let target): requestedTarget = target case .failure(let error): return error.commandResponse(command: "focus") }
// Apply focus operation. guard focusPerformer(requestedTarget) else { return errorResponse(code: CLIErrorCode.focusFailed, message: "Failed to focus requested target.") }
// Bring app window to front. guard bringToFront() else { return errorResponse(code: CLIErrorCode.focusFailed, message: "Failed to bring Prowl to front.") }
// Always return the final active pane context for command chaining. let finalTarget: FocusResolvedTarget switch resolveProvider(.none) { case .success(let target): finalTarget = target case .failure: return errorResponse(code: CLIErrorCode.focusFailed, message: "Focused target could not be resolved.") }
// Contract invariant: successful focus must end at selected tab + focused pane. guard finalTarget.tabSelected, finalTarget.paneFocused else { return errorResponse(code: CLIErrorCode.focusFailed, message: "Focused target was not activated.") }
let payload = FocusCommandPayload( requested: requested, resolvedVia: makeResolvedVia(from: input.selector, requestedTarget: requestedTarget), broughtToFront: true, target: makePayloadTarget(from: finalTarget) )
do { return try CommandResponse( ok: true, command: "focus", schemaVersion: "prowl.cli.focus.v1", data: RawJSON(encoding: payload) ) } catch { return errorResponse(code: CLIErrorCode.focusFailed, message: "Failed to encode response.") } }
private func makeRequestedTarget(from selector: TargetSelector) -> FocusRequestedTarget { switch selector { case .worktree(let value): return FocusRequestedTarget(selector: .worktree, value: value) case .tab(let value): return FocusRequestedTarget(selector: .tab, value: value) case .pane(let value): return FocusRequestedTarget(selector: .pane, value: value) case .auto(let value): return FocusRequestedTarget(selector: .auto, value: value) case .none: return FocusRequestedTarget(selector: .current, value: nil) } }
private func makeResolvedVia( from selector: TargetSelector, requestedTarget: FocusResolvedTarget ) -> FocusResolvedVia { switch selector { case .worktree: return .worktree case .tab: return .tab case .pane, .none: return .pane case .auto(let value): if let uuid = UUID(uuidString: value) { if uuid == requestedTarget.paneID { return .pane } if uuid == requestedTarget.tabID { return .tab } } return .worktree } }
private func makePayloadTarget(from target: FocusResolvedTarget) -> FocusTarget { FocusTarget( worktree: FocusTargetWorktree( id: target.worktreeID, name: target.worktreeName, path: target.worktreePath, rootPath: target.worktreeRootPath, kind: target.worktreeKind.rawValue ), tab: FocusTargetTab( id: target.tabID.uuidString, title: target.tabTitle, selected: target.tabSelected ), pane: FocusTargetPane( id: target.paneID.uuidString, title: target.paneTitle, cwd: target.paneCWD, focused: target.paneFocused ) ) }
private func errorResponse(code: String, message: String) -> CommandResponse { CommandResponse( ok: false, command: "focus", schemaVersion: "prowl.cli.focus.v1", error: CommandError(code: code, message: message) ) }}