Skip to content

fix(macos): title-case profile name when label is missing or blank#30420

Closed
ashleeradka wants to merge 1 commit into
mainfrom
devin/lum-1519-profile-name-titlecase-fallback
Closed

fix(macos): title-case profile name when label is missing or blank#30420
ashleeradka wants to merge 1 commit into
mainfrom
devin/lum-1519-profile-name-titlecase-fallback

Conversation

@ashleeradka
Copy link
Copy Markdown
Contributor

@ashleeradka ashleeradka commented May 12, 2026

Closes LUM-1519.

Prompt / plan

InferenceProfile.displayName is read by the chat composer pill, the
profile list, the default-profile dropdown, the composer settings menu,
and a handful of accessibility labels. It fell back to the raw name
identifier whenever label was nil. On Cloud, the platform-overlay
seeds profiles without populating label (assistant/src/config/seed-inference-profiles.ts
short-circuits re-seeding under isPlatform once a profile entry
exists), so freshly hatched assistants showed kebab-case identifiers
like quality-optimized — or nothing at all if the label happened to
be an empty string rather than nil — in spots that should read
"Quality Optimized".

Make the fallback empty-aware (treat "" and whitespace as missing,
not just nil) and title-case the name when it fires. The name
field itself stays untouched; it's the stable identifier used as the
profile key in lookups, the conversation override, and call-site
overrides.

Noa is separately addressing the underlying data issue (why label is
blank for new assistants on Cloud); this PR is the UI-side hardening
explicitly requested in the ticket.

What changed

  • clients/macos/vellum-assistant/Features/Settings/InferenceProfile.swift
    • displayName now returns label only when it is non-nil and
      non-empty after trimming; otherwise it returns
      Self.titleCased(name).
    • Adds static func titleCased(_:) which splits on -, _, and
      whitespace, capitalises the first character of each non-empty part,
      and joins with single spaces. Falls through to the input unchanged
      when no usable parts remain (e.g. "---").

Why this is safe

  • Pure UI display change. No wire format, persistence, IPC, or
    NotificationCenter payload touches the change.
  • No cross-component skew: the daemon, gateway, and platform never see
    the rendered string. Old daemon + new macOS and new daemon + old
    macOS both behave identically since this lives entirely inside the
    macOS client's render path.
  • All existing call sites of displayName (profile list rows,
    ChatProfilePicker.label, InferenceServiceCard dropdown options,
    ComposerSettingsMenu profile picker, accessibility labels in
    InferenceProfilesSheet) benefit transparently — no call-site
    changes needed.

Alternatives considered

  • Mutate name at decode time so it's pre-formatted. Rejected:
    name is the stable identifier used as a profile key in
    activeProfile lookups, id, call-site overrides, and conversation
    override fields. Display formatting must remain a derived view-only
    concern.
  • Add a fallback at each call site. Rejected: at least five call
    sites depend on displayName. Centralising in the computed property
    keeps the convention enforceable and the test surface narrow.
  • Use String.capitalized directly. Rejected:
    "quality-optimized".capitalized returns "Quality-Optimized"
    because Foundation's capitalized
    treats - and _ as part of a word, not a separator.
  • Backend backfill mirroring #30412
    (provider connections).
    Out of scope per the ticket — Noa is
    handling the root-cause data issue separately. UI hardening also
    guards against future label-blank scenarios.
  • Extract a shared String extension and DRY up with
    MessageInspectorSummaryFormatters.titleCasedProviderLabel.

    Considered but explicitly de-scoped for this PR per request: the
    ticket asks for the minimal UI fallback while the data root cause is
    being fixed separately. The provider-label helper lives in a
    different domain (inspector summary) and a follow-up consolidation
    is safer once both call sites have settled.

Test plan

  • Updates the existing InferenceProfile displayName tests in
    InferenceProfileTests.swift to expect the title-cased fallback:
    • nil label → title-cased name
    • empty-string label → title-cased name
    • whitespace-only label → title-cased name
  • Adds testTitleCasedHandlesCommonIdentifierShapes covering
    balanced, quality-optimized, custom_balanced,
    custom-quality-optimized, surrounding/repeated whitespace, all
    separators (---), and empty input.
  • Updates the affected ChatProfilePickerTests cases that previously
    encoded the raw-kebab fallback ("Default (balanced)",
    "quality-optimized", "Default (cost-optimized)") to the
    title-cased output.
  • CI doesn't build macOS — Xcode build/test must be verified locally
    before merging.

Root cause analysis

  1. How did the code get into this state? displayName was added
    as label ?? name when the label/source/description fields
    landed. At the time, every seeded profile included a label, so
    the fallback was only ever exercised by ad-hoc user-created
    profiles where the kebab name was acceptable.
  2. What mistakes or decisions led to it? The fallback path was
    never explicitly designed for the platform-overlay case where
    label is missing. The decoder normalises "" to nil, which
    masked the empty-string scenario in unit tests and meant the gap
    only surfaced once the Cloud platform started serving
    label-less profiles.
  3. Were there warning signs we missed? #30412
    fixed the same symptom for provider connections (label blank,
    editor renders empty) by backfilling label = name. The same
    pattern across the codebase suggests a generalised "always have a
    sensible display fallback when label is missing" convention is
    missing.
  4. What can we do to prevent this pattern from recurring? Any
    identifier field paired with a user-friendly label should
    compute a display value that is presentable even when label is
    absent. Title-casing the identifier is the cheap, reliable
    default.
  5. Should we add a guideline to AGENTS.md? Not yet. Two data
    points (InferenceProfile, provider connections) is suggestive
    but not enough to warrant a project-wide rule; a follow-up that
    factors out a shared titleCasedIdentifier extension would be the
    right time to codify the convention.

References

  • Apple — String.capitalized
    (why we can't use it directly: keeps - and _ as part of a word)
  • Apple — CharacterSet
  • Apple refs checked (2026-05-12): String.capitalized, CharacterSet.union(_:), String.components(separatedBy:)

Link to Devin session: https://app.devin.ai/sessions/bac5a01cf5be4de69e79e16034b50cfb
Requested by: @ashleeradka


Open in Devin Review

LUM-1519. The chat composer pill, profile list, default-profile dropdown
and accessibility labels read from `InferenceProfile.displayName`,
which fell back to the raw `name` identifier when `label` was absent.
On Cloud, the platform-overlay seeds profiles without populating `label`
(see assistant/src/config/seed-inference-profiles.ts where platform mode
short-circuits re-seeding once a profile entry exists), so freshly hatched
assistants showed kebab-case or empty values in the UI.

Treat empty or whitespace-only `label` as missing (in addition to nil),
and title-case the `name` fallback by splitting on `-`, `_`, and
whitespace. The decoder at `init(name:json:)` already normalises empty
strings, but centralising the empty check at the display layer keeps the
fallback robust against any other mutation path. The `name` field itself
stays untouched — it is the stable identifier used as the profile key.

Noa is separately addressing the underlying data issue (why `label` is
blank for new assistants); this PR is the UI-side hardening.

Closes LUM-1519

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@linear
Copy link
Copy Markdown

linear Bot commented May 12, 2026

LUM-1519

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

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