102 lines
4.1 KiB
Swift
102 lines
4.1 KiB
Swift
import AppIntents
|
|
import NucleicProtocol
|
|
|
|
// MARK: - Open a session (deep-link intent, §4.4)
|
|
|
|
/// Open a specific session in the app. Reuses the existing `nucleic://session/<id>` route via
|
|
/// `RemoteStore.route(to:)` — so a Spotlight result, a Siri phrase, or a Shortcut jumps straight
|
|
/// into the waiting session. `openAppWhenRun` foregrounds the app (this is a navigation action).
|
|
struct OpenSessionIntent: AppIntent {
|
|
static let title: LocalizedStringResource = "Open Session"
|
|
static let description = IntentDescription("Open a Nucleic agent session.")
|
|
static let openAppWhenRun = true
|
|
|
|
@Parameter(title: "Session")
|
|
var session: SessionEntity
|
|
|
|
init() {}
|
|
init(session: SessionEntity) { self.session = session }
|
|
|
|
@MainActor
|
|
func perform() async throws -> some IntentResult {
|
|
RemoteStore.shared.route(to: SessionID(rawValue: session.id))
|
|
return .result()
|
|
}
|
|
|
|
static var parameterSummary: some ParameterSummary {
|
|
Summary("Open \(\.$session)")
|
|
}
|
|
}
|
|
|
|
// MARK: - Unblock with a follow-up (§4.2, approve-scope)
|
|
|
|
/// Send a follow-up prompt to a paused agent — the "type the next step to unblock it" flow that
|
|
/// stays at `approve` scope (docs/UX_IOS §2). Runs in the background over the app's live channel
|
|
/// (no foregrounding); if the Mac isn't reachable it fails clean with a spoken error (§3.1).
|
|
struct SendFollowUpIntent: AppIntent {
|
|
static let title: LocalizedStringResource = "Send Follow-Up"
|
|
static let description = IntentDescription(
|
|
"Send a follow-up prompt to a Nucleic agent to unblock or steer it.")
|
|
|
|
@Parameter(title: "Session")
|
|
var session: SessionEntity
|
|
|
|
@Parameter(title: "Message", requestValueDialog: "What should the agent do next?")
|
|
var text: String
|
|
|
|
init() {}
|
|
init(session: SessionEntity, text: String) {
|
|
self.session = session
|
|
self.text = text
|
|
}
|
|
|
|
@MainActor
|
|
func perform() async throws -> some IntentResult & ProvidesDialog {
|
|
let store = RemoteStore.shared
|
|
guard store.isPaired else { throw IntentError.notPaired }
|
|
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !trimmed.isEmpty else { return .result(dialog: "That message was empty.") }
|
|
guard await store.awaitLiveConnection() else { throw IntentError.macUnreachable }
|
|
store.sendInput(trimmed, to: SessionID(rawValue: session.id))
|
|
return .result(dialog: "Sent to \(session.title).")
|
|
}
|
|
|
|
static var parameterSummary: some ParameterSummary {
|
|
Summary("Tell \(\.$session) to \(\.$text)")
|
|
}
|
|
}
|
|
|
|
// MARK: - "What needs me?" (§4.3)
|
|
|
|
/// List the agents waiting on the user — for approval, or a next prompt — using the same attention
|
|
/// rule as the badge and NEEDS YOU section (`SessionStatus.needsYou`). Answers Siri's "what needs me
|
|
/// in Nucleic?" and feeds Shortcuts automations. Runs in the background; returns the list plus a
|
|
/// spoken summary.
|
|
struct SessionsNeedingMeIntent: AppIntent {
|
|
static let title: LocalizedStringResource = "Sessions Needing Me"
|
|
static let description = IntentDescription(
|
|
"List the Nucleic agents waiting on you — for approval or your next prompt.")
|
|
|
|
@MainActor
|
|
func perform() async throws -> some IntentResult & ReturnsValue<[SessionEntity]> & ProvidesDialog {
|
|
let store = RemoteStore.shared
|
|
guard store.isPaired else { throw IntentError.notPaired }
|
|
store.bootstrapForIntent()
|
|
// Give a cold process a brief moment to bring a channel up so the answer is current; fall
|
|
// back to the persisted list if the Mac stays unreachable.
|
|
_ = await store.awaitLiveConnection(timeout: 4)
|
|
let needy = store.liveSessions
|
|
.filter { $0.status.needsYou($0.disposition) }
|
|
.sorted(by: StatusStyle.attentionThenRecency)
|
|
.map(SessionEntity.init)
|
|
|
|
let dialog: IntentDialog
|
|
switch needy.count {
|
|
case 0: dialog = "Nothing needs you right now."
|
|
case 1: dialog = "One agent needs you: \(needy[0].title)."
|
|
default: dialog = "\(needy.count) agents need you."
|
|
}
|
|
return .result(value: needy, dialog: dialog)
|
|
}
|
|
}
|