Files
nucleic-remote-ios/NucleicRemote/NucleicRemote/AppIntents/SessionEntity.swift
T

81 lines
3.2 KiB
Swift

import AppIntents
import NucleicProtocol
/// A Nucleic agent session exposed to Siri / Shortcuts / Spotlight (docs/APP_INTENTS_OPPORTUNITIES
/// §4.6). Backed by the host's `SessionSummary` projection the phone already holds; the entity id
/// is the wire `SessionID`, so it maps straight onto the deep-link and control APIs.
///
/// A thin, `Sendable` value snapshot — never the live store row. The `EntityQuery` re-reads
/// `RemoteStore` each time so a stale Siri suggestion never acts on outdated state.
struct SessionEntity: AppEntity, Identifiable {
static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Session")
static let defaultQuery = SessionEntityQuery()
/// `SessionID.rawValue`.
var id: String
var title: String
var projectName: String
var statusLabel: String
/// Whether this session is waiting on the user (approval, or a non-completed input turn).
var needsAttention: Bool
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(title)",
subtitle: "\(projectName) · \(statusLabel)")
}
}
extension SessionEntity {
/// Project a wire summary into the entity (falling back to the project name for an untitled
/// session, exactly as the session list does).
init(_ s: WireSessionSummary) {
self.init(
id: s.sessionID.rawValue,
title: s.title.isEmpty ? s.projectName : s.title,
projectName: s.projectName,
statusLabel: StatusStyle.label(s.status, disposition: s.disposition),
needsAttention: s.status.needsYou(s.disposition))
}
}
/// Resolves `SessionEntity` values for Shortcuts/Siri from the phone's current (or cached) session
/// list. Discovery paths (`suggestedEntities`, string matching) bootstrap the channel first so a
/// cold intent process still has data; id resolution stays fast and offline-tolerant.
struct SessionEntityQuery: EntityQuery {
@MainActor
func entities(for identifiers: [SessionEntity.ID]) async throws -> [SessionEntity] {
let wanted = Set(identifiers)
return RemoteStore.shared.liveSessions
.filter { wanted.contains($0.sessionID.rawValue) }
.map(SessionEntity.init)
}
@MainActor
func suggestedEntities() async throws -> [SessionEntity] {
let store = RemoteStore.shared
store.bootstrapForIntent()
return store.liveSessions
.sorted(by: StatusStyle.attentionThenRecency)
.prefix(12)
.map(SessionEntity.init)
}
}
/// Lets Siri match a session by spoken name ("open payment-flow") against title or project.
extension SessionEntityQuery: EntityStringQuery {
@MainActor
func entities(matching string: String) async throws -> [SessionEntity] {
let store = RemoteStore.shared
store.bootstrapForIntent()
let needle = string.lowercased()
return store.liveSessions
.filter {
let title = ($0.title.isEmpty ? $0.projectName : $0.title).lowercased()
return title.contains(needle) || $0.projectName.lowercased().contains(needle)
}
.sorted(by: StatusStyle.attentionThenRecency)
.map(SessionEntity.init)
}
}