···2323## Development
24242525```bash
2626-make check # Run swiftformat and swiftlint
2727-make test # Run tests
2828-make format # Run swift-format
2626+make check # Run swiftformat and swiftlint
2727+make test # Run app/unit tests (xcodebuild)
2828+make build-cli # Build `prowl` CLI via SwiftPM
2929+make test-cli-smoke # Quick CLI smoke checks
3030+make test-cli-integration # End-to-end CLI socket integration tests
3131+make format # Run swift-format
2932```
30333134## Contributing
+16
supacode/App/supacodeApp.swift
···3131final class SupacodeAppDelegate: NSObject, NSApplicationDelegate {
3232 var appStore: StoreOf<AppFeature>?
3333 var terminalManager: WorktreeTerminalManager?
3434+ var cliSocketServer: CLISocketServer?
34353536 func applicationDidFinishLaunching(_ notification: Notification) {
3637 // Disable press-and-hold accent menu so that key repeat works in the terminal.
···5253 }
53545455 func applicationWillTerminate(_ notification: Notification) {
5656+ defer { cliSocketServer?.stop() }
5557 guard appStore?.state.settings.restoreTerminalLayoutOnLaunch == true else { return }
5658 guard appStore?.state.suppressLayoutSaveUntilRelaunch != true else { return }
5759 terminalManager?.persistLayoutSnapshotSync()
···9193 @State private var terminalManager: WorktreeTerminalManager
9294 @State private var worktreeInfoWatcher: WorktreeInfoWatcherManager
9395 @State private var commandKeyObserver: CommandKeyObserver
9696+ @State private var cliSocketServer: CLISocketServer
9497 @State private var store: StoreOf<AppFeature>
95989699 @MainActor init() {
···172175 )
173176 }
174177 _store = State(initialValue: appStore)
178178+179179+ let cliRouter = CLICommandRouter()
180180+ let cliServer = CLISocketServer(router: cliRouter)
181181+ let cliLogger = SupaLogger("CLIService")
182182+ do {
183183+ try cliServer.start()
184184+ cliLogger.info("CLI socket server started at \(ProwlSocket.defaultPath)")
185185+ } catch {
186186+ cliLogger.warning("Failed to start CLI socket server: \(String(describing: error))")
187187+ }
188188+ _cliSocketServer = State(initialValue: cliServer)
189189+175190 runtime.onQuit = { [weak appStore] in
176191 appStore?.send(.requestQuit)
177192 }
178193 appDelegate.appStore = appStore
179194 appDelegate.terminalManager = terminalManager
195195+ appDelegate.cliSocketServer = cliServer
180196 SettingsWindowManager.shared.configure(
181197 store: appStore,
182198 ghosttyShortcuts: shortcuts,
+62
supacode/CLIService/CLICommandRouter.swift
···11+// supacode/CLIService/CLICommandRouter.swift
22+// Routes incoming command envelopes to the appropriate handler.
33+44+import Foundation
55+66+@MainActor
77+final class CLICommandRouter {
88+ private let openHandler: any CommandHandler
99+ private let listHandler: any CommandHandler
1010+ private let focusHandler: any CommandHandler
1111+ private let sendHandler: any CommandHandler
1212+ private let keyHandler: any CommandHandler
1313+ private let readHandler: any CommandHandler
1414+1515+ init(
1616+ openHandler: any CommandHandler = StubCommandHandler(command: "open"),
1717+ listHandler: any CommandHandler = StubCommandHandler(command: "list"),
1818+ focusHandler: any CommandHandler = StubCommandHandler(command: "focus"),
1919+ sendHandler: any CommandHandler = StubCommandHandler(command: "send"),
2020+ keyHandler: any CommandHandler = StubCommandHandler(command: "key"),
2121+ readHandler: any CommandHandler = StubCommandHandler(command: "read")
2222+ ) {
2323+ self.openHandler = openHandler
2424+ self.listHandler = listHandler
2525+ self.focusHandler = focusHandler
2626+ self.sendHandler = sendHandler
2727+ self.keyHandler = keyHandler
2828+ self.readHandler = readHandler
2929+ }
3030+3131+ func route(_ envelope: CommandEnvelope) async -> CommandResponse {
3232+ let handler: any CommandHandler
3333+ switch envelope.command {
3434+ case .open: handler = openHandler
3535+ case .list: handler = listHandler
3636+ case .focus: handler = focusHandler
3737+ case .send: handler = sendHandler
3838+ case .key: handler = keyHandler
3939+ case .read: handler = readHandler
4040+ }
4141+ return await handler.handle(envelope: envelope)
4242+ }
4343+}
4444+4545+// MARK: - Stub handler (placeholder until real handlers are implemented)
4646+4747+struct StubCommandHandler: CommandHandler {
4848+ let command: String
4949+5050+ // swiftlint:disable:next async_without_await
5151+ func handle(envelope: CommandEnvelope) async -> CommandResponse {
5252+ CommandResponse(
5353+ ok: false,
5454+ command: command,
5555+ schemaVersion: "prowl.cli.\(command).v1",
5656+ error: CommandError(
5757+ code: "NOT_IMPLEMENTED",
5858+ message: "Command '\(command)' is not yet implemented."
5959+ )
6060+ )
6161+ }
6262+}
+179
supacode/CLIService/CLISocketServer.swift
···11+// supacode/CLIService/CLISocketServer.swift
22+// Unix domain socket server that listens for CLI command requests.
33+44+#if canImport(Darwin)
55+import Darwin
66+#elseif canImport(Glibc)
77+import Glibc
88+#endif
99+import Foundation
1010+1111+@MainActor
1212+final class CLISocketServer {
1313+ private let router: CLICommandRouter
1414+ private let socketPath: String
1515+ private var serverFD: Int32 = -1
1616+ private var isRunning = false
1717+ private let acceptQueue = DispatchQueue(label: "com.onevcat.prowl.cli-accept", qos: .userInitiated)
1818+1919+ init(router: CLICommandRouter, socketPath: String = ProwlSocket.defaultPath) {
2020+ self.router = router
2121+ self.socketPath = socketPath
2222+ }
2323+2424+ /// Start listening for CLI connections.
2525+ func start() throws {
2626+ // Clean up stale socket file
2727+ unlink(socketPath)
2828+2929+ // Create socket
3030+ serverFD = socket(AF_UNIX, SOCK_STREAM, 0)
3131+ guard serverFD >= 0 else {
3232+ throw CLIServiceError.socketCreationFailed
3333+ }
3434+3535+ // Bind
3636+ var addr = sockaddr_un()
3737+ addr.sun_family = sa_family_t(AF_UNIX)
3838+ let pathBytes = Array(socketPath.utf8)
3939+ let maxLen = MemoryLayout.size(ofValue: addr.sun_path) - 1
4040+ let copyLen = min(pathBytes.count, maxLen)
4141+ withUnsafeMutableBytes(of: &addr.sun_path) { sunPathPtr in
4242+ for idx in 0..<copyLen {
4343+ sunPathPtr[idx] = pathBytes[idx]
4444+ }
4545+ sunPathPtr[copyLen] = 0
4646+ }
4747+4848+ let bindResult = withUnsafePointer(to: &addr) { ptr in
4949+ ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockPtr in
5050+ bind(serverFD, sockPtr, socklen_t(MemoryLayout<sockaddr_un>.size))
5151+ }
5252+ }
5353+5454+ guard bindResult == 0 else {
5555+ close(serverFD)
5656+ throw CLIServiceError.bindFailed
5757+ }
5858+5959+ // Listen
6060+ guard listen(serverFD, 5) == 0 else {
6161+ close(serverFD)
6262+ throw CLIServiceError.listenFailed
6363+ }
6464+6565+ isRunning = true
6666+6767+ // Run the blocking accept loop on a dedicated dispatch queue so it does
6868+ // not occupy a Swift cooperative-thread-pool thread (which would starve
6969+ // the concurrency runtime and hang the app – especially during testing).
7070+ let listeningFD = serverFD
7171+ acceptQueue.async { [weak self] in
7272+ Self.acceptLoop(serverFD: listeningFD, server: self)
7373+ }
7474+ }
7575+7676+ /// Stop the server and clean up.
7777+ func stop() {
7878+ isRunning = false
7979+ if serverFD >= 0 {
8080+ close(serverFD)
8181+ serverFD = -1
8282+ }
8383+ unlink(socketPath)
8484+ }
8585+8686+ // MARK: - Accept loop (runs on acceptQueue, NOT in Swift concurrency)
8787+8888+ private nonisolated static func acceptLoop(serverFD: Int32, server: CLISocketServer?) {
8989+ while true {
9090+ let clientFD = Darwin.accept(serverFD, nil, nil)
9191+ guard clientFD >= 0 else {
9292+ // serverFD was closed (stop() called) or an error occurred – exit.
9393+ return
9494+ }
9595+ if let server {
9696+ Task { @MainActor in
9797+ await server.handleClient(clientFD: clientFD)
9898+ }
9999+ } else {
100100+ Darwin.close(clientFD)
101101+ }
102102+ }
103103+ }
104104+105105+ private func handleClient(clientFD: Int32) async {
106106+ defer { Darwin.close(clientFD) }
107107+108108+ do {
109109+ // Read length-prefixed request
110110+ let lengthData = try Self.fdRead(fildes: clientFD, count: 4)
111111+ let length = lengthData.withUnsafeBytes {
112112+ UInt32(bigEndian: $0.load(as: UInt32.self))
113113+ }
114114+ guard length > 0, length < 10_000_000 else { return }
115115+116116+ let requestData = try Self.fdRead(fildes: clientFD, count: Int(length))
117117+118118+ // Decode envelope
119119+ let decoder = JSONDecoder()
120120+ let envelope = try decoder.decode(CommandEnvelope.self, from: requestData)
121121+122122+ // Route to handler
123123+ let response = await router.route(envelope)
124124+125125+ // Encode and send response
126126+ let encoder = JSONEncoder()
127127+ encoder.outputFormatting = [.sortedKeys]
128128+ let responseData = try encoder.encode(response)
129129+130130+ var responseLength = UInt32(responseData.count).bigEndian
131131+ try withUnsafeBytes(of: &responseLength) { try Self.fdWrite(fildes: clientFD, buffer: $0) }
132132+ try responseData.withUnsafeBytes { try Self.fdWrite(fildes: clientFD, buffer: $0) }
133133+ } catch {
134134+ // Connection-level errors are silently dropped
135135+ }
136136+ }
137137+138138+ // MARK: - Low-level I/O using Darwin read/write
139139+140140+ private static func fdRead(fildes: Int32, count: Int) throws -> Data {
141141+ var data = Data(capacity: count)
142142+ var remaining = count
143143+ let bufferSize = min(count, 65536)
144144+ let buffer = UnsafeMutableRawPointer.allocate(byteCount: bufferSize, alignment: 1)
145145+ defer { buffer.deallocate() }
146146+ while remaining > 0 {
147147+ let toRead = min(remaining, bufferSize)
148148+ let bytesRead = Darwin.read(fildes, buffer, toRead)
149149+ guard bytesRead > 0 else {
150150+ throw CLIServiceError.readFailed
151151+ }
152152+ data.append(buffer.assumingMemoryBound(to: UInt8.self), count: bytesRead)
153153+ remaining -= bytesRead
154154+ }
155155+ return data
156156+ }
157157+158158+ private static func fdWrite(fildes: Int32, buffer: UnsafeRawBufferPointer) throws {
159159+ var offset = 0
160160+ while offset < buffer.count {
161161+ let written = Darwin.write(fildes, buffer.baseAddress!.advanced(by: offset), buffer.count - offset)
162162+ guard written > 0 else {
163163+ throw CLIServiceError.writeFailed
164164+ }
165165+ offset += written
166166+ }
167167+ }
168168+}
169169+170170+// MARK: - Errors
171171+172172+enum CLIServiceError: Error {
173173+ case socketCreationFailed
174174+ case socketPathTooLong
175175+ case bindFailed
176176+ case listenFailed
177177+ case readFailed
178178+ case writeFailed
179179+}
+11
supacode/CLIService/CommandHandlerProtocol.swift
···11+// supacode/CLIService/CommandHandlerProtocol.swift
22+// Protocol for command handlers on the app side.
33+44+import Foundation
55+66+/// Each CLI command has a corresponding handler that executes
77+/// within the app's process context.
88+protocol CommandHandler {
99+ /// Execute the command and return a structured response.
1010+ func handle(envelope: CommandEnvelope) async -> CommandResponse
1111+}
+34
supacode/CLIService/Shared/CommandEnvelope.swift
···11+// ProwlShared/CommandEnvelope.swift
22+// The handoff contract between CLI parser and app command service.
33+44+import Foundation
55+66+public struct CommandEnvelope: Codable, Sendable {
77+ public let output: OutputMode
88+ public let command: Command
99+1010+ public init(output: OutputMode, command: Command) {
1111+ self.output = output
1212+ self.command = command
1313+ }
1414+}
1515+1616+public enum Command: Codable, Sendable {
1717+ case open(OpenInput)
1818+ case list(ListInput)
1919+ case focus(FocusInput)
2020+ case send(SendInput)
2121+ case key(KeyInput)
2222+ case read(ReadInput)
2323+2424+ public var name: String {
2525+ switch self {
2626+ case .open: "open"
2727+ case .list: "list"
2828+ case .focus: "focus"
2929+ case .send: "send"
3030+ case .key: "key"
3131+ case .read: "read"
3232+ }
3333+ }
3434+}
+149
supacode/CLIService/Shared/CommandResponse.swift
···11+// ProwlShared/CommandResponse.swift
22+// Structured response from app command service back to CLI.
33+44+import Foundation
55+66+/// Top-level response wrapper.
77+/// For v1, `data` is left as raw JSON bytes so each command can define
88+/// its own strongly-typed success payload without introducing `Any`.
99+public struct CommandResponse: Codable, Sendable {
1010+ // swiftlint:disable:next identifier_name
1111+ public let ok: Bool
1212+ public let command: String
1313+ public let schemaVersion: String
1414+1515+ /// Raw JSON data payload (success case). Consumers decode into
1616+ /// command-specific types. Nil when `ok == false`.
1717+ public let data: RawJSON?
1818+1919+ /// Error payload (failure case). Nil when `ok == true`.
2020+ public let error: CommandError?
2121+2222+ public init(
2323+ // swiftlint:disable:next identifier_name
2424+ ok: Bool,
2525+ command: String,
2626+ schemaVersion: String,
2727+ data: RawJSON? = nil,
2828+ error: CommandError? = nil
2929+ ) {
3030+ self.ok = ok
3131+ self.command = command
3232+ self.schemaVersion = schemaVersion
3333+ self.data = data
3434+ self.error = error
3535+ }
3636+3737+ enum CodingKeys: String, CodingKey {
3838+ // swiftlint:disable:next identifier_name
3939+ case ok
4040+ case command
4141+ case schemaVersion = "schema_version"
4242+ case data
4343+ case error
4444+ }
4545+}
4646+4747+public struct CommandError: Codable, Sendable {
4848+ public let code: String
4949+ public let message: String
5050+5151+ public init(code: String, message: String) {
5252+ self.code = code
5353+ self.message = message
5454+ }
5555+}
5656+5757+// MARK: - RawJSON
5858+5959+/// A type-safe wrapper around raw JSON bytes.
6060+/// Preserves the original JSON without round-tripping through `Any`.
6161+/// Fully `Sendable` because it only holds `Data`.
6262+public struct RawJSON: Codable, Sendable {
6363+ public let bytes: Data
6464+6565+ public init(_ bytes: Data) {
6666+ self.bytes = bytes
6767+ }
6868+6969+ /// Create from an Encodable value.
7070+ public init<T: Encodable>(encoding value: T) throws {
7171+ let encoder = JSONEncoder()
7272+ encoder.outputFormatting = [.sortedKeys]
7373+ self.bytes = try encoder.encode(value)
7474+ }
7575+7676+ public init(from decoder: Decoder) throws {
7777+ // When decoding as part of a larger structure, capture the raw JSON.
7878+ // This works by re-encoding the decoded JSON value container.
7979+ let container = try decoder.singleValueContainer()
8080+ // Decode as a generic JSON value, then re-encode to bytes.
8181+ let jsonValue = try container.decode(JSONValue.self)
8282+ let encoder = JSONEncoder()
8383+ encoder.outputFormatting = [.sortedKeys]
8484+ self.bytes = try encoder.encode(jsonValue)
8585+ }
8686+8787+ public func encode(to encoder: Encoder) throws {
8888+ // Decode bytes back to JSONValue, then encode inline.
8989+ let decoder = JSONDecoder()
9090+ let jsonValue = try decoder.decode(JSONValue.self, from: bytes)
9191+ var container = encoder.singleValueContainer()
9292+ try container.encode(jsonValue)
9393+ }
9494+9595+ /// Decode the raw JSON into a specific type.
9696+ public func decode<T: Decodable>(as type: T.Type) throws -> T {
9797+ try JSONDecoder().decode(type, from: bytes)
9898+ }
9999+}
100100+101101+// MARK: - JSONValue (internal helper for RawJSON round-tripping)
102102+103103+/// A simple recursive JSON value type that is fully Codable and Sendable.
104104+enum JSONValue: Codable, Sendable {
105105+ case null
106106+ case bool(Bool)
107107+ case int(Int)
108108+ case double(Double)
109109+ case string(String)
110110+ case array([JSONValue])
111111+ case object([String: JSONValue])
112112+113113+ init(from decoder: Decoder) throws {
114114+ let container = try decoder.singleValueContainer()
115115+ if container.decodeNil() {
116116+ self = .null
117117+ } else if let boolValue = try? container.decode(Bool.self) {
118118+ self = .bool(boolValue)
119119+ } else if let intValue = try? container.decode(Int.self) {
120120+ self = .int(intValue)
121121+ } else if let doubleValue = try? container.decode(Double.self) {
122122+ self = .double(doubleValue)
123123+ } else if let stringValue = try? container.decode(String.self) {
124124+ self = .string(stringValue)
125125+ } else if let arrayValue = try? container.decode([JSONValue].self) {
126126+ self = .array(arrayValue)
127127+ } else if let objectValue = try? container.decode([String: JSONValue].self) {
128128+ self = .object(objectValue)
129129+ } else {
130130+ throw DecodingError.dataCorruptedError(
131131+ in: container,
132132+ debugDescription: "Unsupported JSON value"
133133+ )
134134+ }
135135+ }
136136+137137+ func encode(to encoder: Encoder) throws {
138138+ var container = encoder.singleValueContainer()
139139+ switch self {
140140+ case .null: try container.encodeNil()
141141+ case .bool(let boolValue): try container.encode(boolValue)
142142+ case .int(let intValue): try container.encode(intValue)
143143+ case .double(let doubleValue): try container.encode(doubleValue)
144144+ case .string(let stringValue): try container.encode(stringValue)
145145+ case .array(let arrayValue): try container.encode(arrayValue)
146146+ case .object(let objectValue): try container.encode(objectValue)
147147+ }
148148+ }
149149+}
+42
supacode/CLIService/Shared/ErrorCodes.swift
···11+// ProwlShared/ErrorCodes.swift
22+// Stable error codes matching schema.md contracts.
33+44+import Foundation
55+66+public enum CLIErrorCode {
77+ // Common
88+ public static let appNotRunning = "APP_NOT_RUNNING"
99+ public static let invalidArgument = "INVALID_ARGUMENT"
1010+ public static let targetNotFound = "TARGET_NOT_FOUND"
1111+ public static let targetNotUnique = "TARGET_NOT_UNIQUE"
1212+1313+ // Open
1414+ public static let pathNotFound = "PATH_NOT_FOUND"
1515+ public static let pathNotDirectory = "PATH_NOT_DIRECTORY"
1616+ public static let pathNotAllowed = "PATH_NOT_ALLOWED"
1717+ public static let launchFailed = "LAUNCH_FAILED"
1818+ public static let openFailed = "OPEN_FAILED"
1919+2020+ // List
2121+ public static let listFailed = "LIST_FAILED"
2222+2323+ // Focus
2424+ public static let focusFailed = "FOCUS_FAILED"
2525+2626+ // Send
2727+ public static let emptyInput = "EMPTY_INPUT"
2828+ public static let sendFailed = "SEND_FAILED"
2929+3030+ // Key
3131+ public static let invalidRepeat = "INVALID_REPEAT"
3232+ public static let noActivePane = "NO_ACTIVE_PANE"
3333+ public static let unsupportedKey = "UNSUPPORTED_KEY"
3434+ public static let keyDeliveryFailed = "KEY_DELIVERY_FAILED"
3535+3636+ // Read
3737+ public static let readFailed = "READ_FAILED"
3838+3939+ // Transport
4040+ public static let transportFailed = "TRANSPORT_FAILED"
4141+ public static let timeout = "TIMEOUT"
4242+}
+67
supacode/CLIService/Shared/InputModels.swift
···11+// ProwlShared/InputModels.swift
22+// Typed input models matching input.md contract
33+44+import Foundation
55+66+public struct OpenInput: Codable, Sendable {
77+ /// Normalized absolute path, or nil for bare `prowl` (bring to front).
88+ public let path: String?
99+1010+ public init(path: String? = nil) {
1111+ self.path = path
1212+ }
1313+}
1414+1515+public struct ListInput: Codable, Sendable {
1616+ public init() {}
1717+}
1818+1919+public struct FocusInput: Codable, Sendable {
2020+ public let selector: TargetSelector
2121+2222+ public init(selector: TargetSelector = .none) {
2323+ self.selector = selector
2424+ }
2525+}
2626+2727+public struct SendInput: Codable, Sendable {
2828+ public let selector: TargetSelector
2929+ public let text: String
3030+ public let trailingEnter: Bool
3131+3232+ public init(
3333+ selector: TargetSelector = .none,
3434+ text: String,
3535+ trailingEnter: Bool = true
3636+ ) {
3737+ self.selector = selector
3838+ self.text = text
3939+ self.trailingEnter = trailingEnter
4040+ }
4141+}
4242+4343+public struct KeyInput: Codable, Sendable {
4444+ public let selector: TargetSelector
4545+ public let token: String
4646+ public let repeatCount: Int
4747+4848+ public init(
4949+ selector: TargetSelector = .none,
5050+ token: String,
5151+ repeatCount: Int = 1
5252+ ) {
5353+ self.selector = selector
5454+ self.token = token
5555+ self.repeatCount = repeatCount
5656+ }
5757+}
5858+5959+public struct ReadInput: Codable, Sendable {
6060+ public let selector: TargetSelector
6161+ public let last: Int?
6262+6363+ public init(selector: TargetSelector = .none, last: Int? = nil) {
6464+ self.selector = selector
6565+ self.last = last
6666+ }
6767+}
+9
supacode/CLIService/Shared/OutputMode.swift
···11+// ProwlShared/OutputMode.swift
22+// Shared between CLI and App targets
33+44+import Foundation
55+66+public enum OutputMode: String, Codable, Sendable {
77+ case text
88+ case json
99+}
+21
supacode/CLIService/Shared/SocketConstants.swift
···11+// ProwlShared/SocketConstants.swift
22+// Shared socket path convention between CLI client and app server.
33+44+import Foundation
55+66+public enum ProwlSocket {
77+ /// Environment variable for overriding socket path.
88+ public static let environmentKey = "PROWL_CLI_SOCKET"
99+1010+ /// Default Unix domain socket path.
1111+ /// Located in user's temporary directory to avoid permission issues.
1212+ ///
1313+ /// If `PROWL_CLI_SOCKET` is set and not empty, it takes precedence.
1414+ public static var defaultPath: String {
1515+ if let override = ProcessInfo.processInfo.environment[environmentKey], !override.isEmpty {
1616+ return override
1717+ }
1818+ let tmpDir = NSTemporaryDirectory()
1919+ return (tmpDir as NSString).appendingPathComponent("prowl-cli.sock")
2020+ }
2121+}
+13
supacode/CLIService/Shared/TargetSelector.swift
···11+// ProwlShared/TargetSelector.swift
22+// Shared between CLI and App targets
33+44+import Foundation
55+66+/// Exactly zero or one selector is allowed per command.
77+/// Multiple selectors → INVALID_ARGUMENT.
88+public enum TargetSelector: Codable, Sendable, Equatable {
99+ case none
1010+ case worktree(String)
1111+ case tab(String)
1212+ case pane(String)
1313+}
+29
supacode/CLIService/TargetResolver.swift
···11+// supacode/CLIService/TargetResolver.swift
22+// Resolves target selectors against current app state.
33+// Scaffold — actual resolution depends on wiring to WorktreeTerminalManager.
44+55+import Foundation
66+77+@MainActor
88+final class TargetResolver {
99+ /// Resolve a target selector to concrete worktree/tab/pane IDs.
1010+ /// Returns nil if the target cannot be found.
1111+ func resolve(_ selector: TargetSelector) -> ResolvedTarget? {
1212+ // Scaffold: resolution not yet wired to WorktreeTerminalManager.
1313+ // Will be implemented when command handlers are built out.
1414+ switch selector {
1515+ case .none:
1616+ return nil
1717+ case .worktree, .tab, .pane:
1818+ return nil
1919+ }
2020+ }
2121+}
2222+2323+/// Placeholder for resolved target information.
2424+/// Will be populated with actual worktree/tab/pane data when wired.
2525+struct ResolvedTarget {
2626+ let worktreeID: String
2727+ let tabID: String
2828+ let paneID: String
2929+}