Read App Intents Documentation

Nucleic-Session: 56C5F781-4444-466B-91CE-A7734A8E117F
Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
2026-07-06 14:44:03 -07:00
co-authored by Nucleic
parent 3fccb63b3f
commit 4764f483ff
@@ -0,0 +1,80 @@
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)
}
}