Read App Intents Documentation
Nucleic-Session: 56C5F781-4444-466B-91CE-A7734A8E117F Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user