Rework the CONNECTIONS section

- Label each connection with its tailnet and account rather than the
  nickname alone. `tailscale switch --list --json` carries both, and on a
  multi-tailnet setup the nickname is the least useful field — "andre"
  says nothing about which tailnet you would be switching to. Renders as
  "headscale.pertento.ai (andre)", falling back through tailnet, then
  account, then profile id when a field is missing.

- Mark the active connection with colour instead of a filled background:
  brand yellow on its label, currentFill cleared. The theme sets
  selected-border-width to 0, so clearing the fill leaves nothing drawn.
  Hover still highlights through `fill`, so the row keeps its affordance.
  Exit-node and Mullvad-region rows are untouched and still fill.

- Sort the active connection to the top. Done in parseAccounts rather
  than at render time: the keyboard cursor indexes into
  tailscale.accounts, so sorting only in the view would leave j/k acting
  on a different row than the highlighted one. Array.sort is stable, so
  the remaining profiles keep the CLI's order.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 22:17:41 +01:00
co-authored by Claude Opus 5
parent ad997bd6b1
commit ca341c0e71
2 changed files with 39 additions and 5 deletions
+30 -3
View File
@@ -57,11 +57,25 @@ function osIcon(os) {
return "󰟀"
}
// Stock returned the nickname alone, which on a multi-tailnet setup is the
// least useful field — "andre" and "pastilhas" say nothing about which tailnet
// you would be switching to. `tailscale switch --list --json` carries both, so
// show the tailnet with the account in parentheses:
//
// headscale.pertento.ai (andre)
// offscale.pastilhas.dev (pastilhas)
//
// Falls back through the same chain as before when a field is absent, so a
// profile with no tailnet still renders something meaningful.
function accountLabel(account) {
if (!account) return "Unknown account"
if (account.nickname) return String(account.nickname)
if (account.tailnet) return String(account.tailnet)
if (account.account) return String(account.account)
var tailnet = String(account.tailnet || "").trim()
var who = String(account.account || account.nickname || "").trim()
if (tailnet !== "" && who !== "") return tailnet + " (" + who + ")"
if (tailnet !== "") return tailnet
if (who !== "") return who
return String(account.id || "Unknown account")
}
@@ -292,6 +306,19 @@ function parseAccounts(raw) {
if (account.selected === true) selected = account
}
}
// Active connection first, everything else in the order the CLI listed it.
// Sorted here rather than in the view so the Repeater and the keyboard
// cursor (accountIndex) index into the same ordering — sorting only at
// render time would leave j/k selecting a different row than the one
// highlighted. Array.sort is stable, so the unselected profiles keep their
// relative order.
next.sort(function(left, right) {
var l = left.selected === true ? 0 : 1
var r = right.selected === true ? 0 : 1
return l - r
})
return {
accounts: next,
selectedAccountId: selected ? String(selected.id || "") : "",
+9 -2
View File
@@ -854,7 +854,12 @@ Panel {
current: selectedAccount
foreground: root.foreground
fill: root.hoverFill
currentFill: root.selectedFill
// No filled background on the active connection — it reads as the brand
// yellow on its label instead (see the Text below). `current` also selects
// the "selected" border spec, but the theme sets selected-border-width to
// 0, so clearing the fill leaves nothing drawn. Hover still highlights
// through `fill`, so the row keeps its affordance.
currentFill: "transparent"
implicitHeight: accountInner.implicitHeight + Style.spacing.xl
@@ -888,7 +893,9 @@ Panel {
Text {
text: accountRow.accountText
color: root.foreground
// The active connection is marked by colour rather than a filled
// background: brand yellow here, themed white for the others.
color: accountRow.selectedAccount ? root.accent : root.foreground
font.family: root.fontFamily
font.pixelSize: Style.font.body
font.bold: accountRow.selectedAccount