Skip to content

Add optional Hagency support and Robrix2 UI styling - #1068

Open
ymote wants to merge 16 commits into
mainfrom
ymote/hagency-ui
Open

ymote wants to merge 16 commits into
mainfrom
ymote/hagency-ui

Conversation

@ymote

@ymote ymote commented Sep 14, 2026

Copy link
Copy Markdown

Summary

Robrix currently drops Hagency approval message types and rejects its workflow slash commands. This PR adds optional Hagency support and ports the Robrix2 interface styling for the navigation, room list, composer, and settings.

Hagency support

  • Add the opt-in agent_chat Cargo feature, with native approval cards, expiry handling, and encrypted verdict delivery.
  • Accept com.agentchat, com.hagency, and com.hafleet approval events, preserving the request namespace and original binding fields in verdicts. Authorization remains enforced by the bridge.
  • Add agent badges, readable approval previews, companion bridge invitations, and workflow/thread command completion. Slash commands also require the Settings toggle and matching room membership.
  • Apply settings changes to existing composers and record approval send results even after a room closes.

UI changes

  • Port shared RBX design tokens, a dark navigation rail, teal dock tabs, and updated rooms-sidebar styling.
  • Add a composer toolbar, quick emoji row, teal Send button, desktop account menu, and tabbed Settings.
  • Integrate with current upstream SpeechTextInput widgets and preserve dictation in the composer and room filters.

The shared UI styling applies to all builds; the Hagency protocol and agent widgets are gated by agent_chat.

Validation

  • typos --config .github/typos.toml and git diff --check pass.
  • cargo test --locked --lib --features agent_chat: 127 passed on the final branch.
  • cargo test --locked --offline --lib --no-default-features: 105 passed on the final branch.
  • cargo clippy --locked --workspace --all-features --all-targets -- -D warnings: passed.

Build with cargo run --features agent_chat; enable workflow commands in Settings → Preferences.

Scope

Long-reply folding, streaming animation, and agent_ops client sessions are not included. The documented live homeserver soak results are historical; no fresh live homeserver end-to-end test has been run for this PR.

ymote and others added 16 commits September 10, 2026 19:02
…ature

Robrix can now act as the human-facing client for the hagency control plane
(https://github.com/hagency-org/hagency), which runs Claude Code and Codex
coding agents and exposes them in Matrix rooms through a bridge bot. All of it
is behind the new `agent_chat` cargo feature; default builds only gain empty
placeholder widgets (`src/agent_chat_dummy.rs`), mirroring `tsp`/`tsp_dummy`.

* Owner approval cards: `com.agentchat.approval.request.v1` events render as a
  card with tool, command preview, expiry, and the 2-4 decision buttons the
  bridge offered (`approve_once`, `approve_task`, `approve_always`, `deny`).
  Clicking one sends `com.agentchat.approval.verdict.v1` echoing every binding
  field, after refreshing the bridge's device keys and rotating the outbound
  Megolm session. Binding fields are read only from the original event;
  malformed requests and status/verdict notices never show buttons.
* Timeline event filter extended so the SDK does not drop those msgtypes.
* Inviting `@ac_<team>_<role>` also invites `@agent-bridge-<team>` (and the
  legacy `@agent-bridge`), best-effort.
* Agent messages get a role / message-kind badge after the sender name; the
  bridge's leading type emoji and trailing permalink line are stripped.
* `/create-issue`, `/go`, `/review`, `/status` (coordinator present) and
  `/task`, `/thread` (any agent present) are offered in the `/` popup and sent
  as plain text, gated by a new Settings -> Preferences toggle.
* 19 unit tests for the protocol, identity, presentation, and command logic;
  docs in docs/agent-chat.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXUgAs5yG46jKcgcPSJ2Ym
…ms list

Found by driving the real client against a real Palpo homeserver, which no unit
test could have surfaced: agent-chat approval events are custom msgtypes, so
they fell through ruma's `MessageType::_Custom` arm in `event_preview.rs` and
rendered as

    soak_owner: [Custom message]: CustomMessageContent { msgtype: ...

in the rooms list. The bridge already writes a human-readable `body`; show that
instead, gated on the feature so default builds are untouched. The fields are
private, so this reads them through `MessageType`'s public accessors.

Also from the same soak:

* `src/agent_chat/testdata/` holds the exact request and verdict bytes a real
  Palpo stored and returned, with a test asserting they parse into a card and
  that the verdict this client builds matches the one the server accepted,
  field for field.
* `tools/agentchat-probe/probe_approval.py` drives a running app through
  makepad's `--remote` HTTP control surface, which injects input through the
  same path a human click takes. It opens the room, asserts the card, clicks a
  decision button, and confirms the resulting verdict event on the homeserver.
* docs record the verified results (17/17 wire checks, 15/15 UI checks) and the
  upstream makepad bug that rules out the headless renderer on macOS:
  `os/cx_shared.rs` calls `os::apple::metal::note_input_event()` under
  `cfg(target_vendor = "apple")` while `os/mod.rs` gates `pub mod apple;` behind
  `not(headless)`, so headless builds fail with E0433.

122 lib tests pass with the feature on, 102 with it off; clippy clean in both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXUgAs5yG46jKcgcPSJ2Ym
…just com.agentchat

A real soak against the live HAFleet deployment on the remote mini found that
its bridge (v1.2.0) emits `com.hafleet.approval.request.v1` with content key
`com.hafleet.approval`, and accepts verdicts only in that namespace — there is
no `com.agentchat.*` compatibility. So this client (and upstream robrix2, and
the hagency master the port was based on), which parsed only `com.agentchat.*`,
would render NOTHING against that deployment: the timeline filter drops the
events and the parser never fires.

Make the approval layer namespace-agnostic. A `Namespace` derives the event key
and the request/status/verdict msgtypes from a base, and the client accepts
`com.agentchat`, `com.hafleet` and `com.hagency`. A parsed request remembers its
namespace, and `verdict_content` echoes it — the wire name is the bridge's to
choose, and a verdict in the wrong namespace is ignored by the bridge.

Verified end to end:
* server side, against the live HAFleet backend: owner binding, forged-sender
  rejection, digest-tamper rejection, owner approval, single-use replay
  rejection, bridge-secret enforcement (6/6).
* client side, driving the real UI over makepad's remote bridge with a real
  `com.hafleet.*` request on a real Palpo: card renders, Approve once emits a
  real `com.hafleet.approval.verdict.v1` from the owner (3/3).

122 lib tests pass with the feature (incl. a HAFleet-namespace test), 102
without; clippy clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXUgAs5yG46jKcgcPSJ2Ym
The port targeted upstream Robrix, so its approval card and agent badge fell
back to upstream's older visual language and three hardcoded hex colours,
which robrix2's own rules forbid. Restyle just the agent-chat surfaces onto
robrix2's design-token recipes without porting the whole design system:

* `src/agent_chat/tokens.rs` defines only the RBX_* tokens these surfaces use,
  with the values copied from robrix2 under the same names, so a future port
  of the full token layer replaces them in place. Typography keeps robrix2's
  sizes, weights and line spacing on the theme fonts, because robrix2's
  RBX_FONT_* styles load custom font files upstream does not ship.
* The card follows robrix2's AgentApprovalCard recipe (warning-tinted surface,
  RBX_RADIUS_SM, card-title colour, pill status badge, meta hint), and the
  buttons its Primary/Secondary/Danger recipes (RBX_CONTROL_H_MD, 1px semantic
  stroke, tinted surface).
* The agent badge is sized like robrix2's bot badge (16px, 3px radius) and
  takes the accent pair for workflow roles and the neutral pair otherwise,
  applied at populate time since portal-list items are recycled.

No hardcoded hex remains in the card. Verified visually through the remote
bridge against a live request. 123 lib tests with the feature, 102 without;
clippy clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXUgAs5yG46jKcgcPSJ2Ym
robrix2 opens a "Feishu-style" account menu from a dedicated button at the
bottom-left of the desktop rail (robrix2 PR #265); upstream has no such menu,
only the top avatar that opens Settings directly. Port the menu structure:

* `src/home/account_menu.rs`: an anchored overlay card that opens upward from
  the rail — active-account header (avatar, display name, user ID, "Active"),
  a divider, "Account Settings" and a danger-styled "Log Out". Same
  scrim/anchor/clamp pattern as the room context menu; closes on backdrop
  click, Escape, back gesture, or a desktop/mobile layout change.
* A new `AccountSwitcherButton` at the bottom of the desktop rail, built on
  upstream's own `NavigationTabButton` recipe so it shares the rail's hover
  animation. The top avatar keeps opening Settings, unchanged.
* The App hosts and positions the overlay next to its other menus.

robrix2's menu also lists other logged-in accounts and "Log Into More
Accounts"; both depend on its multi-account layer (`account_manager`,
`request_switch_account`), which upstream does not have — it holds exactly
one session. Those rows are deliberately not rendered; the card keeps the same
shape so they can slot back in if multi-account is ever ported.

The RBX token slice moves from the feature-gated agent-chat module to
`src/shared/rbx_tokens.rs` (always compiled) since this menu is not an
agent-chat feature; three tokens it needs are added (scrim, divider, icon
size). The rail button uses robrix2's "people" icon, copied into resources.

Verified live through the remote bridge: the button opens the card with the
logged-in account, Active marker, Account Settings and Log Out. 125 lib tests
with the feature, 104 without; clippy clean; no DSL errors.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXUgAs5yG46jKcgcPSJ2Ym
robrix2's Settings screen (its PR #211) is a header, a row of segmented
category tabs, and a PageFlip with one page per category; upstream's was one
long scrolling column. Port the structure onto upstream's existing sections:

* Tabs: Account, Preferences, Privacy, About. robrix2 also has Devices and
  Labs; upstream has no content for either (no devices page, no agent
  registry), so they are not offered.
* Each page wraps the same sub-widget upstream already had (AccountSettings,
  AppSettings, PrivacySettings, AboutSettings), so nothing behind the tabs
  changed; the TSP settings placeholder stays on the Account page.
* Tab styling follows robrix2 exactly: solid RBX_ACCENT with white text when
  selected, ghost with secondary text otherwise, applied at runtime and
  re-applied after a ScriptReapply. Canvas background, page title and close
  button use the shared RBX token slice, which gains the six tokens this
  screen needs (canvas, on-accent fg, xs radius, page-title type, spacing).
* Populate, ScriptReapply restore, close on X/Escape/back, and the TSP
  modals are unchanged; the page flip is not lazy so populate still reaches
  every sub-widget on open.

Verified live: Account Settings from the account menu lands on the tabbed
screen, and each tab switches its page. 125 lib tests with the feature, 104
without; clippy clean; no DSL errors.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXUgAs5yG46jKcgcPSJ2Ym
Replaces the local rbx_tokens.rs slice with robrix2's design_tokens.rs
(fonts sit on theme.font_* until the system-fonts branch lands), moves
the primary/CTA colour and unread-badge fills to the teal accent palette,
adds the robrix2 icon set, the 4px spacing grid, RADIUS_*, the settings
colour constants and the 76pt navigation rail width.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXUgAs5yG46jKcgcPSJ2Ym
- ExpandArrow draws an open chevron stroke instead of a filled triangle.
- CollapsibleHeader is a transparent, calm group header (secondary-grey
  label, tertiary chevron); only mentions surface in its badge.
- UnreadBadge: robrix2 pill (30x20, token fills) with an "@" prefix on
  mention counts so the two badges differ by more than hue; keeps the
  last-drawn cache.
- RoomsListHeader: regular-weight title, token-coloured sync icons, a
  search icon that emits RoomsListHeaderAction::OpenRoomFilterModal, and
  a hidden directory slot for a future directory screen.
- RoomsListEntry: RBX text tokens, soft teal selected wash and faint hover
  wash (no solid fill, text keeps the same ink in every state), token link
  colour.
- RoomsSideBar / RoomFilterInputBar / status label: surface, canvas and
  inset-field tokens; the mobile sidebar focuses its filter bar on the
  header's search action.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXUgAs5yG46jKcgcPSJ2Ym
Navigation rail: navy RBX_NAV_BG SolidView (flush to the window edge),
transparent nav items with a navy pill on hover/active and a teal accent
bar plus white icon when selected, icons at RBX_ICON_LG, nav-fg dividers,
teal identity fallback for the own-avatar initial. Spaces bar entries get
the translucent teal wash + accent bar and RBX_IDENTITY_TEAL text avatars;
the spaces status label uses the nav foreground. The mobile bottom bar
shares the navy palette so the same button templates read on both.

Dock: active tab filled RBX_ACCENT with white text, inactive tabs on
RBX_BG_SURFACE_SUBTLE with a flat 1px RBX_STROKE_STRONG bottom border,
close button in secondary/danger colours.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXUgAs5yG46jKcgcPSJ2Ym
…button

The room input bar is now a floating white card (RBX_BG_SURFACE on a soft
stroke) with a toolbar row above the text field: attach (opens the file
picker), @ (opens the member-mention popup), a quick emoji row toggle, /
(opens the slash-command popup) and + (the existing upload/location popup
menu). The text field is borderless on the card with a Markdown placeholder,
and the send button is a rounded teal square with a white icon that greys
out while the draft is empty; the lock badge still tracks encryption.

MentionableTextInputRef gains insert_at_cursor(), which inserts text as an
ordinary undo-able edit and re-detects the trigger, so the shortcuts behave
exactly like typing. Matrix-link pills, inline code and quotes, and link
preview titles move onto the RBX accent/link tokens.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXUgAs5yG46jKcgcPSJ2Ym
fill_keep leaves the pill in the sdf shape and box unions with it, so the
bar fill covered the whole tile whenever border_size was zero.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXUgAs5yG46jKcgcPSJ2Ym
@kevinaboos

Copy link
Copy Markdown
Member

Thanks for the PR Yue!

I'm not familiar with "hagency", and searching it on Google yields nothing. For example, I have no idea what any of this means:

Accept com.agentchat, com.hagency, and com.hafleet approval events, preserving the request namespace and original binding fields in verdicts. Authorization remains enforced by the bridge.

Can you explain what that is? Perhaps it should be merged into the a2app fork of Robrix instead, if it's related to agentic workflows.

Lots of the other stuff in this PR description sounds good too, like the various UI improvements. However this PR is simply way too large for me to review at once. Each of those UI changes can be split out into separate PRs, which would make it feasible for me to test and review them.

Can you split each UI change into the smallest PR that it can reasonably be a part of? Thanks.

@kevinaboos

Copy link
Copy Markdown
Member

Ok I see the hagency thing now https://github.com/hagency-org/hagency, so yeah I think Tha belongs in the a2app fork.

@ymote

ymote commented Sep 17, 2026

Copy link
Copy Markdown
Author

Split out six focused UI PRs for individual review, each based directly on current upstream main with no Hagency code or dependency on another split:

Each changes one to four files and passes Clippy with all features/targets and warnings denied, spelling, and whitespace checks. The combined branches apply cleanly and pass all 104 library tests. A local fixture also verified desktop/mobile search focus, Settings tab switching with unsaved edits preserved, and the rendered badges/navigation/room-list styles.

I left dock styling out because #1073 already reworks that area. The larger account-menu/composer changes and shared palette from this PR are not included in this batch; the original branch remains available as their source.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants