Skip to content

feat(hermes): Hermes-only mode — run with no Open WebUI server#522

Merged
cogwheel0 merged 3 commits into
feat/hermes-agentfrom
feat/hermes-only-mode
Jun 24, 2026
Merged

feat(hermes): Hermes-only mode — run with no Open WebUI server#522
cogwheel0 merged 3 commits into
feat/hermes-agentfrom
feat/hermes-only-mode

Conversation

@cogwheel0

@cogwheel0 cogwheel0 commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Summary

Makes Open WebUI optional so a user can install the app and use a self-hosted Hermes Agent exclusively — no OWUI server at all. The mode is derived from what's configured and is switchable anytime; first run shows a backend chooser.

Stacked on feat/hermes-agent (#521) — review that one first; this PR's diff is the Hermes-only delta.

Design — two signals

  • preferredBackendProvider (lib/core/providers/backend_mode_providers.dart) — a synchronous, persisted Notifier<PreferredBackend> (unset | owui | hermes). Read by the router only, for boot determinism — mirrors reviewerModeProvider's synchronous role. On cold boot the router runs redirect() while activeServerProvider is still loading and Hermes secrets are still loading, so a purely-derived gate would bounce a Hermes-only user to server-connection. The flag avoids that; real gating stays derived.
  • hermesOnlyModeProvider (derived, in hermes_providers.dart) — !reviewerMode && hermesConfig.isUsable && activeServer == null. Drives UI gating; widgets rebuild as it settles.

What's included

  • Onboarding: new BackendChooserPage (Connect to Open WebUI / Use a Hermes Agent) + Routes.backendChooser. Reuses HermesSettingsPage(isOnboarding: true) with a "Finish setup" button that validates isUsable, sets preferredBackend = hermes, and enters the app.
  • Router: Hermes-only short-circuits straight to chat before any OWUI server/auth branch; holds splash (not server-connection) while Hermes secrets load; first run → chooser. Disabled-Hermes safety net avoids a stuck splash.
  • Models: Models.build / defaultModelProvider surface + auto-select the synthetic Hermes model when there's no OWUI API. Send guard relaxed (extracted to a pure isSendBlocked() helper) to allow api == null for Hermes models.
  • UI gating: hide chats/notes/terminal/channels across the 3 synchronized tab sites — Hermes becomes the home tab. preferredBackend = owui is set on successful OWUI connect.
  • Composer gating: when a Hermes model is active, hide the OWUI affordances — the "+" overflow button, quick pills, and the iOS keyboard-accessory actions (Hermes has no OWUI tools/web-search/image-gen/attachments and uses its own / skills).
  • iOS native settings sheet (100% data-driven from Dart, no Swift changes): gate the OWUI account sections (profile, AI memory, data connection, password + profile detail sheets, sign-out) on Hermes-only; render a Hermes-branded profile header (name "Hermes Agent", agent host, hermes_agent.png avatar); the About detail skips the server lookup so it no longer errors with no server.
  • Bidirectional switching: a "Connect to Open WebUI" entry in both the native sheet (routed via a control event in main.dart) and the Flutter profile page; the router reaches the OWUI connect/auth routes for a Hermes-only user; preferredBackend resets to unset when a Hermes-only backend is disabled.

Verification

  • flutter analyze clean; flutter test (2218) passing — incl. unit tests for preferredBackend parse/persistence, hermesOnlyMode derivation (usable/no-server, server-present, disabled, incomplete, reviewer precedence), isSendBlocked (the send-guard relaxation), and modelsProvider/defaultModelProvider Hermes surfacing + auto-select when api == null.
  • On-device E2E (erased iOS simulator, fresh install, live Hermes server): backend chooser → "Use a Hermes Agent" → enter URL/key (capabilities + toolsets load) → Finish → lands in Hermes-only chat (header "Hermes Agent", no model dropdown) → sidebar shows only the Hermes tab → kill/relaunch boots straight into Hermes chat (boot determinism) → send created a new server-side session with no OWUI backend.

Note

Add Hermes-only mode to run without an Open WebUI server

  • Introduces a preferredBackendProvider that persists whether the user has chosen Open WebUI or a Hermes Agent as their backend, read synchronously at boot.
  • Adds a /backend-chooser onboarding screen that lets first-time users pick between the two backends, replacing the previous direct redirect to /server-connection.
  • When Hermes is the preferred backend and no Open WebUI server is configured, the router sends users directly to /chat; the sidebar hides Chats, Notes, Terminal, and Channels tabs; and the profile sheet replaces account options with a 'Connect to Open WebUI' entry.
  • modelsProvider and defaultModelProvider now surface and auto-select a synthetic Hermes model when no OpenWebUI API is available, and message send/regenerate are unblocked for Hermes models without an API.
  • The Hermes settings page gains an onboarding mode (triggered via GoRouter state.extra) that auto-enables Hermes, hides the toggle, and routes to chat on completion.
  • Behavioral Change: users with no configured server are now redirected to /backend-chooser instead of /server-connection.
📊 Macroscope summarized 821a735. 17 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f3ec69ea-1631-4584-9330-c07f7f969d2e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/hermes-only-mode

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cogwheel0 cogwheel0 force-pushed the feat/hermes-only-mode branch 2 times, most recently from 42aac43 to 9695628 Compare June 20, 2026 15:09
@cogwheel0 cogwheel0 force-pushed the feat/hermes-agent branch from 6493ded to 1e4713f Compare June 22, 2026 04:54
@cogwheel0 cogwheel0 force-pushed the feat/hermes-only-mode branch from 9695628 to 821a735 Compare June 22, 2026 04:57

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High

final session = await api!.sendMessageSession(

regenerateMessage passes the isSendBlocked guard for Hermes models with api == null, but then unconditionally calls api!.sendMessageSession(...) at line 4215. This throws a null assertion error in Hermes-only mode because the function lacks the Hermes dispatch path that _sendMessageInternal uses. Consider adding Hermes transport routing before the OpenWebUI API call.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @lib/features/chat/providers/chat_providers.dart around line 4215:

`regenerateMessage` passes the `isSendBlocked` guard for Hermes models with `api == null`, but then unconditionally calls `api!.sendMessageSession(...)` at line 4215. This throws a null assertion error in Hermes-only mode because the function lacks the Hermes dispatch path that `_sendMessageInternal` uses. Consider adding Hermes transport routing before the OpenWebUI API call.

if (_hermesAvatarBytesCache != null) return _hermesAvatarBytesCache;
try {
final data = await rootBundle.load('assets/icons/hermes_agent.png');
return _hermesAvatarBytesCache = data.buffer.asUint8List();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low widgets/sidebar_user_pill.dart:37

The data.buffer.asUint8List() call ignores the ByteData offset and length, so if the underlying buffer is larger than the asset, the returned Uint8List includes extra trailing bytes that corrupt the cached avatar image. Consider passing data.offsetInBytes and data.lengthInBytes to asUint8List to return only the exact asset bytes.

Suggested change
return _hermesAvatarBytesCache = data.buffer.asUint8List();
return _hermesAvatarBytesCache = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @lib/features/navigation/widgets/sidebar_user_pill.dart around line 37:

The `data.buffer.asUint8List()` call ignores the `ByteData` offset and length, so if the underlying buffer is larger than the asset, the returned `Uint8List` includes extra trailing bytes that corrupt the cached avatar image. Consider passing `data.offsetInBytes` and `data.lengthInBytes` to `asUint8List` to return only the exact asset bytes.

@cogwheel0 cogwheel0 force-pushed the feat/hermes-only-mode branch from 821a735 to 37dfc29 Compare June 22, 2026 08:23
Make Open WebUI optional so a user can install the app and use a self-hosted
Hermes Agent exclusively. The mode is derived from what's configured and is
switchable; first run shows a backend chooser.

- Mode signals: a synchronous persisted `preferredBackend` (unset|owui|hermes)
  for boot-deterministic routing, plus a derived `hermesOnlyMode` for UI gating
  (mirrors the reviewer-mode precedent).
- Router: Hermes-only short-circuits to chat with no OWUI server/auth; first run
  routes to a new backend chooser; holds splash while Hermes secrets load.
- Onboarding: BackendChooserPage + HermesSettingsPage(isOnboarding) with a
  Finish button that sets preferredBackend=hermes and enters the app.
- Models/defaultModel surface + auto-select the synthetic Hermes model when
  there's no OWUI API; send guard relaxed for Hermes models.
- UI gating: hide chats/notes/terminal/channels (3 synced sites); Hermes is the
  home tab. preferredBackend=owui set on OWUI connect.

Verified end-to-end on an erased iOS simulator (fresh install): chooser →
Hermes setup → Finish → Hermes-only chat → only the Hermes tab → relaunch boots
straight to chat → send creates a server session with no OWUI.
flutter analyze clean; flutter test (2200) passing.
…, tests

Address the Hermes-only follow-ups:

- Composer: hide OWUI affordances (the "+" overflow button, quick pills, and the
  iOS keyboard-accessory actions) when a Hermes model is selected — Hermes has no
  OWUI tools/web-search/image-gen/attachments and uses its own `/` skills.
- iOS native settings sheet: gate the OWUI account sections (profile header,
  AI memory, data connection, password + profile detail sheets, sign-out) on
  Hermes-only, and make the About detail skip the server lookup so it no longer
  errors with no server.
- Bidirectional switching: add a "Connect to Open WebUI" entry in both the native
  sheet (routed via a control event in main.dart) and the Flutter profile page;
  let the router reach the OWUI connect/auth routes for a Hermes-only user; reset
  preferredBackend to unset when a Hermes-only backend is disabled.
- Tests: preferredBackend parse/persistence round-trip and hermesOnlyMode
  derivation (usable/no-server, server-present, disabled, incomplete, reviewer
  precedence).

flutter analyze clean; flutter test (2211) passing.
…tests

Close out the last Hermes-only follow-ups:

- iOS native settings sheet: render a Hermes-branded profile header in
  Hermes-only mode ("Hermes Agent" + the agent host + the hermes_agent.png
  avatar). Fully data-driven from Dart — no Swift change; the avatar bytes are
  pre-loaded (cached) before the sync config builder runs.
- Extract the inline send/regenerate guard into a pure isSendBlocked() helper
  used at both sites, removing the duplicated condition and making the
  Hermes-only relaxation unit-testable.
- Tests: isSendBlocked (null model, OWUI-no-api, api present, reviewer, Hermes
  relaxation); modelsProvider surfaces only the synthetic Hermes model when
  unauthed + usable Hermes; defaultModelProvider auto-selects + writes through
  the Hermes model when api == null.

flutter analyze clean; flutter test (2218) passing.
@cogwheel0 cogwheel0 force-pushed the feat/hermes-only-mode branch from 37dfc29 to b85676e Compare June 22, 2026 08:24
Comment on lines +48 to +49
await notifier.ensureSessionKey();
await ref

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low views/hermes_settings_page.dart:48

After the two await calls (setEnabled and ensureSessionKey), ref.read(preferredBackendProvider.notifier) on line 49 accesses ref on a potentially disposed ConsumerState, which throws. The if (!mounted) return check on line 52 only guards context.go but comes after the unsafe ref.read. Add a mounted check before line 49 to guard that access as well.

 await notifier.ensureSessionKey();
+    if (!mounted) return;
     await ref
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @lib/features/hermes/views/hermes_settings_page.dart around lines 48-49:

After the two `await` calls (`setEnabled` and `ensureSessionKey`), `ref.read(preferredBackendProvider.notifier)` on line 49 accesses `ref` on a potentially disposed `ConsumerState`, which throws. The `if (!mounted) return` check on line 52 only guards `context.go` but comes after the unsafe `ref.read`. Add a `mounted` check before line 49 to guard that access as well.

@cogwheel0 cogwheel0 merged commit b85676e into feat/hermes-agent Jun 24, 2026
2 checks passed
@cogwheel0 cogwheel0 deleted the feat/hermes-only-mode branch June 24, 2026 13:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant