fix(settings): link Settings mentions in prose and correct wrong destinations - #8261
Conversation
GPT 5.6 Review — ✅ human override acceptedHuman judgment by @CrysisDeu overrides the GPT 5.6 finding for This comment is updated in place on each push. The model was not re-run because an authorized human decision supersedes it. False positive or not applicable? A repository writer can comment: |
Design Review (Fable 5) — ✅ PASSDesign-level review of All hunks are accounted for by the description, both Design-Verdict: PASS Guard-aware Suggestions
[DESIGN-REVIEWED] 0fdc545 |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of All checks are done. The change survives every lens: the copy fixes sit at cause level (labels interpolated from the same keys the Capabilities page renders), the deletions are real, the literal sweep is verifiably complete, and the evidence artifacts follow the repo's own recorded convention. First-Principles-Verdict: PASS A fix that lands at cause level — wrong destinations become label interpolations, prose becomes the one shared link, and two things get deleted. What this change shipsIntent: stop telling users to go to Settings destinations they can't click or that don't exist — a FIX.
Watch
[FIRST-PRINCIPLES-REVIEWED] 0fdc545 |
UX Review (Fable 5) — ✅ PASSUX-level review of UX-Verdict: PASS Every rewritten pointer now names a destination that exists, and the one new link keeps its promise: it leaves the remote tab first, then navigates, or stays put when a draft is at stake. Suggestions
[UX-REVIEWED] 0fdc545 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
fa1a9da to
4702193
Compare
4702193 to
ebda78c
Compare
ebda78c to
13b7b01
Compare
13b7b01 to
47c958d
Compare
47c958d to
5946601
Compare
c06a255 to
74c758f
Compare
|
/ai-review override gpt 74c758f: |
|
AI-review override not recorded: keep the reason to 500 characters or fewer. |
|
Status at
I will rebase once after those land and let CI re-run; no change to this branch is planned in the meantime. |
74c758f to
a0cb3db
Compare
|
/ai-review override gpt a0cb3db: |
|
AI-review override not recorded: keep the reason to 500 characters or fewer. |
…inations An audit of every place the product sends the user to Settings found 96 sites; only 21 were clickable, and 7 frontend strings named a destination that does not exist. This lands the frontend half that needs no new infrastructure. Prose that names a real tab now renders the tab name as a <SettingsLink> through the react-i18next Trans idiom (<0>...</0> in every catalog): the remote-instance viewport error footer, which returns to the Local tab before the link navigates the local SPA underneath the opaque remote overlay. SettingsLink is a navigation surface, so it now asks the leave-guard channel (useGuardedLeave) on an unmodified primary click (never for a same-address target or a modified click) and stays put when the page vetoes; onPlainClick fires only for a click that is going to navigate. It is the fourth wired exit on that channel by design -- one declarative link covering every prose link -- and NavigationLeaveGuard's inventory says so; the imperative navigate(settingsPath()) callers stay with the general fix tracked in #8010. The STT prerequisites paragraph drops its "(Settings -> About -> Restart gateway)" route since the restart button sits right beside it. Five audit sites stay as they are: the Ops Mission Control panel holds unsaved provider secrets behind no leave guard, the session color picker is mounted nowhere, the update-failed sentence is unreachable, the onboarding header sits inside an overlay a link would navigate under, and the voice modal already has a primary "Open settings" button two lines below the sentence. Hand-written /settings literals in UpdateFoundModal, UpdatePill, MobileConnectModal, ChatPage's voice branch and TipCard go through settingsPath() / <SettingsLink>. App.tsx's two legacy redirects keep their literals: the feature-map gate reads a rewritten <Route> line as a route swap, and two never-changing redirects do not earn that edit. Copy fixes across all 13 catalogs: Mochi's MCP strings and the md-notebook vault help pointed at "Settings -> MCP" / "Settings" where the real home is Agent Capabilities -> Connections / Knowledge. Those sentences, and the project skills trust dialog's withdraw hint, interpolate {{capabilities}} -> {{tab}} from the rail and tab label keys (capabilitiesVars) instead of re-spelling the words, so a relabel cannot turn them into wrong destinations again. SettingsLink's children become optional because Trans passes the element self-closing and injects the translated fragment at render time. Evidence: website/scripts/capture-settings-redirects-{a,c}.mjs shoot the same fixtured surfaces against a base-branch dist and this branch's dist; 6 before/after pairs are committed under temp-screenshots/settings-redirects/.
a0cb3db to
0fdc545
Compare
|
/ai-review override gpt 0fdc545: ErrorNotice cannot render here: Mochi's Settings window is a separate Vite entry (settings.html, bare createRoot) with no Tailwind stylesheet, so ErrorNotice's Tailwind-class inline variant would be unstyled (Mochi's own PanelErrorBoundary hand-rolls role=alert for the same reason); the flagged span is pre-existing and this PR only interpolates the destination label into its string. |
Human judgment recorded@CrysisDeu marked the gpt AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
Summary
The settings-redirect audit found 96 places where the product tells the user to go to Settings; only 21 were clickable and 7 frontend strings named a destination that does not exist. This PR lands the frontend half that needs no new infrastructure: (a) prose naming a real tab becomes a
<SettingsLink>, (b) hand-written/settings/...literals go through the shared builder, (c) the 5 wrong-destination strings still on main are corrected in all 13 locale catalogs (the other 2, AWS Control's, were fixed upstream by #8471).(a) Prose →
<SettingsLink>(Trans idiom,<0>…</0>in catalogs)InstancesViewporterror footer →instances:SettingsLinkfiresonPlainClickand the viewport returns to its Local tab first, the same overlay rule the panel's hand-off button already follows (the link soft-navigates the local SPA underneath the opaque remote overlay).SettingsLinkitself is now a guard-aware navigation surface: an unmodified primary click goes throughuseGuardedLeave(skipped when the target is the current address) and stays put when the page on screen vetoes, so a hand-authored Settings link can no longer unmount a dirty editor without the confirm the sidebar and palette already give; modified clicks are left to the browser. This makesSettingsLinka fourth wired exit on the leave-guard channel, on purpose: it is the one declarative Settings link, so asking there covers every prose link at once instead of per call site (NavigationLeaveGuard.tsx's inventory is updated to say so). The imperativenavigate(settingsPath(...))callers this PR only re-pointed at the builder (update modal and pill, mobile connect, approval-mode picker, chat voice branch) still do not ask — they are modal or menu actions the user just chose, and wiring them one by one is the per-caller model #8010 exists to retire, so they stay with #8010.SttSettingsdrops the "(Settings → About → Restart gateway)" parenthetical — the restart button is inline beside it.Five audit sites are deliberately not migrated:
OpsMissionControl/SettingsPanel's notifications note sits on a panel that holds unsaved provider secrets and registers no leave guard, so any new exit there discards a half-typed token — the sentence stays plain text (its de/es/fr/it/pt values still carry the English source, as on main — an audit leftover for a follow-up, not this fix);SessionColorPickeris mounted by no production component (dead code — a follow-up should delete it, not decorate it);UpdateFoundModal's update-failed sentence is unreachable (apiFailureguarantees a non-emptyApiError.message), so the pre-PRe.message || i18nT(...)shape stays;OnboardingFlow's "change anytime in Settings → Chat" sits inside afixed inset-0overlay whose open state is internal, so a link there would navigate underneath the overlay — and the copy says later, so it stays plain text;VoiceDisabledModal's "Settings → Voice" sentence sits two lines above a primary Open settings button that already makes the same call, so an inline link would duplicate it (the modal is unchanged).(b) Hand-written literals →
settingsPath()/<SettingsLink>UpdateFoundModal,UpdatePill,MobileConnectModal,ChatPagevoice branch (embedded branch untouched),TipCard(TIPS_SETTINGS_PATHdeleted; no highlight yet — the Feature Tips toggle has no anchor). The two legacy<Navigate to="/settings/…">redirects inApp.tsxare left as literals on purpose: the feature-map gate reads any rewritten<Route>line as a route swap, and a builder call is not worth a feature-map edit for two redirects that never change.(c) Copy fixes (en + 12 locales, en-XA regenerated via
i18n:pseudo)mcp_empty/mcp_error_disabled/mcp_error_probe_failed: "Settings → MCP" → Agent Capabilities → Connections — the rail entry is labeled Agent Capabilities (nav.agent_capabilities) and the tab that hosts MCP servers is labeled Connections (CapabilitiesPage.tsxusesconnections_label). The catalogs do not re-spell those words: every locale carries{{capabilities}} → {{tab}}and the call sites passcapabilitiesVars('connections')(destinationVars.ts), which reads the same label keys the Capabilities page renders — so a relabel cannot re-create the wrong-destination drift this PR fixes.connectHelp: "in Settings" → Agent Capabilities → Knowledge (capabilitiesVars('knowledge')).projectSkillsTrust.withdraw_hint: "the Skills settings page" → Agent Capabilities → Skills (capabilitiesVars('skills'); the withdraw list renders there, not in Settings).SettingsLink.childrenbecomes optional: react-i18next's<Trans components={[<SettingsLink … />]}>passes the element self-closing and injects the translated fragment at render time.No linked issue: follow-up to the settings-redirect audit run in a dashboard session (parent: #8098).
Before / after
Same fixtured surface, same harness (
website/scripts/capture-settings-redirects-{a,c}.mjs), run against a base-branch dist and this branch's dist. Each shot asserts the target sentence is visible before capturing.Not captured (stated, not faked): C7
ProjectSkillsTrustDialogonly opens from a live skill-picker selection.Testing
settingsLinkMigrations.test.tsx(2):/settings/instancesvia catalog value + idiom; the three Capabilities sentences render exactlynav.agent_capabilities → <tab label>and carry only placeholders.SettingsLink.test.tsx(+3): a vetoing leave guard prevents the navigation and skipsonPlainClick;onPlainClickruns only for an allowed unmodified primary click; a same-address target never asks the guard.InstancesViewport.test.tsx(link returns to Local on plain click, not on Cmd/Ctrl click).I18N_BASE_REF=origin/main node scripts/i18n-check.mjs: 19 checks PASS (changed-values,changed-passthroughat zero); every catalog shows exactly 7 modified lines and no new key.tsc -bclean, eslint 0 errors on changed files.Pattern harvest
Rule candidate: lint
Pattern: a user-facing string that names a Settings tab ("Settings → X", "in Settings") without a
<SettingsLink>, a hand-written/settings/literal outsidesettingsPath.ts, or a catalog value that spells a rail / tab label (Agent Capabilities →,Capabilities →) instead of interpolating{{capabilities}} → {{tab}}— all three let the 7 wrong-destination strings drift unnoticed, and all three are greppable. The backend half (43 strings with no catalog path) needs asettings_targetfield convention first and is out of scope here.