Skip to content

Agents: user collectors + Go connection settings card - #11967

Closed
washburnello wants to merge 2 commits into
omacom:quattrofrom
washburnello:agents-go-settings
Closed

washburnello wants to merge 2 commits into
omacom:quattrofrom
washburnello:agents-go-settings

Conversation

@washburnello

Copy link
Copy Markdown

Two commits, one theme: let the agents panel cover accounts Omarchy doesn't ship collectors for, with OpenCode Go limits as the first user.

1. User collectors through the shared update command (bin/omarchy-agent-usage-update, README)

  • Also runs omarchy-agent-usage-* executables from ~/.config/omarchy/agents/collectors/ with the same flags (--force, --limits-only, --except, per-agent filter) and the panel's existing refresh timer — no separate scheduling needed.
  • A collector may print one record or an object of several records keyed by id (one scan feeding many tabs); each fans out to its own usage file, and ids a collector stops emitting are retired via a cache-dir sidecar the panel never reads.

2. Go connection settings card with Test and Apply (shell/plugins/agents/Panel.qml, Main.qml)

  • Gear in the hero, visible only on the opencode-go tab, toggling an inline card under the provider tabs: workspace-URL field (strips the wrk_ ID itself), masked auth-cookie field with one-line hints, Apply + Test buttons driving the shared update command, terse verdicts.
  • Accent dot on the gear replaces the red status box for Go; stale or failed limits dim instead of shouting. Records carry limitsStale so cached-after-failure meters can't pose as fresh.

Compatibility: without a user collector producing an opencode-go record the gear never renders — zero behavior change for stock installs. agents-panel-test.sh and agent-usage-update-test.sh pass; fan-out, stale-id retirement, and forced group updates verified live.

omarchy-agent-usage-update also runs omarchy-agent-usage-* collectors
from ~/.config/omarchy/agents/collectors/, so one refresh timer covers
packaged and user accounts alike. A collector may print one record or
an object of several records keyed by id (one scan feeding many tabs);
each is fanned out to its own usage file, and ids a collector stops
emitting are retired via a cache-dir sidecar the panel never reads.
Gear in the hero (opencode-go tab only) toggles an inline settings
card under the provider tabs: workspace-URL field that strips the
wrk_ ID itself, masked auth-cookie field with one-line hints,
Apply and Test buttons driving the shared update command, and terse
verdicts. An accent dot on the gear replaces the red status box for
Go; stale or failed limits dim instead of shouting.
@omarchybot omarchybot added the enhancement New feature or request label Sep 27, 2026
@llstrk

llstrk commented Sep 28, 2026

Copy link
Copy Markdown

Automated AI review

Community review: Independent automated community review, unaffiliated with the Omarchy team, intended to help prepare PRs for their review.

Verified: with no user collectors, omarchy-agent-usage-update behaves as on the base for the packaged collectors: records are written byte-identically, and --force, --limits-only, --except, the per-agent filter and the exit status all match. Panel.qml loads without QML errors at this head and merged onto current quattro (no conflicts; ShellIpc carried over). Stock tabs keep their status box and full-opacity limits. The existing agent tests pass at this head.

The main open question is the Go settings card, which depends on a collector that is not in this PR. Smaller issues follow in the fan-out sidecar, cookie storage, filters and panel focus.

Go card depends on an undocumented, out-of-tree collector contract

Panel.qml hard-codes a contract that no file in the PR produces or documents:

Hard-coded in Panel.qml Value
Provider id (line 97) opencode-go
Apply/Test target (lines 163, 476) omarchy-agent-usage-update --force opencode
Config file and schema (lines 98, 150) ~/.config/omarchy/agents/opencode-go.json, {workspaceId, authCookie}
Status words read (lines 187-192) expired, not connected, unavailable

The README describes user collectors as omarchy-agent-usage-<group> and names no required group. If the Go collector is named anything other than opencode, Apply and Test run nothing, the updater exits 0, and the card judges the old record. In a synthetic run with a collector named omarchy-agent-usage-go, Apply with a wrong cookie reported "Saved." with undimmed meters; the collector only ran on the next panel refresh, which then showed "Expired cookie.".

Apply -> write opencode-go.json -> update --force opencode
      -> no collector named "opencode" -> exit 0, no record written
      -> after 800 ms, judge the previous record -> "Saved." if it was fresh

It also overlaps with open PRs that add packaged OpenCode collectors:

Those collectors read Go limits from OpenCode's /zen/go/v1/usage endpoint with the API key OpenCode already stores, rather than an opencode.ai auth cookie copied from browser devtools.

Impact: "zero behavior change for stock installs" holds only while no packaged collector emits opencode-go. For user collectors, the card works only when the collector matches an unwritten contract, and a mismatch can show a false success.

Suggested change: ship the Go collector together with the card, or split the PR so the generic user-collector support lands on its own. Document the config file, record fields and status words in the README. Derive the Apply/Test target from the producer sidecar (id to group) or from the record, instead of hard-coding opencode, and report "no collector ran" when nothing matched.

Parallel collectors lose entries in the producer sidecar

fan_out runs inside each backgrounded collect … & (updater line 107) and does an unlocked read, merge and rename of update-producers.json (lines 83-97). When two multi-record user collectors finish together, one group's sidecar update is lost; with two fast synthetic groups starting from an empty sidecar this happened in 60 of 60 runs. An id first emitted in a run whose update is lost is never recorded, so when the group later stops emitting it, its usage file is not retired, even after later runs without contention. Ids already recorded in an earlier run were still retired.

Impact: limited to users with two or more multi-record collectors, but for newly added ids it recreates the frozen-tab problem the sidecar is meant to prevent.

Suggested change: serialize the sidecar step with flock on a lock file next to it, or collect each group's ids and do retirement and the sidecar update once after the wait loop.

Auth cookie is saved world-readable

goConfigFile.setText() goes through Quickshell's FileView and QSaveFile, which creates a new file with mode 0666 minus umask. Under umask 022 the measured result was opencode-go.json at 0644 inside a 0755 directory. The file holds the opencode.ai auth cookie.

Impact: other local users can read the cookie wherever the home directory permissions allow it; 0600 is the usual mode for a credential file.

Suggested change: QSaveFile keeps an existing file's mode (a pre-created 0600 file stayed 0600 after setText, and an existing 0644 file stays 0644). Before setText, create the file with mode 0600 when it is missing and chmod 600 it when it already exists.

Disabling or retrying a fan-out provider targets the wrong name

Main.qml passes provider ids to the updater, as --except <id> for disabled providers (line 152) and as the argument list for retryAdvised reruns (lines 95, 105). wanted() matches collector group names. In a synthetic run, --except opencode-go still ran the opencode group, and --limits-only opencode-go ran nothing and exited 0.

Impact: disabling opencode-go hides the tab but keeps running its collector, with the cookie, on every refresh, which contradicts the README's "Disabled agents are also skipped when the records regenerate". A retryAdvised flag on a fan-out record whose id differs from its group name never triggers a retry.

Suggested change: map ids to groups through the sidecar in wanted(), or document the limitation.

Keyboard focus stays in the hidden Go field after switching tabs

Opening the card focuses the workspace field. Clicking another provider chip hides the card, but focus stays on the now-invisible field. Measured on the real Panel.qml: after switching to the claude tab, the field still had activeFocus, was not visible, and keyCatcher.blocked was true. By source, the panel's h/l/j/k/r shortcuts then go to the hidden field, and the first Escape only closes the hidden card (key presses were not synthesized).

Impact: after a tab switch, by source, panel shortcuts go to an invisible field and a second Escape is needed to close the panel.

Suggested change: close the card, or call keyCatcher.forceActiveFocus(), when isGoProvider becomes false.

Collector names and fan-out ids are not validated

Reproduced with synthetic collectors:

  • A user collector with the same name as a packaged one runs in addition to it; both write <name>.json and the last writer wins. Discover agent usage collectors in ~/.config/omarchy/agents/collectors #12353, which adds the same user collector directory, makes the packaged name win and tests that.
  • grep -qxF "$id" (line 88) reads an id starting with - as an option, so that record is deleted on every run after the first. grep -qxF -- "$id" fixes it.
  • An id such as /../../../../../escape wrote a file outside the usage directory, and retirement later removed it. An id such as a/b fails in mktemp (there is no .a/ directory), but the run still exits 0.
  • A fan-out group that once emitted a packaged id (for example claude) deletes that packaged record when it stops emitting it.
  • Retirement only runs while a group emits a non-empty map: {} is rejected, and a group that switches to a single record never retires its old ids.

Impact: a collector's output can overwrite or delete other records, write or remove files outside the usage directory, or fail to write while the update still exits 0. User collectors are the user's own code, so this matters mainly for collectors that build ids from remote data.

Suggested change: validate ids against a conservative charset (for example [A-Za-z0-9_.-], as safeDeviceId in Main.qml uses), require .[$id].id == $id, skip ids owned by packaged collectors, and propagate write failures to the exit status.

Also noted: FileView writes are asynchronous, and Apply starts the fetch without waiting for onSaved or handling onSaveFailed, so a failed write can still end in "Saved.". Starting the fetch from onSaved would also make the separate mkdir process unnecessary, since FileView creates missing parent directories itself. After a failed Test or Apply, goFetchOk stays false until the next Test or Apply, so the meters stay dimmed and the old failure text stays after a later refresh brings fresh numbers. The panel writes its config under $HOME/.config, while the updater's collector directory honours XDG_CONFIG_HOME. The README does not mention the gear, the card, limitsStale, or that Go replaces the status box with a badge.

Optional test improvement: agent-usage-update-test.sh overrides HOME and XDG_STATE_HOME but not XDG_CONFIG_HOME or XDG_CACHE_HOME. With this PR, running it with XDG_CONFIG_HOME exported executes the caller's real user collectors (a planted collector ran once at this head and never on the base). Setting both variables in the test, and adding cases for fan-out and retirement, would cover the new paths; none of the issues above is caught by the current tests.

Related open PRs:


Review information

Test scope: Source review of the pinned head, plus sandboxed runs: the updater at base and head with synthetic collectors (flag parity, fan-out, retirement, concurrency, name and id edge cases), the existing agent tests at base, head and head merged onto current quattro, and the real Panel.qml loaded under Quickshell's offscreen platform with a stubbed popup window and synthetic Go collectors. Focus and state were measured; nothing was rendered on a desktop and no key events were synthesized. No OpenCode service was contacted and no real cookie was used. Related PRs were read from their diffs, not merged and run.

AI process: Opus 5.5 Medium coordination and synthesis, Opus 5.5 Xhigh technical review and final fact check, GPT 6 Sol Xhigh search for related issues, Opus 5.5 Medium editorial check.

Opt out: To stop receiving these reviews, reply to this comment saying so.

@dhh

dhh commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Thanks! User collector discovery is being considered through #12353 (same idea), and the Go settings card targets the old panel. Closing in favor of #12353.

@dhh dhh closed this Oct 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants