Skip to content

Add agent-to-app rendering infrastructure#127

Open
ZhangHanDong wants to merge 22 commits into
mainfrom
feat/agent-to-app
Open

Add agent-to-app rendering infrastructure#127
ZhangHanDong wants to merge 22 commits into
mainfrom
feat/agent-to-app

Conversation

@ZhangHanDong
Copy link
Copy Markdown

Summary

  • add Splash-first agent-to-app app registry, template host, template cache/preflight checks, weather/news templates, widget manifest, and local function allowlist
  • add OctOS bot rich Markdown streaming renderer support with code/table/Mermaid/diagram modal preview, stable timeline rendering, and reconnect-safe streaming animation behavior
  • document agent-to-app roadmap/specs/issues, including Makepad/Matrix crash notes and future booking fixture work

Verification

  • cargo check
  • cargo test --lib home::room_screen::tests::test_ -- --nocapture
  • cargo build --release
  • git diff --cached --check before commit

Notes

  • Local-only files were intentionally not included: .mcp.json, .playwright-mcp/, .superpowers/, and weather preview screenshots.
  • macOS magnify/pinch API is tracked as issue WEbrtc Voip #13 and is not directly matched in Robrix until Makepad exposes it as a stable API.

ZhangHanDong and others added 22 commits April 15, 2026 01:54
Master spec defines the system-level invariants for delivering bot-authored
mini-apps into the Robrix timeline: protocol envelope composition
(`org.octos.app` + reused `org.octos.actions`, not merged), host identity
keyed on `(room_id, event_id)` not on agent-provided semantic IDs,
`m.replace` immutability aligned with Phase 4c / Phase 5, the L1/L2a/L2b/L3
layering contract, the `RoomScreen`/`PortalList`/`TimelineUiState`
lifecycle integration for L3 hosts, and the type registry whitelist.

Roadmap document is cross-reviewed by Codex across four review rounds and
cites upstream Makepad source (`splash.rs`, `button.rs`, `widget.rs`,
`widget_tree.rs`) plus the existing Robrix Phase 4c action-button wiring
as primary-source evidence that L1 and L2a are immediately feasible and
that L2b/L3 need only a 30-minute micro-PoC rather than a full spike.

`agent-spec lint specs/task-agent-to-app-system.spec.md --min-score 0.7`
returns Quality 100% with 17 scenarios covering protocol routing, host
identity, envelope/actions immutability, layering, lifecycle integration,
and security/validation invariants.

No code is touched by this commit. L1/L2a/L2b/L3 sub-specs derive from
this master contract in subsequent commits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Derives from the agent-to-app master spec to define the first concrete
mini-app: a presentation-only weather card routed through the new
`org.octos.app` type registry, not through the `org.octos.splash_card`
raw-string backdoor.

Contract details:
- JSON schema: location / temp_c / condition required; feels_like_c /
  humidity / wind_kph / updated_at / forecast optional; forecast capped
  at 7 entries; schema versioned.
- Factory interface: `init(initial_state) -> RenderedApp` + `render(state,
  app_language) -> String`. The render function takes `AppLanguage`
  explicitly so label localisation does not leak through global state.
- Splash output constraints (per makepad-2.0-splash skill for the Canvas
  eval path): dot-path inline properties, `draw_bg.radius` (not
  `border_radius`), explicit `Inset{}` and `Align{}` types, trailing-dot
  form for whole-number float literals only, no `ScrollYView`, no
  `show_bg: true` on pre-styled views.
- Validation: missing required fields and out-of-range numerics fail
  closed to plain text with a warning; unknown `condition` enum values do
  NOT fail closed and instead map to "sunny" with a warning; long
  locations truncate at 64 grapheme clusters with a U+2026 ellipsis;
  forecast >7 truncates with a warning.
- Immutability inherited from master spec: the renderer reads the original
  event content only, never `m.new_content`.

14 scenarios cover registry routing, missing/invalid field handling,
grapheme-cluster truncation, Splash-safe string escaping, Canvas
eval-path syntax verification, coexistence with `org.octos.actions`, the
`m.replace` immutability rule, priority over raw `splash_card`, and i18n
label resolution against the current app language.

`agent-spec lint specs/task-agent-to-app-l1-weather-card.spec.md
--min-score 0.7` returns Quality 100%.

Codex code-level review of this spec caught and verified four fixes before
this commit: (1) `render` signature now takes `app_language` explicitly to
avoid global language state, (2) `Allowed Changes` now includes
`src/home/mod.rs` so the new `app_registry` submodule can be re-exported,
(3) float syntax rule split into whole-number-float (trailing dot) vs
fractional-float (unchanged) categories to resolve a self-contradiction,
(4) corrected the `splash_card` widget slot line reference in
`room_screen.rs` from the stale `:1410` to the actual `:1968`.

No code is touched by this commit. L1 code implementation begins in a
follow-up commit after user testing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
First concrete agent-to-app mini-app shipping against the master spec
`specs/task-agent-to-app-system.spec.md` and the L1 sub-spec
`specs/task-agent-to-app-l1-weather-card.spec.md`.

Changes:
- New `src/home/app_registry/` module with an `AppFactory` trait and a
  once-initialised type registry. L1-level factories only need to
  implement `init + render(&state, AppLanguage) -> String`; higher
  layers (L2b in-card controls, L3 stateful hosts) will extend the
  trait without breaking this commit.
- New `weather` type (`src/home/app_registry/weather.rs`) implementing
  the full L1 weather contract: location / temp_c / condition
  required; feels_like_c / humidity / wind_kph / updated_at / forecast
  optional; six-way condition enum (sunny / cloudy / rainy / snowy /
  stormy / foggy); grapheme-cluster truncation for overly long
  locations; forecast cap of 7 entries; Splash-safe string escape
  helper. Unknown condition values fall back to `sunny` with a warning
  rather than failing the whole card.
- Render function produces Canvas eval-path Splash DSL following the
  `makepad-2.0-splash` skill's syntax rules: dot-path inline
  properties, `draw_bg.radius` (not `border_radius`), explicit
  `Inset{}` and `Align{}` types, trailing-dot form for whole-number
  float literals only, no `ScrollYView`, no `show_bg: true` on
  pre-styled views. A dedicated unit test
  (`render_output_obeys_canvas_eval_syntax_requirements`) enforces
  each of these invariants so future edits cannot silently regress
  the eval-path syntax.
- New `org.octos.app` parallel branch in `src/home/room_screen.rs` at
  the content parsing point where `org.octos.splash_card` is
  currently read. The app registry path takes priority when both
  custom fields are present. Event content is read via
  `original_event_content_json` rather than
  `latest_effective_event_content_json` to enforce the master spec's
  m.replace immutability rule: edits to a message carrying an
  `org.octos.app` envelope do not mutate the rendered card.
- Five new i18n keys (`agent_to_app.weather.*`) for feels_like /
  humidity / wind / forecast / updated_at_prefix, in both EN and
  zh-CN. The render function resolves labels against the current
  `AppLanguage` passed in explicitly — no global language state.
- 11 unit tests under `home::app_registry::weather::tests` cover
  valid payload success, missing-required-field fail-closed,
  out-of-range temperature fail-closed, unknown-condition soft
  fallback, optional-field absence, long-location grapheme-cluster
  truncation with ellipsis, forecast truncation, Splash-safe escape,
  Splash DSL injection-attempt escape, Canvas eval syntax
  conformance, and i18n label resolution.

Verified end-to-end by sending a weather event as the appservice
ghost user `@octosbot:127.0.0.1:8128` into the local octos-public
room and observing the native GPU-rendered card (orange background,
CJK location, ☀ symbol, 22° temperature, Feels like / Humidity /
Wind column, three forecast chips with sunny / cloudy / rainy
symbols) in the Robrix timeline. `cargo test home::app_registry::weather`
passes 11/11. `RUSTFLAGS="-D warnings" cargo clippy --workspace
--all-features` is clean.

What this commit does NOT include (follow-up work, deliberately
scoped out per the master spec's producer/consumer boundary):
- OctOS-side producer: bot agent does not yet emit `org.octos.app`
  envelopes in response to natural-language weather requests. That
  belongs in the OctOS repo and will be tracked in a separate spec
  (`task-octos-agent-app-envelope-producer`).
- L2a external refresh button, L2b in-card controls, L3 stateful
  hosts: separate sub-specs and commits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ignup) (#114)

* feat(register): scaffold src/register/ module

Add empty register module with mod.rs, register_screen.rs,
register_status_modal.rs, validation.rs. Register the module
in src/lib.rs and wire the script_mod aggregator per the
login/mod.rs pattern.

Part of specs/task-register-flow.spec.md Phase 1.

* feat(register): homeserver URL normalizer + tests

Accept bare hostname (prepend https://), strip trailing slash,
reject non-http(s) schemes and empty input. 8 unit tests cover
the edge cases.

* feat(register): add RegisterAction + HsCapabilities types

Introduce the data model used across Phase 1-5:
- HsCapabilities with is_mas_native_oidc / registration_enabled /
  uiaa_probe / sso_providers fields matching the spec
- RegisterMode enum (MasWebOnly / Uiaa / Disabled) derived via
  HsCapabilities::mode() — MAS wins over UIAA per element-web rule
- RegisterAction with NavigateToLogin / CapabilitiesDiscovered /
  DiscoveryFailed variants for Phase 1 + None default

* feat(login): dispatch NavigateToRegister for Sign Up click

Add LoginAction::NavigateToRegister variant. Replace
set_signup_mode(true) call with Cx::post_action dispatch so
the main App can route to the new RegisterScreen.

The signup-mode rendering code is removed in a later task.

* feat(register): capability discovery + RegisterScreen UI

Implement MatrixRequest::DiscoverHomeserverCapabilities handler
that probes .well-known, /versions, /v3/login, and empty POST
/register to build HsCapabilities. Results post back as
RegisterAction::CapabilitiesDiscovered / DiscoveryFailed.

RegisterScreen widget renders the homeserver input, Next button,
and three-state status area (MAS / UIAA / Disabled / errors).
The full wizard body is added in Phase 2+.

* refactor(login): remove signup mode residue

Delete confirm_password input, mode-toggle state (signup_mode field,
set_signup_mode, sync_mode_texts), and the signup submit branch in
handle_actions. The "Sign up here" button (DSL id mode_toggle_button)
keeps its text, position and style; its click now posts
LoginAction::NavigateToRegister (wired in f40871d) — the navigation
handler lands in a follow-up commit (Task 8).

Registration logic moves wholesale to src/register/; see
specs/task-register-flow.spec.md.

* feat(app): wire login <-> register navigation

Handle LoginAction::NavigateToRegister and
RegisterAction::NavigateToLogin to toggle visibility between
login_screen_view and the new register_screen_view. Both screens
live as siblings under the overlay_container; default visibility
stays on LoginScreen.

auth_ui_state and update_login_visibility are intentionally left
untouched — Phase 2+ will layer auth-state machinery on top of
this minimal inline toggle.

* fix(register): theme tokens + MSC2965 unstable auth key

Task 9 testing found two bugs:

1. register_screen.rs hard-coded dark-theme colors (#x1F2124 bg,
   #xF1F2F3 text) conflicted with the project's light palette;
   combined with `draw_bg:` (replace) instead of `draw_bg +:`
   (merge, Pitfall #44) the shader silently fell back to
   transparent and text appeared washed out. Replace all hex
   colors with the shared COLOR_SECONDARY / COLOR_TEXT tokens
   and use TITLE_TEXT / REGULAR_TEXT styles, matching
   login_screen conventions.

2. MAS detection only looked at the stable .well-known key
   `m.authentication.issuer`. matrix.org still serves the
   unstable key `org.matrix.msc2965.authentication.issuer`,
   so it was mis-classified as non-MAS and fell through to the
   "registration disabled" branch. Accept both keys (element-web
   does the same).

* fix(register): Makepad 2.0 DSL syntax errors

Runtime DSL errors surfaced at `cargo run`:

1. register_status_modal.rs used Makepad 1.x live_design! form
   `pub RegisterStatusModal := {{RegisterStatusModal}} View {...}`.
   In script_mod! the 2.0 form is
   `mod.widgets.RegisterStatusModal = #(RegisterStatusModal::register_widget(vm)) {...}`
   (Pitfall #43). The 1.x form emitted
   "variable pub not found in scope" +
   "variable RegisterStatusModal not found in scope".

2. register_screen.rs set `empty_message:` on a RobrixTextInput.
   The correct property is `empty_text:` (the error helper
   suggested it exactly). Symptom: the homeserver input was
   never created because its property bag failed to bind.

Both failures pass `cargo build` because script_mod! bodies
are parsed at widget init by the Makepad script VM, not by
rustc. Caught when running the app.

* feat(register): HsCapabilities carries mas_account_url

Phase 2 groundwork. HsCapabilities grows a new Option<String>
field populated from the same .well-known probe Phase 1 already
runs, preferring the explicit `account` field and falling back
to `<issuer>/account/` when absent (alvin.meldry.com currently
omits the field; matrix.org provides it). No extra HTTP round
trips. The URL is consumed in the next commit by RegisterScreen
to launch the system browser.

* feat(register): open system browser for MAS signup

When CapabilitiesDiscovered reports MasWebOnly, call
robius_open::Uri::new(&url).open() on the mas_account_url
captured in the previous commit, then replace the status text
with a user-facing instruction: complete signup in the browser,
return, hit Back to Login, sign in with the new credentials.

Handles three paths:
- browser opens cleanly -> instruction text
- robius_open errors    -> fallback text with the URL for copy
- mas_account_url None  -> defensive message ("advertises but
                           no signup URL was found")

Simple-version scope: no OAuth callback, no token exchange, no
Dynamic Client Registration. Element Desktop's full callback
flow is Phase 2.5 if we decide to ship it. With this commit the
alvin.meldry.com / matrix.org paths are end-to-end usable: user
completes signup in the browser and returns to log in via the
existing password flow.

* fix(register): use <issuer>/register for MAS signup entry

The previous commit opened <issuer>/account/, which is MSC2965's
account-management URI and requires an authenticated session —
hitting it without a cookie loops between /account/ and /login
(verified in incognito against alvin.meldry.com). MAS exposes the
direct self-registration form at <issuer>/register; confirmed to
render a proper "Create account" page with username/email/phone/
password fields on alvin.meldry.com.

Rename the field from mas_account_url to mas_signup_url to match
the real semantics, and drop the `account` field lookup from
discovery — we do not use it and keeping it as a fallback only
invited this bug. Future account-management features in later
phases can add it back when there is a real consumer.

* refactor(register): mirror LoginScreen card layout exactly

Align RegisterScreen's outer shell with origin/main LoginScreen structure:
set_type_default + SolidView base, Overlay root with centered alignment,
ScrollYView with hidden scrollbar, RoundedView card wrapper with 50/50
margin, inner column with spacing 15.0. Logo references the shared
mod.widgets.IMG_APP_LOGO token (registered by login_screen.rs script_mod
which runs first in app.rs:1549).

* docs(register): Phase 1+2 plans and task-register-flow spec

Retroactively commit the planning documents that guided the Phase 1+2
implementation already merged into this branch. These were written during
PR#114 development but never checked in.
)

  - Add UiaaStage trait + DummyStage for the dummy auth flow
  - Wire MatrixRequest::RegisterViaUiaa handler in sliding_sync                                                                                                    
  - Inline RegistrationForm in RegisterScreen for UIAA path                                                                                                        
  - Add localpart + password validators                                                                                                                            
  - Transition to main UI on RegistrationSuccess
 - Add oidc_login module: worker + error taxonomy + MatrixRequest variants
  - Wire LoginScreen MAS branch end-to-end (probe → OIDC flow)
  - Persist OAuth sessions in matrix_state, save on TokensRefreshed
  - Dispatch server logout via client.auth_api() in logout_state_machine
  - Add should_probe_homeserver predicate (TDD-driven)
  - Share homeserver capability state between login and register flows
  - Tighten back navigation and probe proxy handling between flows
  - Add OIDC MAS i18n strings (en + zh-CN)
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