Reorder the agents in the panel, count Grok's tokens, and install Grok through mise (#14004)

* Let the agents in the panel be put in any order

Drag an agent by its mark to move its section; the header it will land
on lights up, and the move happens on release. Each agent's header is
now a keyboard stop with its own highlight, and Ctrl+Up/Down moves the
agent the cursor is in. The order is kept in agents/order.json beside
the usage records. The key catcher turns Ctrl+Up/Down into a reorder
only for panels that opt in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Light only an agent's mark when the cursor or a drag is on it

The mark is the handle the agent moves by, so the keyboard cursor and
the drop spot while dragging box it alone rather than the header line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep a lit agent mark's box from being clipped at the panel's edge

The box overhangs the content's left edge, which the scrolling area
clipped. The scrolling area now reaches a little into the panel's
padding with the content shifted back, so nothing moves and the box
draws whole.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Reuse a Grok session scan only on the day it was made

A limits-only refresh just after midnight reused a scan from the evening
before, which counted yesterday's sessions as today's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep Ctrl+Up/Down from moving an agent when the cursor is outside one

The hero's buttons and the starter tiles carry indices too, and the
lookup read them as accounts, so with several accounts Ctrl+Up/Down on
one of them moved an unrelated agent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Report a failed mise update even after the Grok upkeep runs

The Grok block ran after `mise up` and its last command set the script's
status, so a failed tool update could read as a success to callers that
warn about it. The update's own status is now the script's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Unpack a Grok update into ~/.grok whatever GROK_HOME says

The upkeep replaces ~/.grok's link, but the npm launcher unpacks into
GROK_HOME when it's set, so with a custom home the link never came back
at the new release and the old one was restored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Hold off reordering agents while an account name is being edited

Ctrl+Up/Down reached the key catcher during an inline rename, and moving
the agent rebuilt its section, dropping the unfinished name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Install Grok through mise's first-party package

The npm launcher keeps an old ~/.grok/bin binary because mise skips its
postinstall. Use mise's grok tool, and drop the npm tool so its shim
does not stay ahead of the stub.

* Drop the npm Grok workarounds now that mise installs Grok itself

With Grok installed through mise's first-party package, the CLI is the
binary mise manages, so the update upkeep that repointed ~/.grok/bin and
the shared bin directory for added Grok accounts have nothing left to do.
omarchy-update-mise is back to running mise up and nothing else, and
adding a first Grok account installs the same package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Count Grok's tokens and prompts from its usage ledger

Each Grok session keeps a usage.json, the ledger `grok usage` prints, with
every finished turn's end time and tokens by model. The collector now
reads it for tokens today, by day for the last week, and by model, with
cached input kept apart, and counts today's prompts by the turns that
ended today. Found in #12352's research.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Count cache writes in Grok's daily token totals

Grok's totalTokens leaves cache writes out while the per-model buckets
count them, so a turn with cache writes added less to its day than to
its model. The day's total is now the sum of those same buckets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Remove the npm launcher's old Grok binaries when moving to mise's Grok

Omarchy's npm wrapper ran the binary the launcher unpacked into
~/.grok/bin, where x.ai's installer also puts a copy with a PATH entry
ahead of mise. Once the wrapper is replaced, a binary left there would
keep shadowing the mise tool, so the migration removes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Jesse Miller <jmiller@jmiller.com>
This commit is contained in:
authored and GitHub committed 2026-10-02 03:34:05 +02:00
1 parent f45461a38f
commit 821ae58905
17 files changed
+446 -120

No files matched your search

+46 -1
View File
@@ -226,7 +226,52 @@ Item {
var syncedDisplay = displayProvider({ id: syncedId, name: stats.providerName || syncedId })
if (providerHasData(syncedDisplay)) result.push(syncedDisplay)
}
return result
return orderedProviders(result)
}
// ----------------------------------------------------------------- order
//
// The panel's agents in the order someone dragged them into, kept beside the
// usage records. An agent the order doesn't name yet keeps its place after
// the ones it does.
readonly property string orderPath: (Quickshell.env("XDG_STATE_HOME") || home + "/.local/state") + "/omarchy/agents/order.json"
property var providerOrder: []
function orderedProviders(list) {
var order = providerOrder
var rank = function(p) {
var i = order.indexOf(p.providerId)
return i < 0 ? order.length : i
}
return list.slice().sort(function(a, b) { return rank(a) - rank(b) })
}
// Moves one agent to a position among the ones shown, and saves the result.
function moveProvider(id, to) {
var ids = enabledProviders.map(function(p) { return p.providerId })
var from = ids.indexOf(id)
if (from < 0) return
to = Math.max(0, Math.min(ids.length - 1, to))
if (from === to) return
ids.splice(to, 0, ids.splice(from, 1)[0])
for (var i = 0; i < providerOrder.length; i++)
if (ids.indexOf(providerOrder[i]) < 0) ids.push(providerOrder[i])
providerOrder = ids
orderFile.setText(JSON.stringify(ids) + "\n")
}
FileView {
id: orderFile
path: root.orderPath
watchChanges: true
atomicWrites: true
printErrors: false
onLoaded: {
var parsed = []
try { parsed = JSON.parse(text()) } catch (e) { parsed = [] }
root.providerOrder = Array.isArray(parsed) ? parsed.map(String) : []
}
onFileChanged: reload()
}
function providerEnabled(id) {
+97 -3
View File
@@ -46,8 +46,8 @@ Panel {
// The keyboard walks everything on the page that does something, in
// reading order, one row at a time: the hero's buttons, then each agent's
// Sign-in required link or switchable accounts, and the starter tiles, or
// the agents to add while picking one. Up and down change rows, left and right move along one.
// header, its Sign-in required link or switchable accounts, and the starter
// tiles, or the agents to add while picking one. Up and down change rows, left and right move along one.
// Hovering moves the same cursor, so only one thing is lit.
readonly property var keyRows: {
var rows = []
@@ -66,6 +66,8 @@ Panel {
// fix is skipped.
var entry = 0
for (var p = 0; p < providers.length; p++) {
// Every agent's header is a stop, so Ctrl+Up/Down can move it.
rows.push([{ kind: "provider", index: p }])
var accounts = providerAccounts(providers[p])
if (accounts.length < 2) {
if (needsSignIn(providers[p])) rows.push([{ kind: "providerSignin", index: p }])
@@ -135,6 +137,56 @@ Panel {
}
}
// The agent the cursor is in, by its position on the page.
function providerIndexOfKey() {
var target = keyTarget
if (!target) return -1
if (target.kind === "provider" || target.kind === "providerSignin") return target.index
if (["account", "autoswitch", "signin"].indexOf(target.kind) < 0) return -1
var entry = accountEntries[target.index]
return entry ? providers.indexOf(entry.provider) : -1
}
// Ctrl+Up/Down carries the agent the cursor is in up or down the page, and
// the cursor along with it.
function reorderProvider(dy) {
var from = providerIndexOfKey()
if (from < 0) return
var to = clamp(from + dy, 0, providers.length - 1)
if (to === from) return
usage.moveProvider(providers[from].providerId, to)
Qt.callLater(function() { pointAt("provider", to) })
}
// Dragging an agent by its mark lights the header it would land on, and
// moves it there on release: the sections are rebuilt when the order
// changes, which would drop a drag still in progress.
property string dragProviderId: ""
property int dragTarget: -1
// An account name being edited keeps Ctrl+Up/Down: moving its agent would
// rebuild the section and drop the unfinished name.
property bool renaming: false
function dragProviderOver(y) {
for (var i = 0; i < providerSections.count; i++) {
var item = providerSections.itemAt(i)
if (item && y >= item.y && y < item.y + item.height) {
dragTarget = i
return
}
}
}
function dropProvider() {
var id = dragProviderId
var to = dragTarget
dragProviderId = ""
dragTarget = -1
if (id === "" || to < 0) return
usage.moveProvider(id, to)
Qt.callLater(function() { pointAt("provider", to) })
}
// Keeps whatever the cursor lands on inside the scrolled view.
function revealItem(item) {
if (!item || !panelFlick || !panelFlick.interactive) return
@@ -827,6 +879,8 @@ Panel {
PanelKeyCatcher {
id: keyCatcher
anchors.fill: parent
reorderable: root.addStage === "" && !root.renaming
onReorderRequested: function(dy) { root.reorderProvider(dy) }
onMoveRequested: function(dx, dy) {
// Naming and signing in have their own fields; there the arrows scroll.
@@ -849,7 +903,12 @@ Panel {
Flickable {
id: panelFlick
// Reaches a little into the panel's padding on the left, with the
// content shifted back, so the box around a lit agent mark isn't
// clipped where it overhangs the content's edge.
readonly property real overhang: Style.space(8)
anchors.fill: parent
anchors.leftMargin: -overhang
contentWidth: width
contentHeight: column.implicitHeight
clip: true
@@ -863,9 +922,10 @@ Panel {
Column {
id: column
x: panelFlick.overhang
// When the panel scrolls, the bar gets its own strip rather than
// sitting on top of the right-aligned numbers.
width: panelFlick.width - (panelFlick.interactive ? panelScroll.width + Style.space(6) : 0)
width: panelFlick.width - panelFlick.overhang - (panelFlick.interactive ? panelScroll.width + Style.space(6) : 0)
spacing: Style.space(16)
// ---------- Hero: agents · rotating summary · add ----------
@@ -920,6 +980,7 @@ Panel {
}
Repeater {
id: providerSections
model: root.addStage === "" ? root.providers : []
ProviderSection {
@@ -1186,18 +1247,50 @@ Panel {
readonly property var windows: root.displayWindows(provider)
readonly property var balance: provider ? (provider.balance || null) : null
spacing: Style.space(16)
opacity: root.dragProviderId !== "" && provider && root.dragProviderId === provider.providerId ? 0.5 : 1.0
PanelSeparator { foreground: root.foreground }
Item {
id: sectionHead
width: parent.width
implicitHeight: Math.max(sectionMark.height, sectionName.implicitHeight)
// Lit for the keyboard, and as the drop spot while an agent is dragged.
readonly property bool lit: root.dragProviderId !== "" ? root.dragTarget === section.providerIndex : root.hasKey("provider", section.providerIndex)
onLitChanged: if (lit && root.dragProviderId === "") root.revealItem(sectionHead)
// Only the mark is lit: it's the handle the agent moves by.
CursorSurface {
anchors.fill: sectionMark
anchors.margins: -Style.space(5)
z: -1
hasCursor: sectionHead.lit
foreground: root.foreground
}
ProviderIcon {
id: sectionMark
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
provider: section.provider
// The mark is the handle for dragging the agent up or down the page.
MouseArea {
anchors.fill: parent
anchors.margins: -Style.space(4)
hoverEnabled: true
preventStealing: true
cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor
onPressed: {
root.dragProviderId = section.provider ? section.provider.providerId : ""
root.dragTarget = section.providerIndex
}
onPositionChanged: function(mouse) {
if (pressed) root.dragProviderOver(mapToItem(column, mouse.x, mouse.y).y)
}
onReleased: root.dropProvider()
onCanceled: { root.dragProviderId = ""; root.dragTarget = -1 }
}
}
Text {
@@ -1552,6 +1645,7 @@ Panel {
readonly property string label: renamedTo !== "" ? renamedTo : String(account.label || account.id || "")
onAccountChanged: renamedTo = ""
onEditingChanged: root.renaming = editing
onPickedChanged: if (picked) root.revealItem(head)
// Anywhere on the line counts, so Use can show up when it's hidden.
+7 -4
View File
@@ -72,7 +72,7 @@ light surfaces — and the bar glyph stands in when there is none.
|---|---|---|
| `claude` | Anthropic's OAuth usage endpoint (5-hour session + 7-day weekly) | `~/.claude/projects` transcripts, opencode sessions on an Anthropic provider, plus `stats-cache.json` and `history.jsonl` as fallback |
| `codex` | The Codex app-server RPC | native Codex CLI session files (plus pi and opencode sessions) |
| `grok` | The credits endpoint behind Grok's `/usage` view (the billing period's included usage) | Prompts and sessions from each session's `summary.json` (Grok records no tokens there) |
| `grok` | The credits endpoint behind Grok's `/usage` view (the billing period's included usage) | Each session's `usage.json` (the ledger `grok usage` prints: tokens by model per finished turn), plus `summary.json` for sessions |
| `fireworks` | Estimated prepaid balance: configured funding minus rated account costs | Fireworks billing API, grouped by day and model for the last 30 days |
When `~/.local/state/omarchy/agents/accounts/<claude|codex|grok>.json`
@@ -135,9 +135,12 @@ only adds the meter and the spent-of-funded line under the real figure.
urgent when any account new sessions use is at 90% of a window, or a
prepaid balance is down to its last 10%.
- Panel: the arrows (or `h`/`j`/`k`/`l`) walk a cursor over everything that
does something, row by row: the hero's buttons, each switchable account
(landing on Use, with Autoswitch to its left), and the starter tiles, or the
agents to add. Hovering moves the same cursor. Enter acts on it, or
does something, row by row: the hero's buttons, each agent's header, each
switchable account (landing on Use, with Autoswitch to its left), and the
starter tiles, or the agents to add. Ctrl+Up/Down (or Ctrl+`k`/`j`) moves the
agent the cursor is in up or down the page; dragging an agent by its mark
does the same, lighting the header it will land on. The order is kept in
`~/.local/state/omarchy/agents/order.json`. Hovering moves the same cursor. Enter acts on it, or
refreshes when nothing is lit; `r` refreshes, Tab moves to the neighboring
bar panel, Esc closes.
- Accounts: `1`–`9` jump to an account across every agent, and Enter makes it