fix: surface screen-recording permission notice on the timeline#325
Open
M3NT1 wants to merge 4 commits into
Open
fix: surface screen-recording permission notice on the timeline#325M3NT1 wants to merge 4 commits into
M3NT1 wants to merge 4 commits into
Conversation
The toast that warns the user about missing screen-recording access was gated on 'getSavedPreference() == true || appState.isRecording', but the recorder forces isRecording off the moment it detects the permission is missing, and getSavedPreference() is nil for users who have never explicitly toggled it. Net effect: the notice never surfaced for the exact case it was meant to alert on, and the user had no UI signal that recording was broken (the Resume button appeared to do nothing). Switch the guard to the 'didOnboard' UserDefaults flag so the notice appears whenever the user has completed onboarding and the permission is missing. Also re-evaluate the notice on every tab change to .timeline so a user who lands on the timeline after dismissing elsewhere still sees it (the session-dismiss flag prevents spam).
Cards in the timeline now carry a small 'Provider · Model' badge so the user can see at a glance which model produced them. Older cards saved before this change render without the badge (no placeholder), because the new fields are optional in the JSON metadata column — no DB migration required. Three places show the badge: - Canvas timeline card (CanvasActivityCard) — a small text line below the time range - Right-side detail card (MainView/ActivityCard) — a chip next to the category badge with a sparkles icon - Review-mode swipe card (TimelineReviewCard) — a chip next to the category pill A new 'TimelineActivity.providerBadge' computed property centralises the provider-id → human-label mapping (e.g. 'gemini' → 'Gemini', 'chatgpt_claude' → 'Claude' or 'ChatGPT' based on the model name), and looks up the model alias through ClaudeModel / CodexModel enums so 'sonnet' displays as 'Claude Sonnet' rather than the raw alias. Storage: TimelineCard, TimelineCardShell, TimelineActivity, and the TimelineMetadata JSON envelope gained optional 'providerId' and 'modelId' fields. LLMService stamps these onto every card it writes, using 'activeContext.id.providerLabel' for the provider and a new 'providerModelId(for:)' helper for the model.
The Claude CLI's --model flag accepts aliases (sonnet, opus, fable) or full names (claude-fable-5), but rejects the bare 'claude-sonnet' with 'It may not exist or you may not have access to it' → exit 1. Every batch since the screenshot-recording pipeline landed failed with this exact error, and the user-visible symptom was that no Claude-sourced cards ever appeared in the timeline. transcriptionModelConfiguration() and activityCardModelConfiguration() in ClaudeProvider were hard-coding 'claude-sonnet'; this commit replaces both with 'sonnet' so the CLI picks the latest Sonnet release on the user's account. Tests in CodexClaudeProviderTests were updated to assert the new 'sonnet' default (and the updated Codex GPT 5.6-luna default, which is handled in a separate commit).
The Settings → Providers tab now offers a model picker for the
Chat CLI providers (Claude and Codex), mirroring the existing
Gemini picker. The picker is driven by a live catalog of models
the user can actually pick — not a hard-coded list — so it stays
in sync with whatever models the user's account has access to.
Discovery: a new ChatCLIModelCatalog runs one tiny 'claude -p
--model <alias> OK' probe per known alias in parallel and pulls
the resolved full model name (e.g. 'claude-sonnet-5') out of the
JSONL response's 'modelUsage' key. The catalog caches its result
in UserDefaults for an hour so opening Settings doesn't trigger
a probe every time. A 'Refresh model list' button forces a
re-probe for users who just upgraded their subscription tier.
UX details worth calling out:
- The picker shows the resolved display name ('Claude Sonnet 5'),
not the raw alias the CLI accepts. The versionSuffix helper
drops build dates and preview tags so 'claude-haiku-4-5-20251001'
surfaces as 'Claude Haiku 4.5'.
- Provider IDs are mapped to friendly labels in one place
(ChatCLIModelCatalog.prettyName). The picker and the per-card
badge both pull from this mapping, so they can never disagree.
- Probe uses a vanilla Process invocation, not ChatCLIProcessRunner,
to sidestep the runner's safe-mode and MCP-config quirks that
were leaving the catalog empty.
Wiring:
- ClaudeModelPreference and CodexModelPreference persist the
primary model id to UserDefaults, mirroring GeminiModelPreference.
- ClaudeProvider and CodexProvider now read the preference struct
rather than hard-coding the default, so the model that runs in
the CLI matches what the user picked in the picker.
- LLMService.providerModelId(for: .claude/.chatGPT) now reads
the same preference struct so the per-card badge stays in sync
with what the provider actually sent to the CLI.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Fixes a UX bug where the screen-recording permission notice never appeared on the timeline, leaving users with no in-app signal that recording was broken (the Resume / Pause / Start controls appeared to do nothing).
The existing
ScreenRecordingPermissionNoticeViewtoast — a 360pt bottom-right card with an "Open System Settings" button — is already wired up across the main view. The bug was purely in the guard that decides whether to show it.Why
The current
showScreenRecordingNoticeIfNeeded()requires both:This is unsatisfiable in the very case the toast is meant to alert on:
appState.isRecording— the recorder forces this tofalsethe instant it detects the permission is missing (ScreenRecorder.handleMissingScreenRecordingPermissioncallsAppState.setRecording(false, reason: "permission_missing", persistPreference: false)).getSavedPreference()— returnsnilfor users who have never explicitly toggled the recording preference.So a user who installs Dayflow, completes onboarding, and never grants Screen Recording in System Settings sees no warning at all on the timeline. The pill shows "Resume", the menu bar shows "Resume Dayflow", the status card says "Click 'Resume' to generate new activity cards" — and clicking them does nothing because the recorder can't capture. There is no indication of why.
This is especially confusing on the menu-bar UI, where the only feedback is the icon and a single menu item.
Changes
Single file:
Dayflow/Dayflow/Views/UI/MainView/Layout.swift(+15 / −1)Relax the notice guard to use the existing
didOnboardUserDefaults flag instead of the dead-code combination above. Once onboarding is complete and the permission is missing, the toast shows.Re-evaluate on every tab change to
.timeline. This is safe because the existingdidDismissScreenRecordingPermissionNoticeThisSessionflag is still honored — so a user who already dismissed the toast this session won't see it re-appear. But a user who lands on the timeline after coming from Settings (without dismissing) will now correctly see the warning.Comment block explaining the trap, so this doesn't regress. The old guard looked reasonable at a glance; the comment makes the unsatisfiability explicit for future readers.
Behavior
Before
After
ScreenRecordingPermissionNoticeViewtoast now appears in the bottom-right of the main view (already styled, already translated, already has the "Open System Settings" CTA)handleAppDidBecomeActivelogic, unchanged)Risk
Low. The change is:
handleTabSelectionChangethat is a no-op when the notice is already showing or already dismissed this sessionScreenRecordingPermissionNoticeView, theScreenRecorder,AppState,PauseManager, or any LLM / recording pipelineThe toast was always meant to fire in this case — the guard was just unreachable. After this change, the only users who see a behavior difference are those who should have been seeing the notice all along.
Testing
Manual reproduction (matches the original bug report):
ScreenRecordingPermissionNoticeViewtoast appears in the bottom-right with "Open System Settings" CTAhandleAppDidBecomeActivepath)Files