cmux NEXT: pick each machine's color (appearance.machineColors)

Merged
TE
13d ago
+1094-39

Every machine mark draws a color hashed from the machine's name, and two machines can land on the same or a disliked hue. Now Settings > Accounts > Machines shows a color well on each machine's row: pick a color and that machine's sidebar rows, tab badges and the machine icon on workspace hover cards take it; Automatic removes the pick. Nothing changes until someone picks a color.

Storage. One string_map setting, appearance.machineColors, maps a machine's color key (its name trimmed and lowercased, the key MachineColor already hashes) to #RRGGBB or to one of the eight accent names MachineColor picks from (red … blue), which follow the theme. The card writes hex; the accent names are for cmux.json. It reuses the existing string_map kind, so the daemon's validator and the MDM export need no new kind. A bad entry is dropped with a diagnostic at its key and the rest still apply. Agents may set it (looks only, like appearance.accent).

One resolver. MachineColor.rgb(tokens:overrides:) returns the pick from MachineColorOverrides, else the hash. MachineColor.color reads the picks from ThemeStore.shared.machineColors. ThemeCoordinator feeds the store from the setting, and the store repaints when the picks change. Sidebar rows (SidebarMachineMark) and tab badges already go through MachineColor.color, so neither call site changed its fallback.

Tab badges keep today's look. A remote tab's machine badge stays textTertiary unless its machine has a pick; with a pick it draws the pick (MachineColor.pickedColor, the same resolver). Nothing changes by default.

Hover cards keep today's look. The workspace hover card's machine row tints its icon textSecondary unless the machine has a pick; with a pick the icon draws the pick (HoverCardRowContent.iconMachine, resolved through MachineColor.pickedColor in the row's theme scope). The connection-status dot keeps its own tone.

Settings page. The host lists' machine rows now carry color (the color its marks draw in now) and color_key. The Machines card draws the row; SectionView drops the plain map row for it, as it does for the Chief pet's rows. The well commits on the native change event and keeps other machines' picks. If cmux.json spells a machine another way ("Big-Red "), that entry is replaced, not duplicated.

Not in this PR (follow-ups):

  • Coloring tab badges by default with the automatic machine color is a separate call for the machine-identity work; this PR leaves the default badge grey.
  • The agent pane's machineColor.ts (composer chip, environment picker, inspector) still uses only the hash. The pane gets no settings value today except the agentPane.display.* keys over its own display event. Applying the picks there needs that event, or a new one, to carry appearance.machineColors. I didn't build a new bridge for it.
  • Settings lists SSH and Cloud machines only, as before. A paired server's color can be set in cmux.json under its name.

Changelog

  • Added: Pick a color for each machine in Settings > Accounts > Machines; its sidebar rows, tab badges and hover card machine icon use it.

Testing

  • MachineColorTests (CmuxNextDesign): a picked hex wins, a picked accent follows the theme, an unset machine keeps its automatic color, keys normalize both ways, values that aren't colors are ignored, and the store repaints only on a change.
  • MachineColorsSettingTests (CmuxNextSettings): parse, per-key diagnostics, a non-object value, and the row's kind, section, agent policy and page home.
  • TabMachineBadgeColorTests (CmuxNextAppTests, read from the strip as drawn): with no pick, or a pick for another machine, the badge is textTertiary; with a pick it draws the pick.
  • HoverCardMachineColorTests (CmuxNextAppTests, read from the card as drawn): with no pick, or a pick for another machine, the machine row's icon is textSecondary; with a pick it draws the pick; the connection dot stays .dot(.success) either way.
  • hostSections.test.tsx: the well shows the host's color, a pick writes {…, "build-mac": "#12ab34"} and keeps big-red's pick, Automatic removes only that key, and no plain map row is drawn.

Observed / Not run

  • Fleet app compile at faf389b6b7c: passed (job 98648427c8c25a2f5f804817). At 6c6f7b9cfb6: passed (job d7018e18fac3d3481cc096f6). At 681a1b3862c: passed (job a9a6bf76fdacb6ec3b51cdfc). At 796d390e0fd (feat-cmux-next merged in): passed (job eba7670ad6ea1eb2bbcb0219). At the head 0ae41e3ad24: passed (job 92c3ca34fad937b065cca26c).
  • Settings exports regenerated on the fleet (step ec44103a1e072352675629b3 at faf389b6b7c). ManagedPreferencesManifestTests and SettingsSchemaExportTests passed there. The defaults snapshot adds setting.appearance.machineColors: {} (a new key; no existing default changes).
  • Exports at 796d390e0fd: the fleet export step (cd79b055) waited over 2 hours for a worker, so appearance.machineColors was carried onto feat-cmux-next's exports with three-way text merges (merge base 5363032df88); the JSON and plist parse. feat-cmux-next's own exports are already stale against its sources, so defaults stamp / The settings exports match their sources are red at base f58123484705 too (defaults_guard.py check-stamp exits 1 there); the PR autofix regenerates them.
  • Live repaint: the Codex review at 796d390e0fd found an open hover card's icon kept its old color after a pick (the store's repaint runs view appearance hooks, not the panel's scope responders). d3d6e119ca2 adds aPickWhileTheCardIsOpenRecolorsTheIcon; 0ae41e3ad24 recolors the row in viewDidChangeEffectiveAppearance.
  • Base update at c410c8fb91d: feat-cmux-next 51b295b866e merged in. The Codex review at 891b56609ae (an earlier base merge, conflict in WorkspaceHoverCardContent.swift resolved to base's row plus the pick) found the hand-merged settings exports had dropped rows base added since (Sidebar Style, Needs You, Other Apps, widget ordering). The exports and settings-schema.sources.sha256 are now base's files plus only the appearance.machineColors entries (diff against base adds lines, deletes none); the PR autofix regenerates them. The CmuxNextSettings string catalog is base's text plus the three new keys.
  • editors.test.tsx: react-apps-check at the earlier head failed the accounts editors test (every row on a page needs an editor); 891b56609ae teaches it that appearance.machineColors draws as the Machines card's color wells.
  • Fleet app compile at 891b56609ae: passed (job 710b6059fa88930ed7b42faa). At the head c410c8fb91d: passed (job 80f7b07459b9696c499a5eb4).
  • Static checks: prepush-gate.py (no new violations at the head), check-l10n.sh (0 errors, 21 locales), localization_catalog.py check, and the What's New digest of the new highlight.
  • The first red runs at 7363594ecbf (steps e627224dac794676b6171e10, a0deec311c9d762738fc516b) never ran the tests: their base f1d4be73d5d failed to build the web bundles (the ContextSection.css red fixed by #23935). The red is rerun on cmux/machine-color-override-red (23ffbf61b5a = the test commit merged with feat-cmux-next 5363032df88, no fix).
  • Queued on the fleet, results to follow here:

No capture: the color well is on a webview Settings page (the gallery's machines states now carry a color), and the sidebar rows, tab badges and hover cards look as before until someone picks a color. Not run: a live app check of the repaint after a pick.

Agent session: Air-Blue

🤖 Generated with Claude Code