diff --git a/2026-06-15_085500-android-app-for-hermes.md b/2026-06-15_085500-android-app-for-hermes.md deleted file mode 100644 index 0750a6ec..00000000 --- a/2026-06-15_085500-android-app-for-hermes.md +++ /dev/null @@ -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=` | JSON-RPC over WebSocket | -| **Auth (loopback mode)** | ✅ `Authorization: Bearer ` or `X-Hermes-Session-Token: ` | For REST API | -| **WebSocket Auth** | ✅ `?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 -) - -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 - - @GET("api/sessions") - suspend fun getSessions(): Response - - @GET("api/sessions/{id}/messages") - suspend fun getSessionMessages(@Path("id") sessionId: String): Response - - @GET("api/system/stats") - suspend fun getSystemStats(): Response -} - -// 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 = 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=` 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? // 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=` | -| **REST Auth** | `Authorization: Bearer *** or `X-Hermes-Session-Token: ` | -| **WS Auth** | `?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 ` ✅ works -- `X-Hermes-Session-Token: ` ✅ works -- No header → `401` ✅ -- Bad token → `401` ✅ - -### WebSocket JSON-RPC Protocol - -**Connection:** `ws://HOST:9119/api/ws?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!* 💋 \ No newline at end of file diff --git a/PROGRESS_STATUS.md b/PROGRESS_STATUS.md deleted file mode 100644 index 62332560..00000000 --- a/PROGRESS_STATUS.md +++ /dev/null @@ -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. diff --git a/PROJECT.md b/PROJECT.md deleted file mode 100644 index efede2a4..00000000 --- a/PROJECT.md +++ /dev/null @@ -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=`)**: 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=` -- `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) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d3bffba2..97d0cca7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatBubble.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatBubble.kt index 3c83148b..d4116c16 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatBubble.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatBubble.kt @@ -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)) { diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt index c3023b03..8400244b 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt @@ -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 = diff --git a/app/src/main/java/com/m57/hermescontrol/ui/connect/ConnectScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/connect/ConnectScreen.kt index bd6e710e..e700ba5a 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/connect/ConnectScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/connect/ConnectScreen.kt @@ -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 = diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index 6f3b755b..00000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index 6f3b755b..00000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp index c209e78e..b7168f69 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp index b2dfe3d1..b7168f69 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp index 4f0f1d64..e071501d 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp index 62b611da..e071501d 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index 948a3070..c359de22 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index 1b9a6956..c359de22 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp index 28d4b77f..d5abd717 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index 9287f508..d5abd717 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index aa7d6427..197929f5 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index 9126ae37..197929f5 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ