scrollToItem(last) top-aligns the last row, so a final message taller than the viewport stayed cut off at the bottom. New scrollToBottom() brings the last row into view, then aligns its bottom edge with the viewport bottom. The isAtBottom check now also treats 'can't scroll forward' as at-bottom, so auto-scroll and the back-to-bottom button behave correctly when the newest message is taller than the viewport.
Iris × Hermes
A chat app for hermes-agent: a native Android app and a desktop app (Linux, macOS, Windows) — both built from one shared Kotlin codebase (Compose Multiplatform).
Iris pairs with your running hermes gateway over a private WebSocket and gives you a Telegram-quality chat experience with your personal agent: streaming replies, visible reasoning, structured tool activity, channels, threads, media, search, and push notifications.
Features
- Native Hermes-Gateway integration — your hermes → gateway → Iris app
- Absolute Privacy! — everything stays on your own infrastructure
- No file limit
- No character limit
- Full markdown support — tables, checkmarks, bold, inline code, code blocks + syntax highlighting…
- HTML Artifact Preview — agent-sent HTML/CSS/JS rendered in an in-app WebView
- All settings live in the app, not in hermes
config.yml! Change everything on the fly. - Channels — create channels to keep track of your reports, cronjobs, webhook calls
- Pin channels, rename them, change the color/icon
- Customize how your chat should look like — color? Check! Images? Check!
- Reasoning collapse/expandable in the chat bubble
- Change the default behavior of tool & reasoning verbosity
- Threads — create your own, or let the AI create them with a title
- Search messages from everywhere
How it works
hermes-agent ──> hermes gateway ──(WebSocket :8790)──> Iris app (Android / Desktop)
gateway-plugin/is a hermes platform plugin (android). It runs inside thehermes gatewayprocess and opens a WebSocket server the apps connect to. Zero new Python dependencies, zero hermes-core changes.app/is one Compose Multiplatform Gradle project::shared(KMP, most of the code),:androidApp(native Kotlin + Jetpack Compose client),:desktopApp(the same app, tweaked for a big screen).- The app is a first-class hermes messaging platform, so everything the gateway
already does just works: slash commands, cron delivery,
send_messagerouting, coexistence with Telegram/Discord/etc. - Push notifications: FCM (primary) or ntfy (fallback).
Build from source
Prerequisites
| Where | You need |
|---|---|
| Gateway host | hermes-agent with its venv (uv sync) |
| Android build machine | JDK 17, Android SDK (sdk.dir in app/local.properties or ANDROID_HOME), ADB with a connected device |
| Desktop build machine | JDK 17 only |
No system Gradle needed — both apps use the project wrapper (./gradlew).
1. Gateway (on the gateway host)
# hermes-agent is a separate project (not part of this repo)
cd hermes-agent && uv sync
# install the Iris plugin into the live hermes home
mkdir -p ~/.hermes/plugins
ln -s "$PWD/gateway-plugin" ~/.hermes/plugins/android
hermes gateway status # should list "android"
hermes gateway setup # generates ANDROID_TOKEN, prints the server URL
hermes gateway # run the gateway
2. Android app
cd app
./gradlew :androidApp:installDebug # build + install on the connected ADB device
# or just build the APK:
./gradlew :androidApp:assembleDebug # → app/androidApp/build/outputs/apk/debug/
3. Desktop app
cd app
./gradlew :desktopApp:run # dev run
./gradlew :desktopApp:jpackage # native app-image (bundles the JRE)
4. Pair
On the app's Connect screen:
- Server URL —
ws://<gateway-ip>:8790/ws(printed byhermes gateway setup). - Pairing token — from the setup output, or
ANDROID_TOKENin~/.hermes/.envon the gateway host. - Test & Connect.
Notes:
- The app has no QR scanner — pairing is manual URL + token entry.
- The default bind is
127.0.0.1(desktop on the same machine only). For a phone on the LAN, setANDROID_WS_HOSTto the gateway's LAN IP. - Remote access: Tailscale/WireGuard, or a reverse proxy with CA-signed WSS
(
ANDROID_WS_CERT/ANDROID_WS_KEY).
Full walkthrough, push setup (FCM/ntfy), and troubleshooting:
docs/setup.md.
Contributing
Contributions are welcome! Before you start:
-
Read the docs. The full reference library is in
docs/— start withdocs/00-overview.md, then follow the numbered docs (architecture, wire protocol, plugin design, testing, …). -
Know the layout.
Path What gateway-plugin/Python hermes platform plugin ( android);protocol.pyis the frame source of truthapp/sharedKMP module with most of the client code (shared by Android + Desktop) app/androidAppThin Android shell (package dev.iris.app)app/desktopAppThin desktop shell docs/Numbered reference library -
Run the tests.
- Python (gateway plugin):
cd hermes-agent && scripts/run_tests.sh tests/gateway/test_android.py(never barepytest— hermes's runner sandboxesHERMES_HOME). - Kotlin:
cd app && ./gradlew :shared:testDebugUnitTest - Live check (gateway must be running):
gateway-plugin/tests/ws_probe.pyandgateway-plugin/tests/e2e.py— seegateway-plugin/tests/README.md.
- Python (gateway plugin):
-
Keep the protocol in sync.
gateway-plugin/protocol.py,app/shared/.../protocol/Protocol.kt, anddocs/protocol/frames.schema.jsonmust always agree. -
Pre-commit hooks are configured (
.pre-commit-config.yaml); runpre-commit installonce after cloning.
Open an issue first for anything big, then send a pull request.
License
Apache License 2.0 — see LICENSE.