3 Commits
Author SHA1 Message Date
pastilhasandClaude Opus 5 175595e585 Bracket the widget with whitespace so it groups with Offscale
Adds a trailing spacer to match the leading one, so Offscale + agents read
as their own group rather than running into the bluetooth/network/audio run.

Bar sections lay out with spacing: 0, so this can only come from slack the
widget reserves itself. Bluetooth already contributes ~11px of leading slot
slack; the spacer adds to that, taking the gap from ~13px to ~31px.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 18:07:29 +01:00
pastilhasandClaude Opus 5 0b520d97ac Size the bar icon to match, and close the gap to the label
Three separate contributors, only one of which was the obvious one:

- The glyph rendered at Style.bar.iconFont (26px at base-size 24), noticeably
  smaller than the Offscale mark beside it. Now space(20) = 40px, with the
  canvas sized to hold it.

- The label sat ~19px from the icon. Most of that was not the Row spacing but
  WidgetButton's own horizontalMargin, which defaults to 8.5 and lands at
  ~17px per side at this scale. Trimming the slot and the spacing barely moved
  it until that came down too.

- Trimming the slot flush to the glyph left nothing between this icon and the
  Offscale mark to its left: bar sections lay out with spacing: 0, so
  separation only ever comes from slack each widget reserves. Hence an
  explicit leading spacer rather than relying on the section.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 18:01:17 +01:00
pastilhasandClaude Opus 5 a52ef3c46c 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>
2026-08-17 17:33:00 +01:00
+133 -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,83 @@ 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(1)
// Bar sections lay their widgets out with spacing: 0, so separation between
// neighbouring widgets comes from whatever slack each one reserves. The
// slot is trimmed tight to the glyph below, which leaves nothing between
// this icon and the Offscale mark to its left — hence an explicit spacer.
Item {
width: Style.space(7)
height: 1
anchors.verticalCenter: parent.verticalCenter
}
BarIconButton {
id: button
anchors.verticalCenter: parent.verticalCenter
bar: root.bar
text: "󱚣"
active: root.alarming
// Stock draws this glyph at Style.bar.iconFont (26px at base-size 24)
// inside a Style.bar.iconCanvas box (32px), in a Style.bar.iconSlot of
// reserved width (54px). That left the mark small next to the Offscale
// logo, and ~11px of dead slot either side of it — which read as the
// label floating away from the icon.
//
// Sized to match the Offscale mark (32px), with the canvas just big
// enough to hold it and the slot trimmed to the canvas so the label sits
// right beside the glyph rather than beyond its padding.
fontSize: Style.space(20)
opticalSize: Style.space(21)
slotSize: Style.space(21)
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
// WidgetButton defaults horizontalMargin to 8.5, which at this scale is
// ~17px of padding on each side — that, not the Row spacing, was what
// held the label away from the icon.
horizontalMargin: 1
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()
}
}
// Trailing counterpart to the leading spacer, so Offscale + agents read as
// their own group rather than running into the bluetooth/network/audio run.
// Bluetooth already contributes ~11px of leading slot slack; this adds to
// it rather than replacing it.
Item {
width: Style.space(9)
height: 1
anchors.verticalCenter: parent.verticalCenter
}
}