native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
5.8 kB · 186 lines
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187// App/Sources/CLIService/TabCommandHandler.swift
import Foundationimport ProwlCLIShared
struct TabResolvedTarget: Sendable, Equatable { let worktreeID: String let worktreeName: String let worktreePath: String let worktreeRootPath: String let worktreeKind: String let tabID: String let tabTitle: String let tabSelected: Bool let paneID: String let paneTitle: String let paneCWD: String? let paneFocused: Bool}
extension TabResolvedTarget { init(from resolved: ResolvedTarget) { self.worktreeID = resolved.worktreeID self.worktreeName = resolved.worktreeName self.worktreePath = resolved.worktreePath self.worktreeRootPath = resolved.worktreeRootPath self.worktreeKind = resolved.worktreeKind.rawValue self.tabID = resolved.tabID.uuidString self.tabTitle = resolved.tabTitle self.tabSelected = resolved.tabSelected self.paneID = resolved.paneID.uuidString self.paneTitle = resolved.paneTitle self.paneCWD = resolved.paneCWD self.paneFocused = resolved.paneFocused }}
extension TabTarget { init(from target: TabResolvedTarget) { self.init( worktree: TabTargetWorktree( id: target.worktreeID, name: target.worktreeName, path: target.worktreePath, rootPath: target.worktreeRootPath, kind: target.worktreeKind ), tab: TabTargetTab( id: target.tabID, title: target.tabTitle, selected: target.tabSelected ), pane: TabTargetPane( id: target.paneID, title: target.paneTitle, cwd: target.paneCWD, focused: target.paneFocused ) ) }}
@MainActorfinal class TabCommandHandler: CommandHandler { typealias ResolveProvider = @MainActor (TargetSelector) -> Result<TabResolvedTarget, TargetResolverError> typealias CreateTabProvider = @MainActor (TabResolvedTarget, String?) -> TabResolvedTarget? typealias CloseTabProvider = @MainActor (TabResolvedTarget, Bool) -> Bool
private let resolveProvider: ResolveProvider private let createTab: CreateTabProvider private let closeTab: CloseTabProvider
init( resolveProvider: @escaping ResolveProvider, createTab: @escaping CreateTabProvider, closeTab: @escaping CloseTabProvider ) { self.resolveProvider = resolveProvider self.createTab = createTab self.closeTab = closeTab }
// swiftlint:disable:next async_without_await func handle(envelope: CommandEnvelope) async -> CommandResponse { guard case .tab(let input) = envelope.command else { return errorResponse(code: CLIErrorCode.tabFailed, message: "Invalid command.") }
if input.action == .close, input.selector.isNone { return errorResponse( code: CLIErrorCode.invalidArgument, message: "tab close requires an explicit target selector." ) }
let target: TabResolvedTarget switch resolveProvider(input.selector) { case .success(let resolved): target = resolved case .failure(let error): return error.commandResponse(command: "tab") }
switch input.action { case .create: return handleCreate(input: input, target: target) case .close: return handleClose(input: input, target: target) } }
private func handleCreate(input: TabInput, target: TabResolvedTarget) -> CommandResponse { let path = normalizedAllowedPath(input.path, worktreePath: target.worktreePath) guard input.path == nil || path != nil else { return errorResponse( code: CLIErrorCode.pathNotAllowed, message: "Tab path must be inside the resolved worktree." ) }
guard let createdTarget = createTab(target, path) else { return errorResponse(code: CLIErrorCode.tabFailed, message: "Failed to create tab.") } return success(action: .create, target: createdTarget) }
private func handleClose(input: TabInput, target: TabResolvedTarget) -> CommandResponse { guard closeTab(target, input.force) else { return errorResponse(code: CLIErrorCode.tabFailed, message: "Failed to close tab.") } return success(action: .close, target: target) }
private func normalizedAllowedPath(_ path: String?, worktreePath: String) -> String? { guard let path else { return nil } let normalizedPath = normalize(path) let normalizedWorktree = normalize(worktreePath) guard normalizedPath == normalizedWorktree || normalizedPath.hasPrefix(normalizedWorktree + "/") else { return nil } return normalizedPath }
private func normalize(_ path: String) -> String { URL(fileURLWithPath: path, isDirectory: true) .standardizedFileURL .path(percentEncoded: false) .trimmingTrailingSlash() }
private func success(action: TabAction, target: TabResolvedTarget) -> CommandResponse { do { return try CommandResponse( ok: true, command: "tab", schemaVersion: "prowl.cli.tab.v1", data: RawJSON(encoding: TabCommandPayload(action: action, target: makePayloadTarget(from: target))) ) } catch { return errorResponse(code: CLIErrorCode.tabFailed, message: "Failed to encode response.") } }
private func makePayloadTarget(from target: TabResolvedTarget) -> TabTarget { TabTarget(from: target) }
private func errorResponse(code: String, message: String) -> CommandResponse { CommandResponse( ok: false, command: "tab", schemaVersion: "prowl.cli.tab.v1", error: CommandError(code: code, message: message) ) }}
extension String { fileprivate func trimmingTrailingSlash() -> String { var value = self while value.count > 1, value.hasSuffix("/") { value.removeLast() } return value }}