feat(cockpit-kit): 📸 add bump screenshot overlay

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-06-10 05:00:56 -07:00
parent 2bfb0e080d
commit d114d9d375
7 changed files with 83 additions and 21 deletions

View file

@ -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))
}
}
}
}

View file

@ -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

View file

@ -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 }
}

View file

@ -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
}

View file

@ -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

View file

@ -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 {

View file

@ -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])
}
}