Files
marstemedia/AGENTS.md
Bytemalte 0d531d5b7c docs: add ARCHITECTURE.md and AGENTS.md, split CSS, fix justfile
- Add ARCHITECTURE.md with full system overview (data flow, modules, build)
- Add AGENTS.md as a guide for future AI agents working on this project
- Split monolithic styles.css into focused files under assets/:
  variables, animations, layout, navbar, header, cards, config,
  filters, utils, greet
- Fix justfile: replace `cargo run tauri dev/build` with `cargo tauri dev/build`

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 15:02:36 +01:00

7.8 KiB

Agent Guide: marstemedia

This document is for AI agents working on this codebase. Read it before touching anything.


What is this project?

marstemedia is a cross-platform desktop (+ Android) news reader app built with:

  • Tauri v2 as the native shell
  • Yew (Rust → WebAssembly) for the UI
  • Rust for all backend logic (networking, file I/O, cryptography)

It has three features:

  1. Nostr feed — anonymous browsing of decentralized social posts, filterable by hashtag
  2. RSS reader — fetches and displays articles from a user-configurable list of RSS/Atom feeds
  3. AI news — one-click tech news summary from Groq (Llama 3) or OpenRouter

Repository Layout

marstemedia/
├── src/                        # Frontend (Yew/Wasm)
│   ├── main.rs                 # Wasm bootstrap
│   ├── app.rs                  # Router + top-level layout
│   ├── navbar/
│   │   ├── mod.rs
│   │   └── navbar.rs           # Bottom navigation dock
│   └── pages/
│       ├── mod.rs
│       ├── home.rs             # Nostr feed page
│       ├── news.rs             # RSS + AI news page
│       └── greet.rs            # Placeholder/demo page
│
├── src-tauri/                  # Backend (native Rust)
│   ├── Cargo.toml
│   ├── tauri.conf.json         # App config (window size, build commands)
│   ├── capabilities/
│   │   └── default.json        # Tauri capability permissions
│   └── src/
│       ├── main.rs             # Desktop executable entry
│       ├── lib.rs              # Tauri setup, command registration
│       ├── home.rs             # Nostr: fetch_nostr_posts command
│       └── news.rs             # RSS + AI: all news commands + NewsState
│   └── easy-nostr/             # Local path-dependency (nostr-sdk wrapper)
│
├── Cargo.toml                  # Frontend workspace root
├── styles.css                  # All CSS for the app
├── index.html                  # Trunk entry point
├── Trunk.toml                  # Trunk (Wasm bundler) config
├── justfile                    # Dev shortcuts
└── .gitea/workflows/
    └── android.yaml            # CI: builds Android APK

How to Add a New Page

  1. Create src/pages/yourpage.rs as a Yew #[function_component].
  2. Export it in src/pages/mod.rs: pub mod yourpage;
  3. Add a variant to the Route enum in src/app.rs:
    #[at("/yourpage")]
    YourPage,
    
  4. Add a match arm in the switch function in src/app.rs:
    Route::YourPage => html! { <YourPage /> },
    
  5. Add a <Link<Route>> in src/navbar/navbar.rs.

How to Add a New Backend Command

  1. Write your async function in the appropriate module (src-tauri/src/home.rs, news.rs, or a new file).
  2. Annotate it with #[tauri::command].
  3. If you need shared state, add a field to NewsState in src-tauri/src/news.rs, or create a new state struct and call .manage() in lib.rs.
  4. Register the command in src-tauri/src/lib.rs inside tauri::generate_handler![...].
  5. In the frontend, call it with invoke("your_command_name", args).

How to Call a Backend Command from the Frontend

Every page that needs backend calls declares this extern at the top:

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], catch)]
    async fn invoke(cmd: &str, args: JsValue) -> Result<JsValue, JsValue>;
}

Serialize args with serde_wasm_bindgen::to_value(...), deserialize results with serde_wasm_bindgen::from_value(...). Use spawn_local to run async calls inside Yew callbacks.


Key Patterns to Know

State management (frontend)

Yew uses use_state hooks. There is no global store — state lives inside components. If two pages need to share data, consider lifting state to app.rs or using Tauri's managed state on the backend.

State management (backend)

NewsState in src-tauri/src/news.rs is registered with .manage() and injected into commands via State<'_, NewsState>. All fields are Mutex<T> — always lock, read/write, and drop the lock quickly. API keys are in-memory only (not saved to disk).

Config persistence

RSS feeds are saved to <app_config_dir>/rss.ron using the ron crate. Access the config path via app.path().app_config_dir(). Pattern is: read on load, write on save, keep in-memory state in sync.

Markdown rendering

The News page renders article content as Markdown using pulldown-cmark. The output is injected with Html::from_html_unchecked. Only use this for content you control or have sanitized.

Privacy pattern (Nostr)

A new random keypair is generated on every fetch_nostr_posts call. This is intentional — do not change this to a persistent key without understanding the privacy implications.


CSS

styles.css in the project root is the single entry point — it only contains @import statements. All actual styles are split into files under assets/:

File What's inside
assets/variables.css CSS custom properties (colors, fonts), reset, body/html
assets/layout.css .feed-container, responsive media queries
assets/navbar.css .navbar-dock, .nav-link
assets/header.css .feed-header, .mode-toggle, .toggle-btn, .action-btn, .provider-switch
assets/cards.css .post-card, .markdown-body, .post-category-badge
assets/config.css .config-panel, RSS list, inputs, .add-btn, .delete-btn, .save-master-btn
assets/filters.css Hashtag chips (Home), .category-bar, .cat-btn (News)
assets/animations.css @keyframes float, slideIn, pulse-subtle
assets/utils.css .reload-btn-large, .error-box, .loading-spinner, .status-msg
assets/greet.css .robot-container, .robot-icon, .bubble

When adding styles for a new page, create a new file in assets/ and add an @import line to styles.css. There is no CSS framework — vanilla CSS with custom classes.


Build & Dev

# Start dev server (hot reload)
cargo tauri dev

# Build for desktop release
cargo tauri build

# Build for Android (requires Android SDK + NDK)
cargo tauri android build

# Frontend only (no Tauri shell)
trunk serve

The justfile may contain additional shortcuts — check it first.


Dependencies Worth Knowing

Crate Used for
yew UI components
yew-router Client-side routing
wasm-bindgen / wasm-bindgen-futures Wasm ↔ JS bridge
serde-wasm-bindgen Serialize/deserialize across the Tauri IPC
pulldown-cmark Markdown → HTML in the frontend
nostr-sdk Nostr protocol (via easy-nostr wrapper)
reqwest HTTP client (rustls, no OpenSSL)
feed-rs RSS/Atom feed parsing
ron Config file serialization
tauri-plugin-opener Open URLs in system browser

What to Watch Out For

  • easy-nostr is a local path dependency at src-tauri/easy-nostr/. It is also a private Git repo cloned by CI. If you need to update it, clone it manually into that path.
  • No OpenSSL — both reqwest usages (frontend and backend) use rustls-tls with default-features = false. Keep it that way to avoid cross-compilation pain on Android/CI.
  • wasm32-unknown-unknown target is required for the frontend. aarch64-linux-android and friends are required for Android CI.
  • The Greet page (src/pages/greet.rs) appears to be a leftover demo page from the Tauri template. It is still linked in the navbar — decide whether to keep or remove it.
  • API keys entered by the user are held in Mutex<String> fields in NewsState. They are lost when the app restarts. There is no persistence for keys by design.