diff --git a/NucleicRemote/NucleicRemoteWidgets/SessionLiveActivity.swift b/NucleicRemote/NucleicRemoteWidgets/SessionLiveActivity.swift index 5e2f336..38632c7 100644 --- a/NucleicRemote/NucleicRemoteWidgets/SessionLiveActivity.swift +++ b/NucleicRemote/NucleicRemoteWidgets/SessionLiveActivity.swift @@ -131,15 +131,42 @@ private struct LockScreenView: View { private struct SessionRow: View { let line: NucleicSessionAttributes.SessionLine + /// An approval this row can resolve inline: it's blocked on an approval, carries that approval's + /// id, and isn't high-risk (§3.3 — high-risk keeps the plain tap-to-open row and is answered on + /// the app's guarded card). `nil` until the producer populates `approvalID` — an older host, or + /// today's summary that doesn't yet carry it, so the row stays a deep-link exactly as before. + private var inlineApprovalID: String? { + guard line.kind == .approval, (line.approvalIsHighRisk ?? false) == false else { return nil } + return line.approvalID + } + var body: some View { - if let url = NucleicDeepLink.session(line.id) { + if let approvalID = inlineApprovalID { + actionableRow(approvalID: approvalID) + } else if let url = NucleicDeepLink.session(line.id) { Link(destination: url) { rowContent } } else { rowContent } } - private var rowContent: some View { + /// An approval row with inline Allow/Deny. The identity still deep-links to the session; the + /// buttons fire `ApproveFromActivityIntent`, which the system performs in the app's background + /// process — allow/deny without unlocking (§4.1). + private func actionableRow(approvalID: String) -> some View { + HStack(spacing: 8) { + if let url = NucleicDeepLink.session(line.id) { + Link(destination: url) { rowIdentity } + } else { + rowIdentity + } + Spacer(minLength: 6) + ApprovalButtons(sessionID: line.id, approvalID: approvalID) + } + } + + /// The glyph + title + "project · Backend" — the row's identity, minus the trailing status. + private var rowIdentity: some View { HStack(spacing: 8) { Image(systemName: ActivityPalette.glyph(line.kind)) .font(.footnote) @@ -159,6 +186,12 @@ private struct SessionRow: View { .foregroundStyle(.secondary) .lineLimit(1) } + } + } + + private var rowContent: some View { + HStack(spacing: 8) { + rowIdentity Spacer(minLength: 6) Text(line.detail) .font(.caption2.weight(.medium)) @@ -169,6 +202,33 @@ private struct SessionRow: View { } } +/// The compact inline Allow/Deny pair for an actionable approval row. Each button fires +/// `ApproveFromActivityIntent` (a low/medium-risk decision — high-risk never reaches here). +private struct ApprovalButtons: View { + let sessionID: String + let approvalID: String + + var body: some View { + HStack(spacing: 6) { + Button(intent: intent(allow: false)) { + Image(systemName: "xmark").font(.caption2.bold()) + } + .tint(ActivityPalette.danger) + Button(intent: intent(allow: true)) { + Image(systemName: "checkmark").font(.caption2.bold()) + } + .tint(ActivityPalette.success) + } + .buttonStyle(.borderedProminent) + .controlSize(.small) + } + + private func intent(allow: Bool) -> ApproveFromActivityIntent { + ApproveFromActivityIntent( + approvalID: approvalID, sessionID: sessionID, isHighRisk: false, allow: allow) + } +} + /// Headline count chips: running (blue), waiting (teal), approvals (amber). Only nonzero show. private struct CountChips: View { let state: NucleicSessionAttributes.ContentState