diff --git a/Sources/Features/LogView/LogSessionView.swift b/Sources/Features/LogView/LogSessionView.swift index aa97385..d6dc7b9 100644 --- a/Sources/Features/LogView/LogSessionView.swift +++ b/Sources/Features/LogView/LogSessionView.swift @@ -414,15 +414,10 @@ private struct PackagePicker: View { // Dismiss the popover first, then apply the filter on the next runloop — // mutating ancestor state while the popover tears down can crash on macOS. show = false - // Android & simulator resolve the package/bundle id to PIDs. Physical iOS - // streams structured os_log and scopes by the app's *process name* (the - // display name), so pass that there. `nil` = "All processes". - let value: String - if let app { - value = session.device.platform == .iosDevice ? app.display : app.id - } else { - value = "" - } + // Every platform now scopes by the package / bundle id: Android & simulator + // resolve it to PIDs; physical iOS launches that bundle under devicectl's console + // for un-redacted os_log + print(). Empty = "All processes" (whole-device passive). + let value = app?.id ?? "" DispatchQueue.main.async { onSelect(value) } } } diff --git a/Sources/Model/AppModel.swift b/Sources/Model/AppModel.swift index 415fe7b..33a5db4 100644 --- a/Sources/Model/AppModel.swift +++ b/Sources/Model/AppModel.swift @@ -354,12 +354,19 @@ final class AppModel { case .android: return adb.map { AndroidLogSource(adbURL: $0, serial: device.id) } case .iosSimulator: return SimulatorLogSource(udid: device.id) case .iosDevice: - // Structured logs (level · subsystem · category) via Apple's private - // LoggingSupport engine (OSActivityStream) — the Xcode/Console-grade - // stream. `bundleID` here is the selected app's process/display name and - // narrows the whole-device stream to that app (empty = whole device). - // Falls back to idevicesyslog internally if the private API is unavailable. - return IOSDeviceOSLogSource(udid: device.id, processFilter: bundleID) + // Whole device → passive structured stream via Apple's LoggingSupport + // engine (real level · subsystem · category, no app launch); falls back to + // idevicesyslog internally if the private API is unavailable. Its catch: + // private os_log args arrive as (the relay redacts them). + // + // A targeted app → launch it under devicectl's console with + // OS_ACTIVITY_DT_MODE=enable, which mirrors that app's os_log **un-redacted** + // plus print()/stdout — the Xcode-console experience, scoped to the app. + // `bundleID` is the app's bundle id (launching is by bundle id, so it + // re-launches the app each time capture starts, by design). + return bundleID.isEmpty + ? IOSDeviceOSLogSource(udid: device.id, processFilter: nil) + : IOSDeviceConsoleLogSource(udid: device.id, bundleID: bundleID) } } // Simulators can additionally stream the targeted app's stdout (`print()`), diff --git a/Sources/Model/LogSession.swift b/Sources/Model/LogSession.swift index f7e3bba..31f2695 100644 --- a/Sources/Model/LogSession.swift +++ b/Sources/Model/LogSession.swift @@ -387,9 +387,9 @@ final class LogSession: WorkspaceTab { $0.pids = package.isEmpty ? nil : [] $0.processNameQuery = "" case .iosDevice: - // The structured (LoggingSupport) source narrows to the targeted app's - // process itself, so no per-line LogFilter process query is needed; the - // label is the app's process/display name. + // The *source itself* is swapped per target (whole-device passive stream + // vs. devicectl-console launch of the bundle), so no per-line LogFilter + // process query is needed. The label is the app's bundle id. $0.processNameQuery = "" $0.pids = nil } @@ -399,11 +399,11 @@ final class LogSession: WorkspaceTab { restartPrimaryForTargetChange() } - /// Physical iOS re-scopes its *primary* structured source when the targeted app - /// changes (unlike Android/simulator, which keep one source and filter/launch-console - /// on top): an empty target streams the whole device, a name scopes to that app's - /// process. Stopping the current source lets the reconnect loop respawn with the new - /// scope; the source emits its own "▶︎ structured device logs (…)" marker. + /// Physical iOS swaps its *primary* source when the targeted app changes (unlike + /// Android/simulator, which keep one source and filter on top): an empty target → + /// whole-device passive LoggingSupport stream; a bundle id → a devicectl-console + /// launch of that app (un-redacted os_log + print()). Stopping the current source + /// lets the reconnect loop respawn the right one; each source emits its own marker. private func restartPrimaryForTargetChange() { guard isRunning, device.platform == .iosDevice else { return } swappingSource = true