Show which Claude account is logged in

"Claude Code / MAX 20X" says nothing about which account the plan belongs
to, which matters when switching between a personal and a company login.

- Read oauthAccount.emailAddress from ~/.claude.json. Nothing in the shell's
  own data carries account identity — the usage JSON under
  ~/.local/state/omarchy/agents/usage/claude.json has limits and token counts
  but no email — so it comes from Claude Code's own config. The FileView is
  watched, so switching accounts updates without restarting the shell.

- Append it to the hero meta line, after the tier: "MAX 20X · you@example.com".

- Put the local part in the bar beside the icon, so the account is visible
  without opening the panel. A WidgetButton rather than a Text with a
  MouseArea: WidgetButton registers itself in bar.clickTargets, and while a
  panel is open KeyboardPanel's full-screen overlay forwards bar-strip clicks
  only to registered targets — an unregistered handler opens the panel and
  then dismisses it with the same click. Mirrors the icon's three buttons.

Only the Claude provider gets either; codex and fireworks store their auth
elsewhere and would need their own readers rather than showing a misleading
email.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 17:33:00 +01:00
co-authored by Claude Opus 5
parent f3958bba4b
commit a52ef3c46c
+97 -17
View File
@@ -7,8 +7,8 @@ import qs.Ui
Panel {
id: root
moduleName: "omarchy.agents"
ipcTarget: "omarchy.agents"
moduleName: "pastilhas.agents"
ipcTarget: "pastilhas.agents"
manageIpc: false
readonly property color foreground: bar ? bar.foreground : Color.foreground
@@ -178,12 +178,52 @@ Panel {
// The plan you pay for, under the name of the tool it pays for. Limits live
// in their own section; the hero just says what this is.
// Email of the account Claude Code is currently logged in as, read from
// ~/.claude.json (oauthAccount.emailAddress). Nothing in the shell's own data
// carries it — the usage JSON under
// ~/.local/state/omarchy/agents/usage/claude.json has limits and token counts
// but no account identity — so it comes straight from Claude Code's config.
//
// Watched, so switching accounts updates the panel without a shell restart.
// Only meaningful for the Claude provider; codex and fireworks store their
// auth elsewhere and get nothing appended.
property string claudeAccountEmail: ""
FileView {
path: Quickshell.env("HOME") + "/.claude.json"
watchChanges: true
printErrors: false
onFileChanged: reload()
onLoadFailed: root.claudeAccountEmail = ""
onLoaded: {
try {
var parsed = JSON.parse(text())
var account = parsed && parsed.oauthAccount ? parsed.oauthAccount : {}
root.claudeAccountEmail = String(account.emailAddress || "")
} catch (e) {
root.claudeAccountEmail = ""
}
}
}
function heroMeta(p) {
if (!p) return ""
if (String(p.usageStatusText || "") !== "") return p.usageStatusText
var tier = String(p.tierLabel || "")
if (tier === "") return "Subscription"
return tier.charAt(0).toUpperCase() + tier.slice(1)
var base = ""
if (String(p.usageStatusText || "") !== "") {
base = p.usageStatusText
} else {
var tier = String(p.tierLabel || "")
base = tier === "" ? "Subscription" : tier.charAt(0).toUpperCase() + tier.slice(1)
}
// Which account the plan belongs to matters when you switch between a
// personal and a company login, and "MAX 20X" alone doesn't say.
if (String(p.providerId || "") === "claude" && root.claudeAccountEmail !== "") {
return base + " · " + root.claudeAccountEmail
}
return base
}
// Local calendar date, recomputed from nowMs so a panel left open across
@@ -298,8 +338,17 @@ Panel {
// is invisible, so the icon appears the moment the first scan finds usage and
// stays away entirely on a machine that has never run either CLI.
visible: providers.length > 0
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight
implicitWidth: barContent.implicitWidth
implicitHeight: barContent.implicitHeight
// Local part of the logged-in Claude account, for the bar. Switching between a
// personal and a company login is otherwise invisible until the panel is
// opened. Empty for the other providers, which store their auth elsewhere.
readonly property string accountShortName: {
if (!provider || String(provider.providerId || "") !== "claude") return ""
var at = claudeAccountEmail.indexOf("@")
return at > 0 ? claudeAccountEmail.substring(0, at) : claudeAccountEmail
}
onProviderIndexChanged: if (panelFlick) panelFlick.contentY = 0
onOpenedChanged: if (opened) {
@@ -335,16 +384,47 @@ Panel {
function next(): string { root.selectProvider(root.providerIndex + 1); return "ok" }
}
BarIconButton {
id: button
// Account label beside the icon, matching the tailnet label on the Offscale
// widget. BarIconButton is icon-only by design (labelVisible: false,
// fixedWidth: slotSize, and its `text` feeds a glyph rather than a label), so
// the name sits next to it in a Row rather than inside it.
Row {
id: barContent
anchors.fill: parent
bar: root.bar
text: "󱚣"
active: root.alarming
onPressed: function(buttonCode) {
if (buttonCode === Qt.RightButton) root.launchAgent()
else if (buttonCode === Qt.MiddleButton) root.selectProvider(root.providerIndex + 1)
else root.toggle()
spacing: Style.space(4)
BarIconButton {
id: button
anchors.verticalCenter: parent.verticalCenter
bar: root.bar
text: "󱚣"
active: root.alarming
onPressed: function(buttonCode) {
if (buttonCode === Qt.RightButton) root.launchAgent()
else if (buttonCode === Qt.MiddleButton) root.selectProvider(root.providerIndex + 1)
else root.toggle()
}
}
// A WidgetButton, not a Text with a MouseArea: WidgetButton registers itself
// in bar.clickTargets, and while a panel is open KeyboardPanel's full-screen
// overlay forwards bar-strip clicks only to registered targets. An
// unregistered handler would open the panel and have the same click dismiss
// it. Mirrors the icon's three buttons so the widget reads as one target.
WidgetButton {
id: accountLabel
visible: text !== ""
bar: root.bar
text: root.accountShortName
// A step below the icon font: it is a label, not a glyph, and the bar is
// crowded.
fontSize: Style.font.bodySmall
anchors.verticalCenter: parent.verticalCenter
onPressed: function(buttonCode) {
if (buttonCode === Qt.RightButton) root.launchAgent()
else if (buttonCode === Qt.MiddleButton) root.selectProvider(root.providerIndex + 1)
else root.toggle()
}
}
}