style: modernize UI aesthetics and replace app launcher icons

This commit is contained in:
Hy4ri committed 2026-06-17 00:42:14 +03:00
1 parent d4328d0fb2
commit bd439ba674
19 files changed
+66 -724

No files matched your search

-594
View File
@@ -1,594 +0,0 @@
# Android App for Hermes — Chat & Control
> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task.
**Goal:** Build an Android app that lets m57 chat with his Hermes agent instance and send remote control commands from anywhere, directly from his phone.
**Architecture:** The app talks directly to Hermes' built-in **Dashboard REST API** (port 9119) for management and **Dashboard WebSocket** (`/api/ws`) for real-time chat via JSON-RPC. **No custom bridge needed.**
**Tech Stack:** Android (Kotlin/Jetpack Compose), OkHttp (WebSocket + Retrofit), JSON-RPC protocol, Hermes Dashboard API.
---
## Current State (What Already Works)
| Component | Status | Endpoint |
|-----------|--------|----------|
| **Dashboard REST API** | ✅ Running on `127.0.0.1:9119` | `GET /api/status`, sessions, config, etc. |
| **TUI Gateway WebSocket** | ✅ Running at `/api/ws?token=<TOKEN>` | JSON-RPC over WebSocket |
| **Auth (loopback mode)** | ✅ `Authorization: Bearer <TOKEN>` or `X-Hermes-Session-Token: <TOKEN>` | For REST API |
| **WebSocket Auth** | ✅ `?token=<TOKEN>` query param | For WS upgrade |
| **Dashboard Token** | ✅ Stored in `~/.hermes/.env` | `HERMES_DASHBOARD_SESSION_TOKEN=y9oAS3...iTZ4` |
**Available JSON-RPC Methods (for Chat):**
- `session.list` — List all sessions
- `session.active_list` — List active sessions
- `session.status` — Get session status
- `session.history` — Get session message history
- `session.resume` — Resume an existing session
- `session.create` — Create a new session
- `prompt.submit` — **Send a message to Hermes!** (key endpoint)
- `session.interrupt` — Interrupt a response
- `session.steer` — Steer the agent
- `clarify.respond` — Respond to clarifications
- `approval.respond` — Approve/reject commands
- `session.delete` — Delete sessions
- `session.title` — Set session title
- `config.get` / `config.set` — Manage config
- `image.attach`, `file.attach` — Attach files
- And many more...
**Available REST Endpoints (for Management):**
- `GET /api/status` — Hermes version, gateway status, active sessions
- `GET /api/sessions` — List sessions with metadata
- `GET /api/sessions/{id}/messages` — Get messages for a session
- `GET /api/system/stats` — System stats
- `GET /api/config` — Full config
- `GET /api/logs` — Logs
- `GET /api/cron/jobs` — Cron jobs
- And many more (check /api/docs)
---
## Architecture
```
┌──────────────────────────────────────────────────────┐
│ Android Device │
│ ┌─────────────────┐ ┌────────────────────────┐ │
│ │ Android App │ │ Hermes (Termux/Ubuntu)│ │
│ │ (Kotlin/Compose)│────▶│ Dashboard (127.0.0.1) │ │
│ │ │ │ │ │
│ │ • REST client │────▶│ REST API :9119 │ │
│ │ • WS client │────▶│ WS /api/ws?token=... │ │
│ │ • Local cache │ │ TUI Gateway (JSON-RPC)│ │
│ └─────────────────┘ └────────────────────────┘ │
└──────────────────────────────────────────────────────┘
```
Since Hermes runs on the same Android device in Termux, the app connects to **localhost** — no network issues, no CORS, no latency.
---
## Phase 1: Android Project Setup
### Task 1.1: Scaffold Android Project
**Objective:** Create a new Android project with Jetpack Compose and the right SDK.
**Actions:**
1. Open Android Studio → New Project → Empty Compose Activity
2. Name: `HermesControl`, Package: `com.m57.hermescontrol`
3. Language: **Kotlin**, Minimum SDK: **26**
4. Build system: Gradle with Kotlin DSL
**Files:**
- `app/build.gradle.kts` — Dependencies
- `AndroidManifest.xml` — Internet permission
**Verify:** App builds and runs on device/emulator.
---
### Task 1.2: Add Dependencies
**Objective:** Add OkHttp (WebSocket + HTTP), Gson for JSON-RPC.
**File:** `app/build.gradle.kts`
```kotlin
dependencies {
// Core
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
implementation("androidx.activity:activity-compose:1.8.2")
// Compose BOM
implementation(platform("androidx.compose:compose-bom:2024.02.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
// Networking
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
// JSON
implementation("com.google.code.gson:gson:2.10.1")
// Local storage
implementation("androidx.security:security-crypto:1.1.0-alpha06")
}
```
**Verify:** Gradle sync succeeds.
---
## Phase 2: REST API Client (Management)
### Task 2.1: Data Models
**Objective:** Create Kotlin data classes for REST API responses.
**Files:**
- `data/model/StatusResponse.kt`
- `data/model/SessionListResponse.kt`
- `data/model/SessionMessagesResponse.kt`
```kotlin
// data/model/StatusResponse.kt
data class StatusResponse(
val version: String,
val gateway_running: Boolean,
val active_sessions: Int,
val gateway_platforms: Map<String, PlatformStatus>
)
data class PlatformStatus(
val state: String,
val error_code: String? = null
)
```
**Verify:** Compiles cleanly.
---
### Task 2.2: REST API Interface
**Objective:** Create Retrofit service for Hermes dashboard REST API.
**Files:**
- `data/remote/HermesApiService.kt`
- `data/remote/ApiClient.kt`
```kotlin
// data/remote/HermesApiService.kt
interface HermesApiService {
@GET("api/status")
suspend fun getStatus(): Response<StatusResponse>
@GET("api/sessions")
suspend fun getSessions(): Response<SessionListResponse>
@GET("api/sessions/{id}/messages")
suspend fun getSessionMessages(@Path("id") sessionId: String): Response<SessionMessagesResponse>
@GET("api/system/stats")
suspend fun getSystemStats(): Response<SystemStatsResponse>
}
// data/remote/ApiClient.kt
object ApiClient {
private val client = OkHttpClient.Builder()
.addInterceptor { chain ->
val original = chain.request()
val request = original.newBuilder()
.header("Authorization", "Bearer ${AuthManager.getToken()}")
.build()
chain.proceed(request)
}
.build()
val retrofit = Retrofit.Builder()
.baseUrl("http://127.0.0.1:9119/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
val hermesApi: HermesApiService = retrofit.create(HermesApiService::class.java)
}
```
**Verify:** API calls return valid data.
---
## Phase 3: WebSocket Client (Chat)
### Task 3.1: JSON-RPC Models
**Objective:** Create models for JSON-RPC over WebSocket.
**Files:**
- `data/ws/JsonRpcRequest.kt`
- `data/ws/JsonRpcResponse.kt`
- `data/ws/WsMethods.kt`
```kotlin
// data/ws/JsonRpcRequest.kt
data class JsonRpcRequest(
val jsonrpc: String = "2.0",
val id: String,
val method: String,
val params: Map<String, Any> = emptyMap()
)
// data/ws/WsMethods.kt
object WsMethods {
const val SESSION_LIST = "session.list"
const val SESSION_ACTIVE_LIST = "session.active_list"
const val SESSION_STATUS = "session.status"
const val SESSION_HISTORY = "session.history"
const val SESSION_RESUME = "session.resume"
const val SESSION_CREATE = "session.create"
const val PROMPT_SUBMIT = "prompt.submit"
const val SESSION_INTERRUPT = "session.interrupt"
}
```
**Verify:** Data classes serialize/deserialize correctly with Gson.
---
### Task 3.2: WebSocket Client
**Objective:** Implement WebSocket connection to `/api/ws?token=<TOKEN>` with reconnection support.
**Files:**
- `data/ws/HermesWsClient.kt`
```kotlin
// data/ws/HermesWsClient.kt
class HermesWsClient(
private val token: String,
private val onMessage: (JsonRpcResponse) -> Unit,
private val onConnected: () -> Unit,
private val onDisconnected: (String) -> Unit
) {
private val client = OkHttpClient()
private var ws: WebSocket? = null
private val requestId = AtomicInteger(0)
fun connect() {
val url = "ws://127.0.0.1:9119/api/ws?token=$token"
val request = Request.Builder().url(url).build()
ws = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(ws: WebSocket, response: Response) {
onConnected()
}
override fun onMessage(ws: WebSocket, text: String) {
val response = Gson().fromJson(text, JsonRpcResponse::class.java)
onMessage(response)
}
override fun onFailure(ws: WebSocket, t: Throwable, response: Response?) {
onDisconnected(t.message ?: "Unknown error")
}
override fun onClosed(ws: WebSocket, code: Int, reason: String) {
onDisconnected(reason)
}
})
}
fun sendMessage(text: String) {
val request = JsonRpcRequest(
id = requestId.incrementAndGet().toString(),
method = WsMethods.PROMPT_SUBMIT,
params = mapOf("text" to text)
)
ws?.send(Gson().toJson(request))
}
fun resumeSession(sessionId: String) {
val request = JsonRpcRequest(
id = requestId.incrementAndGet().toString(),
method = WsMethods.SESSION_RESUME,
params = mapOf("id" to sessionId)
)
ws?.send(Gson().toJson(request))
}
fun disconnect() {
ws?.close(1000, "App closing")
}
}
```
**Verify:** App connects to WebSocket, receives JSON-RPC responses.
---
### Task 3.3: WebSocket Event Parser
**Objective:** Handle the event-based responses from Hermes (responses come as events, not just RPC replies).
**Files:**
- `data/ws/EventParser.kt`
The TUI Gateway sends:
- **RPC responses** (to our requests) with `id` matching
- **Events** (tool calls, streamed tokens, clarify requests) with `event` field
```kotlin
data class JsonRpcResponse(
val jsonrpc: String?,
val id: String?,
val result: Any?,
val error: JsonRpcError?,
val event: String?, // Event type (for push events)
val data: Map<String, Any>? // Event payload
)
data class JsonRpcError(
val code: Int,
val message: String
)
```
**Event types to handle:**
- `message.complete` — Agent finished responding
- `message.token` — Streaming token
- `clarify.request` — Agent needs clarification
- `tool.start` / `tool.complete` — Tool calls
- `session.updated` — Session state changed
**Verify:** Parse events correctly from Gson.
---
## Phase 4: UI Screens
### Task 4.1: Connection Screen
**Objective:** Enter/display connection info (token auto-loaded or configurable).
**Files:**
- `ui/connect/ConnectScreen.kt`
- `ui/connect/ConnectViewModel.kt`
**Features:**
- Text field for token (or load from SharedPreferences)
- "Connect" button
- Connection status indicator
---
### Task 4.2: Chat Screen
**Objective:** Full chat UI with real-time messaging.
**Files:**
- `ui/chat/ChatScreen.kt`
- `ui/chat/ChatViewModel.kt`
- `ui/chat/ChatBubble.kt`
**Features:**
- `LazyColumn` for message list (user + agent bubbles)
- Bottom input bar with send button
- Streaming text (update bubble as tokens arrive)
- Image/file attachment support
- Loading / typing indicator
- Session selector (pick which session to resume)
**Message types:**
- User messages (right-aligned, primary color)
- Agent messages (left-aligned, surface color)
- Tool calls (collapsible, monospace)
- System notifications (centered, muted)
---
### Task 4.3: Command Panel
**Objective:** Quick-action buttons for common Hermes commands.
**Files:**
- `ui/commands/CommandPanel.kt`
- `ui/commands/CommandViewModel.kt`
**Commands (can be customized):**
- `/status` — Check system status
- `/skills` — List skills
- `/memory` — Show recent memories
- `/sessions` — List active sessions
- `🔄 Restart Gateway` — Trigger gateway restart
- `📊 System Stats` — Show CPU/memory
- `⚡ Quick Prompt` — Pre-set prompts
---
### Task 4.4: Settings Screen
**Objective:** Configure connection parameters.
**Files:**
- `ui/settings/SettingsScreen.kt`
- `ui/settings/SettingsViewModel.kt`
**Settings:**
- Host/IP (default: `127.0.0.1`)
- Port (default: `9119`)
- Auth token (loaded from encrypted storage)
- Theme (Light/Dark/System)
- Auto-reconnect toggle
---
## Phase 5: Polish & Security
### Task 5.1: Encrypted Token Storage
**Objective:** Store the dashboard token securely.
**Files:**
- `data/local/AuthManager.kt`
Use Android `EncryptedSharedPreferences` to store the `HERMES_DASHBOARD_SESSION_TOKEN`.
**Verify:** Token persists across app restarts; is encrypted at rest.
---
### Task 5.2: Auto-Reconnect
**Objective:** Reconnect WebSocket if connection drops.
**Files:**
- Update `HermesWsClient.kt` — Exponential backoff reconnection.
**Verify:** Kill dashboard process → app shows "disconnected" → restart dashboard → app reconnects.
---
### Task 5.3: Error Handling & Offline Mode
**Objective:** Graceful handling of network errors.
**Files:**
- Update `ChatViewModel.kt` — Show offline banner, cache last N messages locally.
**Verify:** Turn off network → app shows offline state, cached messages visible.
---
## Task-by-Task Summary
| # | Task | Phase | Est. Time | Status |
|---|------|-------|-----------|--------|
| 1.1 | Scaffold Android Project | 1 | 10 min | ⬜ |
| 1.2 | Add Dependencies | 1 | 10 min | ⬜ |
| 2.1 | REST Data Models | 2 | 15 min | ⬜ |
| 2.2 | REST API Client | 2 | 20 min | ⬜ |
| 3.1 | JSON-RPC Models | 3 | 10 min | ⬜ |
| 3.2 | WebSocket Client | 3 | 30 min | ⬜ |
| 3.3 | Event Parser | 3 | 20 min | ⬜ |
| 4.1 | Connection Screen | 4 | 20 min | ⬜ |
| 4.2 | Chat Screen | 4 | 45 min | ⬜ |
| 4.3 | Command Panel | 4 | 25 min | ⬜ |
| 4.4 | Settings Screen | 4 | 20 min | ⬜ |
| 5.1 | Encrypted Token Storage | 5 | 15 min | ⬜ |
| 5.2 | Auto-Reconnect | 5 | 20 min | ⬜ |
| 5.3 | Error Handling & Offline | 5 | 15 min | ⬜ |
---
## What You DON'T Need Anymore (vs Original Plan)
| Originally Planned | Now? | Reason |
|--------------------|------|--------|
| Custom Python Flask bridge | ❌ Removed | Dashboard IS the bridge |
| Custom SQLite reads | ❌ Removed | Dashboard API handles sessions |
| Hermes DB schema analysis | ❌ Removed | REST API abstracts the DB |
| Queue-based message passing | ❌ Removed | WebSocket JSON-RPC handles it |
---
## App Connection Details
| Setting | Value |
|---------|-------|
| **REST API Base** | `http://127.0.0.1:9119/` |
| **WS Endpoint** | `ws://127.0.0.1:9119/api/ws?token=<TOKEN>` |
| **REST Auth** | `Authorization: Bearer *** or `X-Hermes-Session-Token: <TOKEN>` |
| **WS Auth** | `?token=<TOKEN>` query parameter (NOT header-based for WS) |
| **Current Token** | `y9oAS3frCosCSaynhmAofMkeCsuiNuF2ad2XB35iTZ4` (in `~/.hermes/.env`) |
| **Dashboard Binding** | `0.0.0.0:9119` (LAN accessible via `--insecure`) |
---
## 🧪 Verified Protocol Details (E2E Tested)
### REST API
| Endpoint | Auth | Returns |
|----------|------|---------|
| `GET /api/status` | ❌ Public | `version`, `gateway_running`, `active_sessions`, `auth_required` |
| `GET /api/sessions` | ✅ Token | `{"sessions": [...]}` — full session list with message counts |
| `GET /api/config` | ✅ Token | Full config as flat JSON dict |
| `GET /api/system/stats` | ✅ Token | System statistics |
**Auth methods tested:**
- `Authorization: Bearer <token>` ✅ works
- `X-Hermes-Session-Token: <token>` ✅ works
- No header → `401` ✅
- Bad token → `401` ✅
### WebSocket JSON-RPC Protocol
**Connection:** `ws://HOST:9119/api/ws?token=<TOKEN>`
**First message (always):** `gateway.ready` event — server pushes this immediately on connect.
**RPC methods tested:**
| Method | Params | Response | Status |
|--------|--------|----------|--------|
| `session.list` | `{}` | `{"sessions": [...]}` — all TUI sessions | ✅ |
| `session.active_list` | `{}` | `{"sessions": [...]}` — active TUI sessions | ✅ |
| `session.status` | `{}` | `{"sessions": [...]}` — session statuses | ✅ |
| `session.create` | `{}` | `{"session_id": "xxx", "stored_session_id": "xxx", ...}` | ✅ |
| `session.resume` | `{"id": "session_id"}` | Session data | ✅ (when valid session) |
| `prompt.submit` | `{"session_id": "xxx", "text": "message"}` | `{"status": "streaming"}` then events | ✅ |
| `nonexistent.method` | `{}` | `{"error": {"code": -32601, "message": "..."}}` | ✅ |
### Chat Flow (Event Sequence)
After `prompt.submit`, events arrive on the same WebSocket in this order:
1. `session.info` — Session metadata, model info, tool list
2. `message.start` — Processing began
3. `thinking.delta` (0+) — Reasoning tokens (model-dependent)
4. `message.token` (0+) — Streaming text tokens (model-dependent)
5. `status.update` — Status changes
6. `tool.start` / `tool.complete` — Tool execution (when applicable)
7. `clarify.request` (optional) — Agent needs input
8. **`message.complete`** — **Final response!** `payload.text` contains the full response text.
9. `message.done` — Processing fully finished
**Key finding:** The actual response text is in `message.complete.payload.text`, NOT nested under `payload.message.content`. Use:
```kotlin
val responseText = event.payload["text"] as? String
```
### Important Implementation Notes
1. **Agent build time:** After `session.create`, the AI agent needs ~15-20s to initialize (tool discovery, model loading, skill loading). The Android app should:
- Show a loading/connecting state during this time
- Listen for events but ignore them until `message.complete` or set a timer
- OR create a session on app startup and keep it alive for instant responses
2. **Session reuse:** Multiple `prompt.submit` calls work on the same session_id. Create one session at startup and reuse it.
3. **No heartbeat needed:** The WS connection stays alive without pings.
4. **Error codes:**
- `4001` — session not found (invalid/missing session_id)
- `4009` — session busy (agent still processing previous prompt)
- `-32601` — unknown method
- `-32600` — invalid JSON-RPC format
---
## Open Questions
1. **Chat flow**: Do you want the app to resume the current active session, or start fresh each time?
2. **Deep linking**: Want to add a way to share messages/commands from other apps?
3. **Notifications**: Want push notifications when Hermes completes a task? (Would need a server component for push)
---
*Plan updated: 2026-06-15. Simplified architecture using Hermes Dashboard + TUI Gateway. Ready to build!* 💋
-44
View File
@@ -1,44 +0,0 @@
# Progress Status: HermesControl Android App
This document tracks the completed features and the pending backlog for the HermesControl Jetpack Compose Android application.
## 👥 Done (Implemented & Verified)
### 1. Base Infrastructure & Environment
* **Nix Flake Dev Environment (`flake.nix`)**: Fully configured with JDK 17, Android SDK (platforms 34-36), `ktlint`, and Gradle.
* **Scaffolding & Build Pipeline**: Android Jetpack Compose project compiles cleanly using `./gradlew assembleDebug`.
* **Code Formatting**: Code formatting checks integrated using `ktlint` (`./gradlew ktlintCheck`).
* **Unit Testing Suite**: Verified with `./gradlew test` (AuthManager, EventParser, ConnectViewModel, ChatViewModel, and new viewmodels/integration tests).
* **Network Capabilities**: Cleartext traffic enabled in `AndroidManifest.xml` to allow connection to local networks (e.g., `192.168.1.18` to `192.168.1.48`).
* **Git Repository**: Initialized and linked with GitHub (`https://github.com/Hy4ri/hermes-mobile.git`).
### 2. Implemented Features
* **Connection Screen (`ConnectScreen`)**: Saves Host, Port, and Auth Token to `AuthManager` (EncryptedSharedPreferences), with live verification via `GET /api/status`.
* **Chat Screen (`ChatScreen`)**: Live chat over WebSocket (JSON-RPC) message token streaming, thinking delta, tool call indicators, and clarifying inputs.
* **Skills Screen (`SkillsScreen`)**: Lists skills (`GET /api/skills`) and toggles them with optimistic state reversion.
* **Cron Jobs Screen (`CronJobsScreen`)**: Lists cron jobs (`GET /api/cron/jobs`) with actions (Trigger, Pause, Resume, and Delete) with optimistic updates.
* **Gateway Control Screen (`GatewayScreen`)**: Start/Stop/Restart with platform state reporting.
* **Profiles Management Screen (`ProfilesScreen`)**: Switch active profile, update soul personality description, and switch AI model providers.
* **Toolsets Screen (`ToolsetsScreen`)**: List and toggle toolsets.
* **Achievements Screen (`AchievementsScreen`)**: Displays gamified achievements list.
* **Device Pairing Screen (`PairingScreen`)**: List pairing approvals, approve/revoke access.
* **Raw Config Editor (`ConfigScreen`)**: Live edit `/api/config/raw`.
* **MCP Servers Management (`McpServersScreen`)**: List MCP servers, toggle, test connection, and delete.
* **Webhooks Management (`WebhooksScreen`)**: List webhook callback subscriptions and globally toggle webhooks.
* **Model Configuration (`ModelScreen`)**: Inspect model providers list.
* **Settings Screen (`SettingsScreen`)**: Change Host, Port, Token, Theme, and Auto-reconnect.
* **Logs View (`LogsScreen`)**: Live scrolling platform/stdout logs (`GET /api/logs`) with console-style layout.
* **Plugins View (`PluginsScreen`)**: Hub rescan, install, update, disable, enable with optimistic state updates.
* **Channels / Messaging (`ChannelsScreen`)**: Platform configurations and credentials manager (e.g. Telegram bot details).
* **Keys (`KeysScreen`)**: Live environment variables and credentials manager (`GET/PUT /api/env`, reveal option).
* **System View (`SystemScreen`)**: Diagnostics doctor reports (`GET /api/ops/doctor`) and backups trigger (`POST /api/ops/backup`).
* **Kanban View (`KanbanScreen`)**: Full task boards, task columns (To Do, In Progress, Done), new tasks dialog, and task movement with optimistic updates.
* **Navigation Drawer**: Material3 navigation drawer using Navigation3 keys to switch between all screens.
* **Chat Slash Commands**: Local interception and execution of custom commands (`/help`, `/status`, `/sessions`, `/stats` / `/system`, `/new`, `/stop` / `/interrupt`) displaying results inline.
---
## ⏳ Left (To Be Implemented)
* Other minor backlogs include Skills Hub and Advanced Session management.
-66
View File
@@ -1,66 +0,0 @@
# Project: HermesControl Jetpack Compose Android App
## Architecture
The HermesControl Jetpack Compose Android application communicates with the local Hermes agent instance via two communication channels:
1. **REST API (Port 9119)**: For status queries, listing sessions, and system configuration.
2. **WebSocket Gateway (`/api/ws?token=<TOKEN>`)**: For real-time JSON-RPC messaging (chat, prompt submission, stream events, tool execution updates, etc.).
### Package Boundaries
- `com.m57.hermescontrol.data.model` — REST API data classes.
- `com.m57.hermescontrol.data.remote` — Retrofit ApiClient and HermesApiService.
- `com.m57.hermescontrol.data.ws` — JSON-RPC models, HermesWsClient, and EventParser.
- `com.m57.hermescontrol.data.local` — AuthManager (EncryptedSharedPreferences).
- `com.m57.hermescontrol.ui.connect` — ConnectScreen and ConnectViewModel.
- `com.m57.hermescontrol.ui.chat` — ChatScreen, ChatViewModel, and ChatBubble.
- `com.m57.hermescontrol.ui.settings` — SettingsScreen and SettingsViewModel.
- `com.m57.hermescontrol.theme` — Color, Theme, and Type.
---
## Milestones
| # | Name | Scope | Dependencies | Status |
|---|---|---|---|---|
| M1 | Exploration & Setup | Explore codebase, identify dependencies and structure | None | DONE |
| M2 | Networking & Data Layer | Implement/Fix AuthManager, api clients, WS clients, EventParser | M1 | DONE |
| M3 | UI Screen Implementation | Implement ConnectScreen, ChatScreen, SettingsScreen, and Nav integration | M2 | DONE |
| M4 | Unit Testing | Implement unit tests for AuthManager, EventParser, Connect/Chat ViewModels | M2, M3 | DONE |
| M5 | Styling & Audit Validation | Perform ktlint formatting fixes, compiler error fixes, and run Forensic Audit | M4 | DONE |
| M6 | E2E Testing Track | Design and build comprehensive opaque-box integration tests (Tiers 1-4) | M5 | PLANNED |
| M7 | API Integration & Models | Add Retrofit endpoints and JSON serialization models for Skills & Cron Jobs | M5 | PLANNED |
| M8 | Skills Management UI | Implement Viewmodel and UI Screen for skills management | M7 | PLANNED |
| M9 | Cron Jobs Management UI | Implement Viewmodel and UI Screen for cron jobs control | M7 | PLANNED |
| M10 | Navigation Drawer | Integrate Material3 navigation drawer using Navigation3 keys | M8, M9 | PLANNED |
| M11 | E2E & Hardening Phase | Verify all E2E tests, perform adversarial coverage hardening (Tier 5) | M6, M10 | PLANNED |
---
## Interface Contracts
### REST API Service
- `getStatus()`: Returns `StatusResponse` (version, gateway status, active sessions)
- `getSessions()`: Returns `SessionListResponse` (all sessions and metadata)
- `getSessionMessages(sessionId: String)`: Returns `SessionMessagesResponse` (message history)
- `getSystemStats()`: Returns `SystemStatsResponse` (CPU, memory, etc.)
- `getSkills()`: Returns list of skills (`GET api/skills`)
- `toggleSkill(payload)`: Toggles a skill's state (`PUT api/skills/toggle`)
- `getCronJobs()`: Returns list of cron jobs (`GET api/cron/jobs`)
- `pauseCronJob(id)`: Pauses a job (`POST api/cron/jobs/{id}/pause`)
- `resumeCronJob(id)`: Resumes a job (`POST api/cron/jobs/{id}/resume`)
- `triggerCronJob(id)`: Triggers a job immediately (`POST api/cron/jobs/{id}/trigger`)
- `deleteCronJob(id)`: Deletes a job (`DELETE api/cron/jobs/{id}`)
### WebSocket Client (`HermesWsClient`)
- `connect()`: Establish connection to `/api/ws?token=<TOKEN>`
- `disconnect()`: Terminate connection
- `sendMessage(text: String)`: Submit prompt (`prompt.submit` JSON-RPC method)
- `resumeSession(sessionId: String)`: Resume a session (`session.resume` JSON-RPC method)
- Callbacks: `onConnected()`, `onDisconnected(reason: String)`, `onMessage(JsonRpcResponse)`
### Event Parser (`EventParser`)
- Parses incoming JSON messages from WebSocket and extracts event fields (`event`, `data`, `id`, `result`, `error`).
- Categorizes events: `message.complete`, `message.token`, `tool.start`, `tool.complete`, `clarify.request`, etc.
## Code Layout
- `app/src/main/java/com/m57/hermescontrol/` (Main Source)
- `app/src/test/java/com/m57/hermescontrol/` (Unit Tests)
+2
View File
@@ -21,6 +21,7 @@ android {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
signingConfig = signingConfigs.getByName("debug")
}
}
compileOptions {
@@ -79,6 +80,7 @@ dependencies {
// Encrypted storage
implementation(libs.androidx.security.crypto)
implementation("androidx.startup:startup-runtime:1.1.1")
// Local tests: jUnit, coroutines, Android runner
testImplementation(libs.junit)
@@ -6,6 +6,8 @@ import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -35,6 +37,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.text.SpanStyle
@@ -96,6 +99,14 @@ private fun UserBubble(
.padding(horizontal = 12.dp, vertical = 3.dp),
contentAlignment = Alignment.CenterEnd,
) {
val gradientBrush =
Brush.linearGradient(
colors =
listOf(
MaterialTheme.colorScheme.primary,
MaterialTheme.colorScheme.secondary,
),
)
Surface(
modifier =
Modifier
@@ -107,8 +118,8 @@ private fun UserBubble(
bottomStart = 16.dp,
bottomEnd = 4.dp,
),
),
color = UserBubble,
).background(brush = gradientBrush),
color = Color.Transparent,
tonalElevation = 0.dp,
) {
Column(modifier = Modifier.padding(12.dp)) {
@@ -164,6 +175,11 @@ private fun AssistantBubble(
),
),
color = bubbleColor,
border =
BorderStroke(
width = 1.dp,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.1f),
),
tonalElevation = 1.dp,
) {
Column(modifier = Modifier.padding(12.dp)) {
@@ -12,6 +12,7 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
@@ -74,6 +75,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@@ -121,6 +123,16 @@ fun ChatScreen(
)
}
val backgroundGradient =
Brush.verticalGradient(
colors =
listOf(
MaterialTheme.colorScheme.background,
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
MaterialTheme.colorScheme.primary.copy(alpha = 0.05f),
),
)
Scaffold(
modifier = modifier.fillMaxSize(),
topBar = {
@@ -262,6 +274,7 @@ fun ChatScreen(
Modifier
.fillMaxSize()
.padding(paddingValues)
.background(backgroundGradient)
.navigationBarsPadding()
.imePadding(),
) {
@@ -460,13 +473,21 @@ private fun ChatInputBar(
exit = slideOutVertically(targetOffsetY = { it }),
) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 8.dp),
shape = RoundedCornerShape(24.dp),
colors =
CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface,
containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.85f),
),
elevation = CardDefaults.cardElevation(8.dp),
border =
BorderStroke(
width = 1.dp,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.15f),
),
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp),
) {
Column {
val commands =
@@ -3,6 +3,8 @@ package com.m57.hermescontrol.ui.connect
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -38,6 +40,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
@@ -64,10 +67,21 @@ fun ConnectScreen(
var passwordVisible by remember { mutableStateOf(false) }
val backgroundGradient =
Brush.verticalGradient(
colors =
listOf(
MaterialTheme.colorScheme.background,
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
),
)
Box(
modifier =
modifier
.fillMaxSize()
.background(backgroundGradient)
.imePadding(),
contentAlignment = Alignment.Center,
) {
@@ -110,9 +124,14 @@ fun ConnectScreen(
modifier = Modifier.fillMaxWidth(),
colors =
CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface,
containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.7f),
),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
border =
BorderStroke(
width = 1.dp,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.2f),
),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
) {
Column(
modifier =
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 40 KiB