From d114d9d3750cda22e62ddae2934bcc36b006c3df Mon Sep 17 00:00:00 2001 From: Natalie Date: Wed, 10 Jun 2026 05:00:56 -0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(cockpit-kit):=20=F0=9F=93=B8?= =?UTF-8?q?=20add=20bump=20screenshot=20overlay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../CocotteCockpitKit/ActivityView.swift | 29 +++++++++++++++++++ .../CocotteCockpitKit/AssetLibraryView.swift | 22 +------------- .../CocotteCockpitKit/CockpitAPI.swift | 6 ++++ .../CocotteCockpitKit/CockpitModel.swift | 6 ++++ .../CocotteCockpitKit/Components.swift | 21 ++++++++++++++ .../CocotteCockpitKit/LiveCockpitAPI.swift | 5 ++++ .../Endpoints/SurfacesEndpoints.swift | 15 ++++++++++ 7 files changed, 83 insertions(+), 21 deletions(-) create mode 100644 @platform/codebase/@packages/platform-api-client/Sources/CocottePlatformAPIClient/Endpoints/SurfacesEndpoints.swift diff --git a/@platform/codebase/@features/ai-copilot/cockpit-kit/Sources/CocotteCockpitKit/ActivityView.swift b/@platform/codebase/@features/ai-copilot/cockpit-kit/Sources/CocotteCockpitKit/ActivityView.swift index 4c98555..95a1016 100644 --- a/@platform/codebase/@features/ai-copilot/cockpit-kit/Sources/CocotteCockpitKit/ActivityView.swift +++ b/@platform/codebase/@features/ai-copilot/cockpit-kit/Sources/CocotteCockpitKit/ActivityView.swift @@ -11,6 +11,9 @@ public struct ActivityView: View { public var body: some View { Scroll { VStack(alignment: .leading, spacing: 0) { + if let shot = model.bumpScreenshot, let img = decodeImage(shot) { + BumpScreenshotCard(image: img).padding(.bottom, t.s4) + } HStack { SectionLabel(text: "Agent actions · live") Spacer() @@ -41,3 +44,29 @@ public struct ActivityView: View { .background(t.bg) } } + +/// The latest availability-bump overlay screenshot — the visual "here's exactly +/// what the automation focused on" record captured by the Tryst driver each bump. +private struct BumpScreenshotCard: View { + @Environment(\.tokens) private var t + let image: Image + + var body: some View { + Card { + VStack(alignment: .leading, spacing: t.s2) { + HStack(spacing: 6) { + Image(systemName: "cursorarrow.rays").font(.system(size: 12, weight: .bold)) + .foregroundStyle(t.accent) + SectionLabel(text: "Latest Tryst bump") + Spacer() + } + image + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: .infinity) + .clipShape(RoundedRectangle(cornerRadius: t.radiusSm)) + .overlay(RoundedRectangle(cornerRadius: t.radiusSm).strokeBorder(t.line2, lineWidth: 1)) + } + } + } +} diff --git a/@platform/codebase/@features/ai-copilot/cockpit-kit/Sources/CocotteCockpitKit/AssetLibraryView.swift b/@platform/codebase/@features/ai-copilot/cockpit-kit/Sources/CocotteCockpitKit/AssetLibraryView.swift index ae3df00..b8e18ff 100644 --- a/@platform/codebase/@features/ai-copilot/cockpit-kit/Sources/CocotteCockpitKit/AssetLibraryView.swift +++ b/@platform/codebase/@features/ai-copilot/cockpit-kit/Sources/CocotteCockpitKit/AssetLibraryView.swift @@ -1,30 +1,10 @@ import SwiftUI -#if canImport(UIKit) -import UIKit -#elseif canImport(AppKit) -import AppKit -#endif - // Asset library — the photo pool, newest-first, AI-classified. Real thumbnails // stream from platform.api's authenticated image proxy; a class-tinted swatch // stands in until each loads (and stays for mock, which has no backing media). // Shows content-class + quality + whether it's already scheduled into a drop. - -/// Decode image bytes to a SwiftUI `Image` on whichever Apple UI framework is -/// present. NSImage/UIImage decode HEIC natively, so iPhone-original HEICs render -/// without a conversion step. -private func decodeImage(_ data: Data) -> Image? { - #if canImport(UIKit) - guard let ui = UIImage(data: data) else { return nil } - return Image(uiImage: ui) - #elseif canImport(AppKit) - guard let ns = NSImage(data: data) else { return nil } - return Image(nsImage: ns) - #else - return nil - #endif -} +// `decodeImage` is shared from Components.swift. public struct AssetLibraryView: View { @Environment(\.tokens) private var t diff --git a/@platform/codebase/@features/ai-copilot/cockpit-kit/Sources/CocotteCockpitKit/CockpitAPI.swift b/@platform/codebase/@features/ai-copilot/cockpit-kit/Sources/CocotteCockpitKit/CockpitAPI.swift index 0ae7c7b..ea5bab9 100644 --- a/@platform/codebase/@features/ai-copilot/cockpit-kit/Sources/CocotteCockpitKit/CockpitAPI.swift +++ b/@platform/codebase/@features/ai-copilot/cockpit-kit/Sources/CocotteCockpitKit/CockpitAPI.swift @@ -24,6 +24,10 @@ public protocol CockpitAPI: Sendable { /// nil when the asset has no backing media (mock/composed) or the fetch fails — /// the tile falls back to its placeholder swatch, never errors. func fetchImageData(for asset: Asset) async -> Data? + /// Best-effort fetch of the newest availability-bump overlay screenshot for a + /// surface (authenticated proxy). Returns nil when none has been captured yet + /// or the fetch fails — the card simply doesn't render, never errors. + func fetchBumpScreenshot(surface: String) async -> Data? } public enum CockpitAPIError: Error, Sendable { @@ -58,4 +62,6 @@ public struct MockCockpitAPI: CockpitAPI { } // No real media in the demo dataset — tiles render their placeholder swatch. public func fetchImageData(for asset: Asset) async -> Data? { nil } + // No live browser in the demo — the bump screenshot card stays hidden. + public func fetchBumpScreenshot(surface: String) async -> Data? { nil } } diff --git a/@platform/codebase/@features/ai-copilot/cockpit-kit/Sources/CocotteCockpitKit/CockpitModel.swift b/@platform/codebase/@features/ai-copilot/cockpit-kit/Sources/CocotteCockpitKit/CockpitModel.swift index f5bffb3..3ddedab 100644 --- a/@platform/codebase/@features/ai-copilot/cockpit-kit/Sources/CocotteCockpitKit/CockpitModel.swift +++ b/@platform/codebase/@features/ai-copilot/cockpit-kit/Sources/CocotteCockpitKit/CockpitModel.swift @@ -22,6 +22,9 @@ public final class CockpitModel { public private(set) var receipts: [ApprovalReceipt] = [] /// Ingestion control + progress (nil until first loaded from the API). public private(set) var ingest: IngestStatus? + /// Newest availability-bump overlay screenshot (PNG bytes); nil until one is + /// captured — the durable "what the automation focused on" record. + public private(set) var bumpScreenshot: Data? public private(set) var connection: ConnectionState = .idle private let api: CockpitAPI @@ -93,6 +96,9 @@ public final class CockpitModel { await load({ try await api.fetchSpecialists() }) { specialists = $0 } await load({ try await api.fetchMetrics() }) { metrics = $0 } await load({ try await api.fetchIngestStatus() }) { ingest = $0 } + // Best-effort, non-throwing: keep the prior image on a nil (transient or + // none-yet) so the card never flickers empty between captures. + if let shot = await api.fetchBumpScreenshot(surface: "tryst") { bumpScreenshot = shot } return out } diff --git a/@platform/codebase/@features/ai-copilot/cockpit-kit/Sources/CocotteCockpitKit/Components.swift b/@platform/codebase/@features/ai-copilot/cockpit-kit/Sources/CocotteCockpitKit/Components.swift index fa3a4ec..6cb9ab4 100644 --- a/@platform/codebase/@features/ai-copilot/cockpit-kit/Sources/CocotteCockpitKit/Components.swift +++ b/@platform/codebase/@features/ai-copilot/cockpit-kit/Sources/CocotteCockpitKit/Components.swift @@ -1,7 +1,28 @@ import SwiftUI +#if canImport(UIKit) +import UIKit +#elseif canImport(AppKit) +import AppKit +#endif + // Shared cockpit components — every color/space comes from @Environment(\.tokens). +/// Decode image bytes to a SwiftUI `Image` on whichever Apple UI framework is +/// present. NSImage/UIImage decode HEIC + PNG natively. Shared by the asset +/// library tiles and the bump-screenshot card. +func decodeImage(_ data: Data) -> Image? { + #if canImport(UIKit) + guard let ui = UIImage(data: data) else { return nil } + return Image(uiImage: ui) + #elseif canImport(AppKit) + guard let ns = NSImage(data: data) else { return nil } + return Image(nsImage: ns) + #else + return nil + #endif +} + struct SectionLabel: View { @Environment(\.tokens) private var t let text: String diff --git a/@platform/codebase/@features/ai-copilot/cockpit-kit/Sources/CocotteCockpitKit/LiveCockpitAPI.swift b/@platform/codebase/@features/ai-copilot/cockpit-kit/Sources/CocotteCockpitKit/LiveCockpitAPI.swift index f3d7ef3..db200ba 100644 --- a/@platform/codebase/@features/ai-copilot/cockpit-kit/Sources/CocotteCockpitKit/LiveCockpitAPI.swift +++ b/@platform/codebase/@features/ai-copilot/cockpit-kit/Sources/CocotteCockpitKit/LiveCockpitAPI.swift @@ -120,6 +120,11 @@ public struct LiveCockpitAPI: CockpitAPI { return try? await client.sendData(Endpoint.contentAssetImage(id: assetId), scope: scope) } + public func fetchBumpScreenshot(surface: String) async -> Data? { + // 404 (none captured yet) surfaces as a thrown APIError → nil; the card hides. + try? await client.sendData(Endpoint.bumpScreenshotLatest(surface: surface), scope: scope) + } + // MARK: - Platform wire → Kit model mapping private static func kitDrop(_ d: CocottePlatformModels.ContentDrop, assetCount: Int = 0) -> ContentDrop { diff --git a/@platform/codebase/@packages/platform-api-client/Sources/CocottePlatformAPIClient/Endpoints/SurfacesEndpoints.swift b/@platform/codebase/@packages/platform-api-client/Sources/CocottePlatformAPIClient/Endpoints/SurfacesEndpoints.swift new file mode 100644 index 0000000..4a9bcd7 --- /dev/null +++ b/@platform/codebase/@packages/platform-api-client/Sources/CocottePlatformAPIClient/Endpoints/SurfacesEndpoints.swift @@ -0,0 +1,15 @@ +import Foundation + +/// Per-surface automation endpoints (bump policy / tier / screenshots) backed by +/// platform.api's `/surfaces/*`. The cockpit reads these for the bookings-* +/// specialists. +extension Endpoint { + + /// Authenticated proxy for the newest availability-bump overlay screenshot — + /// the "here's what the automation focused on" image. The file path is + /// resolved server-side from the tenant-scoped audit row; the client only + /// supplies the surface. + public static func bumpScreenshotLatest(surface: String = "tryst") -> Endpoint { + Endpoint(path: "/surfaces/bump-screenshot/latest", method: .get, queryParams: ["surface": surface]) + } +}