Appearance
Agent Abilities — Architecture
The Agent screen is a distinct interaction mode from the chat interface. It uses a plan-driven execution model rather than a free-form tool loop.
Data Model
Ability
Sources/Models/Ability.swift
| Field | Type | Purpose |
|---|---|---|
id | UUID | Stable identity — defaults use fixed UUIDs so reset/gallery detection works correctly |
label | String | Short display name (≤12 chars) shown under the icon |
icon | String | SF Symbol name |
colorHex | String | Hex color for icon background tint |
prompt | String | Full instruction passed to the agent after variable resolution |
planKeyRaw | String? | Optional raw value mapping to AgentAbilityPlanKey |
sortOrder | Int | Display order in the drawer |
isDefault | Bool | Whether this is a factory default (affects reset behavior) |
preferredTimeSlot | TimeSlot? | Optional morning/afternoon/evening hint (used for sorting) |
Default abilities use fixed stable UUIDs (A0000001-0000-0000-0000-00000000000X) so AbilityStore can detect them on reinstall and AbilityTemplateGallery can mark them as already installed.
AbilityTemplate
Sources/Models/AbilityTemplate.swift
Read-only catalog used by the template gallery. Maps to Ability via toAbility(sortOrder:). Has no UUID — a new UUID() is generated at install time.
AbilityStore
Sources/Services/AbilityStore.swift
ObservableObject backed by JSON persistence. Exposes:
sortedForDisplay()— sorted bysortOrder, with time-slot weighting at runtimeupdate(_:)/remove(id:)/reorder(fromID:toID:)— CRUDresetToDefaults()— restores factory defaults by stable ID
Execution Flow
User taps ability
│
▼
AbilityVariableResolver.resolve(prompt)
│ resolves {{today}}, {{name}}, {{next_event}}, {{location}}
▼
AgentViewModel.submitTask(prompt:planKey:)
│
├─ planKey != nil ──► AgentViewModel.abilityPlan(for:date:)
│ returns AgentAbilityPlan (ordered tool list)
│
└─ planKey == nil ──► agent infers tools from the prompt
│
▼
AgentViewModel.runAbilityPlan(task:plan:planKey:documents:)
│
├─ executes each tool step (calendar, health, gmail, weather, eta, notes, …)
├─ appends AgentStep with status .running → .done
├─ collects AgentObservationPayload from rich results
│
▼
assemblePayload(from:planKey:)
│ → structured AgentObservationPayload for UI cards
▼
synthesis (cloud or on-device)
│ → finalAnswer string
▼
AgentAnswerSheetView (fullScreenCover)
│ shows payload cards + plain text answer + original prompt echoPlan Keys
AgentAbilityPlanKey (enum in AgentViewModel.swift) maps to structured tool sequences:
| Key | Tools called | Notes |
|---|---|---|
morningBriefing | calendar, health, weather, gmail | Parallel execution |
calendarToday | calendar | Rich calendar payload |
meetingsThisWeek | calendar | Week overview |
freeSlotsToday | calendar | Gap analysis |
scheduleForFocus | calendar, habits | Finds deep-work block |
healthToday | health | Full metric grid |
sleepLastNight | health (sleep subset) | Sleep-focused |
gmailUrgent | gmail | Action-required only |
gmailSummarize | gmail | Full inbox scan |
draftReply | gmail | Reads latest, synthesizes draft |
habitsCheck | habits | Habit grid payload |
logHabits | habits | Records an entry |
weatherNow | weather | Current conditions |
planDay | calendar, habits, health | Morning synthesis |
planAfternoon | calendar, habits | Afternoon block synthesis |
whatToFocusOn | calendar, habits, health | Convergence verdict |
trafficToOffice | eta | Saved destination from settings |
notesSearch | notes | Cross-references today's calendar |
reminderCreate | reminders | Prompts user, then sets |
Variable Resolution
AbilityVariableResolver (Sources/Services/AbilityVariableResolver.swift) resolves template variables in the prompt before the agent sees it.
| Variable | Resolution | Cost |
|---|---|---|
| Full date string | Sync, free |
| First name from UIDevice.current.name | Sync, free |
| Next calendar event title via CalendarService | Async, cheap |
| Current city via LocationService | Async, GPS |
Expensive variables are only resolved if the prompt string contains them — no unnecessary fetches.
Answer Sheet
AgentAnswerSheetView (Sources/Views/AgentView.swift:362) renders the result as a .sheet with presentationDetents([.medium, .large]) and presentationDragIndicator(.visible). The system drag indicator and swipe-to-dismiss are handled natively by SwiftUI — no custom gesture or dismiss button needed.
Key behaviors:
- Echoes the original prompt at the top (
SheetRequestBubble) so the sheet is self-contained without context from the main view. - Shows during streaming (
isStreaming: viewModel.taskState == .synthesizing) — the sheet opens whencurrentPayload != nileven before synthesis completes. AgentAnswerWidgetViewrenders rich payload cards (briefing tiles, calendar list, health grid).- Falls back to plain text
FinalAnswerCardif no structured payload. - Opens at
.mediumdetent; user can pull up to.largeor pull down to dismiss.
Ability Drawer
AbilityDrawer / AbilityDockView (Sources/Views/AgentView.swift:690+)
The drawer uses three fixed detents: peek (200 pt), half (52% screen height), full (87% screen height). Height is calculated from UIScreen.main.bounds.height — not GeometryReader — to prevent the overlay from intercepting orb touches.
Drag gesture uses velocity prediction (predictedEndTranslation) with a 0.3 damping factor for natural snapping.
Jiggle mode (iOS home screen style) is per-cell with randomized wobble amount, scale range, and y-shift so icons have distinct personalities. Animation phase is staggered per cell using a stored seed value.
Prompt Design Principles
The default and template prompts follow these rules:
- Name the data sources — the agent doesn't guess; it must be told which tools to call.
- Request synthesis, not retrieval — "compare to my 7-day average" vs. "show me my HRV."
- Specify output format — "three bullets" or "one sentence" constrains verbosity.
- End with an action — every prompt should produce something the user can act on.
- No vague quantifiers — "recent" is replaced with "last 7 days", "tomorrow morning" maps to 8am.
This makes prompts resilient to model variation — a well-constrained prompt produces a useful answer regardless of which model (local or cloud) executes it.
Custom Ability Sharing
AbilityShareSheet serializes the Ability struct to JSON and wraps it in a .lucidability file via UIActivityViewController. The receiver's app opens the file via a document import handler and calls AbilityStore.install(_:).