You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Corrected after reading the code. The first version of this issue claimed voice settings were hidden inside the Candidate Profile section and proposed creating a Preferences page. Both were wrong: /preferences already exists (store/console/src/pages/Preferences.tsx, routed in App.tsx:54, in the nav in Layout.tsx:22) and already carries Appearance, Voice and Translation. The real defect is narrower and worse — the same setting is editable in two places, and one of them silently loses.
1. Two UIs write the same global voice setting, and one of them is dead
Where
Writes
Read path
/preferences → VoiceFields (Preferences.tsx:110)
PUT /v1/preferences → users.preferences.voice
merged server-side by effectiveVoice (routes/instances.ts:522) into /v1/instances/:id/voice-settings
PUT /v1/profile → voiceMuteWords / voiceRepeatWords / …
client-side fallback in config.ts:118-130, applied only when the effective value is empty
So the precedence is: per-instance override → account preferences → profile fields. A user who has ever set command words in Preferences will find that editing them on the Profile page does nothing — their input is written, stored, and then never read, because the Preferences value already won.
The Profile block is the legacy path from #129 ("move hands-free control words to the global user profile"), left behind when Preferences was built. It should be deleted, not restyled.
This is also why the section looks over-long: the bloat is misplaced content, not too many legitimate settings.
2. Apply-specific fields are shown to every user
The "Candidate Profile" section is gated on cpFields.length > 0 (Profile.tsx:190) — i.e. whether the profile schema returned any fields — rather than on whether the user actually runs an apply agent. A Coder-only or Repo-Chat-only user sees job-application PII fields (work authorisation, salary, target roles) they have no use for.
The correct pattern already exists in the codebase: SettingsTab.tsx:904 renders its candidate copy behind {isApply && …}, derived from the instance's capabilities.surfaces including 'apply'. Profile is user-level rather than instance-level, so it needs "does this user have ANY apply-surface instance" — available from /v1/instances/my/instances, which already returns resolved capabilities per instance.
3. The wording
Two user-visible "Candidate" strings remain (Profile.tsx:192 header, :229 button). Tracked separately as #174 as a ship-in-minutes copy fix; this issue subsumes it if done first.
Scope
Delete the voiceFields block from Profile.tsx (:215-227) and its state. Preferences is the single home for voice command words.
Apply-gate the remaining candidate fields on "user has an apply-surface instance", and rename the section to something honest ("Job application details").
Decide the fallback's fate. With the Profile UI gone, config.ts:118-130 still reads voiceMuteWords etc. from /v1/profile. Keep it as a read-only migration path for users whose words only exist there, or write a one-time migration into users.preferences.voice and delete it. Do not leave a writable orphan.
Tabs on Profile are not part of this. Once the voice block is gone and the candidate fields are gated, what remains is Account / Edit Profile / API Token / Billing / API Keys / Sign out — coherent and short enough to stay one page. Revisit only if it grows again.
Acceptance criteria
Voice command words are editable in exactly ONE place (/preferences).
No user-visible "Candidate" string on /profile.
A user with no apply-surface instance sees no job-application fields on /profile.
A user who previously saved command words via /v1/profile still has them take effect (migrated or read-fallback) — no silent loss on upgrade.
SettingsTab.tsx:904 unchanged; it is already correctly gated.
1. Two UIs write the same global voice setting, and one of them is dead
/preferences→VoiceFields(Preferences.tsx:110)PUT /v1/preferences→users.preferences.voiceeffectiveVoice(routes/instances.ts:522) into/v1/instances/:id/voice-settings/profile→ "Hands-free voice commands" (Profile.tsx:215-227)PUT /v1/profile→voiceMuteWords/voiceRepeatWords/ …config.ts:118-130, applied only when the effective value is emptySo the precedence is: per-instance override → account preferences → profile fields. A user who has ever set command words in Preferences will find that editing them on the Profile page does nothing — their input is written, stored, and then never read, because the Preferences value already won.
The Profile block is the legacy path from #129 ("move hands-free control words to the global user profile"), left behind when Preferences was built. It should be deleted, not restyled.
This is also why the section looks over-long: the bloat is misplaced content, not too many legitimate settings.
2. Apply-specific fields are shown to every user
The "Candidate Profile" section is gated on
cpFields.length > 0(Profile.tsx:190) — i.e. whether the profile schema returned any fields — rather than on whether the user actually runs an apply agent. A Coder-only or Repo-Chat-only user sees job-application PII fields (work authorisation, salary, target roles) they have no use for.The correct pattern already exists in the codebase:
SettingsTab.tsx:904renders its candidate copy behind{isApply && …}, derived from the instance'scapabilities.surfacesincluding'apply'. Profile is user-level rather than instance-level, so it needs "does this user have ANY apply-surface instance" — available from/v1/instances/my/instances, which already returns resolvedcapabilitiesper instance.3. The wording
Two user-visible "Candidate" strings remain (
Profile.tsx:192header,:229button). Tracked separately as #174 as a ship-in-minutes copy fix; this issue subsumes it if done first.Scope
voiceFieldsblock fromProfile.tsx(:215-227) and its state. Preferences is the single home for voice command words.config.ts:118-130still readsvoiceMuteWordsetc. from/v1/profile. Keep it as a read-only migration path for users whose words only exist there, or write a one-time migration intousers.preferences.voiceand delete it. Do not leave a writable orphan.Tabs on Profile are not part of this. Once the voice block is gone and the candidate fields are gated, what remains is Account / Edit Profile / API Token / Billing / API Keys / Sign out — coherent and short enough to stay one page. Revisit only if it grows again.
Acceptance criteria
/preferences)./profile./profile./v1/profilestill has them take effect (migrated or read-fallback) — no silent loss on upgrade.SettingsTab.tsx:904unchanged; it is already correctly gated./profileor/preferences(Shared <Page> container to kill the missing-w-full horizontal-scroll class at the source #148/CI test: assert no mobile horizontal overflow across all console routes #149 hold).Related
unmuteWordsfield; it belongs in Preferences, not ProfileLayout.tsx:20comment cites "[docs] Update Coder docs for runner-node picker, engines, and package paths #211" for the Preferences page, but [docs] Update Coder docs for runner-node picker, engines, and package paths #211 is a Coder docs issue. The reference is stale; fix the comment while nearby.