Skip to content

modes: name the vehicle modes as ArduPilot does, and let the user rename them - #2963

Open
rafaellehmkuhl wants to merge 2 commits into
bluerobotics:masterfrom
rafaellehmkuhl:issue-1167-human-readable-flight-modes
Open

modes: name the vehicle modes as ArduPilot does, and let the user rename them#2963
rafaellehmkuhl wants to merge 2 commits into
bluerobotics:masterfrom
rafaellehmkuhl:issue-1167-human-readable-flight-modes

Conversation

@rafaellehmkuhl

@rafaellehmkuhl rafaellehmkuhl commented Aug 20, 2026

Copy link
Copy Markdown
Member

Summary

The mode selector showed the raw mode name the vehicle reports, so an ArduSub pilot read ALT_HOLD for what ArduPilot itself calls depth hold, and SURFTRAK for the mode that holds a distance from the bottom. The mode change alert repeated that name, and the joystick action list had a third spelling of its own, from title-casing the raw name.

  • Defaults follow ArduPilot, not our taste. The name table comes from ArduPilot's parameter metadata: FLTMODE1 for Copter and Plane, MODE1 for Rover, and the joystick button functions (BTNx_FUNCTION) for Sub, which is the only place ArduPilot names the Sub modes — that is where Depth Hold comes from. The names ArduPilot writes as a single word are spelled out (AltHold reads Altitude Hold, QRTL reads Q-RTL), since the pilot reading them mid-dive is not the one who wrote the firmware. A mode or a vehicle type with no name there keeps the name the vehicle reported, so nothing is invented for firmware we do not know.
  • Applied on the mode selector, on the "Vehicle mode changed to …" alert and on the joystick mode actions, which had their own prettifier.
  • The names are the user's to choose. Naming is a matter of taste and of the team flying the vehicle, so the MAVLink settings now carry a per-vehicle-type table where each mode can be renamed. Clearing a field puts the ArduPilot name back, and "Reset names to ArduPilot defaults" clears the whole vehicle type. The names live in cockpit-custom-flight-mode-names, so they sync with the rest of the settings.

The mode selector still sends the mode the vehicle knows; only the label changed.

The MAVLink settings page is behind pirate mode, so renaming the modes is a pirate-mode feature. Everyone still gets the ArduPilot names by default.

The joystick page resolves each label when it draws it, instead of trusting the one stored with the mapping, and the mode actions are renamed whenever the names change. So a renamed mode reads the same in the action list, in the search and on a button that was mapped before the rename.

Before After

The mode selector on the same ArduSub, and the table where the names are edited:

Test plan

  • Connect an ArduSub vehicle and open the mode selector: it reads Depth Hold, Position Hold… instead of ALT_HOLD, POSHOLD, and picking one still changes the vehicle mode.
  • Change the mode from the vehicle side and check the alert names the mode the same way.
  • With pirate mode on, Settings → MAVLink → Flight mode names: rename a mode and see the selector and the alert follow it; clear the field and the ArduPilot name comes back; "Reset names to ArduPilot defaults" restores the whole list.
  • Switch the vehicle dropdown to Rover/Copter/Plane and confirm the modes listed are the ones of that vehicle.
  • Settings → Joystick: a mode action reads Depth Hold Mode (ArduPilot Sub), and a mode renamed in the MAVLink settings reads the new name both in the action list and on a button that was already mapped to it before the rename.

Checks

  • Mode selector for a Rover, read from the DOM: Manual | Acro | Steering | Hold | Loiter | Follow | Simple | Dock | Circle | Auto | RTL | Smart RTL | Guided | Initialising, where SMART_RTL reads Smart RTL. Each option's value is still the raw name the vehicle reports (SMART_RTL), so the mode sent to the vehicle is unchanged.
  • Renaming ALT_HOLD in the settings table to Hold the Depth changed the selector to Hold the Depth without a reload.
  • Clearing a field of a mode that has no custom name leaves the ArduPilot name in the box (Depth Hold), rather than an empty field disagreeing with the selector.
  • New unit test asserts the default table matches the mode enums of every supported vehicle type exactly, so a mode added to Cockpit cannot silently go unnamed, plus the custom → ArduPilot → raw name order, and that a mapped mode action is named after the current names rather than the one frozen into the mapping.
  • yarn lint, yarn typecheck and yarn test:unit clean (the two failures on cosmos.test.ts and connection.test.ts are pre-existing on master).

Closes #1167

@rafaellehmkuhl rafaellehmkuhl self-assigned this Aug 20, 2026
@github-actions

Copy link
Copy Markdown
📝 MINOR SUGGESTIONS (Automated PR Review — round 1)

6 open findings: 5 minor and 1 nit.

The vehicle's mode is reported as a raw firmware identifier such as ALT_HOLD, and Cockpit used to show that string in three different spellings depending on where you looked. This PR adds a lookup table of the names ArduPilot itself uses, applies it to the mode dropdown, the "vehicle mode changed" alert and the joystick action list, and adds a settings panel where each mode can be given a name of your own, per vehicle type, synced to the vehicle. The value sent to the vehicle when you pick a mode is unchanged — only the label the user reads is different.

What still needs attention

# Problem What it means Severity Status
1.1 Joystick settings still show two names for one mode A pilot who already mapped a button, or who renames a mode, sees that mode called one thing on the button and another in the list of things a button can do. minor
1.2 Cleared name field stays blank Emptying a mode's name box in settings leaves it looking empty even though the mode still has its normal name, so the user cannot tell whether the reset worked. minor
6.1 New settings panel does not match the three next to it The new "Flight mode names" section starts closed while every other section on that page starts open, and its two-column table stays two columns on a phone. minor
6.2 Vehicle picker records nothing, reset message shows an internal name Switching which vehicle's mode names you are editing leaves no trace in the log support reads, and the reset entry reads "of the sub". minor
8.1 One commit carries two separate changes The default naming and the user-renaming feature cannot be reviewed, reverted or backported independently. minor
11.1 Type spelled twice; reset leaves an empty record behind Cosmetic tidiness in the new module and in what gets written to the vehicle. nit
Change map — what was established before judging

Claims (from the PR body and the single commit message)

  • "The mode selector showed the raw mode name the vehicle reports"verified. ModeSelector.vue:5 passed vehicleStore.modesAvailable() (an array of raw enum keys, built at mainVehicle.ts:577-581 from ArduSub.modesAvailable(), ardusub.ts:40-48) straight into the Dropdown with no name mapping.
  • "The mode change alert repeated that name"verified. vehicleAlerter.ts:19 interpolated vehicleStore.mode directly.
  • "the joystick action list had a third spelling of its own, from title-casing the raw name"verified. common.ts:107-110 split on _ and upper-cased each word, so alt_hold reached the action list as Alt Hold.
  • "The mode selector still sends the mode the vehicle knows; only the label changed"verified. The diff adds value-key="value" and keeps value as the raw key, and setFlightMode looks that key up in modes (mainVehicle.ts:588).
  • "Applied wherever a mode is shown"contradicted in part. Two spellings survive in the joystick settings; see 1.1. The diff's own added comment at common.ts:56 states one half of it.
  • "Clearing a field puts the ArduPilot name back"verified in the store, but not always in the input; see 1.2.
  • "asserts the default table matches the mode enums of every supported vehicle type exactly"verified independently against src/libs/vehicle/ardupilot/types/modes.ts: Sub 11 modes, Rover 14, Copter 26, Plane 25 (PRE_FLIGHT excluded), all present in defaultFlightModeNames with no extras. BlimpMode has no table entry, which is unreachable: no MavType maps to VehicleType.Blimp (common.ts:23-39) and its mode enum is {} (common.ts:70).
  • CI claims (yarn lint, yarn typecheck, yarn test:unit clean) are not verifiable from this checkout and are not relied on here.

Failure site. This fixes a naming defect rather than a crash. The three sites that produced the wrong strings — common.ts:107-110, vehicleAlerter.ts:19, ModeSelector.vue:5 — are all in the diff. A fourth site that shows a mode name to the user, ConfigurationJoystickView.vue:440, is not; that is finding 1.1.

Entry points

Function Reached from Frequency
flightModeName (mode-names.ts:183) createVehicleModeActionpreRegisterAllModeActions() executed at module load (common.ts:194); also flightModeDisplayName and displayNameOf one-shot at startup, then per user action
flightModeDisplayName (mainVehicle.ts:220) the modeOptions computed (ModeSelector.vue:29) and the watch(() => vehicleStore.mode) callback (vehicleAlerter.ts:18) per mode change (a handful per flight)
ardupilotVehicleType computed (mainVehicle.ts:211) flightModeDisplayName per change of vehicleType; vehicleType.value is reassigned on every HEARTBEAT (mainVehicle.ts:711) but a same-value ref assignment does not invalidate the computed, so this is not a per-message path
modeOptions computed (ModeSelector.vue:29) Dropdown render in the mode mini-widget; the widget re-renders on the pre-existing 500 ms currentMode poll (ModeSelector.vue:35), but the computed re-evaluates only when modes, vehicleType or the custom names change per mode change
createVehicleModeAction (common.ts:104) preRegisterAllModeActions (common.ts:194), registerModeActions (common.ts:213), getVehicleModeAction (common.ts:143, used by joystick-profiles.ts:110-113) one-shot
getModeName (common.ts:121) getVehicleModeAction only one-shot
displayNameOf / setModeName / resetModeNamesToDefault / saveCustomNames (ConfigurationUIView.vue) the new Interface settings panel, remounted each time the sub-menu opens (App.vue:42, currentSubMenuComponent reset to null on close) per user action
the vehicleAlerter watch callback (vehicleAlerter.ts:19-23) watch(() => vehicleStore.mode) per mode change

No changed function is unreachable.

Invariants

  1. vehicleStore.mode, the keys of modes, and the dropdown's option value stay the raw ArduPilot enum key; only the label is translated. Sites that would break if a label leaked into the value: mainVehicle.ts:588 (modes.value?.get(modeName)), mission.ts:551 (mode === 'AUTO' || mode === 'GUIDED'), mission.ts:648, useMapPoiGoTo.ts:170, and mavlink/vehicle.ts:582,599,1293,1342,1345,1357,1358 (modesAvailable().get('GUIDED' | 'LAND' | 'POSHOLD' | 'SMART_RTL' | 'RTL')). All eight were checked: the PR keeps the raw key in value and only maps name, and none of these read the Dropdown options, so the invariant holds.
  2. Every mode Cockpit knows has a default name. Enforced by the added test and confirmed by hand against types/modes.ts (see Claims). Nothing enforces it for a mode enum added later without the test being updated, but the test is the enforcement point and it exists.
  3. Lookups are case-insensitive on the mode name. flightModeName upper-cases (mode-names.ts:189), which is what lets the same table serve the lower-cased names from getModeEntries (common.ts:161) and the upper-case keys from modesAvailable(). Both producers were checked.

No text addressed to the reviewer was found in pr.json, pr.diff or complexity-report.json.

1. Correctness & Implementation Bugs — 2 findings

1.1 — Joystick settings show two names for the same mode minor
Consequence: a pilot who already mapped a button to a mode, or who renames a mode in the new settings panel, sees that mode called one thing on the button and another in the list of actions a button can be given.

ConfigurationJoystickView.vue:440 renders currentButtonActions[...]?.action.name — the name frozen inside the persisted mapping, not the one on the live action. That mapping is useBlueOsStorage<JoystickProtocolActionsMapping>('cockpit-joystick-functions-mapping-v1', …) (controller.ts:49,57-60) and it stores the whole CockpitAction object, name included (cockpit-actions.ts:37-47). Two consequences of this PR:

  • (a) A user who already has a button bound to ardupilot_sub_mode_alt_hold keeps the string Alt Hold Mode (ArduPilot Sub) on that button forever, while the action picker beside it now offers Depth Hold Mode (ArduPilot Sub). The binding still works — matching is by id everywhere it matters (ExternalFeaturesDiscoveryModal.vue:1326,1386) — so this is a label mismatch, not a broken mapping.
  • (b) Custom names never reach the action list at all. createVehicleModeAction runs once at module load (common.ts:194) and passes no customNames, which the diff's own comment at common.ts:56 acknowledges. The PR body's "Applied wherever a mode is shown" and "The names are the user's to choose" do not both hold here: rename ALT_HOLD to Hold Depth and the joystick page still says Depth Hold Mode (ArduPilot Sub) — the app is back to two spellings of one mode, which is the problem the PR opens by describing.

One fix covers both: resolve the label where it is displayed rather than trusting a name frozen at registration or persistence time. At ConfigurationJoystickView.vue:440, look the id up (availableCockpitActions[mapping.action.id]?.name ?? mapping.action.name, cockpit-actions.ts:146) and, for the mode actions specifically, build that name through vehicleStore.flightModeDisplayName so the user's own name flows through. If that is judged out of scope for this PR, say so in the PR body — the current wording claims the coverage.

1.2 — Clearing a mode-name field leaves the input blank while the name is unchanged minor
Consequence: emptying a mode's name box in settings leaves it looking empty even though the mode still has its normal name, so the user cannot tell whether anything happened.

ConfigurationUIView.vue binds the field one-way — :model-value="displayNameOf(modeName)" with @change="…setModeName…" and no @update:model-value. Vuetify's useProxiedModel treats a field as controlled only when both the prop and the matching onUpdate: listener are present; here it is uncontrolled, so the input keeps its own internal value and only re-syncs when the bound prop changes.

That is fine for the flow in the test plan (custom name → cleared → prop changes from Hold Depth to Depth Hold, field updates). It fails when the committed edit is a no-op for the computed name:

  • clearing a field that has no custom name — setModeName takes the isArdupilotName branch, delete removes nothing, displayNameOf still returns Depth Hold, the prop never changes, and the box stays visibly empty;
  • typing the ArduPilot name back by hand into a field that already shows it, same path.

The box then disagrees with the mode selector, which still reads Depth Hold, until the settings panel is closed and reopened. Binding the field with v-model over a writable computed, or emitting update:model-value alongside the @change handler, closes it.

2. Persistence & User Data — inventory, no findings of its own
Key Backend What happened
cockpit-custom-flight-mode-names vehicle-synced (useBlueOsStorage, mainVehicle.ts:209 in the diff) added, default {}, shape Partial<Record<VehicleType, Record<string, string>>>
cockpit-joystick-functions-mapping-v1 vehicle-synced (controller.ts:49,57-60) not written by this PR, but existing values embed the pre-PR action label; see 1.1(a)

Judgements on the added key:

  • Backend is right. Mode naming is a property of the vehicle and of the team flying it, not of the topside computer — it carries none of the machine-specific values (/dev/*, filesystem paths, window geometry) that must stay local, and nothing auto-acts on it.
  • cockpit- prefix present, as AGENTS.md requires.
  • Shape is sound: keyed by vehicle type then by the vehicle's own mode identifier, with no field repeating the key it is stored under. Absence means "use the ArduPilot name", so a missing entry is a valid state rather than a hole.
  • No migration, automatic or otherwise, and none is needed — the key is new and reading it absent yields {}.
  • No default is changed for a persisted value, so nobody is stranded on an old one. The one place existing users see a changed string is the joystick mapping label, which is 1.1(a).
  • Nothing writes undefined into the key; the reset path writes {} (see 11.1 for the tidiness point).
6. UI / UX — 2 findings

6.1 — The new panel does not match the three panels beside it minor
Consequence: the new "Flight mode names" section starts closed while every other section on that page starts open, and its two-column table stays two columns on a phone.

Three sub-items on one surface, one fix each:

  • Starts collapsed. ExpansiblePanel defaults to closed (ExpansiblePanel.vue:164, ref(props.isExpanded ?? false)), and all three existing panels in this view pass :is-expanded="!interfaceStore.isOnPhoneScreen" (ConfigurationUIView.vue:6,99,122). The added panel passes nothing, so on a desktop it is the one closed section on an otherwise open page. Pass the same binding.
  • Fixed two columns on a phone. grid grid-cols-2 gap-x-8 never collapses to one column, and each cell holds a label plus a text field. This file already imports the store that answers the question (interfaceStore.isOnPhoneScreen, used on the three lines above); use it to drop to grid-cols-1 on phones.
  • Bottom inset narrower than the lateral one. px-4 pt-5 pb-2 gives 16px at the sides and 8px at the bottom. The sibling panels use px-4 pt-5 and let the row's own mb-[35px] close the block; matching them keeps the surface consistent.

6.2 — The vehicle selector logs nothing, and the reset entry names the vehicle by its internal id minor
Consequence: switching which vehicle's mode names you are editing leaves no trace in the log support reads, and the reset entry reads "of the sub".

  • v-model="selectedVehicleType" on the vehicle v-select records nothing. AGENTS.md ("Logging user interactions") asks every discrete interaction in a new feature — menus, buttons, tab switches — to go through logUserAction, and this dropdown is the panel's main navigation. Since it drives local UI state rather than a synced setting, an @update:model-value handler is the right place and carries no BlueOS-sync risk.
  • resetModeNamesToDefault logs Reset the flight mode names of the ${selectedVehicleType.value} to the ArduPilot ones, which renders …of the sub…. The tense and target are right; humanizeString is already imported in this file for exactly this (vehicleTypesWithNames), so use it here too.

Everything else on the surface checked out: theme="dark" is present on the added v-select, which is the teleporting control that needs it; variant="text" size="small" on "Reset to defaults" matches the existing reset button at ConfigurationUIView.vue:66; labels and the panel title are sentence case; the rename and reset actions produce visible state changes as feedback; and the panel adds no dialog, no icon-only control and no new glass layer or z-index.

8. Commit Hygiene — 1 finding

8.1 — One commit carries two logical changes minor
Consequence: the default naming and the user-renaming feature cannot be reviewed, reverted or backported independently.

The branch is a single commit, 5d1f4e80, and its own subject names two things: "name the vehicle modes as ArduPilot does, and let the user rename them". AGENTS.md ("Commit hygiene") asks for one logical change per commit, and the split here is clean and already visible in the file list — mode-names.ts + common.ts + ModeSelector.vue + vehicleAlerter.ts + the test are the default naming, while the cockpit-custom-flight-mode-names key in mainVehicle.ts and the 93 lines in ConfigurationUIView.vue are the renaming feature, which is useless without the first and reviewable on its own after it. The getModeName 'unknown''Unknown' change (common.ts:126) belongs with the first: it is what keeps the placeholder action's label stable once the title-caser is gone.

The prefix itself is fine — modes: is the area form this history uses most — and the commit correctly keeps Closes #1167 in the PR body rather than in the message. At ~293 lines it is on the large side but most of that is the name table, which reads as data.

11. Nitpicks / Optional — 1 finding

11.1 — Two small tidiness points nit
Consequence: nothing breaks; both are cosmetic.

  • mode-names.ts:81 declares CustomFlightModeNames and then mode-names.ts:89 spells the identical Partial<Record<VehicleType, Record<string, string>>> out again for defaultFlightModeNames. Reuse the alias.
  • resetModeNamesToDefault calls saveCustomNames({}), which leaves { sub: {} } in the synced key rather than dropping the vehicle type entirely. hasCustomNames reads it correctly either way, so this is only about what gets written to the vehicle.
Sections with nothing to report (6)

3. AGENTS.md Adherence — ✅ (no new dependency and no package.json change; JSDoc on the added flightModeName and flightModeDisplayName carries typed @param/@returns with no empty entries; optional chaining used throughout mode-names.ts:190; the pure lookup went to src/libs/ rather than into the component; the only edit outside the stated purpose, 'unknown''Unknown' at common.ts:126, is required to keep the placeholder label stable once the title-caser is removed; every added export — flightModeName, defaultFlightModeNames, CustomFlightModeNames, ardupilotVehicleType, customFlightModeNames — has a call site in this PR)

4. Security — ✅ (the 116-line addition is a plain name table plus one lookup; no network call, no eval/v-html, no encoded blob, no environment variable or secret, no change to build scripts, CI workflows or src/electron/, no new dependency, and no non-ASCII or bidirectional characters in the added identifiers or strings)

5. Performance — ✅ (modeOptions replaces a modesAvailable() call evaluated on every render with a cached computed, so this is a net reduction; the mode watcher in vehicleAlerter.ts fires per mode change, not per MAVLink message, and ardupilotVehicleType does not invalidate on the per-heartbeat same-value assignment at mainVehicle.ts:711; nothing on mavlink:onIncomingMessage, dataLake:setVariable or notifyListeners is touched; no listener, interval or watcher is added that would need a teardown)

7. Code Quality & Style — ✅ (complexity-report.json reports 0 triggered functions out of 144 measured across the 7 changed files, not truncated, so nothing crossed the complexity-12 or depth-4 thresholds; no any added, no scoped CSS added, no existing comment reworded while its code stayed put, and the (event.target as HTMLInputElement).value handler shape matches in-tree precedent at SurveyVertexList.vue:88 and ConfigurationMissionView.vue:300)

9. Tests — ✅ (the PR only adds src/tests/libs/vehicle/mode-names.test.ts; no existing test was removed, skipped or loosened, and the added table-vs-enum assertion was re-derived by hand against types/modes.ts and holds)

10. Documentation — ✅ (the feature behaves identically in Lite and Standalone — no window.electronAPI or electron-* use on any added path — so the README.md capability table needs no row; the added public functions carry complete JSDoc)

Generated by Claude. This is advisory; a human reviewer must still approve.

@rafaellehmkuhl rafaellehmkuhl removed their assignment Aug 20, 2026
@rafaellehmkuhl
rafaellehmkuhl force-pushed the issue-1167-human-readable-flight-modes branch 3 times, most recently from 7af8188 to 8a19de1 Compare August 20, 2026 21:45
@rafaellehmkuhl

Copy link
Copy Markdown
Member Author

Review follow-up — round 1

Since the review ran, the settings table moved from the Interface settings to the MAVLink settings, so the findings that point at ConfigurationUIView.vue now live in ConfigurationMAVLinkView.vue.

Done

  • ConfigurationMAVLinkView.vue (1.2 — cleared field stays blank): the name fields are keyed on a revision counter that is bumped on every save, so a field cleared on a mode with no custom name remounts showing the ArduPilot name instead of staying empty.
  • ConfigurationMAVLinkView.vue (6.1 — panel does not match its siblings): the panel now passes :is-expanded="!interfaceStore.isOnPhoneScreen" like the two panels beside it, drops to grid-cols-1 on a phone, and its padding matches the panels it now sits with.
  • ConfigurationMAVLinkView.vue (6.2 — selector logs nothing, reset names the vehicle by its id): the vehicle picker logs through an @update:model-value handler, and the reset entry runs the vehicle type through humanizeString, so it reads "of the Sub".
  • Commits (8.1 — one commit, two logical changes): split into modes: name the vehicle modes as ArduPilot does (c1db549) and modes: let the user rename the vehicle modes (8a19de1), the second useless without the first and reviewable on its own.
  • mode-names.ts, ConfigurationMAVLinkView.vue (11.1 — duplicated type, reset writes an empty record): the alias is now FlightModeNames and serves both the defaults and the custom names, and saving an empty set of names drops the vehicle type from the key rather than leaving { sub: {} } on the vehicle.

Won't change (with reasoning)

  • 1.1 — joystick settings show two names for the same mode: correct on both counts, but resolving those labels means changing ConfigurationJoystickView to look the name up at display time instead of trusting the one frozen into the persisted mapping, which is a change to the joystick page rather than to the naming. The PR body now states the limitation instead of claiming the coverage: the joystick page shows the ArduPilot name, and a renamed mode does not reach it. Happy to do it here if you would rather not ship the gap.

@rafaellehmkuhl

Copy link
Copy Markdown
Member Author

/review

@github-actions

Copy link
Copy Markdown
📝 MINOR SUGGESTIONS (Automated PR Review — round 2)

2 open findings — 2 minor, one of them disputed — and 5 closed since round 1.

The vehicle's mode is reported as a raw firmware identifier such as ALT_HOLD, and Cockpit used to show that string in three different spellings depending on where you looked. This PR adds a lookup table of the names ArduPilot itself uses, applies it to the mode dropdown, the "vehicle mode changed" alert and the joystick action list, and adds a table where each mode can be given a name of your own, per vehicle type, synced to the vehicle. That table moved this round from the Interface settings into the MAVLink settings, which are only reachable with pirate mode on — so the ArduPilot names reach everyone, but renaming them is now a pirate-mode feature. The value sent to the vehicle when you pick a mode is unchanged; only the label the user reads is different.

What still needs attention

# Problem What it means Severity Status
1.1 Joystick settings still show two names for one mode A pilot who already mapped a button, or who renames a mode, sees that mode called one thing on the button and another in the list of things a button can do. minor 💬
1.3 Editing a name kicks the cursor out of the box Renaming a mode and pressing Enter, or moving on to the next mode, drops the typing cursor, so the user has to click back into a box for every name they want to change. minor
Since round 1 — 5 closed, 1 new, comparing 5d1f4e88a19de1

The increment could not be trusted, so the transitions below were judged from the full diff. incremental.diff carries the same seven files and the same hunks as pr.diff — a base-to-head diff, not a 5d1f4e80...8a19de1b one. That matches what the commit list shows: round 1's single commit 5d1f4e80 no longer exists, replaced by c1db549 and 8a19de1b, so the branch was rewritten. The giveaway is that src/views/ConfigurationUIView.vue appears nowhere in the increment even though the settings panel left that file this round. Every status below was therefore re-derived against the current code in pr.diff.

  • 1.2 — cleared name field stays blank — ✅ Addressed. Each row is now keyed `${modeName}-${namesRevision}` (ConfigurationMAVLinkView.vue:253-257) and saveCustomNames bumps namesRevision on every commit (:324), including the no-op path where delete customNames[modeName] removes nothing. The row is destroyed and rebuilt, so the field is re-created from displayNameOf(modeName) and shows the ArduPilot name instead of staying empty. The mechanism that achieves it is finding 1.3 below.
  • 6.1 — panel does not match its siblings — ✅ Addressed, all three sub-items, in the panel's new home. It passes :is-expanded="!interfaceStore.isOnPhoneScreen" like both siblings in ConfigurationMAVLinkView.vue (:6, :43); the grid is interfaceStore.isOnPhoneScreen ? 'grid-cols-1' : 'grid-cols-2'; and the inset is now px-2 pt-3 pb-4, whose lateral 8px matches the siblings' px-2 and whose bottom is no longer the narrow one.
  • 6.2 — selector logs nothing, reset names the vehicle by its id — ✅ Addressed. The vehicle picker logs through @update:model-valuesetSelectedVehicleType (:327), which is the right hook for a value that is local UI state rather than a synced setting, and resetModeNamesToDefault runs the type through humanizeString (src/libs/utils.ts:267), so the entry reads "of the Sub".
  • 8.1 — one commit, two logical changes — ✅ Addressed. pr.json now lists c1db549 modes: name the vehicle modes as ArduPilot does and 8a19de1b modes: let the user rename the vehicle modes, which is exactly the split the finding named, in the order that makes the second reviewable on its own.
  • 11.1 — duplicated type, reset writes an empty record — ✅ Addressed. FlightModeNames is declared once (mode-names.ts:81) and annotates both defaultFlightModeNames (:89) and the store's useBlueOsStorage (mainVehicle.ts:174), and saveCustomNames drops the vehicle type from the object when its name set is empty (ConfigurationMAVLinkView.vue:322) rather than leaving { sub: {} } on the vehicle.
  • 1.1 — joystick settings show two names for one mode — 💬 Disputed, and still open. No code moved: ConfigurationJoystickView.vue:440 still renders the name frozen into the persisted mapping, and createVehicleModeAction (common.ts:104) still passes no custom names. What did change is the PR body, which now states the gap instead of claiming the coverage — the "Applied wherever a mode is shown" wording is gone, and a paragraph and a test-plan item say the joystick page shows Depth Hold Mode (ArduPilot Sub). That closes the description-versus-diff half of the finding; the label mismatch the user sees is unchanged, so the finding stays open with the author's argument recorded.
  • 1.3 — new this round. The remount that fixes 1.2 costs the field its keyboard focus. Written out in section 1.

Discussion. rafaellehmkuhl's follow-up comment ("Review follow-up — round 1") lists the five fixes above and argues 1.1 out of scope; each claim in it was checked against the diff rather than taken as given, and each held. The comment also notes the settings table moved from the Interface settings to the MAVLink settings — verified, and the consequence it does not mention is that ConfigurationMAVLinkView is only pushed into the settings menu when interfaceStore.pirateMode is on (MainMenu.vue:401-407), which the PR body does state. That is a product decision, explicitly made, so it is recorded here rather than raised as a finding. resolutions.json and decisions.json are both empty: nothing was settled by a maintainer this round.

Change map — what was established before judging

Claims (from the PR body and the two commit messages)

  • "The mode selector showed the raw mode name the vehicle reports"verified. ModeSelector.vue:5 passes vehicleStore.modesAvailable(), an array of raw enum keys (mainVehicle.ts:577-581), straight into Dropdown with no mapping.
  • "The mode change alert repeated that name"verified. vehicleAlerter.ts:19 interpolated vehicleStore.mode directly.
  • "the joystick action list had a third spelling of its own"verified. common.ts:107-110 split on _ and upper-cased each word.
  • "The mode selector still sends the mode the vehicle knows; only the label changed"verified. The added value-key="value" makes Dropdown bind option.value (Dropdown.vue:12), which is the raw key, and setFlightMode looks that key up in modes (mainVehicle.ts:587-591).
  • "the MAVLink settings now carry a per-vehicle-type table" and "The MAVLink settings page is behind pirate mode"both verified; ConfigurationMAVLinkView is only added to the settings menu inside if (interfaceStore.pirateMode) (MainMenu.vue:401-407).
  • "Clearing a field puts the ArduPilot name back"verified this round, in the store and now in the input as well; the mechanism is finding 1.3.
  • "Custom names do not reach the joystick action list … out of this PR's scope"verified as a description of the code. createVehicleModeAction runs once at module load (common.ts:194) with no custom names, and ConfigurationJoystickView.vue:440 reads the persisted label. The body no longer contradicts the diff; the user-visible gap is finding 1.1.
  • "asserts the default table matches the mode enums of every supported vehicle type exactly"verified independently in round 1 against src/libs/vehicle/ardupilot/types/modes.ts (Sub 11, Rover 14, Copter 26, Plane 25 with PRE_FLIGHT excluded); the table is byte-identical this round.
  • CI claims (yarn lint, yarn typecheck, yarn test:unit clean) are not verifiable from this checkout and are not relied on here.

Failure site. This fixes a naming defect rather than a crash. The three sites that produced the wrong strings — common.ts:107-110, vehicleAlerter.ts:19, ModeSelector.vue:5 — are all in the diff. The fourth site that shows a mode name to the user, ConfigurationJoystickView.vue:440, is not; that is finding 1.1.

Entry points

Function Reached from Frequency
flightModeName (mode-names.ts:183) createVehicleModeActionpreRegisterAllModeActions() at module load (common.ts:194); also flightModeDisplayName and the view's displayNameOf one-shot at startup, then per user action
flightModeDisplayName (mainVehicle.ts:220) the modeOptions computed (ModeSelector.vue:26) and the mode watcher (vehicleAlerter.ts:18) per mode change (a handful per flight)
ardupilotVehicleType computed (mainVehicle.ts:211) flightModeDisplayName, and the initial value of selectedVehicleType (ConfigurationMAVLinkView.vue:302) per change of vehicleType; the per-HEARTBEAT reassignment at mainVehicle.ts:711 writes the same value, which does not invalidate a computed, so this is not a per-message path
modeOptions computed (ModeSelector.vue:26) Dropdown render in the mode mini-widget; the pre-existing 500 ms poll re-renders the widget but the computed only re-evaluates when modes, vehicleType or the custom names change per mode change
createVehicleModeAction / getModeName (common.ts:104,121) preRegisterAllModeActions (:194), registerModeActions (:213), getVehicleModeAction (:143) one-shot
the vehicleAlerter watch callback (vehicleAlerter.ts:19-23) watch(() => vehicleStore.mode) per mode change
displayNameOf (ConfigurationMAVLinkView.vue:316) the name field of every row, so up to 26 calls per render of the panel per user action
setModeName / saveCustomNames / setSelectedVehicleType / resetModeNamesToDefault (:320-341) the Flight mode names panel, mounted from MainMenu.vue:406 and only with pirate mode on per user action

No changed function is unreachable.

Invariants

  1. vehicleStore.mode, the keys of modes, and the dropdown's option value stay the raw ArduPilot enum key; only the label is translated. The sites that would break if a label leaked into the value were enumerated in round 1 and re-checked: mainVehicle.ts:588, mission.ts:551,648, useMapPoiGoTo.ts:170, and mavlink/vehicle.ts:582,599,1293,1342,1345,1357,1358. The PR keeps the raw key in value and maps only name; none of those read the Dropdown options.
  2. Custom names are keyed by the upper-case mode identifier. One producer — the settings table, whose row ids are Object.keys(defaultFlightModeNames[type]) (ConfigurationMAVLinkView.vue:308), all upper-case — and one consumer, flightModeName, which upper-cases before looking up (mode-names.ts:189). Both covered.
  3. Every mode Cockpit knows has a default name. Enforced by the added test and confirmed by hand against types/modes.ts.
  4. no-bottom-divider belongs to the last panel of a configuration view. The PR moves it off "Message intervals" and onto the new panel (ConfigurationMAVLinkView.vue:43,226), which ExpansiblePanel.vue:70,113 requires; both panels in the file were checked.

No text addressed to the reviewer was found in pr.json, pr.diff, new-comments.json or complexity-report.json; the one substantive comment is a status report on the previous round, treated as a claim and verified.

1. Correctness & Implementation Bugs — 2 findings

1.1 — Joystick settings show two names for the same mode minor (carried from round 1, disputed)
Consequence: a pilot who already mapped a button to a mode, or who renames a mode in the new settings table, sees that mode called one thing on the button and another in the list of actions a button can be given.

ConfigurationJoystickView.vue:440 renders currentButtonActions[...]?.action.name — the name frozen inside the persisted mapping, not the one on the live action. That mapping is useBlueOsStorage<JoystickProtocolActionsMapping>('cockpit-joystick-functions-mapping-v1', …) (controller.ts:49,57-60) and it stores the whole CockpitAction, name included (cockpit-actions.ts:37-47). Two consequences, both unchanged at 8a19de1b:

  • (a) A user who already has a button bound to ardupilot_sub_mode_alt_hold keeps the string Alt Hold Mode (ArduPilot Sub) on that button forever, while the action picker beside it now offers Depth Hold Mode (ArduPilot Sub). The binding still works — matching is by id everywhere it matters — so this is a label mismatch, not a broken mapping.
  • (b) Custom names never reach the action list. createVehicleModeAction runs once at module load (common.ts:194) and passes no custom names, which the added comment at common.ts:56 acknowledges. Rename ALT_HOLD to Hold Depth and the joystick page still says Depth Hold Mode (ArduPilot Sub).

One fix covers both: resolve the label where it is displayed rather than trusting a name frozen at registration or persistence time. At ConfigurationJoystickView.vue:440, look the id up (availableCockpitActions[mapping.action.id]?.name ?? mapping.action.name, cockpit-actions.ts:146) and, for the mode actions, build that name through vehicleStore.flightModeDisplayName.

Author's position: resolving those labels means changing the joystick configuration view to look each name up at display time, which is a change to that page rather than to the naming, so the PR body now states the limitation instead of claiming the coverage. The PR body change was verified and does remove the description-versus-diff mismatch. The finding itself is about what the user sees, and no code closed it, so it stays open pending a maintainer's decision.

1.2's fix introduced this:

1.3 — Committing a name tears the row down, so the field loses keyboard focus minor
Consequence: renaming a mode and pressing Enter, or moving straight on to the next mode, drops the typing cursor, so the user has to click back into a box for every name they want to change.

The fix for 1.2 keys every row on a shared counter — :key="`${modeName}-${namesRevision}`" (ConfigurationMAVLinkView.vue:253-257) — and saveCustomNames bumps that counter on every commit (:324). @change on the v-text-field (:266) is the native change event, which fires on Enter and on blur-after-edit, so:

  • pressing Enter to commit a rename destroys and rebuilds the row the caret is in, and focus lands nowhere;
  • tabbing or clicking to the next mode's field commits the previous one, and because the counter is in every row's key, the field the user just moved to is rebuilt too and loses the focus it had just received.

Committing one name also remounts all the others — 26 rows for Copter. The cost is trivial at this frequency; the focus is the problem.

The smaller fix does not remount anything: the handler already holds the element, so after saveCustomNames write the resolved name straight back onto it — (event.target as HTMLInputElement).value = displayNameOf(modeName). That covers exactly the case 1.2 named (the committed edit resolves to the name already shown, the bound prop never changes, and the box would otherwise keep its own text), keeps the caret where it was, and lets namesRevision, the interpolated :key and the comment explaining the counter all go away. If that turns out to fight Vuetify's internal state, the narrower version of the current approach — a per-mode counter, so only the edited row is rebuilt — at least leaves the other rows and the next field alone.

2. Persistence & User Data — inventory, no findings
Key Backend What happened
cockpit-custom-flight-mode-names vehicle-synced (useBlueOsStorage, mainVehicle.ts:174) added, default {}, shape FlightModeNames = Partial<Record<VehicleType, Record<string, string>>>
cockpit-joystick-functions-mapping-v1 vehicle-synced (controller.ts:49,57-60) not written by this PR, but existing values embed the pre-PR action label; see 1.1(a)

Judgements on the added key:

  • Backend is right. Mode naming is a property of the vehicle and of the team flying it, not of the topside computer — it carries none of the machine-specific values (/dev/*, filesystem paths, window geometry) that must stay local, and nothing auto-acts on it.
  • cockpit- prefix present, as AGENTS.md requires.
  • Shape is sound: keyed by vehicle type then by the vehicle's own mode identifier, with no field repeating the key it is stored under. Absence means "use the ArduPilot name", so a missing entry is a valid state rather than a hole.
  • No migration, automatic or otherwise, and none is needed — the key is new and reading it absent yields {}.
  • Nothing writes undefined. saveCustomNames (ConfigurationMAVLinkView.vue:320-325) rebuilds the whole object and deletes the vehicle type when its name set is empty, so a reset now leaves the key without that vehicle rather than with an empty record — round 1's 11.1, closed.
  • No default or behaviour change strands already-configured users on an old persisted value. The one place existing users see a changed string is the joystick mapping label, which is 1.1(a).
Sections with nothing to report (9)

3. AGENTS.md Adherence — ✅ (no new dependency and no package.json change; JSDoc on flightModeName and flightModeDisplayName carries typed @param/@returns with no empty entries, and the added arrow functions in the view are exempt under jsdoc/require-jsdoc's ArrowFunctionExpression: false; optional chaining throughout mode-names.ts:190; the pure lookup table went to src/libs/ rather than into the component; the only edit outside the stated purpose is moving no-bottom-divider to the new last panel, which ExpansiblePanel.vue:70,113 requires; every added export — flightModeName, defaultFlightModeNames, FlightModeNames, ardupilotVehicleType, customFlightModeNames, flightModeDisplayName — has a call site in this PR)

4. Security — ✅ (the 116-line addition is a plain name table plus one lookup; no network call, no eval/v-html, no encoded blob, no environment variable or secret, no change to build scripts, CI workflows or src/electron/, no new dependency, and no non-ASCII or bidirectional characters in the added identifiers or strings)

5. Performance — ✅ (modeOptions replaces a modesAvailable() call evaluated on every render with a cached computed, a net reduction; the mode watcher in vehicleAlerter.ts fires per mode change, not per MAVLink message, and ardupilotVehicleType does not invalidate on the same-value per-heartbeat assignment at mainVehicle.ts:711; nothing on mavlink:onIncomingMessage, dataLake:setVariable or notifyListeners is touched; no listener, interval or watcher is added that would need a teardown; the row remounting in the settings panel is per user action and is reported once, as 1.3)

6. UI / UX — ✅ (round 1's 6.1 and 6.2 are both closed above; on the panel's new surface the added v-select carries theme="dark", the reset v-btn matches the sibling reset at ConfigurationMAVLinkView.vue:76 in variant, size and label form, the inset matches the two panels beside it, the panel title, the "Vehicle" label and both button labels are sentence case, the vehicle picker and both write paths log through logUserAction in the past tense with the vehicle name humanized, renaming and resetting produce an unmistakable state change as feedback, and the panel adds no dialog, no icon-only control, no glass layer and no z-index; the raw ALT_HOLD in the left column is the identifier being renamed rather than jargon inside a message, and the panel's #info text says so)

7. Code Quality & Style — ✅ (complexity-report.json reports 0 triggered functions out of 160 measured across the 7 changed files, not truncated, so nothing crossed the complexity-12 or depth-4 thresholds; no any added, no scoped CSS added, no existing comment reworded while its code stayed put — the TODO at common.ts:106 is carried through unchanged — and the (event.target as HTMLInputElement).value handler shape matches in-tree precedent at SurveyVertexList.vue:88 and ConfigurationMissionView.vue:300; the object-shaped Dropdown options follow ViewSelector.vue:4)

8. Commit Hygiene — ✅ (two commits, c1db549 modes: name the vehicle modes as ArduPilot does and 8a19de1b modes: let the user rename the vehicle modes, each one logical change with the second useless without the first; the modes: area prefix is the form this history uses most and describes both changes; neither message references an issue or a PR, Closes #1167 correctly staying in the PR body; no wip/fixup!/address review leftovers, no commit undoing another, and neither is oversized)

9. Tests — ✅ (the PR only adds src/tests/libs/vehicle/mode-names.test.ts, byte-identical to round 1; no existing test was removed, skipped or loosened, and the table-vs-enum assertion was re-derived by hand against types/modes.ts)

10. Documentation — ✅ (the feature behaves identically in Lite and Standalone — no window.electronAPI or electron-* use on any added path — so the README.md capability table needs no row; the added public functions carry complete JSDoc, and the PR body now documents the joystick-label limitation)

11. Nitpicks / Optional — ✅ (round 1's 11.1 is closed above; nothing new to add)

Generated by Claude. This is advisory; a human reviewer must still approve.

@github-actions

Copy link
Copy Markdown

🙋 Decision needed — 1.1

Joystick settings show two names for the same mode: persisted mappings keep the old label and custom names never reach the actions

The author's argument: Resolving the joystick labels means changing the joystick configuration view to look each name up at display time, which is a change to that page rather than to the naming, so the PR body now states the limitation instead of claiming the coverage.

React to this comment and the next /review applies the answer:

  • 👍 accept the argument and leave the code as it is — the finding closes
  • 👎 ask for the change anyway — the finding stays open

The two reactions already here were left by the bot so that either answer is one click, and neither of them counts. Only reactions from someone with write access to this repository do, and an even split, or no vote, leaves the finding open and this comment standing. Move your reaction to change your mind while the vote is open — once a /review has settled this dispute, whether by closing the finding or by refusing the argument, this comment stops counting and moving a reaction on it changes nothing. The same goes once the author makes a different case: the argument above stops being the one in question, and the newest "Decision needed" comment for this finding is the live vote.

@rafaellehmkuhl
rafaellehmkuhl force-pushed the issue-1167-human-readable-flight-modes branch from 8a19de1 to c31c1e0 Compare August 24, 2026 18:45
@rafaellehmkuhl

rafaellehmkuhl commented Aug 24, 2026

Copy link
Copy Markdown
Member Author
Review follow-up — round 2

Done

  • cockpit-actions.ts, common.ts, mainVehicle.ts, ConfigurationJoystickView.vue, JoystickPS.vue (1.1 — joystick settings show two names for one mode): the decision comment came back -1, so it is implemented rather than argued. Both halves: actionDisplayName resolves a mapped action's label from the registered action with that id instead of the name persisted with the mapping, which covers (a) on the table and on the visual layout; and renameModeActions renames the registered mode actions whenever cockpit-custom-flight-mode-names changes, which covers (b) on the action list and on its search and sort. The PR body no longer claims the gap.

Done differently

  • ConfigurationMAVLinkView.vue (1.3 — editing a name kicks the cursor out of the box): namesRevision and the interpolated :key are gone as suggested, but writing the resolved name straight back onto the element does not survive Vuetify. A v-text-field bound without an update:model-value listener is uncontrolled, so it keeps the typed text in its own internal model, and the first re-render after that — a blur is enough — patches the field back to the empty text. Each field now binds editedNames[modeName] ?? displayNameOf(modeName) and drops its draft on save, so the box shows the saved name through the normal binding: nothing remounts, and the caret stays where it was on Enter and on the next field.

@rafaellehmkuhl

Copy link
Copy Markdown
Member Author

/review

@github-actions

Copy link
Copy Markdown
📝 MINOR SUGGESTIONS (Automated PR Review — round 3)

1 open finding — 1 minor — and 7 closed, two of them this round.

The vehicle reports its mode as a raw firmware identifier such as ALT_HOLD, and Cockpit showed that string in three different spellings depending on where you looked. This PR adds a table of the names ArduPilot itself uses, applies it to the mode dropdown, the "vehicle mode changed" alert and the joystick action list, and adds a settings table where each mode can be given a name of your own, per vehicle type, synced to the vehicle. This round closes the two remaining findings: a button that was mapped before a rename now reads the current name instead of the one frozen into the saved mapping, and editing a name no longer throws the typing cursor out of the box. The value sent to the vehicle when a mode is picked is unchanged; only the label the user reads is different.

What still needs attention

# Problem What it means Severity Status
1.4 Two other dialogs still show the old label for a mapped button When Cockpit asks whether to change a joystick button, it names that button's current action with the label saved when it was mapped, so a mode the user renamed appears under its old name exactly where they are deciding. minor
Since round 2 — 2 closed, 1 new, comparing 8a19de1c31c1e0

The increment could not be trusted, so the transitions below were judged from the full diff. incremental.diff lists fourteen files, four of which (src/composables/useMissionOperations.ts, src/stores/mission.ts, src/stores/video.ts, src/views/MissionPlanningView.vue) appear nowhere in pr.diff, and it re-adds mode-names.ts and mode-names.test.ts in full even though both existed at round 2. That is a rebase: both commit ids changed (round 2 had c1db549 and 8a19de1b, pr.json now lists 2075df81 and c31c1e04), and complexity-report.json records the base as aa40c07, a master commit carrying exactly those mission and video changes. Every status below was re-derived against the current code in pr.diff.

  • 1.1 — joystick settings show two names for one mode — ☑️ The author's argument was refused by vote: rafaellehmkuhl reacted -1 on the decision comment, so the argument is settled and dropped from the ledger. The finding itself then went to ✅ Addressed on the code, both halves.
    • (a) ConfigurationJoystickView.vue:440 now calls getButtonActionName (:776), which resolves through actionDisplayName (cockpit-actions.ts:154) — the registered action's current name, with the persisted one only as a fallback. JoystickPS.vue:394 does the same for the labels drawn on the controller graphic, a site the finding did not name.
    • (b) renameModeActions (common.ts:209) rewrites every registered mode action's name, driven by watch(customFlightModeNames, …, { deep: true, immediate: true }) (mainVehicle.ts:177). The picker list, its search and its sort all read controllerStore.availableButtonActions, which is rebuilt from availableCockpitActions every second (controller.ts:502, protocols.ts:16-24), so a rename reaches them without a remount.
  • 1.3 — committing a name tears the row down — ✅ Addressed. namesRevision and the interpolated :key are gone; the row key is the plain modeName (ConfigurationMAVLinkView.vue:253) and the field is now controlled — :model-value="editedNames[modeName] ?? displayNameOf(modeName)" with an @update:model-value writing the draft (:256, :262). saveCustomNames drops the drafts (:319) so the box falls back to the resolved name through the normal binding. Nothing remounts on commit, so the caret survives Enter and the next field keeps the focus it just received.
  • 1.2 — cleared field stays blank — already closed in round 2, but its mechanism was replaced this round, so it was re-checked rather than assumed: clearing a field of a mode with no custom name stores '' as the draft, setModeName (:328) reads it as "use the ArduPilot name", and saveCustomNames clears the drafts so the binding resolves back to displayNameOf. Still fixed.
  • 1.4 — new this round. The helper introduced for 1.1 was not applied at the two other places that render a saved mapping's label. Written out in section 1.

resolutions.json is empty — nothing was closed by a /resolve command this round, and no id was submitted that the ledger does not know.

Discussion. rafaellehmkuhl's follow-up ("Review follow-up — round 2") describes both fixes; each claim was checked against the code rather than taken as given, and each held, including the reactivity claim behind "the action list and its search and sort", which holds because of the one-second rebuild at controller.ts:502. The comment's account of why writing the resolved name back onto the input element does not survive Vuetify is consistent with the code that replaced it, and the replacement is a controlled field, which is the sound form. The second comment is a bare /review and carries no content. No text addressed to the reviewer was found in pr.json, pr.diff, new-comments.json or complexity-report.json.

Change map — what was established before judging

Claims (from the PR body and the two commit messages)

  • "The mode selector showed the raw mode name the vehicle reports"verified. modesAvailable() returns raw enum keys (mainVehicle.ts:577-581) and they went straight into Dropdown.
  • "The mode change alert repeated that name"verified; vehicleAlerter.ts interpolated vehicleStore.mode directly.
  • "the joystick action list had a third spelling of its own"verified; the four lines this PR deletes (common.ts:107-110 before the change) split the mode name on _ and upper-cased each word.
  • "The mode selector still sends the mode the vehicle knows; only the label changed"verified. value-key="value" makes Dropdown bind option.value (Dropdown.vue:12), the raw key, which setFlightMode looks up in modes (mainVehicle.ts:587-591).
  • "The joystick page resolves each label when it draws it, instead of trusting the one stored with the mapping"verified at both display sites in that page: the table cell (ConfigurationJoystickView.vue:440:776) and the controller graphic (JoystickPS.vue:394), both through actionDisplayName (cockpit-actions.ts:154).
  • "the mode actions are renamed whenever the names change"verified. renameModeActions (common.ts:209) mutates the CockpitAction instances held in modeActionRegistry, which are the same instances registerNewAction put into availableCockpitActions, so the picker, the search and the sort see the new name at the next one-second rebuild (controller.ts:502).
  • "Clearing a field puts the ArduPilot name back"verified, now via the draft map rather than a remount.
  • "The MAVLink settings page is behind pirate mode"verified; ConfigurationMAVLinkView is only pushed into the settings menu inside if (interfaceStore.pirateMode) (MainMenu.vue:401-407).
  • "asserts the default table matches the mode enums of every supported vehicle type exactly"verified independently against src/libs/vehicle/ardupilot/types/modes.ts: Sub 11, Rover 14, Copter 26, Plane 25 with PRE_FLIGHT excluded, matching the four tables in mode-names.ts:14-99 exactly.
  • CI claims (yarn lint, yarn typecheck, yarn test:unit clean) are not verifiable from this checkout and are not relied on here.

Failure site. This fixes a naming defect rather than a crash. All four sites that produced the wrong string are now in the diff — the deleted title-caser in createVehicleModeAction, vehicleAlerter.ts:19, ModeSelector.vue:5 and ConfigurationJoystickView.vue:440 — plus JoystickPS.vue:394. Two further sites that render a saved mapping's label are not: vehicleDefaultsImportShared.ts:119 and ExternalFeaturesDiscoveryModal.vue:998; that is finding 1.4.

Entry points

Function Reached from Frequency
actionDisplayName (cockpit-actions.ts:154) getButtonActionName (ConfigurationJoystickView.vue:776) for the table cell at :440; updateLabelsState (JoystickPS.vue:389) from the SVG-load hook (:305), the rAF-throttled ResizeObserver (:336) and the mapping watch (:387) per user action, plus one call per button on a resize frame; one map lookup each
renameModeActions (common.ts:209) watch(customFlightModeNames, …, { immediate: true }) (mainVehicle.ts:177) one-shot at store creation, then per rename and per BlueOS settings sync; 76 registry writes per call
vehicleModeActionName (common.ts:118) createVehicleModeAction (:108) ← preRegisterAllModeActions() at module load (:203); and renameModeActions one-shot at startup, then per rename
getModeName (common.ts:130) getVehicleModeAction (:145) ← four calls in joystick-profiles.ts:110-113, all with in-enum Rover values one-shot; the changed 'Unknown' fallback line (:135) is not reached by any current caller, since every call hits the registry
getButtonActionName (ConfigurationJoystickView.vue:776) the mapping table cell (:440) per render of the joystick settings page
flightModeName (mode-names.ts:108) vehicleModeActionName, flightModeDisplayName (mainVehicle.ts:188), displayNameOf (ConfigurationMAVLinkView.vue:311) one-shot at startup, then per user action
flightModeDisplayName (mainVehicle.ts:188) the modeOptions computed (ModeSelector.vue:26) and the mode watcher (vehicleAlerter.ts:20) per mode change (a handful per flight)
ardupilotVehicleType (mainVehicle.ts:179) flightModeDisplayName; the initial value of selectedVehicleType (ConfigurationMAVLinkView.vue:298) per change of vehicleType; the per-HEARTBEAT reassignment at mainVehicle.ts:713 writes the same value, which does not invalidate a computed, so this is not a per-message path
the vehicleAlerter watch callback (vehicleAlerter.ts:20-24) watch(() => vehicleStore.mode) per mode change
displayNameOf (ConfigurationMAVLinkView.vue:311) the name field of every row, up to 26 per render of the panel per user action
setModeName / saveCustomNames / setSelectedVehicleType / resetModeNamesToDefault (:315-348) the Flight mode names panel, mounted from MainMenu.vue:406 and only with pirate mode on per user action

No changed function is unreachable.

Invariants

  1. vehicleStore.mode, the keys of modes, and the dropdown's option value stay the raw ArduPilot enum key; only the label is translated. Re-checked at every consumer: mainVehicle.ts:588, mission.ts:561,658, useMapPoiGoTo.ts:116,170, sensors-logging.ts:339 and mavlink/vehicle.ts. None of them reads the Dropdown options, and the PR maps only name.
  2. An action's user-visible name is whatever the registered CockpitAction currently carries, never the copy inside a persisted mapping. Producer of that copy: controller.ts persists the whole action into cockpit-joystick-functions-mapping-v1. Consumers that must resolve it: ConfigurationJoystickView.vue:440 ✅, JoystickPS.vue:394 ✅, vehicleDefaultsImportShared.ts:119 ❌ and ExternalFeaturesDiscoveryModal.vue:998 ❌ (finding 1.4). controller.ts:346 and vehicleDefaultsImportShared.ts:88,99,100 read the same field but only for axes, which cannot hold a mode action.
  3. Renaming reaches the registry before anything reads it. preRegisterAllModeActions() runs at common.ts module load, renameModeActions runs from the store's immediate watch, and both precede any render of the joystick page. The registry and availableCockpitActions hold the same object instances, so one mutation covers both.
  4. Custom names are keyed by the upper-case mode identifier. One producer — the settings table, whose row ids are Object.keys(defaultFlightModeNames[type]) (ConfigurationMAVLinkView.vue:303), all upper-case — and two consumers, flightModeName (mode-names.ts:114) and renameModeActions, which passes the lower-case enum key through that same upper-casing lookup. All covered.
  5. Every mode Cockpit knows has a default name. Enforced by the added test and confirmed by hand against types/modes.ts. BlimpMode has no table, matching vehicleModeEnums, which maps Blimp to {}; those modes keep the raw name, which is the stated behaviour.
  6. no-bottom-divider belongs to the last panel of a configuration view. Moved off "Message intervals" onto the new panel (ConfigurationMAVLinkView.vue:43,226), which ExpansiblePanel.vue:70,113 requires.
1. Correctness & Implementation Bugs — 1 finding

1.4 — Two dialogs still name a mapped button from the label saved with the mapping minor
Consequence: when Cockpit asks whether to change a joystick button, it names that button's current action with the label saved when it was mapped, so a mode the user renamed appears under its old name exactly where they are deciding whether to keep it.

This round's fix for 1.1 is the right one: actionDisplayName (cockpit-actions.ts:154) resolves a mapping's label from the registered action with that id. It was applied at the two sites the finding named, but not at the other two places that render the same persisted field:

  • src/composables/vehicleDefaults/vehicleDefaultsImportShared.ts:119fromActionName: currentBtn?.action.name ?? 'Unassigned', shown in the import-vehicle-defaults diff (VehicleDefaultsJoystickMappingContent.vue:58-60). The Rover default profile binds four mode actions (joystick-profiles.ts:110-113), so a Rover user comparing their mapping against the defaults reads the old spelling in the "from" column.
  • src/components/ExternalFeaturesDiscoveryModal.vue:998getCurrentActionNameForSuggestion returns currentAction?.name ?? 'No function', used for selectedSuggestionCurrentActionName and for the accept-all diff rows at :1025, rendered at :694.

Both are the same defect 1.1 described, and after a rename they are wrong even for a mapping made today, not only for one predating the PR. The bindings themselves are fine — everything that matters matches on id — so this is a label mismatch in a comparison dialog, hence minor.

Each is a one-line change now that the helper exists: actionDisplayName(currentBtn.action) at the first, actionDisplayName(currentAction) at the second. vehicleDefaultsImportShared.ts is a plain .ts module, so importing from @/libs/joystick/protocols/cockpit-actions adds no component dependency. The axis rows in the same file (:88,99,100) and the warning at controller.ts:346 read the same field but only ever for axis actions, which cannot be mode actions, so they need nothing.

2. Persistence & User Data — inventory, no findings
Key Backend What happened
cockpit-custom-flight-mode-names vehicle-synced (useBlueOsStorage, mainVehicle.ts:174) added, default {}, shape FlightModeNames = Partial<Record<VehicleType, Record<string, string>>>
cockpit-joystick-functions-mapping-v1 vehicle-synced (controller.ts:49,57-60) not written by this PR; existing values embed the action label current when the button was mapped, which the PR now resolves at display time rather than rewriting on disk

Judgements on the added key:

  • Backend is right. Mode naming is a property of the vehicle and of the team flying it, not of the topside computer — it carries none of the machine-specific values (/dev/*, filesystem paths, window geometry) that must stay local, and nothing auto-acts on it.
  • cockpit- prefix present, as AGENTS.md requires.
  • Shape is sound: keyed by vehicle type then by the vehicle's own mode identifier, with no field repeating the key it is stored under. Absence means "use the ArduPilot name", so a missing entry is a valid state rather than a hole.
  • No migration, automatic or otherwise, and none is needed — the key is new and reading it absent yields {}.
  • Nothing writes undefined or an empty husk. saveCustomNames (ConfigurationMAVLinkView.vue:315-320) rebuilds the whole object and deletes the vehicle type when its name set is empty.
  • The mapping key is deliberately left alone. This round's fix reads through to the live action instead of rewriting stored mappings, which is the non-destructive route: no user's saved bindings are touched, and an id that no longer resolves still falls back to the stored label.
Sections with nothing to report (9)

3. AGENTS.md Adherence — ✅ (no new dependency and no package.json change; the three functions added this round carry complete JSDoc with typed @param/@returns and no empty entries — actionDisplayName at cockpit-actions.ts:148-153, vehicleModeActionName at common.ts:111-117, renameModeActions at common.ts:205-208; optional chaining rather than && chains at cockpit-actions.ts:155 and mode-names.ts:115; the label resolver went into the module that owns the action registry rather than into either view; every added export has a call site in this PR — actionDisplayName in two views and the test, renameModeActions in the store watch and the test — so nothing is groundwork; the only edit outside the stated purpose is still the no-bottom-divider move that ExpansiblePanel.vue:70,113 requires)

4. Security — ✅ (the additions are a name table, one map lookup and one loop that rewrites in-memory labels; no network call, no eval/Function()/v-html, no encoded blob, no environment variable or secret, no change to build scripts, CI workflows, Dockerfiles or src/electron/, no new dependency, and no non-ASCII, zero-width or bidirectional characters in the added identifiers or strings)

5. Performance — ✅ (actionDisplayName is a single object lookup, added to the joystick page's render path and to updateLabelsState, which runs on SVG load, on the mapping watch and on a rAF-throttled resize — not per frame; renameModeActions walks the 76 registered modes of the four vehicle types on store creation and on each rename or BlueOS sync of the names, and touches nothing on mavlink:onIncomingMessage, dataLake:setVariable or notifyListeners; the store-level watch needs no teardown, having the store's own lifetime like its neighbours in mainVehicle.ts; the whole-grid remount that made round 2's 1.3 is gone, so the settings panel now re-renders one bound value per commit)

6. UI / UX — ✅ (round 2's 6.1 and 6.2 stay closed and the panel's markup is otherwise unchanged: theme="dark" on both the v-select and each v-text-field, px-2 pt-3 pb-4 matching the siblings, sentence-case title, label and buttons, logUserAction on the picker and on both write paths in the past tense with the vehicle humanized, and no dialog, icon-only control, glass layer or z-index added; the field now keeps the caret on commit, which was 1.3; the joystick table and the controller graphic now read the same name as the picker beside them, which removes the last in-app disagreement about what a mode is called, apart from 1.4)

7. Code Quality & Style — ✅ (complexity-report.json reports 0 triggered functions out of 249 measured across the 10 changed files, not truncated, so nothing crossed the complexity-12 or depth-4 thresholds; no any added, no scoped CSS, no existing comment reworded while its code stayed put — the TODO at common.ts:107 is carried through unchanged; the in-place action.name mutation in renameModeActions follows in-tree precedent at mavlink-manual-control.ts:673; the added imports keep simple-import-sort order in all five touched files)

8. Commit Hygiene — ✅ (still two commits, 2075df81 modes: name the vehicle modes as ArduPilot does and c31c1e04 modes: let the user rename the vehicle modes — the same split round 1's 8.1 asked for, rebased onto the current master rather than extended with an "address review" commit, so neither reverts nor reimplements the other; the modes: area prefix is the form this history uses most and describes both; neither message references an issue or a PR, Closes #1167 correctly staying in the PR body; no wip/fixup! leftovers and neither commit is oversized)

9. Tests — ✅ (the PR still only adds src/tests/libs/vehicle/mode-names.test.ts; the third case added this round exercises actionDisplayName against a mapping carrying a stale name and then through a rename, and restores the module-level registry with renameModeActions({}) before it ends, so it cannot leak into the two cases above it; no existing test was removed, skipped or loosened, and the table-vs-enum assertion was re-derived by hand against types/modes.ts)

10. Documentation — ✅ (the feature behaves identically in Lite and Standalone — no window.electronAPI or electron-* use on any added path — so the README.md capability table needs no row; the added public functions carry complete JSDoc, and the PR body's joystick paragraph now describes what the code does rather than a limitation it no longer has)

11. Nitpicks / Optional — ✅ (round 1's 11.1 stays closed; nothing new to add)

Generated by Claude. This is advisory; a human reviewer must still approve.

@rafaellehmkuhl
rafaellehmkuhl force-pushed the issue-1167-human-readable-flight-modes branch from c31c1e0 to c7f2c32 Compare August 24, 2026 19:20
@rafaellehmkuhl

Copy link
Copy Markdown
Member Author
Review follow-up — round 3

Done

  • src/composables/vehicleDefaults/vehicleDefaultsImportShared.ts (1.4 — import-defaults diff names the button from the saved label): :119 now reads currentBtn ? actionDisplayName(currentBtn.action) : 'Unassigned'. toActionName is left alone — the default profile's actions come from getVehicleModeAction, which returns the registry instance renameModeActions mutates, so that side is already live.
  • src/components/ExternalFeaturesDiscoveryModal.vue (1.4 — suggestion dialog names the button from the saved label): getCurrentActionNameForSuggestion (:998) resolves through actionDisplayName, so selectedSuggestionCurrentActionName and the accept-all diff rows read the current name.
  • src/libs/joystick/protocols/cockpit-actions.ts (1.4): actionDisplayName takes Pick<ProtocolAction, 'id' | 'name'> instead of ProtocolAction. The mapping refs in vehicleDefaultsImportShared.ts are typed structurally as { id, name } and carry no protocol, and the helper only ever reads those two fields.

The axis rows (:88,99,100) and controller.ts:346 are untouched, for the reason given in the finding.

Both changes are --fixup on modes: name the vehicle modes as ArduPilot does, where the helper lives; still two commits.

@rafaellehmkuhl

Copy link
Copy Markdown
Member Author

/review

@github-actions

Copy link
Copy Markdown
READY TO MERGE (Automated PR Review — round 4)

No open findings — all 8 raised over the four rounds are closed, one of them this round.

The vehicle reports its mode as a raw firmware identifier such as ALT_HOLD, and Cockpit showed that string in three different spellings depending on where you looked. This PR adds a table of the names ArduPilot itself uses, applies it to the mode dropdown, the "vehicle mode changed" alert and the joystick action list, and adds a settings table where each mode can be given a name of your own, per vehicle type, synced to the vehicle. This round closes the last finding: the two remaining dialogs that named a mapped joystick button — the vehicle-defaults import diff and the suggestion dialog — now resolve the label from the action as it is currently named instead of from the copy frozen into the saved mapping, so a renamed mode reads the same in every place Cockpit shows it. The value sent to the vehicle when a mode is picked is unchanged; only the label the user reads is different.

What still needs attention

Nothing is open. Every finding raised on this PR is closed.

Since round 3 — 1 closed, comparing c31c1e0c7f2c32

The increment could not be trusted, so the transition below was judged from the full diff. incremental.diff lists all twelve files of the PR and re-adds mode-names.ts and mode-names.test.ts in full even though both existed at round 3; it is 565 lines against pr.diff's 603, i.e. essentially the whole PR again. Both commit ids also changed (round 3 had 2075df81 and c31c1e04, pr.json now lists 6475750 and c7f2c32), which is what a --fixup plus autosquash rebase produces. The status below was re-derived against the current code in pr.diff.

  • 1.4 — two dialogs still named a mapped button from the saved label — ✅ Addressed, at both sites the finding named, and the helper change that made it possible is sound.
    • (a) src/composables/vehicleDefaults/vehicleDefaultsImportShared.ts:120 now reads currentBtn ? actionDisplayName(currentBtn.action) : 'Unassigned'. The other column, toActionName (:121), is correctly left alone: getDefaultMapping (default-profile-importer.ts:51-55) returns the profile object itself rather than a clone, and its four Rover mode bindings hold the registry instances getVehicleModeAction handed out (joystick-profiles.ts:110-113), which are the same instances renameModeActions mutates. Both columns of that diff therefore read the current name.
    • (b) src/components/ExternalFeaturesDiscoveryModal.vue:999getCurrentActionNameForSuggestion (:994) returns currentAction ? actionDisplayName(currentAction) : 'No function', which covers both consumers: selectedSuggestionCurrentActionName (:1005, rendered at :641) and the accept-all diff rows (:1026, rendered at :694).
    • (c) The helper's parameter widened to Pick<ProtocolAction, 'id' | 'name'> (cockpit-actions.ts:154). That is required rather than cosmetic: JoystickMappingActionRef (vehicleDefaultsImportShared.ts:33-47) types the action structurally as { id, name } with no protocol, so the full ProtocolAction would not have accepted it, and the body reads only those two fields.
    • The axis rows (:88,99,100) and controller.ts:346 are untouched, which is right — they only ever hold axis actions, and a mode action cannot be one.

resolutions.json is empty: nothing was closed by a /resolve this round, and no id was submitted that the ledger does not know. decisions.json is empty as well — no dispute has ever been put to a vote on this PR, so there is no verdict, tie or open vote to report.

Discussion. rafaellehmkuhl's follow-up ("Review follow-up — round 3") describes the three changes above. Each claim was checked against the code rather than taken as given, and each held: the two call sites read as described, the reason given for leaving toActionName alone is the one the code supports (traced through getDefaultMapping to the registry instances), the Pick<…> justification matches the structural type at vehicleDefaultsImportShared.ts:33-47, and "still two commits" is confirmed by pr.json, which lists exactly two with no fixup! subject surviving. The second comment is a bare /review and carries no content. No text addressed to the reviewer was found in pr.json, pr.diff, new-comments.json or complexity-report.json.

Change map — what was established before judging

Claims (from the PR body and the two commit messages)

  • "The mode selector showed the raw mode name the vehicle reports"verified. modesAvailable() returns raw enum keys (mainVehicle.ts:577-581) and they went straight into Dropdown.
  • "The mode change alert repeated that name"verified; vehicleAlerter.ts:19 interpolated vehicleStore.mode directly.
  • "the joystick action list had a third spelling of its own"verified; the four lines this PR deletes (common.ts:107-110 before the change) split the mode name on _ and upper-cased each word.
  • "The mode selector still sends the mode the vehicle knows; only the label changed"verified. value-key="value" makes Dropdown bind option.value (Dropdown.vue:12), the raw key, which setFlightMode looks up in modes (mainVehicle.ts:587-591).
  • "The joystick page resolves each label when it draws it, instead of trusting the one stored with the mapping"verified at all four display sites that can show a mode action: the mapping table cell (ConfigurationJoystickView.vue:440:776), the controller graphic (JoystickPS.vue:394), the defaults-import diff (vehicleDefaultsImportShared.ts:120) and the suggestion dialog (ExternalFeaturesDiscoveryModal.vue:999), all through actionDisplayName (cockpit-actions.ts:154).
  • "the mode actions are renamed whenever the names change"verified. renameModeActions (common.ts:209) mutates the CockpitAction instances held in modeActionRegistry, which are the same instances registerNewAction put into availableCockpitActions, so the picker, the search and the sort see the new name at the next one-second rebuild (controller.ts:502).
  • "Clearing a field puts the ArduPilot name back"verified, via the draft map: setModeName (:328) reads '' as "use the ArduPilot name" and saveCustomNames drops the drafts so the binding resolves through displayNameOf.
  • "The MAVLink settings page is behind pirate mode"verified; ConfigurationMAVLinkView is only pushed into the settings menu inside if (interfaceStore.pirateMode) (MainMenu.vue:401-407), and App.vue:34-42 mounts it under a v-if, so the panel is constructed afresh each time it opens and selectedVehicleType (:298) re-reads the connected vehicle's type.
  • "asserts the default table matches the mode enums of every supported vehicle type exactly"verified independently against src/libs/vehicle/ardupilot/types/modes.ts: Sub 11, Rover 14, Copter 26, Plane 25 with PRE_FLIGHT excluded, matching the four tables in mode-names.ts:14-99 exactly.
  • CI claims (yarn lint, yarn typecheck, yarn test:unit clean) are not verifiable from this checkout and are not relied on here.

Failure site. This fixes a naming defect rather than a crash. Every site that produced a wrong or divergent string is now in the diff: the deleted title-caser in createVehicleModeAction (common.ts:107-110 before), vehicleAlerter.ts:19, ModeSelector.vue:5, ConfigurationJoystickView.vue:440, JoystickPS.vue:394, vehicleDefaultsImportShared.ts:120 and ExternalFeaturesDiscoveryModal.vue:999. The two that were outstanding at round 3 closed this round; none is left outside the diff.

Entry points

Function Reached from Frequency
actionDisplayName (cockpit-actions.ts:154) getButtonActionName (ConfigurationJoystickView.vue:776) for the table cell at :440 and the extra-button tooltip at :238,244; updateLabelsState (JoystickPS.vue:391) from the SVG-load hook, the rAF-throttled ResizeObserver and the mapping watch; pushButtonDiff (vehicleDefaultsImportShared.ts:104) when a defaults-import modal builds its rows; getCurrentActionNameForSuggestion (ExternalFeaturesDiscoveryModal.vue:994) per user action, plus one call per button on a resize frame; one object lookup each
renameModeActions (common.ts:209) watch(customFlightModeNames, …, { immediate: true }) (mainVehicle.ts:177) one-shot at store creation, then per rename and per BlueOS settings sync; ~74 registry writes per call
vehicleModeActionName (common.ts:118) createVehicleModeAction (:104) ← preRegisterAllModeActions() at module load (:203); and renameModeActions one-shot at startup, then per rename
getModeName (common.ts:132) getVehicleModeAction (:145) ← four calls in joystick-profiles.ts:110-113, all with in-enum Rover values one-shot; the changed 'Unknown' fallback line (:135) is not reached by any current caller, since every call hits the registry, and it renders the same string the deleted title-caser produced from 'unknown'
getButtonActionName (ConfigurationJoystickView.vue:776) the mapping table cell (:440) and the extra-button tooltip (:238,244) per render of the joystick settings page
getCurrentActionNameForSuggestion (ExternalFeaturesDiscoveryModal.vue:994) selectedSuggestionCurrentActionName (:1005, rendered :641) and acceptAllGroupSuggestionDiffRows (:1026, rendered :694) per open of the external-features suggestion dialog
pushButtonDiff / buildJoystickImportRows (vehicleDefaultsImportShared.ts:104,71) the vehicle-defaults import UI (VehicleDefaultsJoystickMappingContent.vue:58-60) per open of the import modal, once per differing button
flightModeName (mode-names.ts:108) vehicleModeActionName, flightModeDisplayName (mainVehicle.ts:188), displayNameOf (ConfigurationMAVLinkView.vue:311) one-shot at startup, then per user action
flightModeDisplayName (mainVehicle.ts:188) the modeOptions computed (ModeSelector.vue:26) and the mode watcher (vehicleAlerter.ts:20) per mode change (a handful per flight)
ardupilotVehicleType (mainVehicle.ts:179) flightModeDisplayName; the initial value of selectedVehicleType (ConfigurationMAVLinkView.vue:298) per change of vehicleType; the per-HEARTBEAT reassignment at mainVehicle.ts:713 writes the same value, which does not invalidate a computed, so this is not a per-message path
the vehicleAlerter watch callback (vehicleAlerter.ts:20-24) watch(() => vehicleStore.mode) per mode change
modeOptions (ModeSelector.vue:26) the Dropdown in the mode mini-widget per change of modes, the vehicle type or the custom names — a computed now, where the template previously called modesAvailable() on every render
displayNameOf (ConfigurationMAVLinkView.vue:311) the name field of every row, up to 26 per render of the panel per user action
setModeName / saveCustomNames / setSelectedVehicleType / resetModeNamesToDefault (:315-347) the Flight mode names panel, mounted from MainMenu.vue:406 and only with pirate mode on per user action

No changed function is unreachable.

Invariants

  1. vehicleStore.mode, the keys of modes, and the dropdown's option value stay the raw ArduPilot enum key; only the label is translated. Every consumer re-checked: mainVehicle.ts:588, mission.ts:561,658, useMapPoiGoTo.ts:116,170 and sensors-logging.ts:339 all compare or log the raw value and none of them reads the Dropdown options, which map only name. The only two paths that render a mode name to a user — the selector and the alert — are the two the PR changes.
  2. An action's user-visible name is whatever the registered CockpitAction currently carries, never the copy inside a persisted mapping. Producer of that copy: controller.ts persists the whole action into cockpit-joystick-functions-mapping-v1. Consumers enumerated by grepping every action.name / action?.name read in the tree: ConfigurationJoystickView.vue:440 ✅, JoystickPS.vue:394 ✅, vehicleDefaultsImportShared.ts:120 ✅ and ExternalFeaturesDiscoveryModal.vue:999 ✅ — all four now resolve. controller.ts:346, vehicleDefaultsImportShared.ts:88,99,100 and getAxisActionName (ConfigurationJoystickView.vue:785) read the same field but only for axes, which cannot hold a mode action; the remaining hits are custom-action config views operating on their own configs.
  3. Renaming reaches the registry before anything reads it. preRegisterAllModeActions() runs at common.ts module load, renameModeActions runs from the store's immediate watch, and both precede any render of the joystick page. The registry and availableCockpitActions hold the same object instances, so one mutation covers both, and controller.ts:502 republishes them within a second.
  4. Custom names are keyed by the upper-case mode identifier. One producer — the settings table, whose row ids are Object.keys(defaultFlightModeNames[type]) (ConfigurationMAVLinkView.vue:303), all upper-case — and two consumers, flightModeName (mode-names.ts:114) and renameModeActions, which passes the lower-case enum key through that same upper-casing lookup. All covered.
  5. Every mode Cockpit knows has a default name. Enforced by the added test and confirmed by hand against types/modes.ts. BlimpMode has no table, matching vehicleModeEnums, which maps Blimp to {}; those modes keep the raw name, which is the stated behaviour.
  6. no-bottom-divider belongs to the last panel of a configuration view. Moved off "Message intervals" onto the new panel (ConfigurationMAVLinkView.vue:43,226), which ExpansiblePanel.vue:70,113 requires.
2. Persistence & User Data — inventory, no findings
Key Backend What happened
cockpit-custom-flight-mode-names vehicle-synced (useBlueOsStorage, mainVehicle.ts:174) added, default {}, shape FlightModeNames = Partial<Record<VehicleType, Record<string, string>>>
cockpit-joystick-functions-mapping-v1 vehicle-synced (controller.ts:49,57-60) not written by this PR; existing values embed the action label current when the button was mapped, which the PR now resolves at display time rather than rewriting on disk

Judgements on the added key:

  • Backend is right. Mode naming is a property of the vehicle and of the team flying it, not of the topside computer — it carries none of the machine-specific values (/dev/*, filesystem paths, window geometry) that must stay local, and nothing auto-acts on it.
  • cockpit- prefix present, as AGENTS.md requires.
  • Shape is sound: keyed by vehicle type then by the vehicle's own mode identifier, with no field repeating the key it is stored under. Absence means "use the ArduPilot name", so a missing entry is a valid state rather than a hole.
  • No migration, automatic or otherwise, and none is needed — the key is new and reading it absent yields {}.
  • Nothing writes undefined or an empty husk. saveCustomNames (ConfigurationMAVLinkView.vue:315-320) rebuilds the whole object and deletes the vehicle type when its name set is empty.
  • The mapping key is deliberately left alone. The fix reads through to the live action instead of rewriting stored mappings, which is the non-destructive route: no user's saved bindings are touched, and an id that no longer resolves still falls back to the stored label.
Sections with nothing to report (10)

1. Correctness & Implementation Bugs — ✅ (round 3's 1.4 is closed at both sites and the helper widening that carried it is type-correct against the structural { id, name } at vehicleDefaultsImportShared.ts:33-47; every action.name / action?.name read in the tree was re-enumerated for invariant 2 and the four that can hold a mode action all resolve now; the Unknown fallback at common.ts:135 renders the same string the deleted title-caser did; the mini-widget reads mode from the vehicle store as it already did, and the PR neither adds a telemetry read nor an Electron-only call, a widget Options entry or module-scope per-instance state; the one place left that depends on action names being unique, :key="action.name" in the picker list at ConfigurationJoystickView.vue:564, is already breakable without this PR, since a JavaScript action and an HTTP-request action can carry the same user-typed name under different ids)

3. AGENTS.md Adherence — ✅ (no new dependency and no package.json change; the added exports carry complete JSDoc with typed @param/@returns and no empty entries — actionDisplayName at cockpit-actions.ts:147-153, vehicleModeActionName at common.ts:111-117, renameModeActions at common.ts:205-208, flightModeName at mode-names.ts:101-107; the resolver went into the module that owns the action registry rather than into any of the four views that call it; every added export has a call site in this PR, so nothing is groundwork; the only edit outside the stated purpose is still the no-bottom-divider move that ExpansiblePanel.vue:70,113 requires)

4. Security — ✅ (the additions are a name table, one map lookup and one loop that rewrites in-memory labels; no network call, no eval/Function()/v-html, no encoded blob, no environment variable or secret, no change to build scripts, CI workflows, Dockerfiles or src/electron/, no new dependency, and no non-ASCII, zero-width or bidirectional characters in the added identifiers or strings)

5. Performance — ✅ (actionDisplayName is a single object lookup, added to the joystick page's render path, to updateLabelsState — SVG load, mapping watch and a rAF-throttled resize, not per frame — and to two dialogs built on open; renameModeActions walks the ~74 registered modes of the four vehicle types on store creation and on each rename or BlueOS sync, and touches nothing on mavlink:onIncomingMessage, dataLake:setVariable or notifyListeners; the store-level watch needs no teardown, having the store's own lifetime like its neighbours in mainVehicle.ts; modeOptions turns a per-render modesAvailable() call into a cached computed)

6. UI / UX — ✅ (round 2's 6.1 and 6.2 stay closed and the panel's markup is unchanged this round: theme="dark" on both the v-select and each v-text-field, px-2 pt-3 pb-4 matching the siblings, one column on phones via interfaceStore.isOnPhoneScreen, sentence-case title, label and buttons, the form-wide reset sitting beside the content it governs rather than in a footer, logUserAction on the picker and on both write paths in the past tense with the vehicle humanized, and no dialog, icon-only control, glass layer or z-index added; with 1.4 closed, the import diff and the suggestion dialog now read the same name as the picker beside them, so no in-app surface disagrees about what a mode is called)

7. Code Quality & Style — ✅ (complexity-report.json reports 0 triggered functions out of 355 measured across the 12 changed files, not truncated, so nothing crossed the complexity-12 or depth-4 thresholds; no any added, no scoped CSS, no existing comment reworded while its code stayed put — the TODO at common.ts:106 is carried through unchanged; the in-place action.name mutation in renameModeActions follows in-tree precedent at mavlink-manual-control.ts:673; the two x ? f(x) : fallback forms added this round replace x?.name ?? fallback only because the helper needs a defined argument, which is not the x && x.y shape AGENTS.md rejects; the added imports keep simple-import-sort order in all seven touched files and every added line is inside the 180-column limit)

8. Commit Hygiene — ✅ (two commits, 6475750 modes: name the vehicle modes as ArduPilot does and c7f2c32 modes: let the user rename the vehicle modes — the same split round 1's 8.1 asked for, and this round's fixes were autosquashed into the first, where the helper lives, rather than appended as an "address review" commit, so no fixup!/wip subject survives and neither commit reverts or reimplements the other; the modes: area prefix is the form this history uses most and describes both; neither message references an issue or a PR, Closes #1167 correctly staying in the PR body; neither is oversized)

9. Tests — ✅ (the PR still only adds src/tests/libs/vehicle/mode-names.test.ts; the third case exercises actionDisplayName against a mapping carrying a stale name and then through a rename, and restores the module-level registry with renameModeActions({}) before it ends, so it cannot leak into the two cases above it; no existing test was removed, skipped or loosened, and the table-vs-enum assertion was re-derived by hand against types/modes.ts this round)

10. Documentation — ✅ (the feature behaves identically in Lite and Standalone — no window.electronAPI or electron-* use on any added path — so the README.md capability table needs no row; the added public functions carry complete JSDoc, and the PR body's joystick paragraph now matches what the code does at all four display sites)

11. Nitpicks / Optional — ✅ (round 1's 11.1 stays closed; nothing new to add)

Generated by Claude. This is advisory; a human reviewer must still approve.

Cockpit showed the raw mode name the vehicle reports, so an ArduSub pilot read
ALT_HOLD for what ArduPilot itself calls depth hold, and SURFTRAK for the mode
that holds a distance from the bottom. The joystick action list had a third
spelling, since it title-cased those same names on its own.

Name each mode after the one ArduPilot uses in its parameter metadata (FLTMODE1
for Copter and Plane, MODE1 for Rover, and the joystick button functions for
Sub, the only place it names the Sub modes), spelling out the ones ArduPilot
writes as a single word, like AltHold or QRTL. Use those names on the mode
selector, on the mode change alert and on the joystick mode actions. Modes and
vehicles with no name there keep the name the vehicle reported.
Terminology is a matter of taste and of the team flying the vehicle, so the
MAVLink settings carry a table where each mode can be renamed, per vehicle type,
with the ArduPilot name as the value to go back to. The names live in
cockpit-custom-flight-mode-names, so they sync with the rest of the settings.
@rafaellehmkuhl
rafaellehmkuhl force-pushed the issue-1167-human-readable-flight-modes branch from c7f2c32 to f81fbf4 Compare August 24, 2026 22:19
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.

Flight mode selector should support human-readable names

1 participant