Commit Graph
90 Commits
Author SHA1 Message Date
ARIA 81f42ab761 Local message cache: instant start + offline reading (SQLDelight)
- Cache.sq / ChatDb: lanes, channels and last_lane persisted as JSON
  snapshots; sanitize on restore (Pending->Failed, streaming->false);
  ephemeral items (tool, system) skipped
- Platform drivers via expect/actual: AndroidSqliteDriver (app db dir)
  / JdbcSqliteDriver (~/.iris/iris_cache.db)
- IrisController: restore before connect, debounced (750ms) snapshot
  persistence, synchronous flush on dispose, clearAll on forget
- History robustness: historyLoaded marked only when the response is
  processed (lost request/response retried on reconnect); events
  collector wrapped in try/catch; onHelloAck fast path so the history
  request fires on the WS thread instead of the starved state
  collector; frame-decode and history-load logging
- Protocol: HistoryMessage.media nullable (defensive vs older
  gateways that sent "media": null)
- Tests: ChatDbTest (jvmTest, JDBC in-memory), ChatStoreCacheTest,
  HistoryPayloadTest, HistoryWireTest (real captured 92KB response)
- docs/10: §10.7 implemented schema, new §10.9 cache behavior
2026-08-21 20:56:20 +02:00
ARIA 9a519e3c5a Omit null media in history frames (schema-conformant)
history() wrote "media": null for streaming finals, but the frame
schema says array. The app's HistoryMessage.media was non-nullable, so
deserialization threw and every history page was silently dropped.
Omit the key when media is absent instead.
2026-08-21 20:56:16 +02:00
ARIA 17bf41a0b9 Make thread/channel/message deletion complete (hard delete)
Deleting a thread, channel, or message was a no-op/soft-delete: messages
were only dropped from the plugin outbox (still in hermes' session store,
hence searchable/recoverable) and channels/threads were merely archived.

Now deletion is complete and non-recoverable, with no search trace:

- purge.py (new): hard-delete from hermes' session store (state.db).
  delete_lane wipes a channel's/thread's sessions + messages; deleting a
  messages row also drops it from the FTS5 index via the delete triggers.
  delete_message removes one message, matched by (session, role, exact
  content, closest timestamp) since plugin m_<hex> ids aren't persisted.
- channels.py: delete() hard-deletes the row (and a channel's child
  threads) instead of archiving.
- outbox.py: add delete_lane() (wipe all frames for a lane) and
  message_info() (read a message's final role/text/ts for the match).
- adapter.py: on_channel_delete wipes outbox + session store;
  on_message_delete purges the session-store row per message.
- App: delete confirmations no longer claim history stays for search;
  ChannelStore removes a channel's threads on channel delete.
- Docs updated to describe hard deletion.
2026-08-21 19:47:55 +02:00
ARIA 9286937e2d Runtime footer: app-controlled model/context/cwd/latency/cost under replies
Hermes can append a text "runtime footer" (model, context %, workdir,
latency, cost) to final replies, but only when display.runtime_footer is
enabled in the hermes config. We want the same info but controlled by the
APP, not the gateway config. So the gateway now ALWAYS sends the data as a
structured `runtime` object on final assistant messages, and the app decides
whether/what to show.

Gateway (gateway-plugin/):
- protocol.py: new runtime_footer() helper + `runtime` field on the
  message / message.stop frames. Keys (all optional, absent when the data
  is unavailable — e.g. no cost for local models): model (vendor prefix
  dropped), context_pct (0-100), cwd (home-relative), latency (seconds),
  cost (USD).
- adapter.py: a post_api_request plugin hook captures the turn's model +
  prompt tokens + start time (platform-filtered to android so other
  platforms don't pollute the buffer). _build_runtime_footer() resolves the
  model's context window (cached, best-effort, off the event loop via
  asyncio.to_thread with a timeout) and computes context_pct. The runtime
  object is attached on every final send (streaming message.stop and
  non-streaming message, plus the fallback paths).
- outbox.py: `runtime` preserved in history reconstruction so the footer
  survives a restart / first open.

App (app/shared/):
- Protocol.kt: RuntimeMeta data class + `runtime` on MessagePayload /
  MessageStopPayload / HistoryMessage.
- ChatStore.kt: `runtime` on MessageItem, wired through live + history
  reconciliation.
- SecureStore.kt (+ Android/Desktop actuals): runtimeFooterEnabled +
  runtimeFooterFields (persisted per device).
- IrisController.kt: StateFlows + toggleRuntimeFooter() /
  toggleRuntimeField(); RUNTIME_FIELD_KEYS / default set / parser.
- SettingsScreen.kt: "Runtime footer" switch; when on, an expandable chip
  menu (Model · Context % · Workdir · Latency · Cost) to pick fields.
- ChatScreen.kt: footer rendered on the SAME line as the timestamp (footer
  left, time right, Telegram-style), only for final non-streaming assistant
  answers; Inspector pane now shows the runtime fields too.

Docs: 04-wire-protocol.md + frames.schema.json document the `runtime`
object.

Verified end-to-end on device: final replies carry
`qwen3.8-27B-exl3-4.5bpw · 53% · ~ · 38s` with the time right-aligned on
the same line; 69/69 gateway tests pass, Kotlin builds + tests pass.
2026-08-21 19:32:05 +02:00
ARIA 678c0344c8 Clean up lint/LSP across gateway, Android, and desktop (alpha -> stable)
Gateway (gateway-plugin/):
- Fix interactive_setup broken imports: print helpers were imported from the
  wrong hermes module (hermes_cli.config instead of hermes_cli.cli_output) plus
  a non-existent print_code; the try/except swallowed the ImportError so
  `hermes gateway setup` for android always bailed out early.
- Fix release_scoped_lock type error (str | None passed where str required).
- Rewrite empty `except: pass` blocks as contextlib.suppress with rationale.
- Restructure two ambiguous ws_server try blocks (hello-auth, frame loop).
- Ruff cleanup: type annotations, import sorting, line wrapping, magic values
  -> named constants, `raise ... from e`, complexity. Add gateway-plugin/ruff.toml.
- Add pyrightconfig.json so the Python LSP resolves hermes-runtime imports.
- Suppress verified false positives inline (parameterized SQL, column-name
  "secrets", hermes-generated media path).

Android (app/androidApp + app/shared):
- Consolidate launcher icons into a single mipmap-anydpi (minSdk 29 >= 26) with
  the monochrome layer; clears ObsoleteSdkInt + MonochromeLauncherIcon.
- Bump core-splashscreen 1.0.1 -> 1.2.0; pin targetSdk 34 (deliberate).
- Suppress verified findings inline (LAN ws:// default, correct GCM IV usage).

Desktop (app/desktopApp):
- Move the desktop to a Java 21 runtime (org.gradle.java.home) and set the
  desktop jvmTarget to 21 (Android stays JVM 17 / minSdk 29). Fixes the startup
  UnsupportedClassVersionError and restores Markdown renderer 0.44.0.

Tooling/config:
- .pi-lens.json: disable verified-noisy heuristics (documented in docs).
- .gitleaks.toml: allowlist git-ignored false-positive paths.
- docs/18-code-review.md: full findings + verification.

Verified: ruff clean, pyright 0 errors, 64/64 gateway tests, all Kotlin tests,
Android lint 0 issues, Android installed+launched on device, desktop launches
on JDK 21.
2026-08-21 18:47:03 +02:00
ARIA 9f3f9842c8 Push notification dedupe: one message = one notification — an offline message was notified twice (FCM push, then again when the app synced the outbox and mirrored the replayed frames). Fix: the gateway records the highest outbox cursor delivered per device via push (devices.last_pushed_cursor, advanced only on successful send) and returns it in hello.ack; sync-replayed frames carry their outbox cursor in the envelope; the app skips system notifications for replayed frames at/below the watermark (live frames never suppressed — that is the case where no push fired). Also: 5s per-chat push coalescing so a cron delivery (notification frame + message frame) pushes once, and the FCM handler no longer posts a redundant notification (skips when WS is connected or FCM already displayed the notification payload; data-only messages are the exception). Docs: frames.schema.json, 04-wire-protocol.md, 08-push.md §8.8 2026-08-21 17:21:54 +02:00
ARIA acd5fb4ad0 Ntfy listener: only run when the paired gateway pushes via ntfy — the foreground service (and its permanent 'Listening for messages' notification) is now started/stopped based on the gateway's push backend from hello.ack server_caps (persisted as pushBackend); FCM gateways no longer keep a persistent listener alive, and switching gateways toggles the service on the next connect 2026-08-21 17:21:47 +02:00
ARIA 75d491fd30 Added default backdrops to the chat. 2026-08-21 15:48:30 +02:00
ARIA 96b60daf08 Update dependencies to latest + migrate to AGP 9.x
Kotlin 2.1.0->2.4.10, Compose Multiplatform 1.7.3->1.11.1, AGP 8.7.3->9.3.1, Gradle 8.10.2->9.7.1. The two libs that pinned us to Compose 1.7.x -- compose-webview-multiplatform (1.9.40->2.0.3) and multiplatform-markdown-renderer (0.33.0->0.44.0) -- now have Compose 1.8+ releases, so the whole stack moves. Also coroutines/serialization 1.11.0, okhttp 5.5.0, media3 1.11.0, activity-compose 1.13.0, firebase-messaging 25.1.2, compose-bom 2026.08.00, google-services 4.5.0, KCEF 2025.03.23 (JBR pin ->17.0.14); material3 1.9.0 / material-icons-extended 1.7.3 (each one's latest stable -- they lag the core).

AGP 9 migration: :shared swaps com.android.library->com.android.kotlin.multiplatform.library (android config moves into kotlin{android{}}, withHostTest{} keeps host tests); :androidApp drops kotlin(android) for AGP 9 built-in Kotlin. compileSdk 34->37 (minSdk stays 29 / Android 10).

Code fixes for API breaks: markdown link->textLink + highlights->highlightsBuilder; kotlinOptions{jvmTarget}->compilerOptions{jvmTarget}.
2026-08-21 12:14:09 +02:00
ARIA ec53769325 Chat bubbles: preserve single newlines in agent messages too — the hard-break rule from user bubbles now applies to agent replies as well, so status lines and model-wrapped text render one line per field instead of collapsing into a wall of text; unit tests for preserveNewlinesAsHardBreaks 2026-08-21 10:26:35 +02:00
ARIA 1bcadcf950 Added slash command handling 2026-08-21 10:18:48 +02:00
ARIA 10e9565e2e Channels: default-only threading + automation (read-only) channels — threading (topic switcher, Ctrl+T, auto-threading) is now only active on the default channel, and the gateway ignores auto_thread for other channels; new channel.set_automation frame marks a channel read-only for cron/webhook output (app hides the composer behind a notice, gateway rejects message.send, default channel cannot be marked, flag syncs to all devices via the full-entry channel.renamed response, gear badge in the list); header pill drops the 'Bot' subtitle and shows the chat_id on automation channels (tap to copy for cron delivery targeting); also fixes a pre-existing stray brace that made frames.schema.json invalid JSON 2026-08-21 09:39:32 +02:00
ARIA d7cd59b685 AGENTS.md: document the uiautomator workflow for ADB UI taps — never guess coordinates from a screenshot; dump the hierarchy, find the node by text/content-desc/resource-id, tap the center of its bounds, re-dump after navigation 2026-08-21 09:02:52 +02:00
ARIA 6846ab1e19 Settings screen: add 8dp spacing between cards (they were touching, which read as compressed) and move 'Reset appearance' above the 'Font size' card 2026-08-21 08:59:38 +02:00
ARIA abf1374c1e Font size setting: app-wide text scale (0.8x-1.5x, default 1.0x) via Settings > Appearance slider — applied through LocalDensity.fontScale so all sp text scales while dp layout keeps its proportions; the multiplier stacks on top of the system font scale; persisted per platform (EncryptedSharedPreferences / settings.json) like the other appearance settings 2026-08-21 08:54:48 +02:00
ARIA f79c54fead README: rewrite for users — what it is, feature list, build-from-source (gateway/Android/desktop/pairing), contributing, Apache-2.0 license; add LICENSE file 2026-08-21 01:17:23 +02:00
ARIA 73815b032a HTML artifacts (Android): enable DOM storage + pin a real base URL so localStorage/sessionStorage work — the library defaults domStorageEnabled off and a null base URL leaves the page on the opaque about:blank origin where the storage APIs throw, so interactive artifacts that persist state appeared broken; basic JS (DOM/event handlers) already ran 2026-08-21 01:05:31 +02:00
ARIA ac4097e9a1 android gateway: raise MAX_MESSAGE_LENGTH to 1M so long replies (and complete HTML artifacts) aren't chunked across fence-reopened messages — the stream consumer defaults to 4096 when the adapter sets no cap, and WS has no message-size limit 2026-08-21 00:43:42 +02:00
ARIA 7ba5632a03 HTML artifacts: render agent-sent HTML/CSS/JS code blocks in an in-app WebView (Android platform WebView, desktop JCEF/KCEF) via a full-screen preview; artifact card with Preview button + code toggle, shown only once the message stops streaming; webview-multiplatform 1.9.40 + KCEF deps, detection + unit tests 2026-08-21 00:43:39 +02:00
ARIA a1814d016c User bubbles: move timestamp + delivery checkmarks to their own bottom-right line (Telegram look, matching agent bubbles) instead of inline at the end of the text 2026-08-21 00:01:13 +02:00
ARIA f8effb4152 Chat bubbles: render markdown (bold/italic/tables/syntax-highlighted code) for agent + user messages; tap-to-copy inline code with flash, code-block copy button; preserve user newlines; removed stale HANDOFF doc 2026-08-20 23:29:43 +02:00
ARIA a8920d8d26 Channel badges: flat Material star (default) + heart (favorite) icons instead of text glyphs, so favorites no longer look like the default channel 2026-08-20 21:07:52 +02:00
ARIA d16b47abdf Channel context menu: hide Delete for the default channel (it can be renamed but not deleted; server already enforces this) 2026-08-20 21:00:36 +02:00
ARIA 49b7a3f577 Channel context menu: long-press/right-click a row → rename, favorite/unfavorite, add/change/remove icon (color or image), delete; removed star/delete row buttons; favorite + icon/color synced across devices 2026-08-20 20:55:24 +02:00
ARIA 079ef664a2 Back-to-bottom button: chevron FAB appears when scrolled up, tap glides to latest; new messages no longer yank the view while reading history; lane switch always lands on latest 2026-08-20 20:15:12 +02:00
ARIA 1ec705727a App icon: winged-bubble adaptive icon (Android) + window/jpackage icon (desktop); concept art in app/icons/ 2026-08-20 19:48:57 +02:00
ARIA 37c13b4d02 Compact single-row header: tappable status bubble (green/yellow-pulsing/red) + search, status toast on tap; removed overflow menu (rename/forget) 2026-08-20 19:33:45 +02:00
ARIA 0e8f69a43c User theme: customizable bubble colors + background (color/image) in Settings → Appearance
- UserTheme model (ARGB ints) + LocalUserTheme, persisted via SecureStore
- HSV color picker dialog (Apply keeps open, OK applies+closes, Cancel)
- Background: solid color or image (SAF on Android, JFileChooser on desktop)
- Reasoning block: 0.6 black overlay on agent bubble color (adapts automatically)
- Contrast-aware text on bubbles + pinned contentColor on root Surface
2026-08-20 19:14:35 +02:00
ARIA 86a4c8ee70 Added official hermes-gateway status messages (new comment category instead of tool calls and messages) 2026-08-20 18:11:36 +02:00
ARIA e9e1aed0f2 Long-press message selection + delete (message.delete frame, outbox removal, all-device sync) 2026-08-20 16:51:19 +02:00
ARIA efecf2732e Auto-threading, history pagination, streaming toggle + tool/reasoning display settings
- Auto-threading (Telegram topic-mode workflow): message.send {auto_thread}
  mints a fresh AI-named thread (instant derived title, LLM upgrade via
  channel.renamed); channel.created {auto:true}; the app jumps into the new
  thread and relocates the optimistic pending bubble.
- history frame: paged full message history for initial channel open /
  scroll-up pagination (reconstructed from the outbox log).
- Streaming on/off: gateway side (display.platforms.android.streaming) plus a
  per-device app toggle (Settings → Streaming); reasoning/model/tokens carried
  on message frames.
- Context menu: long-press (touch) / right-click (desktop) thread affordances
  via a KMP rightClick expect/actual.
- Settings → Reasoning: auto-collapse long reasoning blocks (default on).
- Tool detail: the gateway now always supplies full tool data — it forces
  verbose tool progress (full args → tool.start.args) and captures each
  completed call via the post_tool_call hook (output/duration/ok → tool.end).
  The app reveals the full call + output on expand (Truncated) and
  auto-expands cards in Everything mode.
2026-08-20 16:26:52 +02:00
ARIA e678caafdc Composer auto-grow (1→5 lines) + in-field attach; settings screen + persisted prefs
- Composer input starts at one line and auto-grows up to 5 lines (then scrolls),
  measured via onTextLayout; attach (paperclip) moved inside the field, right of
  the text, per the Telegram reference.

- New SettingsScreen (drawer/rail entry) for Threads + Tool detail; both now
  persist via SecureStore (Android prefs / desktop JSON).

- Add AGENTS.md; rename reference screenshot to telegram_screenshot_reference.jpg.
2026-08-20 13:12:24 +02:00
ARIA bf6bf7e8bd M7: polish + E2E + docs (layout pass, theming, states, e2e driver, schema, setup.md, security) 2026-08-20 12:00:13 +02:00
ARIA 0cc8b7aafe M6: desktop app (tray, two-pane layout, shortcuts, inspector, jpackage) 2026-08-19 20:43:47 +02:00
ARIA 2ecfe1c05c M5: push (FCM + ntfy) + offline catch-up + background notifications
Server (gateway-plugin):
- push.py: FCM (HTTP v1 service-account / legacy key) + ntfy backends; NtfyBackend.server_url for app discovery
- adapter.py: push on offline broadcast + high-priority push when live; fcm.register; server_caps.push_ntfy_server; outbox now ALWAYS appends so a reconnecting app catches up on live-delivered frames (fixes empty chat after notification tap / activity recreation)
- protocol.py / ws_server.py / outbox.py / plugin.yaml: M5 frames + env vars

App (Kotlin CMP):
- Protocol.kt: notification / fcm.register / sync frames + push caps
- GatewayClient.kt: hello carries push creds; auto-sync on reconnect
- IrisController.kt: banners, deep-link, notifyMessageIfBackgrounded (system notification on a regular reply when backgrounded)
- Android: PlatformPush, AppBridge, IrisNotifications, NtfyListenerService, IrisFirebaseMessagingService, AndroidPush, AndroidSecureStore
- Desktop: DesktopPush, DesktopSecureStore
- build files + manifest (permissions, services, deep-links)

Tests: 35-test tests/gateway/test_android.py suite passes (incl. new regression test_live_delivered_frame_still_parked_for_sync). E2E verified on device: push fire, reconnect sync, background notification, and message replay after ChatStore reset.
2026-08-19 19:20:55 +02:00
ARIA 913ee91024 M4: media upload/download/playback (both directions)
Gateway plugin:
- media.upload (chunked binary) -> size/sha256 verify + MIME re-sniff ->
  cache_*_from_bytes -> media.upload.ack
- media.offer / media.pull (chunked) for agent-sent media, delivery-path
  security re-checked at pull time
- send_* overrides mint media_id and emit media.offer
- message.send media_refs resolve to cached inbound media
- per-send + per-chunk timeouts so a stalled peer can't starve the rest

App (Kotlin CMP):
- Protocol: media frame types/payloads/builders
- GatewayClient: binary session, uploadMedia (chunked + streaming sha256),
  pullMedia serialized via Mutex so concurrent offers don't interleave
- ChatStore/IrisController: MediaItem, attachments, auto-pull on offer
- Platform media: SAF picker, ExoPlayer (audio mini-player + video), image
  loader, FileProvider document open (Android); AWT-free desktop actuals
- ChatScreen: attach button + chips, media rendering, keyboard dismiss on send

UI polish:
- preserve image aspect ratio (no stretching), cap dominant dimension
- adjustResize so only chat content squeezes for the keyboard
- clear focus (hide keyboard) on send

Docs: media.upload.ack in 04-wire-protocol.md + frames.schema.json +
07-media.md; M4 marked complete in 14-milestones.md.

Tests: 17-test tests/gateway/test_android.py suite passes.
2026-08-19 17:29:39 +02:00
ARIA 60296b33fe docs: mark M3 complete (channels/threads/cron/search) 2026-08-19 14:55:53 +02:00
ARIA 9b511fdd28 M3: channels/threads + cron delivery + search + outbox sync
Gateway plugin:
- ChannelDirectory (SQLite): channels + threads, friendly-name resolution
- FTS5 search bridge over state.db (scope all/chat, LIKE fallback)
- Outbox (monotonic cursor, 72h retention) for reconnect catch-up
- adapter: channel.* handlers, search, sync, cron target parsing
- ws_server: M3 frame routing + hello.ack sync cursor

App (KMP):
- ChannelStore (directory cache) + lane-aware ChatStore (per chat/thread)
- GatewayClient.sendFrame; IrisController channel/search/nav actions
- ChatScreen: channel drawer, thread toggle, topic switcher, search overlay
2026-08-19 14:53:48 +02:00
ARIA 218c50d688 M1+M2: gateway core loop + agent transparency
M1 (gateway core loop / text round-trip):
- WS server (ws_server.py): bind, hello auth (constant-time), hello.ack, heartbeat, connection registry
- pairing.py: token generation, pairing store, QR payload
- adapter.py: send() -> message frame; inbound message.send -> MessageEvent -> handle_message
- app: Connect screen, GatewayClient (connect + reconnect), ChatScreen send/render, SecureStore (Android/Desktop)
- tests/ws_probe.py: probe harness driving a real turn

M2 (streaming + reasoning + tools + commentary):
- protocol.py: M2 frame types (message.start/update/stop, tool.start/progress/end, commentary)
- adapter.py: per-chat turn-state machine; classify outbound into frames; _split_reasoning; tool-line parsing
- reasoning in streaming: capture via on_stream_delta hook (kind=reasoning, gated by plugins.stream_reasoning_deltas) with a FIFO barrier, attach to message.stop
- app: live streaming bubble, ReasoningBlock (collapse + copy), ToolCard (Everything/Truncated/Nothing), dimmed commentary, typing
- docs/14-milestones.md: M1/M2 marked done; reasoning note corrected
2026-08-19 13:56:19 +02:00
ARIA 59acf66c89 M0: toolchain, monorepo scaffold, gateway plugin skeleton, CMP app
- gateway-plugin/: android platform plugin (plugin.yaml + adapter.py
  register(ctx) + no-op AndroidAdapter) + stub modules for M1-M5
- app/: Compose Multiplatform project (shared KMP + androidApp +
  desktopApp) with Gradle wrapper; builds :androidApp:assembleDebug
  and :desktopApp:compileKotlin
- scripts/guard_hermes_agent.sh + pre-commit hook: fail if hermes-agent/
  is staged (read-only reference, never committed)
- .gitignore excludes hermes-agent/; docs/ reference library
2026-08-19 11:27:02 +02:00