[{"id":"13257358685","type":"PullRequestEvent","actor":{"id":175486,"login":"ilblackdragon","display_login":"ilblackdragon","gravatar_id":"","url":"https://api.github.com/users/ilblackdragon","avatar_url":"https://avatars.githubusercontent.com/u/175486?"},"repo":{"id":1148615527,"name":"nearai/ironclaw","url":"https://api.github.com/repos/nearai/ironclaw"},"payload":{"action":"merged","number":7163,"pull_request":{"url":"https://api.github.com/repos/nearai/ironclaw/pulls/7163","id":4205857119,"number":7163,"head":{"ref":"feat/document-round-trip","sha":"b9efa58aeb46093bd13ef4e859d8455675643dab","repo":{"id":1148615527,"url":"https://api.github.com/repos/nearai/ironclaw","name":"ironclaw"}},"base":{"ref":"main","sha":"5308b83d068186eb2a80126019d6760ae160d376","repo":{"id":1148615527,"url":"https://api.github.com/repos/nearai/ironclaw","name":"ironclaw"}}}},"public":true,"created_at":"2026-08-13T22:25:05Z","org":{"id":29134221,"login":"nearai","gravatar_id":"","url":"https://api.github.com/orgs/nearai","avatar_url":"https://avatars.githubusercontent.com/u/29134221?"}},{"id":"13126532248","type":"PullRequestEvent","actor":{"id":175486,"login":"ilblackdragon","display_login":"ilblackdragon","gravatar_id":"","url":"https://api.github.com/users/ilblackdragon","avatar_url":"https://avatars.githubusercontent.com/u/175486?"},"repo":{"id":1148615527,"name":"nearai/ironclaw","url":"https://api.github.com/repos/nearai/ironclaw"},"payload":{"action":"merged","number":6997,"pull_request":{"url":"https://api.github.com/repos/nearai/ironclaw/pulls/6997","id":4182577113,"number":6997,"head":{"ref":"feat/anthropic-cache-breakpoints","sha":"fd365269dd74064dc34ed8f364c4e2408e26da8d","repo":{"id":1148615527,"url":"https://api.github.com/repos/nearai/ironclaw","name":"ironclaw"}},"base":{"ref":"main","sha":"ed8ac2743e7f1ea89fe37a5050c4747981cce3e4","repo":{"id":1148615527,"url":"https://api.github.com/repos/nearai/ironclaw","name":"ironclaw"}}}},"public":true,"created_at":"2026-08-11T19:50:05Z","org":{"id":29134221,"login":"nearai","gravatar_id":"","url":"https://api.github.com/orgs/nearai","avatar_url":"https://avatars.githubusercontent.com/u/29134221?"}},{"id":"17370616777","type":"PushEvent","actor":{"id":199935,"login":"rigdern","display_login":"rigdern","gravatar_id":"","url":"https://api.github.com/users/rigdern","avatar_url":"https://avatars.githubusercontent.com/u/199935?"},"repo":{"id":909184137,"name":"EducationalGameClub/EducationalGameClub.github.io","url":"https://api.github.com/repos/EducationalGameClub/EducationalGameClub.github.io"},"payload":{"repository_id":909184137,"push_id":39688902794,"ref":"refs/heads/main","head":"775e30641c63edd7919b11677f1eff09c4a28a26","before":"3494569a59e159826ad2e360acec4f2d2705d224"},"public":true,"created_at":"2026-08-11T07:56:47Z","org":{"id":193025738,"login":"EducationalGameClub","gravatar_id":"","url":"https://api.github.com/orgs/EducationalGameClub","avatar_url":"https://avatars.githubusercontent.com/u/193025738?"}},{"id":"17370610327","type":"PushEvent","actor":{"id":199935,"login":"rigdern","display_login":"rigdern","gravatar_id":"","url":"https://api.github.com/users/rigdern","avatar_url":"https://avatars.githubusercontent.com/u/199935?"},"repo":{"id":909184137,"name":"EducationalGameClub/EducationalGameClub.github.io","url":"https://api.github.com/repos/EducationalGameClub/EducationalGameClub.github.io"},"payload":{"repository_id":909184137,"push_id":39688896429,"ref":"refs/heads/gh-pages","head":"f73cc231187d6c565b5b9702a96fd4e967623b4c","before":"3110b1e8d3237681d4c78649961011ec23d33f6f"},"public":true,"created_at":"2026-08-11T07:56:41Z","org":{"id":193025738,"login":"EducationalGameClub","gravatar_id":"","url":"https://api.github.com/orgs/EducationalGameClub","avatar_url":"https://avatars.githubusercontent.com/u/193025738?"}},{"id":"17335245391","type":"PushEvent","actor":{"id":175486,"login":"ilblackdragon","display_login":"ilblackdragon","gravatar_id":"","url":"https://api.github.com/users/ilblackdragon","avatar_url":"https://avatars.githubusercontent.com/u/175486?"},"repo":{"id":1327471515,"name":"nearai/ironwire","url":"https://api.github.com/repos/nearai/ironwire"},"payload":{"repository_id":1327471515,"push_id":39647722381,"ref":"refs/heads/main","head":"0fd64840b7fd9f039237596f7fb8e135e3b709c4","before":"3a58897f930b9d523c7905397e3a66cb447fbd66"},"public":true,"created_at":"2026-08-10T19:53:20Z","org":{"id":29134221,"login":"nearai","gravatar_id":"","url":"https://api.github.com/orgs/nearai","avatar_url":"https://avatars.githubusercontent.com/u/29134221?"}},{"id":"17330767706","type":"PushEvent","actor":{"id":175486,"login":"ilblackdragon","display_login":"ilblackdragon","gravatar_id":"","url":"https://api.github.com/users/ilblackdragon","avatar_url":"https://avatars.githubusercontent.com/u/175486?"},"repo":{"id":1327471515,"name":"nearai/ironwire","url":"https://api.github.com/repos/nearai/ironwire"},"payload":{"repository_id":1327471515,"push_id":39646575784,"ref":"refs/heads/main","head":"3a58897f930b9d523c7905397e3a66cb447fbd66","before":"7bcbaec6cd46dc4aeb236ae7811cd8a906cef7f8"},"public":true,"created_at":"2026-08-10T19:37:50Z","org":{"id":29134221,"login":"nearai","gravatar_id":"","url":"https://api.github.com/orgs/nearai","avatar_url":"https://avatars.githubusercontent.com/u/29134221?"}},{"id":"13055938884","type":"IssueCommentEvent","actor":{"id":175486,"login":"ilblackdragon","display_login":"ilblackdragon","gravatar_id":"","url":"https://api.github.com/users/ilblackdragon","avatar_url":"https://avatars.githubusercontent.com/u/175486?"},"repo":{"id":1327471515,"name":"nearai/ironwire","url":"https://api.github.com/repos/nearai/ironwire"},"payload":{"action":"created","issue":{"url":"https://api.github.com/repos/nearai/ironwire/issues/8","repository_url":"https://api.github.com/repos/nearai/ironwire","labels_url":"https://api.github.com/repos/nearai/ironwire/issues/8/labels{/name}","comments_url":"https://api.github.com/repos/nearai/ironwire/issues/8/comments","events_url":"https://api.github.com/repos/nearai/ironwire/issues/8/events","html_url":"https://github.com/nearai/ironwire/issues/8","id":5099793919,"node_id":"I_kwDOTx-bm88AAAABL_it_w","number":8,"title":"macOS menu bar app over the control API (M5)","user":{"login":"ilblackdragon","id":175486,"node_id":"MDQ6VXNlcjE3NTQ4Ng==","avatar_url":"https://avatars.githubusercontent.com/u/175486?v=4","gravatar_id":"","url":"https://api.github.com/users/ilblackdragon","html_url":"https://github.com/ilblackdragon","followers_url":"https://api.github.com/users/ilblackdragon/followers","following_url":"https://api.github.com/users/ilblackdragon/following{/other_user}","gists_url":"https://api.github.com/users/ilblackdragon/gists{/gist_id}","starred_url":"https://api.github.com/users/ilblackdragon/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ilblackdragon/subscriptions","organizations_url":"https://api.github.com/users/ilblackdragon/orgs","repos_url":"https://api.github.com/users/ilblackdragon/repos","events_url":"https://api.github.com/users/ilblackdragon/events{/privacy}","received_events_url":"https://api.github.com/users/ilblackdragon/received_events","type":"User","user_view_type":"public","site_admin":false},"labels":[{"id":11769725287,"node_id":"LA_kwDOTx-bm88AAAACvYfBZw","url":"https://api.github.com/repos/nearai/ironwire/labels/enhancement","name":"enhancement","color":"a2eeef","default":true,"description":"New feature or request"}],"state":"closed","locked":false,"assignees":[],"milestone":null,"comments":2,"created_at":"2026-08-08T22:38:54Z","updated_at":"2026-08-10T16:36:10Z","closed_at":"2026-08-10T16:36:10Z","assignee":null,"issue_field_values":[],"type":null,"active_lock_reason":null,"sub_issues_summary":{"total":0,"completed":0,"percent_completed":0},"issue_dependencies_summary":{"blocked_by":0,"total_blocked_by":0,"blocking":0,"total_blocking":0},"body":"## Problem\n\nIronWire's most important state changes happen while the user is not looking at a terminal.\n\nThe daemon cannot write into a coding agent's UI. `crates/ironwire_proxy/src/events.rs` states the constraint precisely: the only writable channel into the agent is the response stream, and injecting text there would put words in the model's mouth and corrupt the transcript the agent stores and replays. So every announcement IronWire makes is a side channel.\n\nToday those side channels are: `ironwire status` (polled by hand), `ironwire watch` (a second terminal the user must have deliberately opened), and `ironwire statusline` (one line, inside Claude Code only, and only for users who ran `ironwire connect claude`). A user running Codex, Cline, or Aider has no ambient signal at all.\n\nThe events that matter are exactly the ones a terminal will not deliver: a cross-family descent (rung 3, the one IronWire is obliged to announce), a backend's circuit opening, a subscription window resetting, a spend cap being reached. `docs/DESIGN.md` §6 and `docs/ROADMAP.md` M5 both specify a menu bar app as the answer, and `crates/ironwire_proxy/src/control.rs` already carries the doc comment \"`ironwire status` and the macOS menu bar app are both clients of this\".\n\nThe daemon side is finished. `/_ironwire/status` returns a complete `StatusView`, `/_ironwire/events` is a live SSE bus, `/_ironwire/pin` accepts a route change, and `/_ironwire/health` is unauthenticated for probing. Nothing has been built against them.\n\n## Proposed Solution\n\nA SwiftUI `MenuBarExtra` app, in a new top-level `macos/` directory, that is a **pure client** of the control API.\n\n### The rule that governs the whole app\n\n**No routing logic in Swift.** `docs/DESIGN.md` §6: the daemon is the only brain. The app renders `StatusView` and posts to `/_ironwire/pin`. It must not decide anything — not which backend is \"best\", not whether a percentage counts as low, not what to do when a backend fails. Any conditional in Swift that would need to change when routing policy changes is a bug in this app. If the app needs a derived value, add it to `StatusView` and compute it in Rust.\n\nThis is not stylistic. Two implementations of \"is this backend usable\" that drift is the specific failure mode a GUI over a daemon invites.\n\n### Layout\n\n```\nmacos/\n  IronWire.xcodeproj\n  IronWire/\n    IronWireApp.swift        MenuBarExtra scene\n    ControlClient.swift      status polling + SSE, token discovery\n    Models.swift             Codable mirrors of StatusView / Event\n    MenuContent.swift        the dropdown\n    Notifications.swift      UNUserNotificationCenter\nREADME.md                    build instructions, since CI cannot build it\n```\n\n### Menu bar icon\n\nThe icon is the ambient signal, so it must carry state without being read:\n\n| State | Icon |\n|---|---|\n| Healthy, on a preferred route | monochrome template icon (system-tinted, blends in) |\n| Degraded (last route rung > Preferred) | icon with a filled dot |\n| Cross-family, or a circuit open, or no backend available | icon with a coloured dot |\n| Daemon unreachable | dimmed icon |\n\n`StatusView` already carries `last_route` (with `backend`, `model`, `from`, `at`) and `backends[].health.circuit`. Rung is not currently in `LastRouteView` — **add `rung: Rung` to it in `control.rs`**, since the app must not infer degradation from backend names.\n\n### Dropdown\n\nMirror `render::status()` — the CLI is the reference for what is honest to show:\n\n- **Backends.** Name, kind, a capacity bar from `headroom`, and the observation age. `HeadroomView::Unknown` renders the word `unknown` with **no bar**. A bar drawn at zero, or greyed at 50%, is a fabricated number and violates the rule `docs/CRITIQUE.md` §4 exists for. This is the single most likely place for this app to go wrong.\n- **Current route.** From `last_route`: `claude-sub → nearai` when `from` is set, otherwise the backend, plus the model and relative time.\n- **Spend.** `balance.spend_today_usd`, formatted as currency, absent when `nil`.\n- **Privacy.** `status.privacy` verbatim. Never reformatted, never turned into a shield icon or the word \"protected\" — `docs/TRUST.md` I7 forbids describing the filter by what the user is safe from, and an icon is a description.\n- **Pin control.** A submenu listing `backends[].id` plus \"Automatic\", POSTing to `/_ironwire/pin`. Reflect the current `status.pin`.\n- **Update notice.** From `status.update`, when it is not `UpToDate`. Notify-only: link to the release, never offer to install. `docs/UPDATES.md` §1 — IronWire never updates itself, and a GUI button is the most tempting place to break that.\n- **Quit / Open logs / Copy control URL.**\n\n### Connection\n\n- **Token.** Read `$IRONWIRE_HOME/control.token` (default `~/.ironwire/control.token`) at launch and on every 401. Send as `Authorization: Bearer`. The app is sandboxed by default under SwiftUI templates — either disable the sandbox or add a user-selected read entitlement for that path, and document which.\n- **Port.** Default 8463. Read `server.port` from `~/.ironwire/config.toml` if present; a TOML parse in Swift is not worth a dependency, so a regex for `port = N` under `[server]` is acceptable — document it as best-effort with the default as fallback.\n- **Status poll.** Every 5 seconds while the menu is closed, every 1 second while open. `/_ironwire/status` calls `BackendRegistry::statuses()`, which does a credential check per backend (a Keychain read for the Claude backend), so faster polling is not free.\n- **Events.** Hold `/_ironwire/events` open with `URLSession` bytes streaming. Reconnect with exponential backoff. The stream emits `: connected` and `: lagged N` comment frames — parse and ignore comments; do not treat them as data.\n- **Daemon absent.** \"IronWire is not running\" plus a Start action that launches the installed binary, or instructions when none is found. Never an error dialog: the daemon being down is a normal state.\n\n### Notifications\n\nOff by default, opt-in in the menu. Only two events warrant one — `Event::Routed` where `rung == CrossFamily`, and `Event::Failed`. Both are already on the bus. A notification per route change would be unusable; the bus is deliberately lossy and this app must be too.\n\n### Distribution\n\nOut of scope for this issue beyond a build that runs locally: signing, notarisation, and a `.dmg` need an Apple Developer certificate. Add a `macos/README.md` with build steps and note the gap in `docs/PACKAGING.md`. Do not add the Xcode project to any CI workflow — Linux runners cannot build it and a red matrix on every push is worse than no CI.\n\n## Architecture Notes\n\n```\n   ┌──────────────────────┐\n   │  IronWire.app        │\n   │  (SwiftUI, no logic) │\n   └──────────┬───────────┘\n     poll 5s  │  SSE (held open)\n              ▼\n   GET  /_ironwire/status     ── StatusView\n   GET  /_ironwire/events     ── Event stream\n   POST /_ironwire/pin        ── the only write\n              │  Bearer $IRONWIRE_HOME/control.token\n              ▼\n   ┌──────────────────────┐\n   │  ironwire serve      │  ← every decision happens here\n   └──────────────────────┘\n```\n\n**Why `StatusView` is the contract.** It is already `Serialize`/`Deserialize` and already consumed by `render.rs` and `statusline.rs`. Mirroring it in Swift `Codable` structs keeps CLI and GUI showing the same facts. When they need to show something new, the field goes in `StatusView` — never into a Swift-side computation.\n\n**Unknown enum cases.** `HeadroomView` is `#[serde(tag = \"state\")]` and `UpdateStatus` is a tagged enum. A Swift `Codable` enum that fails to decode an unknown tag will blank the entire menu when the daemon is newer than the app. Decode unknown variants to an explicit `.unknown` case and render them as absent.\n\n## Code Pointers\n\n- `crates/ironwire_proxy/src/control.rs` — `StatusView`, `BackendView`, `HeadroomView`, `HealthView`, `BalanceView`, `LastRouteView`, `router()`, `authorize()`. This is the entire API surface.\n- `crates/ironwire_proxy/src/events.rs` — `Event`, and the doc comment explaining why the side channel exists at all. Read it before designing the notification behaviour.\n- `crates/ironwire_cli/src/render.rs` — `status()`, `backend_block()`, `headroom()`, `meter()`, `balance_block()`. The reference for what to show and, more importantly, what not to.\n- `crates/ironwire_cli/src/commands/statusline.rs` — the closest existing client: short timeout, print nothing on failure, never invent. Same three properties apply here.\n- `crates/ironwire_cli/src/commands/control_client.rs` — token discovery and request shape.\n- `docs/DESIGN.md` §6, `docs/ROADMAP.md` M5, `docs/TRUST.md` I7, `docs/UPDATES.md` §1.\n\n## Acceptance Criteria\n\n- [ ] `MenuBarExtra` app launches, finds a running daemon on the default port, and renders every backend from `/_ironwire/status`.\n- [ ] A backend with `HeadroomView::Unknown` renders the word `unknown` and **no capacity bar**.\n- [ ] A backend with an open circuit is visibly distinguished from an idle one.\n- [ ] The icon changes state on a cross-family route and returns when the route does.\n- [ ] `LastRouteView` gains `rung`, and the app reads degradation from it rather than inferring it.\n- [ ] The pin submenu lists every backend plus Automatic, POSTs to `/_ironwire/pin`, and reflects the resulting `status.pin` on the next poll.\n- [ ] `status.privacy` is rendered verbatim; no shield, no lock, no \"protected\".\n- [ ] An available update is shown with a link and **no** install action.\n- [ ] Daemon down renders \"not running\" with no error dialog and no console spam; it recovers automatically when the daemon returns.\n- [ ] A 401 re-reads the token file and retries once before showing an auth error.\n- [ ] The SSE stream reconnects with backoff after a daemon restart; `: connected` and `: lagged` frames are ignored.\n- [ ] Notifications are off by default, opt-in, and fire only for cross-family routes and failures.\n- [ ] An unknown `HeadroomView` or `UpdateStatus` tag from a newer daemon degrades that one field, never the whole menu.\n- [ ] Poll rate is 5s closed / 1s open, and closing the app stops all polling.\n- [ ] No Swift code decides which backend to use, whether a route is acceptable, or what a percentage means.\n- [ ] `macos/README.md` documents build and run; `docs/PACKAGING.md` notes signing/notarisation as outstanding.\n- [ ] No CI workflow attempts to build the Xcode project.\n\n## Pitfalls & Landmines\n\n- **The capacity bar is the trap.** SwiftUI's `ProgressView` wants a `Double`. `HeadroomView::Unknown` has none. Any default — 0, 50, greyed — is a number IronWire made up, in the one surface a user glances at without reading. Render no bar. This is the acceptance criterion most likely to be quietly failed.\n- **Do not add an \"Update\" button.** The daemon holds credentials in the middle of streamed responses and `docs/UPDATES.md` §1 is explicit that it never updates itself. A menu bar app is where someone will add this because it seems obviously nice.\n- **`/_ironwire/status` is not free.** It calls `Backend::status()` per backend, which re-reads the Claude credential (Keychain on macOS). A 1-second poll while the menu is open is acceptable; a 1-second background poll is not.\n- **Token file permissions.** `control.token` is 0600. A sandboxed app cannot read `~/.ironwire/` without an entitlement. Decide and document; do not copy the token elsewhere.\n- **Do not reimplement `meter()`.** Match the CLI's thresholds by reading its source, or the two surfaces will disagree about what \"nearly full\" looks like.\n- **The event bus drops on lag** (`CAPACITY = 256`). Never treat the event stream as a complete history — `/_ironwire/status` is the source of truth and events are hints that it changed.\n- **`Rung` serialises as snake_case** (`preferred`, `smaller_model`, `alternate_credential`, `cross_family`). Match it exactly.\n\n## Non-Goals\n\n- Code signing, notarisation, `.dmg`, Sparkle, or App Store distribution.\n- Windows or Linux tray equivalents.\n- Any routing, credential, or consent logic in the app. Consent stays a CLI decision with a recorded, versioned acceptance.\n- Showing conversation content, request bodies, or ledger rows. The app shows state, not traces.\n- Starting/stopping the daemon beyond a best-effort launch when it is not running.\n","reactions":{"url":"https://api.github.com/repos/nearai/ironwire/issues/8/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"timeline_url":"https://api.github.com/repos/nearai/ironwire/issues/8/timeline","performed_via_github_app":null,"state_reason":"completed","pinned_comment":null},"comment":{"url":"https://api.github.com/repos/nearai/ironwire/issues/comments/5243120744","html_url":"https://github.com/nearai/ironwire/issues/8#issuecomment-5243120744","issue_url":"https://api.github.com/repos/nearai/ironwire/issues/8","id":5243120744,"node_id":"IC_kwDOTx-bm88AAAABOIOsaA","user":{"login":"ilblackdragon","id":175486,"node_id":"MDQ6VXNlcjE3NTQ4Ng==","avatar_url":"https://avatars.githubusercontent.com/u/175486?v=4","gravatar_id":"","url":"https://api.github.com/users/ilblackdragon","html_url":"https://github.com/ilblackdragon","followers_url":"https://api.github.com/users/ilblackdragon/followers","following_url":"https://api.github.com/users/ilblackdragon/following{/other_user}","gists_url":"https://api.github.com/users/ilblackdragon/gists{/gist_id}","starred_url":"https://api.github.com/users/ilblackdragon/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ilblackdragon/subscriptions","organizations_url":"https://api.github.com/users/ilblackdragon/orgs","repos_url":"https://api.github.com/users/ilblackdragon/repos","events_url":"https://api.github.com/users/ilblackdragon/events{/privacy}","received_events_url":"https://api.github.com/users/ilblackdragon/received_events","type":"User","user_view_type":"public","site_admin":false},"created_at":"2026-08-10T16:36:10Z","updated_at":"2026-08-10T16:36:10Z","body":"Done. The app is built, in `macos/`, and every acceptance criterion above holds.\n\n**What landed since the last comment.** The bulk of it went in over the commits from `671fb8a` onwards: the `MenuBarExtra` with its four icon states, the one-pane dropdown mirroring `render::status()`, consent switches, the privacy ladder, the tools section, login item, notifications, and the `IronWire-macos.zip` the release job builds. Two things were outstanding and are fixed in `c2bafca`:\n\n- **A live test had grown its own copy of a routing rule.** It asserted \"anything named under `trusted_backends` means `full` is selectable\". `65d87b4` made `nearai` register whether or not a key was found, so a named destination stopped implying a usable one, and `full_is_blocked` asks both questions — the test failed against a real daemon, exactly as the \"no routing logic in Swift\" rule predicts. It now asserts the contract the pane is built on: an unselectable mode always arrives with the sentence it is greyed out with. `trustedBackends` was dropped from the Swift model with it, since decoding the input to a rule and leaving it beside the answer is how the copy came to be written.\n- **\"Copy control URL\"**, the one dropdown item on the list that was missing. It uses the port the daemon reported rather than the one discovery guessed, and `Discovery.controlURL` is now the single construction of that address with the client calling through it. The token is not copied with it; the tooltip names the file it lives in.\n\n**Verified:** `swift build` and `make -C macos app` clean, and `IRONWIRE_LIVE=1 swift test` gives 119 tests / 0 failures against a running daemon — including all 14 `LiveDaemonTests`, which cover the status and settings documents decoding, the 401 re-read-and-retry, a pin round trip, and a privacy mode change being applied and undone.\n\n**Two deliberate divergences from the issue, both written down:**\n\n- **SwiftPM, not `IronWire.xcodeproj`.** The release job has to build the bundle from a script either way, and a hand-authored `.pbxproj` is several hundred lines of UUIDs nobody reads. Xcode opens `Package.swift` directly. Reasoning is in `macos/README.md`.\n- **CI does build it, on the macOS leg only** (`if: matrix.os == 'macos-latest'`). The constraint the issue was protecting — Linux runners cannot build Swift and a red matrix on every push is worse than no CI — is honoured; gating on the runner keeps the coverage without it. The alternative was finding a Swift compile break during a tag.\n\n`NotificationPolicy` also fires for `CapReached` alongside cross-family routes and failures, following `Event::is_user_visible` rather than holding a second opinion. It is published once per backend per window, so it cannot become the per-request noise the issue warns about, and the issue's own problem statement names a spend cap among the events that matter.\n\n**Still outstanding, and not this issue:** signing and notarisation. The bundle is ad-hoc signed, which is enough for `brew` and a local build but not for a direct download — Gatekeeper wants notarisation and that needs an Apple Developer certificate. Recorded in `docs/PACKAGING.md` and in the M5 row of the ROADMAP table.","pin":null,"reactions":{"url":"https://api.github.com/repos/nearai/ironwire/issues/comments/5243120744/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"performed_via_github_app":null,"minimized":null}},"public":true,"created_at":"2026-08-10T16:36:10Z","org":{"id":29134221,"login":"nearai","gravatar_id":"","url":"https://api.github.com/orgs/nearai","avatar_url":"https://avatars.githubusercontent.com/u/29134221?"}},{"id":"13055937953","type":"IssuesEvent","actor":{"id":175486,"login":"ilblackdragon","display_login":"ilblackdragon","gravatar_id":"","url":"https://api.github.com/users/ilblackdragon","avatar_url":"https://avatars.githubusercontent.com/u/175486?"},"repo":{"id":1327471515,"name":"nearai/ironwire","url":"https://api.github.com/repos/nearai/ironwire"},"payload":{"action":"closed","issue":{"url":"https://api.github.com/repos/nearai/ironwire/issues/8","repository_url":"https://api.github.com/repos/nearai/ironwire","labels_url":"https://api.github.com/repos/nearai/ironwire/issues/8/labels{/name}","comments_url":"https://api.github.com/repos/nearai/ironwire/issues/8/comments","events_url":"https://api.github.com/repos/nearai/ironwire/issues/8/events","html_url":"https://github.com/nearai/ironwire/issues/8","id":5099793919,"node_id":"I_kwDOTx-bm88AAAABL_it_w","number":8,"title":"macOS menu bar app over the control API (M5)","user":{"login":"ilblackdragon","id":175486,"node_id":"MDQ6VXNlcjE3NTQ4Ng==","avatar_url":"https://avatars.githubusercontent.com/u/175486?v=4","gravatar_id":"","url":"https://api.github.com/users/ilblackdragon","html_url":"https://github.com/ilblackdragon","followers_url":"https://api.github.com/users/ilblackdragon/followers","following_url":"https://api.github.com/users/ilblackdragon/following{/other_user}","gists_url":"https://api.github.com/users/ilblackdragon/gists{/gist_id}","starred_url":"https://api.github.com/users/ilblackdragon/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ilblackdragon/subscriptions","organizations_url":"https://api.github.com/users/ilblackdragon/orgs","repos_url":"https://api.github.com/users/ilblackdragon/repos","events_url":"https://api.github.com/users/ilblackdragon/events{/privacy}","received_events_url":"https://api.github.com/users/ilblackdragon/received_events","type":"User","user_view_type":"public","site_admin":false},"labels":[{"id":11769725287,"node_id":"LA_kwDOTx-bm88AAAACvYfBZw","url":"https://api.github.com/repos/nearai/ironwire/labels/enhancement","name":"enhancement","color":"a2eeef","default":true,"description":"New feature or request"}],"state":"closed","locked":false,"assignees":[],"milestone":null,"comments":2,"created_at":"2026-08-08T22:38:54Z","updated_at":"2026-08-10T16:36:10Z","closed_at":"2026-08-10T16:36:10Z","assignee":null,"issue_field_values":[],"type":null,"active_lock_reason":null,"sub_issues_summary":{"total":0,"completed":0,"percent_completed":0},"issue_dependencies_summary":{"blocked_by":0,"total_blocked_by":0,"blocking":0,"total_blocking":0},"body":"## Problem\n\nIronWire's most important state changes happen while the user is not looking at a terminal.\n\nThe daemon cannot write into a coding agent's UI. `crates/ironwire_proxy/src/events.rs` states the constraint precisely: the only writable channel into the agent is the response stream, and injecting text there would put words in the model's mouth and corrupt the transcript the agent stores and replays. So every announcement IronWire makes is a side channel.\n\nToday those side channels are: `ironwire status` (polled by hand), `ironwire watch` (a second terminal the user must have deliberately opened), and `ironwire statusline` (one line, inside Claude Code only, and only for users who ran `ironwire connect claude`). A user running Codex, Cline, or Aider has no ambient signal at all.\n\nThe events that matter are exactly the ones a terminal will not deliver: a cross-family descent (rung 3, the one IronWire is obliged to announce), a backend's circuit opening, a subscription window resetting, a spend cap being reached. `docs/DESIGN.md` §6 and `docs/ROADMAP.md` M5 both specify a menu bar app as the answer, and `crates/ironwire_proxy/src/control.rs` already carries the doc comment \"`ironwire status` and the macOS menu bar app are both clients of this\".\n\nThe daemon side is finished. `/_ironwire/status` returns a complete `StatusView`, `/_ironwire/events` is a live SSE bus, `/_ironwire/pin` accepts a route change, and `/_ironwire/health` is unauthenticated for probing. Nothing has been built against them.\n\n## Proposed Solution\n\nA SwiftUI `MenuBarExtra` app, in a new top-level `macos/` directory, that is a **pure client** of the control API.\n\n### The rule that governs the whole app\n\n**No routing logic in Swift.** `docs/DESIGN.md` §6: the daemon is the only brain. The app renders `StatusView` and posts to `/_ironwire/pin`. It must not decide anything — not which backend is \"best\", not whether a percentage counts as low, not what to do when a backend fails. Any conditional in Swift that would need to change when routing policy changes is a bug in this app. If the app needs a derived value, add it to `StatusView` and compute it in Rust.\n\nThis is not stylistic. Two implementations of \"is this backend usable\" that drift is the specific failure mode a GUI over a daemon invites.\n\n### Layout\n\n```\nmacos/\n  IronWire.xcodeproj\n  IronWire/\n    IronWireApp.swift        MenuBarExtra scene\n    ControlClient.swift      status polling + SSE, token discovery\n    Models.swift             Codable mirrors of StatusView / Event\n    MenuContent.swift        the dropdown\n    Notifications.swift      UNUserNotificationCenter\nREADME.md                    build instructions, since CI cannot build it\n```\n\n### Menu bar icon\n\nThe icon is the ambient signal, so it must carry state without being read:\n\n| State | Icon |\n|---|---|\n| Healthy, on a preferred route | monochrome template icon (system-tinted, blends in) |\n| Degraded (last route rung > Preferred) | icon with a filled dot |\n| Cross-family, or a circuit open, or no backend available | icon with a coloured dot |\n| Daemon unreachable | dimmed icon |\n\n`StatusView` already carries `last_route` (with `backend`, `model`, `from`, `at`) and `backends[].health.circuit`. Rung is not currently in `LastRouteView` — **add `rung: Rung` to it in `control.rs`**, since the app must not infer degradation from backend names.\n\n### Dropdown\n\nMirror `render::status()` — the CLI is the reference for what is honest to show:\n\n- **Backends.** Name, kind, a capacity bar from `headroom`, and the observation age. `HeadroomView::Unknown` renders the word `unknown` with **no bar**. A bar drawn at zero, or greyed at 50%, is a fabricated number and violates the rule `docs/CRITIQUE.md` §4 exists for. This is the single most likely place for this app to go wrong.\n- **Current route.** From `last_route`: `claude-sub → nearai` when `from` is set, otherwise the backend, plus the model and relative time.\n- **Spend.** `balance.spend_today_usd`, formatted as currency, absent when `nil`.\n- **Privacy.** `status.privacy` verbatim. Never reformatted, never turned into a shield icon or the word \"protected\" — `docs/TRUST.md` I7 forbids describing the filter by what the user is safe from, and an icon is a description.\n- **Pin control.** A submenu listing `backends[].id` plus \"Automatic\", POSTing to `/_ironwire/pin`. Reflect the current `status.pin`.\n- **Update notice.** From `status.update`, when it is not `UpToDate`. Notify-only: link to the release, never offer to install. `docs/UPDATES.md` §1 — IronWire never updates itself, and a GUI button is the most tempting place to break that.\n- **Quit / Open logs / Copy control URL.**\n\n### Connection\n\n- **Token.** Read `$IRONWIRE_HOME/control.token` (default `~/.ironwire/control.token`) at launch and on every 401. Send as `Authorization: Bearer`. The app is sandboxed by default under SwiftUI templates — either disable the sandbox or add a user-selected read entitlement for that path, and document which.\n- **Port.** Default 8463. Read `server.port` from `~/.ironwire/config.toml` if present; a TOML parse in Swift is not worth a dependency, so a regex for `port = N` under `[server]` is acceptable — document it as best-effort with the default as fallback.\n- **Status poll.** Every 5 seconds while the menu is closed, every 1 second while open. `/_ironwire/status` calls `BackendRegistry::statuses()`, which does a credential check per backend (a Keychain read for the Claude backend), so faster polling is not free.\n- **Events.** Hold `/_ironwire/events` open with `URLSession` bytes streaming. Reconnect with exponential backoff. The stream emits `: connected` and `: lagged N` comment frames — parse and ignore comments; do not treat them as data.\n- **Daemon absent.** \"IronWire is not running\" plus a Start action that launches the installed binary, or instructions when none is found. Never an error dialog: the daemon being down is a normal state.\n\n### Notifications\n\nOff by default, opt-in in the menu. Only two events warrant one — `Event::Routed` where `rung == CrossFamily`, and `Event::Failed`. Both are already on the bus. A notification per route change would be unusable; the bus is deliberately lossy and this app must be too.\n\n### Distribution\n\nOut of scope for this issue beyond a build that runs locally: signing, notarisation, and a `.dmg` need an Apple Developer certificate. Add a `macos/README.md` with build steps and note the gap in `docs/PACKAGING.md`. Do not add the Xcode project to any CI workflow — Linux runners cannot build it and a red matrix on every push is worse than no CI.\n\n## Architecture Notes\n\n```\n   ┌──────────────────────┐\n   │  IronWire.app        │\n   │  (SwiftUI, no logic) │\n   └──────────┬───────────┘\n     poll 5s  │  SSE (held open)\n              ▼\n   GET  /_ironwire/status     ── StatusView\n   GET  /_ironwire/events     ── Event stream\n   POST /_ironwire/pin        ── the only write\n              │  Bearer $IRONWIRE_HOME/control.token\n              ▼\n   ┌──────────────────────┐\n   │  ironwire serve      │  ← every decision happens here\n   └──────────────────────┘\n```\n\n**Why `StatusView` is the contract.** It is already `Serialize`/`Deserialize` and already consumed by `render.rs` and `statusline.rs`. Mirroring it in Swift `Codable` structs keeps CLI and GUI showing the same facts. When they need to show something new, the field goes in `StatusView` — never into a Swift-side computation.\n\n**Unknown enum cases.** `HeadroomView` is `#[serde(tag = \"state\")]` and `UpdateStatus` is a tagged enum. A Swift `Codable` enum that fails to decode an unknown tag will blank the entire menu when the daemon is newer than the app. Decode unknown variants to an explicit `.unknown` case and render them as absent.\n\n## Code Pointers\n\n- `crates/ironwire_proxy/src/control.rs` — `StatusView`, `BackendView`, `HeadroomView`, `HealthView`, `BalanceView`, `LastRouteView`, `router()`, `authorize()`. This is the entire API surface.\n- `crates/ironwire_proxy/src/events.rs` — `Event`, and the doc comment explaining why the side channel exists at all. Read it before designing the notification behaviour.\n- `crates/ironwire_cli/src/render.rs` — `status()`, `backend_block()`, `headroom()`, `meter()`, `balance_block()`. The reference for what to show and, more importantly, what not to.\n- `crates/ironwire_cli/src/commands/statusline.rs` — the closest existing client: short timeout, print nothing on failure, never invent. Same three properties apply here.\n- `crates/ironwire_cli/src/commands/control_client.rs` — token discovery and request shape.\n- `docs/DESIGN.md` §6, `docs/ROADMAP.md` M5, `docs/TRUST.md` I7, `docs/UPDATES.md` §1.\n\n## Acceptance Criteria\n\n- [ ] `MenuBarExtra` app launches, finds a running daemon on the default port, and renders every backend from `/_ironwire/status`.\n- [ ] A backend with `HeadroomView::Unknown` renders the word `unknown` and **no capacity bar**.\n- [ ] A backend with an open circuit is visibly distinguished from an idle one.\n- [ ] The icon changes state on a cross-family route and returns when the route does.\n- [ ] `LastRouteView` gains `rung`, and the app reads degradation from it rather than inferring it.\n- [ ] The pin submenu lists every backend plus Automatic, POSTs to `/_ironwire/pin`, and reflects the resulting `status.pin` on the next poll.\n- [ ] `status.privacy` is rendered verbatim; no shield, no lock, no \"protected\".\n- [ ] An available update is shown with a link and **no** install action.\n- [ ] Daemon down renders \"not running\" with no error dialog and no console spam; it recovers automatically when the daemon returns.\n- [ ] A 401 re-reads the token file and retries once before showing an auth error.\n- [ ] The SSE stream reconnects with backoff after a daemon restart; `: connected` and `: lagged` frames are ignored.\n- [ ] Notifications are off by default, opt-in, and fire only for cross-family routes and failures.\n- [ ] An unknown `HeadroomView` or `UpdateStatus` tag from a newer daemon degrades that one field, never the whole menu.\n- [ ] Poll rate is 5s closed / 1s open, and closing the app stops all polling.\n- [ ] No Swift code decides which backend to use, whether a route is acceptable, or what a percentage means.\n- [ ] `macos/README.md` documents build and run; `docs/PACKAGING.md` notes signing/notarisation as outstanding.\n- [ ] No CI workflow attempts to build the Xcode project.\n\n## Pitfalls & Landmines\n\n- **The capacity bar is the trap.** SwiftUI's `ProgressView` wants a `Double`. `HeadroomView::Unknown` has none. Any default — 0, 50, greyed — is a number IronWire made up, in the one surface a user glances at without reading. Render no bar. This is the acceptance criterion most likely to be quietly failed.\n- **Do not add an \"Update\" button.** The daemon holds credentials in the middle of streamed responses and `docs/UPDATES.md` §1 is explicit that it never updates itself. A menu bar app is where someone will add this because it seems obviously nice.\n- **`/_ironwire/status` is not free.** It calls `Backend::status()` per backend, which re-reads the Claude credential (Keychain on macOS). A 1-second poll while the menu is open is acceptable; a 1-second background poll is not.\n- **Token file permissions.** `control.token` is 0600. A sandboxed app cannot read `~/.ironwire/` without an entitlement. Decide and document; do not copy the token elsewhere.\n- **Do not reimplement `meter()`.** Match the CLI's thresholds by reading its source, or the two surfaces will disagree about what \"nearly full\" looks like.\n- **The event bus drops on lag** (`CAPACITY = 256`). Never treat the event stream as a complete history — `/_ironwire/status` is the source of truth and events are hints that it changed.\n- **`Rung` serialises as snake_case** (`preferred`, `smaller_model`, `alternate_credential`, `cross_family`). Match it exactly.\n\n## Non-Goals\n\n- Code signing, notarisation, `.dmg`, Sparkle, or App Store distribution.\n- Windows or Linux tray equivalents.\n- Any routing, credential, or consent logic in the app. Consent stays a CLI decision with a recorded, versioned acceptance.\n- Showing conversation content, request bodies, or ledger rows. The app shows state, not traces.\n- Starting/stopping the daemon beyond a best-effort launch when it is not running.\n","reactions":{"url":"https://api.github.com/repos/nearai/ironwire/issues/8/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"timeline_url":"https://api.github.com/repos/nearai/ironwire/issues/8/timeline","performed_via_github_app":null,"state_reason":"completed","pinned_comment":null}},"public":true,"created_at":"2026-08-10T16:36:11Z","org":{"id":29134221,"login":"nearai","gravatar_id":"","url":"https://api.github.com/orgs/nearai","avatar_url":"https://avatars.githubusercontent.com/u/29134221?"}},{"id":"13054864382","type":"PublicEvent","actor":{"id":175486,"login":"ilblackdragon","display_login":"ilblackdragon","gravatar_id":"","url":"https://api.github.com/users/ilblackdragon","avatar_url":"https://avatars.githubusercontent.com/u/175486?"},"repo":{"id":1327471515,"name":"nearai/ironwire","url":"https://api.github.com/repos/nearai/ironwire"},"payload":{},"public":true,"created_at":"2026-08-08T06:14:24Z","org":{"id":29134221,"login":"nearai","gravatar_id":"","url":"https://api.github.com/orgs/nearai","avatar_url":"https://avatars.githubusercontent.com/u/29134221?"}},{"id":"12961048280","type":"PullRequestEvent","actor":{"id":175486,"login":"ilblackdragon","display_login":"ilblackdragon","gravatar_id":"","url":"https://api.github.com/users/ilblackdragon","avatar_url":"https://avatars.githubusercontent.com/u/175486?"},"repo":{"id":1148615527,"name":"nearai/ironclaw","url":"https://api.github.com/repos/nearai/ironclaw"},"payload":{"action":"opened","number":7384,"pull_request":{"url":"https://api.github.com/repos/nearai/ironclaw/pulls/7384","id":4232164422,"number":7384,"head":{"ref":"fix/nearai-session-auth-diagnosis","sha":"ff38a1ac51ce3260fbfc73a87e6d3b9a908ee94d","repo":{"id":1148615527,"url":"https://api.github.com/repos/nearai/ironclaw","name":"ironclaw"}},"base":{"ref":"fix/credential-owner-scoping","sha":"1c9b1b6e22afdd041a62d0a1a55a81247330a191","repo":{"id":1148615527,"url":"https://api.github.com/repos/nearai/ironclaw","name":"ironclaw"}}}},"public":true,"created_at":"2026-08-07T22:59:16Z","org":{"id":29134221,"login":"nearai","gravatar_id":"","url":"https://api.github.com/orgs/nearai","avatar_url":"https://avatars.githubusercontent.com/u/29134221?"}},{"id":"17090314467","type":"CreateEvent","actor":{"id":175486,"login":"ilblackdragon","display_login":"ilblackdragon","gravatar_id":"","url":"https://api.github.com/users/ilblackdragon","avatar_url":"https://avatars.githubusercontent.com/u/175486?"},"repo":{"id":1148615527,"name":"nearai/ironclaw","url":"https://api.github.com/repos/nearai/ironclaw"},"payload":{"ref":"fix/nearai-session-auth-diagnosis","ref_type":"branch","full_ref":"refs/heads/fix/nearai-session-auth-diagnosis","master_branch":"main","description":"IronClaw is an Agent OS focused on privacy, security and extensibility","pusher_type":"user"},"public":true,"created_at":"2026-08-07T22:47:53Z","org":{"id":29134221,"login":"nearai","gravatar_id":"","url":"https://api.github.com/orgs/nearai","avatar_url":"https://avatars.githubusercontent.com/u/29134221?"}},{"id":"17084505144","type":"PushEvent","actor":{"id":4579561,"login":"ddagunts","display_login":"ddagunts","gravatar_id":"","url":"https://api.github.com/users/ddagunts","avatar_url":"https://avatars.githubusercontent.com/u/4579561?"},"repo":{"id":1326828708,"name":"ddagunts/chromesurf","url":"https://api.github.com/repos/ddagunts/chromesurf"},"payload":{"repository_id":1326828708,"push_id":39394382631,"ref":"refs/heads/main","head":"563c42212f6c68be7619a3efcdfe26b971e3a14c","before":"5af7aaa391132ec9b24d3bf3540b7d7eeaa13280"},"public":true,"created_at":"2026-08-07T18:25:03Z"},{"id":"17083185865","type":"PushEvent","actor":{"id":175486,"login":"ilblackdragon","display_login":"ilblackdragon","gravatar_id":"","url":"https://api.github.com/users/ilblackdragon","avatar_url":"https://avatars.githubusercontent.com/u/175486?"},"repo":{"id":1148615527,"name":"nearai/ironclaw","url":"https://api.github.com/repos/nearai/ironclaw"},"payload":{"repository_id":1148615527,"push_id":39397571182,"ref":"refs/heads/fix/credential-owner-scoping","head":"1c9b1b6e22afdd041a62d0a1a55a81247330a191","before":"1f8bbbd927ceac7175622702fa0aef8ab9bd1a34"},"public":true,"created_at":"2026-08-07T19:19:30Z","org":{"id":29134221,"login":"nearai","gravatar_id":"","url":"https://api.github.com/orgs/nearai","avatar_url":"https://avatars.githubusercontent.com/u/29134221?"}},{"id":"17078506396","type":"PushEvent","actor":{"id":4579561,"login":"ddagunts","display_login":"ddagunts","gravatar_id":"","url":"https://api.github.com/users/ddagunts","avatar_url":"https://avatars.githubusercontent.com/u/4579561?"},"repo":{"id":1326828708,"name":"ddagunts/chromesurf","url":"https://api.github.com/repos/ddagunts/chromesurf"},"payload":{"repository_id":1326828708,"push_id":39395345575,"ref":"refs/heads/main","head":"5cc973ed75eb95e5e0777b373c2a13de0b65455e","before":"9ce05848b20517582d41e735379a6f71f030e5bb"},"public":true,"created_at":"2026-08-07T18:41:03Z"},{"id":"17077979806","type":"PushEvent","actor":{"id":175486,"login":"ilblackdragon","display_login":"ilblackdragon","gravatar_id":"","url":"https://api.github.com/users/ilblackdragon","avatar_url":"https://avatars.githubusercontent.com/u/175486?"},"repo":{"id":1148615527,"name":"nearai/ironclaw","url":"https://api.github.com/repos/nearai/ironclaw"},"payload":{"repository_id":1148615527,"push_id":39394916786,"ref":"refs/heads/fix/credential-owner-scoping","head":"2523358ef5d4a30c4a87012782fed6abd0ac7258","before":"eb2a54ce96494c7b5864695bf15eb12e33396379"},"public":true,"created_at":"2026-08-07T18:33:50Z","org":{"id":29134221,"login":"nearai","gravatar_id":"","url":"https://api.github.com/orgs/nearai","avatar_url":"https://avatars.githubusercontent.com/u/29134221?"}},{"id":"17077712413","type":"PushEvent","actor":{"id":4579561,"login":"ddagunts","display_login":"ddagunts","gravatar_id":"","url":"https://api.github.com/users/ddagunts","avatar_url":"https://avatars.githubusercontent.com/u/4579561?"},"repo":{"id":1326828708,"name":"ddagunts/chromesurf","url":"https://api.github.com/repos/ddagunts/chromesurf"},"payload":{"repository_id":1326828708,"push_id":39394580177,"ref":"refs/heads/main","head":"9ce05848b20517582d41e735379a6f71f030e5bb","before":"563c42212f6c68be7619a3efcdfe26b971e3a14c"},"public":true,"created_at":"2026-08-07T18:28:17Z"},{"id":"17077147192","type":"PushEvent","actor":{"id":4579561,"login":"ddagunts","display_login":"ddagunts","gravatar_id":"","url":"https://api.github.com/users/ddagunts","avatar_url":"https://avatars.githubusercontent.com/u/4579561?"},"repo":{"id":1326828708,"name":"ddagunts/chromesurf","url":"https://api.github.com/repos/ddagunts/chromesurf"},"payload":{"repository_id":1326828708,"push_id":39393669233,"ref":"refs/heads/main","head":"5af7aaa391132ec9b24d3bf3540b7d7eeaa13280","before":"c5df83582bb8859369052e13f46a056227ab2088"},"public":true,"created_at":"2026-08-07T18:13:34Z"},{"id":"17076754077","type":"PushEvent","actor":{"id":4579561,"login":"ddagunts","display_login":"ddagunts","gravatar_id":"","url":"https://api.github.com/users/ddagunts","avatar_url":"https://avatars.githubusercontent.com/u/4579561?"},"repo":{"id":1326828708,"name":"ddagunts/chromesurf","url":"https://api.github.com/repos/ddagunts/chromesurf"},"payload":{"repository_id":1326828708,"push_id":39393181887,"ref":"refs/heads/main","head":"c5df83582bb8859369052e13f46a056227ab2088","before":"20f5c4ec27951155711d7f89dff3c526df004a98"},"public":true,"created_at":"2026-08-07T18:05:59Z"},{"id":"17076545736","type":"CreateEvent","actor":{"id":175486,"login":"ilblackdragon","display_login":"ilblackdragon","gravatar_id":"","url":"https://api.github.com/users/ilblackdragon","avatar_url":"https://avatars.githubusercontent.com/u/175486?"},"repo":{"id":1148615527,"name":"nearai/ironclaw","url":"https://api.github.com/repos/nearai/ironclaw"},"payload":{"ref":"fix/credential-owner-scoping","ref_type":"branch","full_ref":"refs/heads/fix/credential-owner-scoping","master_branch":"main","description":"IronClaw is an Agent OS focused on privacy, security and extensibility","pusher_type":"user"},"public":true,"created_at":"2026-08-07T16:19:33Z","org":{"id":29134221,"login":"nearai","gravatar_id":"","url":"https://api.github.com/orgs/nearai","avatar_url":"https://avatars.githubusercontent.com/u/29134221?"}},{"id":"17075264986","type":"PushEvent","actor":{"id":4579561,"login":"ddagunts","display_login":"ddagunts","gravatar_id":"","url":"https://api.github.com/users/ddagunts","avatar_url":"https://avatars.githubusercontent.com/u/4579561?"},"repo":{"id":1326828708,"name":"ddagunts/chromesurf","url":"https://api.github.com/repos/ddagunts/chromesurf"},"payload":{"repository_id":1326828708,"push_id":39392996150,"ref":"refs/heads/main","head":"20f5c4ec27951155711d7f89dff3c526df004a98","before":"07a73ede786d05e648edce716330138a70ec3fd0"},"public":true,"created_at":"2026-08-07T18:03:09Z"},{"id":"17075051180","type":"PushEvent","actor":{"id":4579561,"login":"ddagunts","display_login":"ddagunts","gravatar_id":"","url":"https://api.github.com/users/ddagunts","avatar_url":"https://avatars.githubusercontent.com/u/4579561?"},"repo":{"id":1326828708,"name":"ddagunts/chromesurf","url":"https://api.github.com/repos/ddagunts/chromesurf"},"payload":{"repository_id":1326828708,"push_id":39388753526,"ref":"refs/heads/main","head":"cb2cefd850fb5d9b6bedfb44722424338f3ed9a0","before":"5a6085c8ab6f8e9f3505f13575203bb3369b8c07"},"public":true,"created_at":"2026-08-07T16:59:52Z"},{"id":"17073777758","type":"PushEvent","actor":{"id":175486,"login":"ilblackdragon","display_login":"ilblackdragon","gravatar_id":"","url":"https://api.github.com/users/ilblackdragon","avatar_url":"https://avatars.githubusercontent.com/u/175486?"},"repo":{"id":1148615527,"name":"nearai/ironclaw","url":"https://api.github.com/repos/nearai/ironclaw"},"payload":{"repository_id":1148615527,"push_id":39391084110,"ref":"refs/heads/fix/credential-owner-scoping","head":"eb2a54ce96494c7b5864695bf15eb12e33396379","before":"523c7ade3e3b83a952d84247710a9c2973753e3b"},"public":true,"created_at":"2026-08-07T17:34:53Z","org":{"id":29134221,"login":"nearai","gravatar_id":"","url":"https://api.github.com/orgs/nearai","avatar_url":"https://avatars.githubusercontent.com/u/29134221?"}},{"id":"17073288811","type":"PushEvent","actor":{"id":175486,"login":"ilblackdragon","display_login":"ilblackdragon","gravatar_id":"","url":"https://api.github.com/users/ilblackdragon","avatar_url":"https://avatars.githubusercontent.com/u/175486?"},"repo":{"id":1148615527,"name":"nearai/ironclaw","url":"https://api.github.com/repos/nearai/ironclaw"},"payload":{"repository_id":1148615527,"push_id":39395463095,"ref":"refs/heads/fix/credential-owner-scoping","head":"1f8bbbd927ceac7175622702fa0aef8ab9bd1a34","before":"2523358ef5d4a30c4a87012782fed6abd0ac7258"},"public":true,"created_at":"2026-08-07T18:43:05Z","org":{"id":29134221,"login":"nearai","gravatar_id":"","url":"https://api.github.com/orgs/nearai","avatar_url":"https://avatars.githubusercontent.com/u/29134221?"}},{"id":"12951740735","type":"ReleaseEvent","actor":{"id":4579561,"login":"ddagunts","display_login":"ddagunts","gravatar_id":"","url":"https://api.github.com/users/ddagunts","avatar_url":"https://avatars.githubusercontent.com/u/4579561?"},"repo":{"id":1326828708,"name":"ddagunts/chromesurf","url":"https://api.github.com/repos/ddagunts/chromesurf"},"payload":{"action":"published","release":{"url":"https://api.github.com/repos/ddagunts/chromesurf/releases/366944798","assets_url":"https://api.github.com/repos/ddagunts/chromesurf/releases/366944798/assets","upload_url":"https://uploads.github.com/repos/ddagunts/chromesurf/releases/366944798/assets{?name,label}","html_url":"https://github.com/ddagunts/chromesurf/releases/tag/v0.4.1","id":366944798,"author":{"login":"ddagunts","id":4579561,"node_id":"MDQ6VXNlcjQ1Nzk1NjE=","avatar_url":"https://avatars.githubusercontent.com/u/4579561?v=4","gravatar_id":"","url":"https://api.github.com/users/ddagunts","html_url":"https://github.com/ddagunts","followers_url":"https://api.github.com/users/ddagunts/followers","following_url":"https://api.github.com/users/ddagunts/following{/other_user}","gists_url":"https://api.github.com/users/ddagunts/gists{/gist_id}","starred_url":"https://api.github.com/users/ddagunts/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ddagunts/subscriptions","organizations_url":"https://api.github.com/users/ddagunts/orgs","repos_url":"https://api.github.com/users/ddagunts/repos","events_url":"https://api.github.com/users/ddagunts/events{/privacy}","received_events_url":"https://api.github.com/users/ddagunts/received_events","type":"User","user_view_type":"public","site_admin":false},"node_id":"RE_kwDOTxXMpM4V3yIe","tag_name":"v0.4.1","target_commitish":"main","name":"chromesurf 0.4.1 — bookmarks back, simpler appearance","draft":false,"immutable":false,"prerelease":false,"created_at":"2026-08-07T18:41:03Z","updated_at":"2026-08-07T18:41:06Z","published_at":"2026-08-07T18:41:06Z","assets":[{"url":"https://api.github.com/repos/ddagunts/chromesurf/releases/assets/505522821","id":505522821,"node_id":"RA_kwDOTxXMpM4eIaqF","name":"chromesurf-0.4.1.apk","label":"","uploader":{"login":"ddagunts","id":4579561,"node_id":"MDQ6VXNlcjQ1Nzk1NjE=","avatar_url":"https://avatars.githubusercontent.com/u/4579561?v=4","gravatar_id":"","url":"https://api.github.com/users/ddagunts","html_url":"https://github.com/ddagunts","followers_url":"https://api.github.com/users/ddagunts/followers","following_url":"https://api.github.com/users/ddagunts/following{/other_user}","gists_url":"https://api.github.com/users/ddagunts/gists{/gist_id}","starred_url":"https://api.github.com/users/ddagunts/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ddagunts/subscriptions","organizations_url":"https://api.github.com/users/ddagunts/orgs","repos_url":"https://api.github.com/users/ddagunts/repos","events_url":"https://api.github.com/users/ddagunts/events{/privacy}","received_events_url":"https://api.github.com/users/ddagunts/received_events","type":"User","user_view_type":"public","site_admin":false},"content_type":"application/vnd.android.package-archive","state":"uploaded","size":13301337,"digest":"sha256:1360b63d38f0ac1581c6ea27bad8e09aee110289343e6317d68de476f73178e8","download_count":2,"created_at":"2026-08-07T18:41:05Z","updated_at":"2026-08-07T18:41:06Z","browser_download_url":"https://github.com/ddagunts/chromesurf/releases/download/v0.4.1/chromesurf-0.4.1.apk"}],"tarball_url":"https://api.github.com/repos/ddagunts/chromesurf/tarball/v0.4.1","zipball_url":"https://api.github.com/repos/ddagunts/chromesurf/zipball/v0.4.1","body":"### Bookmarks weren't lost — they were off-screen\nThe root layout was never scrollable. Every control card added since 0.2.0 pushed the\nbookmarks further past the bottom of the screen, with no way to scroll to them. Nothing\nwas ever deleted; your saved URLs are still in storage and reappear on update.\n\nThe list also had to stop being a `LazyColumn`, since a lazy list inside a scrolling\nparent is measured with unbounded height and throws.\n\n### Appearance is now one button\nJust **Invert colors**. The Day/Night chips were disabled on this hardware anyway — the\ndevice ignores the colour-scheme hint — so they amounted to three controls and two\nparagraphs explaining why none of them did anything.\n\nThe `theme` message stays in the receiver protocol and is reachable from `castctl`, for\nCast devices whose browser does support it. Every status reply carries\n`colorSchemePropagates` so you can tell.","short_description_html":"<h3>Bookmarks weren't lost — they were off-screen</h3>\n<p>The root layout was never scrollable. Every control card added since 0.2.0 pushed the<br>\nbookmarks further past the bottom of the screen, with no way to s…</p>","is_short_description_html_truncated":true}},"public":true,"created_at":"2026-08-07T18:41:06Z"},{"id":"12951198103","type":"ReleaseEvent","actor":{"id":4579561,"login":"ddagunts","display_login":"ddagunts","gravatar_id":"","url":"https://api.github.com/users/ddagunts","avatar_url":"https://avatars.githubusercontent.com/u/4579561?"},"repo":{"id":1326828708,"name":"ddagunts/chromesurf","url":"https://api.github.com/repos/ddagunts/chromesurf"},"payload":{"action":"published","release":{"url":"https://api.github.com/repos/ddagunts/chromesurf/releases/366938598","assets_url":"https://api.github.com/repos/ddagunts/chromesurf/releases/366938598/assets","upload_url":"https://uploads.github.com/repos/ddagunts/chromesurf/releases/366938598/assets{?name,label}","html_url":"https://github.com/ddagunts/chromesurf/releases/tag/v0.4.0","id":366938598,"author":{"login":"ddagunts","id":4579561,"node_id":"MDQ6VXNlcjQ1Nzk1NjE=","avatar_url":"https://avatars.githubusercontent.com/u/4579561?v=4","gravatar_id":"","url":"https://api.github.com/users/ddagunts","html_url":"https://github.com/ddagunts","followers_url":"https://api.github.com/users/ddagunts/followers","following_url":"https://api.github.com/users/ddagunts/following{/other_user}","gists_url":"https://api.github.com/users/ddagunts/gists{/gist_id}","starred_url":"https://api.github.com/users/ddagunts/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ddagunts/subscriptions","organizations_url":"https://api.github.com/users/ddagunts/orgs","repos_url":"https://api.github.com/users/ddagunts/repos","events_url":"https://api.github.com/users/ddagunts/events{/privacy}","received_events_url":"https://api.github.com/users/ddagunts/received_events","type":"User","user_view_type":"public","site_admin":false},"node_id":"RE_kwDOTxXMpM4V3wnm","tag_name":"v0.4.0","target_commitish":"main","name":"chromesurf 0.4.0 — daytime appearance","draft":false,"immutable":false,"prerelease":false,"created_at":"2026-08-07T18:28:16Z","updated_at":"2026-08-07T18:28:19Z","published_at":"2026-08-07T18:28:19Z","assets":[{"url":"https://api.github.com/repos/ddagunts/chromesurf/releases/assets/505510053","id":505510053,"node_id":"RA_kwDOTxXMpM4eIXil","name":"chromesurf-0.4.0.apk","label":"","uploader":{"login":"ddagunts","id":4579561,"node_id":"MDQ6VXNlcjQ1Nzk1NjE=","avatar_url":"https://avatars.githubusercontent.com/u/4579561?v=4","gravatar_id":"","url":"https://api.github.com/users/ddagunts","html_url":"https://github.com/ddagunts","followers_url":"https://api.github.com/users/ddagunts/followers","following_url":"https://api.github.com/users/ddagunts/following{/other_user}","gists_url":"https://api.github.com/users/ddagunts/gists{/gist_id}","starred_url":"https://api.github.com/users/ddagunts/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ddagunts/subscriptions","organizations_url":"https://api.github.com/users/ddagunts/orgs","repos_url":"https://api.github.com/users/ddagunts/repos","events_url":"https://api.github.com/users/ddagunts/events{/privacy}","received_events_url":"https://api.github.com/users/ddagunts/received_events","type":"User","user_view_type":"public","site_admin":false},"content_type":"application/vnd.android.package-archive","state":"uploaded","size":13317721,"digest":"sha256:2203aa9614a244578e5423dddb902602cb941910045a5a3faf10a2474b642f04","download_count":1,"created_at":"2026-08-07T18:28:18Z","updated_at":"2026-08-07T18:28:19Z","browser_download_url":"https://github.com/ddagunts/chromesurf/releases/download/v0.4.0/chromesurf-0.4.0.apk"}],"tarball_url":"https://api.github.com/repos/ddagunts/chromesurf/tarball/v0.4.0","zipball_url":"https://api.github.com/repos/ddagunts/chromesurf/zipball/v0.4.0","body":"Adds an appearance control for forcing a site's daytime look.\n\nCast devices report `prefers-color-scheme: dark` — confirmed on this hardware,\n`prefersDark: true` — so any site with a dark theme serves it, which is wrong for a\nroom in daylight.\n\n### Two mechanisms, because the clean one doesn't work here\n**Day / Night** declares `color-scheme` on the iframe. The CSS Working Group resolved\nthat colour-scheme propagates into cross-origin iframes, but it's newer than the\nChromium on these devices. The receiver probes its own support at boot with a\nthrowaway same-origin frame; on `CrKey/1.56.500000` (Chrome 92) it reports\n`colorSchemePropagates: false`, and the app **disables the Day/Night chips** rather\nthan leaving controls that quietly do nothing.\n\n**Invert** always works, because the filter is applied by the receiver rather than\nrequested from the site — `invert(1) hue-rotate(180deg)`, turning a light-on-dark page\ninto dark-on-light with hues roughly preserved. It inverts photos and video too, and\ndarkens sites that were already light, so it's a toggle rather than a set-and-forget.\n\nVerified on the device: the invert toggle round-trips and applies.","short_description_html":"<p>Adds an appearance control for forcing a site's daytime look.</p>\n<p>Cast devices report <code>prefers-color-scheme: dark</code> — confirmed on this hardware,<br>\n<code>prefersDark: true</code> — so any site with a dark theme serves i…</p>","is_short_description_html_truncated":true}},"public":true,"created_at":"2026-08-07T18:28:19Z"},{"id":"17070918916","type":"PushEvent","actor":{"id":4579561,"login":"ddagunts","display_login":"ddagunts","gravatar_id":"","url":"https://api.github.com/users/ddagunts","avatar_url":"https://avatars.githubusercontent.com/u/4579561?"},"repo":{"id":1326828708,"name":"ddagunts/chromesurf","url":"https://api.github.com/repos/ddagunts/chromesurf"},"payload":{"repository_id":1326828708,"push_id":39387236996,"ref":"refs/heads/main","head":"5a6085c8ab6f8e9f3505f13575203bb3369b8c07","before":"67acb22da109026d833900a23b9d7f6dffa8c9c7"},"public":true,"created_at":"2026-08-07T16:36:58Z"},{"id":"12950574790","type":"ReleaseEvent","actor":{"id":4579561,"login":"ddagunts","display_login":"ddagunts","gravatar_id":"","url":"https://api.github.com/users/ddagunts","avatar_url":"https://avatars.githubusercontent.com/u/4579561?"},"repo":{"id":1326828708,"name":"ddagunts/chromesurf","url":"https://api.github.com/repos/ddagunts/chromesurf"},"payload":{"action":"published","release":{"url":"https://api.github.com/repos/ddagunts/chromesurf/releases/366931843","assets_url":"https://api.github.com/repos/ddagunts/chromesurf/releases/366931843/assets","upload_url":"https://uploads.github.com/repos/ddagunts/chromesurf/releases/366931843/assets{?name,label}","html_url":"https://github.com/ddagunts/chromesurf/releases/tag/v0.3.1","id":366931843,"author":{"login":"ddagunts","id":4579561,"node_id":"MDQ6VXNlcjQ1Nzk1NjE=","avatar_url":"https://avatars.githubusercontent.com/u/4579561?v=4","gravatar_id":"","url":"https://api.github.com/users/ddagunts","html_url":"https://github.com/ddagunts","followers_url":"https://api.github.com/users/ddagunts/followers","following_url":"https://api.github.com/users/ddagunts/following{/other_user}","gists_url":"https://api.github.com/users/ddagunts/gists{/gist_id}","starred_url":"https://api.github.com/users/ddagunts/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ddagunts/subscriptions","organizations_url":"https://api.github.com/users/ddagunts/orgs","repos_url":"https://api.github.com/users/ddagunts/repos","events_url":"https://api.github.com/users/ddagunts/events{/privacy}","received_events_url":"https://api.github.com/users/ddagunts/received_events","type":"User","user_view_type":"public","site_admin":false},"node_id":"RE_kwDOTxXMpM4V3u-D","tag_name":"v0.3.1","target_commitish":"main","name":"chromesurf 0.3.1 — explain blank screens","draft":false,"immutable":false,"prerelease":false,"created_at":"2026-08-07T18:13:34Z","updated_at":"2026-08-07T18:13:37Z","published_at":"2026-08-07T18:13:37Z","assets":[{"url":"https://api.github.com/repos/ddagunts/chromesurf/releases/assets/505494739","id":505494739,"node_id":"RA_kwDOTxXMpM4eITzT","name":"chromesurf-0.3.1.apk","label":"","uploader":{"login":"ddagunts","id":4579561,"node_id":"MDQ6VXNlcjQ1Nzk1NjE=","avatar_url":"https://avatars.githubusercontent.com/u/4579561?v=4","gravatar_id":"","url":"https://api.github.com/users/ddagunts","html_url":"https://github.com/ddagunts","followers_url":"https://api.github.com/users/ddagunts/followers","following_url":"https://api.github.com/users/ddagunts/following{/other_user}","gists_url":"https://api.github.com/users/ddagunts/gists{/gist_id}","starred_url":"https://api.github.com/users/ddagunts/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ddagunts/subscriptions","organizations_url":"https://api.github.com/users/ddagunts/orgs","repos_url":"https://api.github.com/users/ddagunts/repos","events_url":"https://api.github.com/users/ddagunts/events{/privacy}","received_events_url":"https://api.github.com/users/ddagunts/received_events","type":"User","user_view_type":"public","site_admin":false},"content_type":"application/vnd.android.package-archive","state":"uploaded","size":13301333,"digest":"sha256:81cf605eac8c86c5932b64fa19be1256a69e786eba135834ebe5f141fd321bfb","download_count":1,"created_at":"2026-08-07T18:13:36Z","updated_at":"2026-08-07T18:13:37Z","browser_download_url":"https://github.com/ddagunts/chromesurf/releases/download/v0.3.1/chromesurf-0.3.1.apk"}],"tarball_url":"https://api.github.com/repos/ddagunts/chromesurf/tarball/v0.3.1","zipball_url":"https://api.github.com/repos/ddagunts/chromesurf/zipball/v0.3.1","body":"Explains the blank TV that `google.com` and `yahoo.com` produced.\n\nThe receiver was reporting `ok` for pages that never rendered — and it cannot do\nbetter. When Chrome refuses a frame for `X-Frame-Options` or CSP `frame-ancestors`,\nit commits an opaque error document, so from the parent page `contentDocument` reads\n`null`: exactly what a *successful* cross-origin load looks like. No browser API\nseparates the two.\n\nSo the check moved to the phone, which can read response headers directly. It does a\nHEAD (falling back to GET for servers that reject HEAD), parses `X-Frame-Options` and\nCSP `frame-ancestors` against the receiver's own origin, and warns:\n\n> Likely blank on the TV: the site sends X-Frame-Options: SAMEORIGIN.\n\nIt runs alongside sending rather than gating it — the phone is a different client on a\ndifferent connection, so its answer is advisory and must never keep a page off the\nscreen.\n\nCovered by unit tests: DENY, SAMEORIGIN against both third-party and same-origin\ntargets, `frame-ancestors` with `'none'`, `*`, explicit origins, wildcard subdomains,\nand CSP correctly overriding X-Frame-Options.","short_description_html":"<p>Explains the blank TV that <code>google.com</code> and <code>yahoo.com</code> produced.</p>\n<p>The receiver was reporting <code>ok</code> for pages that never rendered — and it cannot do<br>\nbetter. When Chrome refuses a frame for <code>X-Frame-Options</code> …</p>","is_short_description_html_truncated":true}},"public":true,"created_at":"2026-08-07T18:13:37Z"},{"id":"12950253332","type":"ReleaseEvent","actor":{"id":4579561,"login":"ddagunts","display_login":"ddagunts","gravatar_id":"","url":"https://api.github.com/users/ddagunts","avatar_url":"https://avatars.githubusercontent.com/u/4579561?"},"repo":{"id":1326828708,"name":"ddagunts/chromesurf","url":"https://api.github.com/repos/ddagunts/chromesurf"},"payload":{"action":"published","release":{"url":"https://api.github.com/repos/ddagunts/chromesurf/releases/366927892","assets_url":"https://api.github.com/repos/ddagunts/chromesurf/releases/366927892/assets","upload_url":"https://uploads.github.com/repos/ddagunts/chromesurf/releases/366927892/assets{?name,label}","html_url":"https://github.com/ddagunts/chromesurf/releases/tag/v0.3.0","id":366927892,"author":{"login":"ddagunts","id":4579561,"node_id":"MDQ6VXNlcjQ1Nzk1NjE=","avatar_url":"https://avatars.githubusercontent.com/u/4579561?v=4","gravatar_id":"","url":"https://api.github.com/users/ddagunts","html_url":"https://github.com/ddagunts","followers_url":"https://api.github.com/users/ddagunts/followers","following_url":"https://api.github.com/users/ddagunts/following{/other_user}","gists_url":"https://api.github.com/users/ddagunts/gists{/gist_id}","starred_url":"https://api.github.com/users/ddagunts/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ddagunts/subscriptions","organizations_url":"https://api.github.com/users/ddagunts/orgs","repos_url":"https://api.github.com/users/ddagunts/repos","events_url":"https://api.github.com/users/ddagunts/events{/privacy}","received_events_url":"https://api.github.com/users/ddagunts/received_events","type":"User","user_view_type":"public","site_admin":false},"node_id":"RE_kwDOTxXMpM4V3uAU","tag_name":"v0.3.0","target_commitish":"main","name":"chromesurf 0.3.0 — scroll and pan","draft":false,"immutable":false,"prerelease":false,"created_at":"2026-08-07T18:05:58Z","updated_at":"2026-08-07T18:06:03Z","published_at":"2026-08-07T18:06:03Z","assets":[{"url":"https://api.github.com/repos/ddagunts/chromesurf/releases/assets/505486109","id":505486109,"node_id":"RA_kwDOTxXMpM4eIRsd","name":"chromesurf-0.3.0.apk","label":"","uploader":{"login":"ddagunts","id":4579561,"node_id":"MDQ6VXNlcjQ1Nzk1NjE=","avatar_url":"https://avatars.githubusercontent.com/u/4579561?v=4","gravatar_id":"","url":"https://api.github.com/users/ddagunts","html_url":"https://github.com/ddagunts","followers_url":"https://api.github.com/users/ddagunts/followers","following_url":"https://api.github.com/users/ddagunts/following{/other_user}","gists_url":"https://api.github.com/users/ddagunts/gists{/gist_id}","starred_url":"https://api.github.com/users/ddagunts/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ddagunts/subscriptions","organizations_url":"https://api.github.com/users/ddagunts/orgs","repos_url":"https://api.github.com/users/ddagunts/repos","events_url":"https://api.github.com/users/ddagunts/events{/privacy}","received_events_url":"https://api.github.com/users/ddagunts/received_events","type":"User","user_view_type":"public","site_admin":false},"content_type":"application/vnd.android.package-archive","state":"uploaded","size":13301337,"digest":"sha256:f03e03fd0977d3213573ee5d226f183fcc08aa339cea7b7566cad9e523cc09f8","download_count":0,"created_at":"2026-08-07T18:06:00Z","updated_at":"2026-08-07T18:06:02Z","browser_download_url":"https://github.com/ddagunts/chromesurf/releases/download/v0.3.0/chromesurf-0.3.0.apk"}],"tarball_url":"https://api.github.com/repos/ddagunts/chromesurf/tarball/v0.3.0","zipball_url":"https://api.github.com/repos/ddagunts/chromesurf/zipball/v0.3.0","body":"Adds directional controls for moving around a page, and fixes the wrapped button labels.\n\n### Scrolling and panning\nA cross-origin iframe can't be scrolled from the parent — the browser blocks it. So the\npage is laid out on a canvas taller than the screen (4 screens by default) and the frame\nslides underneath a fixed viewport.\n\n**Zoom changes meaning as a result.** It now magnifies the canvas instead of re-laying the\npage out at a narrower width, so zooming no longer flips sites into their mobile\nbreakpoint — and zooming past 1 is what creates the room to pan left and right at all. At\nzoom 1 there is nothing to pan horizontally, and the arrows grey out to say so.\n\nThe trade-off: the page's viewport is 4 screens tall, so `100vh` elements render\nproportionally taller and sticky headers sit at the top of the canvas. `canvasScreensTall`\nin `docs/config.js` tunes or disables this.\n\n### UI\nHome and Reload shared a row with the zoom buttons and were squeezed until their labels\nwrapped mid-word. They now get their own row, with pan and zoom in a directional pad below.\n\n### Also\n`tools/castctl.py` drives a receiver from the command line using the same protocol —\nhandy for testing, and as the basis for an unattended sender.\n\nVerified against a live receiver: pan steps, zoom, and clamping at the canvas edges.","short_description_html":"<p>Adds directional controls for moving around a page, and fixes the wrapped button labels.</p>\n<h3>Scrolling and panning</h3>\n<p>A cross-origin iframe can't be scrolled from the parent — the browser blocks it. So the<br>\n…</p>","is_short_description_html_truncated":true}},"public":true,"created_at":"2026-08-07T18:06:03Z"},{"id":"12948403676","type":"ReleaseEvent","actor":{"id":4579561,"login":"ddagunts","display_login":"ddagunts","gravatar_id":"","url":"https://api.github.com/users/ddagunts","avatar_url":"https://avatars.githubusercontent.com/u/4579561?"},"repo":{"id":1326828708,"name":"ddagunts/chromesurf","url":"https://api.github.com/repos/ddagunts/chromesurf"},"payload":{"action":"published","release":{"url":"https://api.github.com/repos/ddagunts/chromesurf/releases/366905650","assets_url":"https://api.github.com/repos/ddagunts/chromesurf/releases/366905650/assets","upload_url":"https://uploads.github.com/repos/ddagunts/chromesurf/releases/366905650/assets{?name,label}","html_url":"https://github.com/ddagunts/chromesurf/releases/tag/v0.2.1","id":366905650,"author":{"login":"ddagunts","id":4579561,"node_id":"MDQ6VXNlcjQ1Nzk1NjE=","avatar_url":"https://avatars.githubusercontent.com/u/4579561?v=4","gravatar_id":"","url":"https://api.github.com/users/ddagunts","html_url":"https://github.com/ddagunts","followers_url":"https://api.github.com/users/ddagunts/followers","following_url":"https://api.github.com/users/ddagunts/following{/other_user}","gists_url":"https://api.github.com/users/ddagunts/gists{/gist_id}","starred_url":"https://api.github.com/users/ddagunts/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ddagunts/subscriptions","organizations_url":"https://api.github.com/users/ddagunts/orgs","repos_url":"https://api.github.com/users/ddagunts/repos","events_url":"https://api.github.com/users/ddagunts/events{/privacy}","received_events_url":"https://api.github.com/users/ddagunts/received_events","type":"User","user_view_type":"public","site_admin":false},"node_id":"RE_kwDOTxXMpM4V3oky","tag_name":"v0.2.1","target_commitish":"main","name":"chromesurf 0.2.1 — real error messages","draft":false,"immutable":false,"prerelease":false,"created_at":"2026-08-07T17:23:49Z","updated_at":"2026-08-07T17:23:52Z","published_at":"2026-08-07T17:23:52Z","assets":[{"url":"https://api.github.com/repos/ddagunts/chromesurf/releases/assets/505441497","id":505441497,"node_id":"RA_kwDOTxXMpM4eIGzZ","name":"chromesurf-0.2.1.apk","label":"","uploader":{"login":"ddagunts","id":4579561,"node_id":"MDQ6VXNlcjQ1Nzk1NjE=","avatar_url":"https://avatars.githubusercontent.com/u/4579561?v=4","gravatar_id":"","url":"https://api.github.com/users/ddagunts","html_url":"https://github.com/ddagunts","followers_url":"https://api.github.com/users/ddagunts/followers","following_url":"https://api.github.com/users/ddagunts/following{/other_user}","gists_url":"https://api.github.com/users/ddagunts/gists{/gist_id}","starred_url":"https://api.github.com/users/ddagunts/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ddagunts/subscriptions","organizations_url":"https://api.github.com/users/ddagunts/orgs","repos_url":"https://api.github.com/users/ddagunts/repos","events_url":"https://api.github.com/users/ddagunts/events{/privacy}","received_events_url":"https://api.github.com/users/ddagunts/received_events","type":"User","user_view_type":"public","site_admin":false},"content_type":"application/vnd.android.package-archive","state":"uploaded","size":13301333,"digest":"sha256:4efc2618929aa025d08722a3749663d84435a9b5cf1ac5f6c9ac95a0b202fcdf","download_count":3,"created_at":"2026-08-07T17:23:50Z","updated_at":"2026-08-07T17:23:52Z","browser_download_url":"https://github.com/ddagunts/chromesurf/releases/download/v0.2.1/chromesurf-0.2.1.apk"}],"tarball_url":"https://api.github.com/repos/ddagunts/chromesurf/tarball/v0.2.1","zipball_url":"https://api.github.com/repos/ddagunts/chromesurf/zipball/v0.2.1","body":"First release where the whole chain is confirmed working on hardware: discovery,\nTLS control session, LAUNCH, custom-namespace round trip, and `https://` pages\nrendering full-screen.\n\n### Fixes\nTwo different failures both looked like an unexplained wait.\n\n- **`http://` URLs \"timed out\".** They were blocked as mixed content — the receiver\n  is served over HTTPS, so Chrome refuses to embed an `http://` page before any\n  request goes out. The frame neither loads nor errors, so the watchdog reported a\n  meaningless 8-second timeout. Now detected up front and named.\n- **A device refusing the App ID stalled for 10s.** `LAUNCH_ERROR` was never parsed,\n  even though the device answers `NOT_FOUND` in well under a second. Now fails\n  immediately with the reason and what to do about it.\n\n### Measured baseline\n`Chrome/92.0.4515.0 CrKey/1.56.500000 DeviceType/AndroidTV` — mid-2021 Chrome, so\ndashboard pages can use ES2015+, grid and custom properties freely.","short_description_html":"<p>First release where the whole chain is confirmed working on hardware: discovery,<br>\nTLS control session, LAUNCH, custom-namespace round trip, and <code>https://</code> pages<br>\nrendering full-screen.</p>\n<h3>Fixes</h3>\n<p>Two differ…</p>","is_short_description_html_truncated":true}},"public":true,"created_at":"2026-08-07T17:23:52Z"},{"id":"17065647141","type":"PushEvent","actor":{"id":4579561,"login":"ddagunts","display_login":"ddagunts","gravatar_id":"","url":"https://api.github.com/users/ddagunts","avatar_url":"https://avatars.githubusercontent.com/u/4579561?"},"repo":{"id":1326828708,"name":"ddagunts/chromesurf","url":"https://api.github.com/repos/ddagunts/chromesurf"},"payload":{"repository_id":1326828708,"push_id":39390335331,"ref":"refs/heads/main","head":"07a73ede786d05e648edce716330138a70ec3fd0","before":"9fe5820a300c7455978a11aa81c832585053a13a"},"public":true,"created_at":"2026-08-07T17:23:49Z"}]