Skip to content

feat(ai): add Claude and Codex model picker with CLI discovery#328

Open
M3NT1 wants to merge 4 commits into
JerryZLiu:mainfrom
M3NT1:feat/chat-cli-model-picker
Open

feat(ai): add Claude and Codex model picker with CLI discovery#328
M3NT1 wants to merge 4 commits into
JerryZLiu:mainfrom
M3NT1:feat/chat-cli-model-picker

Conversation

@M3NT1

@M3NT1 M3NT1 commented Jul 19, 2026

Copy link
Copy Markdown

Summary

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.

The 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.

UX details worth calling out

  • The picker shows the resolved display name ("Claude Sonnet 5"), not the raw alias the CLI accepts. A 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.
  • Older "installed" sample cards and Claude batches that landed before this PR still surface correctly — the new fields are optional in the JSON metadata column.

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 (PR feat: surface provider and model on each generated card #326) stays in sync with what the provider actually sent to the CLI.

Files changed

New:

  • Dayflow/Core/AI/ClaudeModelPreference.swift
  • Dayflow/Core/AI/CodexModelPreference.swift
  • Dayflow/Core/AI/ChatCLIModelCatalog.swift

Modified:

  • Dayflow/Core/AI/ClaudeProvider+Transcription.swift — reads ClaudeModelPreference (supersedes the sonnet hard-code from PR fix(ai): pass the sonnet alias to Claude CLI, not claude-sonnet #327)
  • Dayflow/Core/AI/ClaudeProvider+ActivityCards.swift — same
  • Dayflow/Core/AI/CodexProvider+Transcription.swift — reads CodexModelPreference
  • Dayflow/Core/AI/CodexProvider+ActivityCards.swift — same
  • Dayflow/Core/AI/LLMService.swiftproviderModelId(for: .claude/.chatGPT) now reads the preference struct
  • Dayflow/Views/UI/Settings/ProvidersSettingsViewModel.swift — owns the catalog + persists the picker choices
  • Dayflow/Views/UI/Settings/SettingsProvidersTabView.swift — new "Claude model preference" and "Codex model preference" sections
  • DayflowTests/CodexClaudeProviderTests.swift — updated the GPT 5.6-sol assertion (5.6-sol is no longer the default; 5.6-luna is)

Note on branch chain

This branch (feat/chat-cli-model-picker) is based on top of fix/claude-cli-model-alias (PR #327) and feat/per-card-provider-badge (PR #326), so the diff includes both of those PRs' changes. That's intentional — the model picker depends on the badge metadata and on the CLI alias fix. Reviewers can read the diff in commit order, or merge PR #326 → PR #327 → this one in sequence.

Test plan

  • xcodebuild build -configuration Debug — succeeds
  • xcodebuild build -configuration Release — succeeds
  • xcodebuild test -only-testing:DayflowTests — all unit tests pass
  • Manual: open Settings → Providers, switch to Claude, confirm the picker shows "Claude Sonnet 5 / Claude Opus 4.8 / Claude Fable 5 / Claude Haiku 4.5" with the live display names (after a Refresh)
  • Manual: select Opus, wait for the next batch, confirm the badge on the new card reads "Claude · Opus" and llm_calls.model = "opus"

M3NT1 added 4 commits July 19, 2026 21:51
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant