Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90b5ad8d99 | ||
|
|
57a4f573f6 | ||
|
|
c1320b4712 | ||
|
|
d3b693524f | ||
|
|
66f95e08c2 | ||
|
|
1a4d56c215 | ||
|
|
b2c2f32584 | ||
|
|
15fde033c3 | ||
|
|
10a1e57c9b | ||
|
|
4f10080501 | ||
|
|
f8ea02c14d | ||
|
|
122fe955b6 | ||
|
|
017d7f1eca | ||
|
|
cabda6b77a | ||
|
|
33fca2383c | ||
|
|
be951a4d1d | ||
|
|
bba9a236c3 |
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -33,14 +33,14 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Extract semver tags: e.g. v0.28 -> 0.28, latest
|
||||
# Extract tags from the git ref (supports vX.Y and vX.Y.Z formats)
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=match,pattern=v(.*),group=1
|
||||
type=match,pattern=v(\d+\.\d+(?:\.\d+)?),group=1
|
||||
type=raw,value=latest
|
||||
|
||||
# Build and push multi-arch image (amd64 + arm64)
|
||||
|
||||
153
CHANGELOG.md
153
CHANGELOG.md
@@ -5,6 +5,157 @@
|
||||
|
||||
---
|
||||
|
||||
## [v0.30.1] CLI Session Bridge Fixes
|
||||
*April 4, 2026 | 424 tests*
|
||||
|
||||
### Bug Fixes
|
||||
- **CLI sessions not appearing in sidebar.** Three frontend gaps: `sessions.js`
|
||||
wasn't rendering CLI sessions (missing `is_cli_session` check in render loop),
|
||||
sidebar click handler didn't trigger import, and the "cli" badge CSS selector
|
||||
wasn't matching the rendered DOM structure. (#58)
|
||||
- **CLI bridge read wrong profile's state.db.** `get_cli_sessions()` resolved
|
||||
`HERMES_HOME` at server launch time, not at call time. After a profile switch,
|
||||
it kept reading the original profile's database. Now resolves dynamically via
|
||||
`get_active_hermes_home()`. (#59)
|
||||
- **Silent SQL error swallowed all CLI sessions.** The `sessions` table in
|
||||
`state.db` has no `profile` column — the query referenced `s.profile` which
|
||||
caused a silent `OperationalError`. The `except Exception: return []` handler
|
||||
swallowed it, returning zero CLI sessions. Removed the column reference and
|
||||
added explicit column-existence checks. (#60)
|
||||
|
||||
### Features
|
||||
- **"Show CLI sessions" toggle in Settings.** New checkbox in the Settings panel
|
||||
to show/hide CLI sessions in the sidebar. Persisted server-side in
|
||||
`settings.json` (`show_cli_sessions`, default `true`). When disabled, CLI
|
||||
sessions are excluded from `/api/sessions` responses. (#61)
|
||||
|
||||
---
|
||||
|
||||
## [v0.30] CLI Session Bridge (Community: @thadreber-web)
|
||||
*April 4, 2026 | 424 tests*
|
||||
|
||||
### Features
|
||||
- **CLI session bridge.** The WebUI now reads sessions from the hermes-agent's
|
||||
SQLite store (`state.db`). CLI sessions appear in the sidebar with a gold
|
||||
"cli" indicator badge. Click to import into the WebUI store with full message
|
||||
history — replies then work through the normal agent pipeline.
|
||||
- **`/api/session/import_cli` endpoint.** Imports a CLI session into the WebUI
|
||||
JSON store. Idempotent — returns existing session if already imported.
|
||||
Derives title from first message, inherits active profile and workspace.
|
||||
- **`/api/sessions` merges CLI sessions.** Sidebar shows both WebUI and CLI
|
||||
sessions sorted by last activity. Deduplication ensures WebUI sessions take
|
||||
priority when the same session_id exists in both stores.
|
||||
- **CLI session fallback on `/api/session`.** If a session_id isn't found in
|
||||
the WebUI store, falls back to reading from the CLI SQLite store.
|
||||
|
||||
### Architecture
|
||||
- `api/models.py`: `get_cli_sessions()`, `get_cli_session_messages()`,
|
||||
`import_cli_session()`. All use parameterized SQL queries and `with` for
|
||||
connection management. Graceful fallback on missing sqlite3 or state.db.
|
||||
- `api/routes.py`: CLI fallback in GET `/api/session`, merged list in
|
||||
GET `/api/sessions`, POST `/api/session/import_cli`.
|
||||
- `static/style.css`: `.cli-session` indicator styles (gold border + badge).
|
||||
|
||||
---
|
||||
|
||||
## [v0.29] Sprint 23: Agentic Transparency + Polish
|
||||
*April 4, 2026 | 424 tests*
|
||||
|
||||
### Features
|
||||
|
||||
- **Token/cost display.** Agent usage (input tokens, output tokens, estimated
|
||||
cost) is now read after each conversation and persisted on the session.
|
||||
A muted badge appears below the last assistant message when enabled.
|
||||
Off by default — toggle via the Settings panel checkbox or `/usage` slash
|
||||
command. Persists server-side across refreshes.
|
||||
|
||||
- **Subagent delegation cards.** `subagent_progress` events now render with
|
||||
a 🔀 icon and a blue indented left border to visually distinguish child
|
||||
tool activity from parent tool calls. `delegate_task` cards display as
|
||||
"Delegate task" with cleaner formatting.
|
||||
|
||||
- **Skill picker in cron create form.** The "New Job" form now has a search
|
||||
input + tag chip picker for attaching skills to cron jobs. Skills fetched
|
||||
from `/api/skills`, filtered on keyup, added/removed as tag chips.
|
||||
`submitCronCreate()` sends `skills` array in the POST body. Backend already
|
||||
supported the field — this was a pure frontend gap.
|
||||
|
||||
- **Skill linked files viewer.** Skill preview panel now renders a "Linked
|
||||
Files" section below SKILL.md content when a skill has `references/`,
|
||||
`templates/`, `scripts/`, or `assets/` subdirectories. Clicking a file
|
||||
loads it in the preview panel with syntax highlighting.
|
||||
New `file` query param on `GET /api/skills/content` serves linked files
|
||||
with path traversal protection.
|
||||
|
||||
- **Workspace tree state persists across refreshes.** Expanded directory
|
||||
paths are saved to `localStorage` keyed by workspace path
|
||||
(`hermes-webui-expanded:{path}`). On every root load (page refresh,
|
||||
session switch), the saved state is restored and previously-expanded
|
||||
directories are pre-fetched so the tree renders fully on first paint.
|
||||
|
||||
- **Timestamps fixed.** `api/streaming.py` now stamps `timestamp` on every
|
||||
message that lacks one at conversation completion. The `done` SSE event
|
||||
also stamps `_ts` on the last assistant message immediately. Timestamps
|
||||
were already rendered in the UI (Sprint 14, hover-to-reveal) but most
|
||||
messages had no timestamp field, so nothing ever showed.
|
||||
|
||||
- **`/usage` slash command.** Instant toggle for token usage display.
|
||||
Shows a toast, persists to server, updates the Settings checkbox if open,
|
||||
re-renders immediately.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **XSS via inline onclick + esc().** Skill names and file paths embedded in
|
||||
`onclick` HTML attributes used `esc()` for encoding. `esc()` converts `'`
|
||||
to `'` (HTML-safe) but browsers decode it back before executing JS,
|
||||
allowing skill names with apostrophes to break out of string literals.
|
||||
Fixed by switching to `data-*` attributes + `addEventListener`.
|
||||
|
||||
- **rglob wildcard injection.** The `name` query param for
|
||||
`/api/skills/content?file=` was passed directly to `SKILLS_DIR.rglob()`,
|
||||
which accepts glob patterns. `name=*` would match an arbitrary directory
|
||||
and use it as the trust base for path traversal checking.
|
||||
Fixed by rejecting names containing `* ? [ ]` metacharacters with 400.
|
||||
|
||||
- **`_fmtTokens(null)` returned "null".** `String(null)` = `"null"` would
|
||||
appear in the usage badge for sessions missing fields. Fixed with a
|
||||
`!n || n < 0` guard returning `'0'`.
|
||||
|
||||
- **Usage badge on wrong row.** Badge used `:last-child` which could target
|
||||
a user message row. Fixed by adding `data-role` to message rows and
|
||||
scanning backwards for the last `assistant` row.
|
||||
|
||||
- **Tool name resolution.** Tool call entries in session JSON sometimes
|
||||
stored the literal string `"tool"` as the name when the call ID couldn't
|
||||
be resolved. Fixed: defaults to empty string and skips unresolvable entries.
|
||||
|
||||
- **Inline import inside loop.** `import json as _j2` inside the done-handler
|
||||
loop in `streaming.py` moved to module-level.
|
||||
|
||||
### Session Model
|
||||
|
||||
- Added `input_tokens`, `output_tokens`, `estimated_cost` fields to Session
|
||||
(defaults: 0, 0, None). Included in `compact()`, session JSON, and all
|
||||
API responses. Backward-compatible via `**kwargs`.
|
||||
|
||||
- Added `args` capture to `tool_calls` session JSON entries (truncated
|
||||
snapshot of tool inputs, up to 6 keys / 120 chars each).
|
||||
|
||||
### Settings
|
||||
|
||||
- New `show_token_usage` boolean setting (default: `false`). Stored in
|
||||
`settings.json`, loaded on boot alongside `send_key`.
|
||||
|
||||
### Tests
|
||||
|
||||
- Renamed `test_sprint24.py` → `test_sprint23.py`.
|
||||
- Strengthened session usage assertions (explicit field presence checks).
|
||||
- Added: path traversal rejection test, wildcard name rejection test,
|
||||
cron create with skills array test.
|
||||
- Total: 424 tests (up from 415).
|
||||
|
||||
---
|
||||
|
||||
## [v0.28.1] CI Pipeline + Multi-Arch Docker Builds
|
||||
*April 3, 2026 | 426 tests*
|
||||
|
||||
@@ -917,4 +1068,4 @@ Three-panel layout: sessions sidebar, chat area, workspace panel.
|
||||
|
||||
---
|
||||
|
||||
*Last updated: v0.28.1, April 3, 2026 | Tests: 426*
|
||||
*Last updated: v0.30.1, April 4, 2026 | Tests: 424*
|
||||
|
||||
375
HERMES.md
Normal file
375
HERMES.md
Normal file
@@ -0,0 +1,375 @@
|
||||
# Why Hermes
|
||||
|
||||
Hermes is a persistent, autonomous AI agent that lives on your server. It remembers everything,
|
||||
schedules work while you sleep, and gets more capable the longer it runs. This document explains
|
||||
the mental model, why that matters, and how Hermes compares to every major AI tool available today.
|
||||
|
||||
---
|
||||
|
||||
## The Core Idea: Assistants Forget. Agents Don't.
|
||||
|
||||
Every time you open Claude Code, Codex, or a chat window, the tool starts from zero. It does not
|
||||
know who you are, what you worked on yesterday, how your repo is structured, or what bugs you
|
||||
already fixed. You re-explain yourself every single session. The tool is powerful in the moment
|
||||
and useless the next day.
|
||||
|
||||
Hermes fills that gap. It runs on your server, retains context across every session, and acts
|
||||
on your behalf whether or not you are at a keyboard.
|
||||
|
||||
```
|
||||
Assistant model: You -> [Tool] -> Answer -> Done
|
||||
(tool forgets everything when the window closes)
|
||||
|
||||
Agent model: You <-> [Hermes] <-> (memory, skills, schedule, tools)
|
||||
(persistent, learns your stack, acts on your behalf, runs while you're offline)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The Three Pillars
|
||||
|
||||
### 1. Memory That Compounds
|
||||
|
||||
Hermes has layered memory that survives every session, every reboot, every model swap:
|
||||
|
||||
- **User profile** -- who you are, your preferences, your communication style, things you've
|
||||
corrected Hermes on
|
||||
- **Agent memory** -- facts about your environment, your toolchain, your project conventions
|
||||
- **Skills** -- reusable procedures Hermes discovers and saves; it never has to relearn how to
|
||||
deploy your app, run your tests, or review a PR
|
||||
- **Session history** -- every past conversation is searchable; Hermes can recall what you
|
||||
worked on last Tuesday
|
||||
|
||||
When you correct Hermes, it remembers. When it solves a tricky problem, it saves the approach.
|
||||
When it learns your stack, that knowledge carries into every future session.
|
||||
|
||||
### 2. Autonomous Scheduling
|
||||
|
||||
Hermes can run jobs without you present -- every hour, every morning, on any cron schedule.
|
||||
It fires up a fresh session, runs the task, and delivers the result to wherever you want it:
|
||||
Telegram, Discord, Slack, Signal, WhatsApp, SMS, email, and more.
|
||||
|
||||
Things Hermes can do while you sleep:
|
||||
|
||||
- Review new pull requests on your GitHub repo and post a full verdict comment
|
||||
- Send you a morning briefing of news, markets, or anything else you care about
|
||||
- Run your test suite and alert you if something breaks
|
||||
- Watch a competitor's blog for new posts and summarize them
|
||||
- Monitor a datasource and notify you when a threshold is crossed
|
||||
|
||||
### 3. Reach It From Anywhere
|
||||
|
||||
Hermes runs on your server and is reachable from every surface: terminal over SSH, the web UI
|
||||
(this project), and messaging apps including Telegram, Discord, Slack, WhatsApp, Signal, and
|
||||
Matrix. Start a task from your phone, check it from the browser on your laptop, continue it in
|
||||
a terminal on a remote server. The same agent, memory, and history follow you everywhere.
|
||||
|
||||
---
|
||||
|
||||
## A Framework for AI Tools
|
||||
|
||||
There are four distinct categories of AI tool. Understanding the category tells you what a tool
|
||||
can and cannot do.
|
||||
|
||||
### Category 1: Chat Assistants
|
||||
*Claude.ai, ChatGPT, Gemini*
|
||||
|
||||
You open a window, ask something, get an answer. No persistent memory beyond the conversation,
|
||||
no ability to run code or touch files, no way to act on your behalf. Excellent for Q&A,
|
||||
drafting, and brainstorming. You re-explain your context every session.
|
||||
|
||||
### Category 2: IDE Integrations
|
||||
*GitHub Copilot, Cursor, Windsurf, Zed AI*
|
||||
|
||||
Deep inside your editor. Autocomplete, inline diffs, refactors -- all excellent. Windsurf was
|
||||
earliest with workspace-scoped memory (Cascade Memories); Copilot has been shipping repo-level
|
||||
memory since late 2025 and is catching up. Cursor has no native memory as of early 2026. None
|
||||
have scheduling or messaging access. Tied to one machine and one editor.
|
||||
|
||||
### Category 3: Agentic CLI Tools
|
||||
*Claude Code, Codex CLI, OpenCode, Aider*
|
||||
|
||||
The current frontier for most developers. Can use real tools -- run shell commands, read and
|
||||
write files, search the web, call APIs. Great for deep, multi-step tasks in a single terminal
|
||||
session. All are adding memory and scheduling features to varying degrees (see comparisons below),
|
||||
but the core model is still session-scoped: you invoke it, it works, it stops.
|
||||
|
||||
### Category 4: Persistent Autonomous Agents
|
||||
*Hermes, OpenClaw (as of early 2026)*
|
||||
|
||||
All the tool use of Category 3, plus memory that accumulates across sessions, plus always-on
|
||||
scheduling, plus multi-modal access from any device or messaging app. Gets more useful over time
|
||||
rather than resetting to zero. Hermes and OpenClaw are the two primary open-source, self-hosted
|
||||
tools in this category. OpenClaw is a gateway-centric automation platform; Hermes is a
|
||||
self-improving agent that writes and reuses its own procedures from experience.
|
||||
|
||||
---
|
||||
|
||||
## How Hermes Compares
|
||||
|
||||
### vs. OpenClaw
|
||||
|
||||
OpenClaw is the most direct comparison to Hermes and the question most people ask first.
|
||||
Both are open-source, self-hosted, always-on agents with persistent memory, cron scheduling,
|
||||
and messaging app integration. If you're evaluating Hermes, you should evaluate OpenClaw too.
|
||||
|
||||
OpenClaw (MIT, ~347k GitHub stars) is built around a **Gateway** control plane written in
|
||||
Node.js/TypeScript. It excels at broad personal automation: native Chrome/Chromium control for
|
||||
browser automation, the widest messaging platform support in the space (WhatsApp, Telegram,
|
||||
Signal, iMessage, LINE, WeChat, Slack, Discord, Teams, Matrix, and more), voice wake words,
|
||||
and a ClawHub skill marketplace where users share pre-built automations. The community is large
|
||||
and the ecosystem is growing fast.
|
||||
|
||||
Hermes takes a different approach. It is built in Python and centers on a **self-improving
|
||||
agent loop** rather than a gateway control plane. The core difference is in how skills work:
|
||||
OpenClaw skills are primarily human-authored plugins installed from a marketplace; Hermes
|
||||
**writes and saves its own skills automatically** as part of every session. When Hermes solves
|
||||
a problem a new way, it saves the procedure and reuses it going forward without any user effort.
|
||||
|
||||
Beyond the skills architecture, there are two other practical differences worth knowing:
|
||||
|
||||
**Stability.** OpenClaw's community forums and GitHub issues document a recurring pattern of
|
||||
update-breaking regressions -- for example, Telegram integration was broken across multiple
|
||||
releases in early 2026. The unofficial WhatsApp Web protocol OpenClaw uses is known to
|
||||
disconnect and requires periodic re-pairing (this is documented in OpenClaw's own FAQ).
|
||||
Hermes has had no equivalent release breakages.
|
||||
|
||||
**Security.** ClawHub's open publishing model has been exploited repeatedly. A community audit
|
||||
identified over a thousand malicious skills in the marketplace including prompt injections and
|
||||
tool-poisoning payloads; the community-maintained awesome-openclaw-skills list tracks confirmed
|
||||
removals and flags known bad actors. Hermes has no third-party marketplace and a correspondingly
|
||||
smaller attack surface.
|
||||
|
||||
**OpenClaw's genuine strengths** are worth stating plainly: it has broader messaging coverage
|
||||
(iMessage, LINE, WeChat, Teams -- platforms Hermes does not support), native browser and
|
||||
computer control via Chrome CDP, voice wake words on macOS and iOS, a larger community, and
|
||||
more third-party integrations than Hermes. If those capabilities matter most to you, OpenClaw
|
||||
is worth a serious look.
|
||||
|
||||
Where Hermes is the better fit: you want an agent that self-improves from experience without
|
||||
manual plugin authoring, you work in Python and want access to the ML/data science ecosystem,
|
||||
you want a stable deployment that does not break between updates, or you want a full web chat
|
||||
UI rather than a monitoring dashboard.
|
||||
|
||||
| | OpenClaw | Hermes |
|
||||
|---|---|---|
|
||||
| Persistent memory | Yes | Yes |
|
||||
| Scheduled jobs (cron) | Yes | Yes |
|
||||
| Messaging app access | Yes (15+ platforms, incl. iMessage/WeChat) | Yes (10+ platforms) |
|
||||
| Web UI | Gateway dashboard (monitoring only) | Full three-panel chat UI |
|
||||
| Self-hosted | Yes | Yes |
|
||||
| Open source | Yes (MIT) | Yes |
|
||||
| Self-improving skills | Partial (AI can generate skills; not the default loop) | Yes (automatic, first-class) |
|
||||
| Browser / computer control | Yes (native Chrome CDP) | Via shell / tools |
|
||||
| Voice wake words | Yes (macOS/iOS) | No |
|
||||
| Python / ML ecosystem | No (Node.js) | Yes |
|
||||
| Orchestrates Claude Code / Codex | No | Yes |
|
||||
| Multi-profile support | Via binding-rule routing | Yes (first-class named profiles) |
|
||||
| Provider-agnostic | Yes | Yes |
|
||||
| Update reliability | Moderate (documented regressions) | High |
|
||||
|
||||
### vs. Claude Code (Anthropic)
|
||||
|
||||
Claude Code is Anthropic's official agentic CLI and one of the best tools in Category 3.
|
||||
In a single focused session it is capable -- deep code understanding, shell access, file
|
||||
editing, multi-step reasoning.
|
||||
|
||||
Claude Code has been adding features rapidly and the gap is narrowing:
|
||||
|
||||
- **Hooks system** -- 13 event types (SessionStart, PreToolUse, PostToolUse, Stop, etc.) with
|
||||
4 handler types (shell command, HTTP endpoint, LLM prompt, sub-agent); deterministic
|
||||
non-LLM control over the agent lifecycle
|
||||
- **Plugins / Skills** -- installable via `/plugin install`, hot-reloaded from `~/.claude/skills`,
|
||||
with a marketplace; skills and slash commands unified as of v2.1.0
|
||||
- **Scheduling** -- `/loop` (session-scoped), cloud-managed cron via `claude.ai/code/scheduled`
|
||||
(Anthropic infrastructure, minimum interval applies), and desktop app automations
|
||||
- **Messaging channels** -- Telegram, Discord, iMessage, and webhooks via the Channels feature
|
||||
(research preview, v2.1.80+); deep Slack integration that triggers cloud sessions and creates PRs
|
||||
- **Claude Cowork** -- a separate product for knowledge workers; connects to 38+
|
||||
services via MCP including Slack, Gmail, Microsoft Teams, Notion, Jira, Salesforce, and more
|
||||
- **Memory** -- CLAUDE.md and MEMORY.md for project-level context; auto-memory rolling out
|
||||
|
||||
These are real features. The key differences that remain:
|
||||
|
||||
- Claude Code's scheduling runs on **Anthropic's cloud** (or requires the desktop app open),
|
||||
not a self-hosted server; cloud jobs have a minimum interval and your data leaves your hardware
|
||||
- Memory is **project-file-based** (CLAUDE.md / MEMORY.md), not a knowledge graph that
|
||||
accumulates automatically across all your work; auto-memory is still rolling out
|
||||
- **Not provider-agnostic** -- routes through Bedrock or Vertex but always hits a Claude model;
|
||||
you cannot switch to GPT, Gemini, or a local model
|
||||
- **Not open source** -- proprietary; the CLI ships obfuscated JavaScript
|
||||
- Messaging channels are a **research preview** requiring Bun runtime; not yet production-grade
|
||||
|
||||
Hermes can use Claude Code as a sub-agent. For large implementation tasks, Hermes can spawn
|
||||
Claude Code to handle the heavy lifting and fold the result back into its own memory and history.
|
||||
|
||||
| | Claude Code | Hermes |
|
||||
|---|---|---|
|
||||
| Persistent memory (automatic) | Partial (CLAUDE.md / MEMORY.md, rolling out) | Yes |
|
||||
| Skills / hooks system | Yes (Hooks + Plugin/Skills marketplace) | Yes (auto-generated from experience) |
|
||||
| Scheduled jobs (self-hosted) | No (cloud or desktop-app only) | Yes |
|
||||
| Messaging access | Partial (Telegram/Discord/iMessage via research preview; Slack native) | Yes (10+ platforms, production) |
|
||||
| Cowork connectors (Slack, Gmail, etc.) | Yes (via Claude Cowork, separate product) | Via agent tool use |
|
||||
| Web UI | Yes (claude.ai/code, Anthropic-hosted) | Yes (self-hosted) |
|
||||
| Provider-agnostic | No (Claude models only, via Bedrock/Vertex) | Yes (any provider) |
|
||||
| Self-hosted scheduling | No | Yes |
|
||||
| Open source | No | Yes |
|
||||
| Runs as sub-agent of Hermes | Yes | N/A |
|
||||
|
||||
### vs. Codex CLI (OpenAI)
|
||||
|
||||
Codex CLI is OpenAI's open-source agentic terminal tool (Apache 2.0, ~73k GitHub stars). It
|
||||
supports 10+ providers including Anthropic, Google, Mistral, Groq, and local models via Ollama.
|
||||
It added persistent session memory in v0.100.0 with `codex resume`. The desktop app has an
|
||||
Automations feature for scheduled local tasks.
|
||||
|
||||
The CLI itself has no native scheduling (open feature request as of early 2026). Memory is
|
||||
session-history-based rather than a living knowledge graph. No messaging app access. A strong
|
||||
tool for single-session coding; Hermes adds the always-on layer on top.
|
||||
|
||||
| | Codex CLI | Hermes |
|
||||
|---|---|---|
|
||||
| Persistent memory | Partial (session history + AGENTS.md) | Yes (automatic, layered) |
|
||||
| Scheduled jobs | Partial (desktop app only; CLI has none) | Yes |
|
||||
| Messaging app access | No | Yes |
|
||||
| Web UI | No | Yes (self-hosted) |
|
||||
| Provider-agnostic | Yes (10+ providers) | Yes (10+ providers) |
|
||||
| Self-hosted | Yes | Yes |
|
||||
| Open source | Yes (Apache 2.0) | Yes |
|
||||
|
||||
### vs. OpenCode
|
||||
|
||||
OpenCode is an open-source TUI agentic coding assistant, provider-agnostic across 75+ providers.
|
||||
It has a WebUI embedded in its binary and an official desktop app. It uses SQLite for session
|
||||
history and AGENTS.md for project context.
|
||||
|
||||
No native scheduled jobs (a community background plugin exists), no first-party messaging
|
||||
integration (community Telegram bots exist but require manual setup), and no automatic
|
||||
cross-session semantic memory. Good for interactive terminal coding sessions.
|
||||
|
||||
| | OpenCode | Hermes |
|
||||
|---|---|---|
|
||||
| Persistent memory | Partial (session history + AGENTS.md) | Yes (automatic, layered) |
|
||||
| Scheduled jobs | No (community plugin only) | Yes |
|
||||
| Messaging app access | No (community Telegram bot only) | Yes (first-party, 10+ platforms) |
|
||||
| Web UI | Yes (embedded + desktop app) | Yes (self-hosted) |
|
||||
| Mobile access | No | Yes |
|
||||
| Skills system | No | Yes |
|
||||
| Provider-agnostic | Yes (75+ providers) | Yes |
|
||||
| Open source | Yes | Yes |
|
||||
|
||||
### vs. Cursor / Windsurf / Copilot
|
||||
|
||||
Category 2 tools -- exceptional at in-editor autocomplete, inline diffs, and code review.
|
||||
Not competing for the same job as Hermes, and they work well alongside it.
|
||||
|
||||
Windsurf was earliest with workspace-scoped memory (Cascade Memories); Copilot has been
|
||||
shipping repo-level memory since late 2025. Cursor has no native cross-session memory as of
|
||||
early 2026. None have scheduling or messaging access.
|
||||
|
||||
| | Cursor | Windsurf | Copilot | Hermes |
|
||||
|---|---|---|---|---|
|
||||
| In-editor autocomplete | Excellent | Excellent | Excellent | No |
|
||||
| Inline diff / refactor | Yes | Yes | Yes | Via shell |
|
||||
| Cross-session memory | No | Yes (workspace) | Partial (repo, early access) | Yes |
|
||||
| Scheduled background jobs | No | No | No | Yes |
|
||||
| Messaging app / mobile | No | No | No | Yes |
|
||||
| Terminal tool use | Limited | Limited | Limited | Full |
|
||||
| Self-hosted | No | No | No | Yes |
|
||||
| Provider-agnostic | Partial | Partial | No | Yes |
|
||||
| Open source | No | No | No | Yes |
|
||||
|
||||
### vs. Claude.ai / ChatGPT
|
||||
|
||||
Category 1. For drafting, Q&A, and brainstorming in the moment, both are excellent.
|
||||
|
||||
Claude.ai memory has been improving -- it now generates memory from chat history, not just
|
||||
user-curated entries. Claude.ai can also execute code and read/write files in a sandboxed
|
||||
environment via Artifacts. These are real capabilities, just not the same as direct filesystem
|
||||
or shell access on your own server.
|
||||
|
||||
| | Claude.ai / ChatGPT | Hermes |
|
||||
|---|---|---|
|
||||
| Memory across conversations | Yes (improving; auto-generated from history) | Yes (deep, automatic) |
|
||||
| Runs shell commands | No | Yes |
|
||||
| Code execution | Sandboxed (Artifacts) | Yes (full shell) |
|
||||
| Reads / writes files | Sandboxed (Artifacts) | Yes (full filesystem) |
|
||||
| Schedules background jobs | No | Yes |
|
||||
| Web UI | Yes | Yes |
|
||||
| Messaging apps | No | Yes |
|
||||
| Self-hosted | No | Yes |
|
||||
| Provider-agnostic | No | Yes |
|
||||
| Open source | No | Yes |
|
||||
|
||||
---
|
||||
|
||||
## The Compounding Advantage
|
||||
|
||||
What matters most about Hermes is that it improves over time. That is the point.
|
||||
|
||||
Every time Hermes encounters a new environment, it saves facts to memory. Every time it solves
|
||||
a problem a new way, it saves the approach as a skill. Every time you correct it, it updates its
|
||||
profile of you. Every session, every scheduled job, every tool call, the agent gets more
|
||||
calibrated to you and your workflow.
|
||||
|
||||
A Claude Code session on day one and day one hundred are identical. A Hermes agent on day one
|
||||
and day one hundred is smarter about you -- it knows your stack, your conventions, your
|
||||
preferences, and the solutions that have worked before.
|
||||
|
||||
---
|
||||
|
||||
## Who Hermes Is For
|
||||
|
||||
**Solo developers and power users** who don't want to re-explain their stack every session and
|
||||
want an AI that actually knows their environment.
|
||||
|
||||
**Teams on a shared server** where multiple people want Claude-quality AI access without each
|
||||
paying for a separate subscription or running local tooling.
|
||||
|
||||
**Automation-heavy workflows** where you want an AI running tasks on a schedule, delivering
|
||||
results to your phone, without babysitting it.
|
||||
|
||||
**Privacy-conscious users** who want their conversations, memory, and files on their own
|
||||
hardware.
|
||||
|
||||
**Multi-model users** who want to switch between OpenAI, Anthropic, Google, DeepSeek, and
|
||||
others based on cost, capability, or rate limits, without rebuilding their workflow each time.
|
||||
|
||||
---
|
||||
|
||||
## Scope and Limits
|
||||
|
||||
**Hermes lives in the terminal, browser, and messaging apps.** For in-editor autocomplete and
|
||||
inline diffs, use Cursor or Windsurf alongside it -- they do that job better.
|
||||
|
||||
**You run Hermes on your own server.** That means initial setup, but your data stays on your
|
||||
hardware and you control the schedule, the models, and the costs.
|
||||
|
||||
**Hermes is an orchestration and memory layer.** It makes whatever model you point it at more
|
||||
useful over time. The models do the reasoning; Hermes makes sure that reasoning accumulates into
|
||||
something durable.
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| | OpenClaw | Claude Code | Codex CLI | OpenCode | Cursor | Claude.ai | Hermes |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Persistent memory (auto) | Yes | Partial† | Partial | Partial | No | Yes (improving) | **Yes** |
|
||||
| Scheduled / background jobs | Yes | Partial‡ | Partial§ | No | No | No | **Yes (self-hosted)** |
|
||||
| Messaging app access | Yes (15+ platforms) | Partial (Telegram/Discord preview; Slack native) | No | No | No | No | **Yes (10+ platforms)** |
|
||||
| Web UI | Dashboard only | Yes (Anthropic cloud) | No | Yes | No | Yes | **Yes (self-hosted)** |
|
||||
| Skills system | Yes (marketplace) | Yes (Hooks + Plugins) | No | No | No | No | **Yes** |
|
||||
| Self-improving skills | Partial | No | No | No | No | No | **Yes** |
|
||||
| Browser / computer control | Yes (Chrome CDP) | No | No | No | No | No | Via shell |
|
||||
| Python / ML ecosystem | No (Node.js) | No | No | No | No | No | **Yes** |
|
||||
| In-editor autocomplete | No | No | No | No | Yes | No | No |
|
||||
| Orchestrates other agents | No | No | No | No | No | No | **Yes** |
|
||||
| Provider-agnostic | Yes | No (Claude only) | Yes | Yes | Partial | No | **Yes** |
|
||||
| Self-hosted | Yes | No | Yes | Yes | No | No | **Yes** |
|
||||
| Open source | Yes (MIT) | No | Yes | Yes | No | No | **Yes** |
|
||||
| Always-on / autonomous | Yes | No | No | No | No | No | **Yes** |
|
||||
|
||||
† Claude Code has CLAUDE.md / MEMORY.md project context and rolling auto-memory, but not full automatic cross-session recall
|
||||
‡ Claude Code scheduling: cloud-managed (Anthropic infrastructure) or desktop-app only; no self-hosted cron
|
||||
§ Codex scheduling: desktop app Automations only; CLI has no native scheduling
|
||||
77
README.md
77
README.md
@@ -10,9 +10,70 @@ and vanilla JS.
|
||||
Layout: three-panel Claude-style. Left sidebar for sessions and tools,
|
||||
center for chat, right for workspace file browsing.
|
||||
|
||||
<img width="1392" height="854" alt="image" src="https://github.com/user-attachments/assets/79cd3c0d-3167-42ed-9434-447a742c25c3" />
|
||||
<img width="1392" alt="Hermes Web UI — three-panel layout" src="https://github.com/user-attachments/assets/79cd3c0d-3167-42ed-9434-447a742c25c3" />
|
||||
|
||||
This gives you nearly **1:1 parity with Hermes CLI from a convenient web UI** which you can access securely through an SSH tunnel from your Hermes setup. Single command to start this up, and a single command to SSH tunnel for access on your computer. Every single part of the web UI leverages your existing Hermes agent, existing models, without requiring any setup.
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%" align="center">
|
||||
<img alt="Workspace file browser with inline preview" src="docs/images/ui-workspace.png" />
|
||||
<br /><sub>Workspace file browser with inline preview</sub>
|
||||
</td>
|
||||
<td width="50%" align="center">
|
||||
<img alt="Session projects, tags, and tool call cards" src="docs/images/ui-sessions.png" />
|
||||
<br /><sub>Session projects, tags, and tool call cards</sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
This gives you nearly **1:1 parity with Hermes CLI from a convenient web UI** which you can access securely through an SSH tunnel from your Hermes setup. Single command to start this up, and a single command to SSH tunnel for access on your computer. Every single part of the web UI uses your existing Hermes agent and existing models, without requiring any additional setup.
|
||||
|
||||
---
|
||||
|
||||
## Why Hermes
|
||||
|
||||
Most AI tools reset every session. They don't know who you are, what you worked on, or what
|
||||
conventions your project follows. You re-explain yourself every time.
|
||||
|
||||
Hermes retains context across sessions, runs scheduled jobs while you're offline, and gets
|
||||
smarter about your environment the longer it runs. It uses your existing Hermes agent setup,
|
||||
your existing models, and requires no additional configuration to start.
|
||||
|
||||
What makes it different from other agentic tools:
|
||||
|
||||
- **Persistent memory** — user profile, agent notes, and a skills system that saves reusable
|
||||
procedures; Hermes learns your environment and does not have to relearn it
|
||||
- **Self-hosted scheduling** — cron jobs that fire while you're offline and deliver results to
|
||||
Telegram, Discord, Slack, Signal, email, and more
|
||||
- **10+ messaging platforms** — the same agent available in the terminal is reachable from your phone
|
||||
- **Self-improving skills** — Hermes writes and saves its own skills automatically from experience;
|
||||
no marketplace to browse, no plugins to install
|
||||
- **Provider-agnostic** — OpenAI, Anthropic, Google, DeepSeek, OpenRouter, and more
|
||||
- **Orchestrates other agents** — can spawn Claude Code or Codex for heavy coding tasks and bring
|
||||
the results back into its own memory
|
||||
- **Self-hosted** — your conversations, your memory, your hardware
|
||||
|
||||
**vs. the field** *(landscape is actively shifting — see [HERMES.md](HERMES.md) for the full breakdown)*:
|
||||
|
||||
| | OpenClaw | Claude Code | Codex CLI | OpenCode | Hermes |
|
||||
|---|---|---|---|---|---|
|
||||
| Persistent memory (auto) | Yes | Partial† | Partial | Partial | Yes |
|
||||
| Scheduled jobs (self-hosted) | Yes | No‡ | No | No | Yes |
|
||||
| Messaging app access | Yes (15+ platforms) | Partial (Telegram/Discord preview) | No | No | Yes (10+) |
|
||||
| Web UI (self-hosted) | Dashboard only | No | No | Yes | Yes |
|
||||
| Self-improving skills | Partial | No | No | No | Yes |
|
||||
| Python / ML ecosystem | No (Node.js) | No | No | No | Yes |
|
||||
| Provider-agnostic | Yes | No (Claude only) | Yes | Yes | Yes |
|
||||
| Open source | Yes (MIT) | No | Yes | Yes | Yes |
|
||||
|
||||
† Claude Code has CLAUDE.md / MEMORY.md project context and rolling auto-memory, but not full automatic cross-session recall
|
||||
‡ Claude Code has cloud-managed scheduling (Anthropic infrastructure) and session-scoped `/loop`; no self-hosted cron
|
||||
|
||||
**The closest competitor is OpenClaw** — both are always-on, self-hosted, open-source agents
|
||||
with memory, cron, and messaging. The key differences: Hermes writes and saves its own skills
|
||||
automatically as a core behavior (OpenClaw's skill system centers on a community marketplace);
|
||||
Hermes is more stable across updates (OpenClaw has documented release regressions and ClawHub
|
||||
has had security incidents involving malicious skills); and Hermes runs natively in the Python
|
||||
ecosystem. See [HERMES.md](HERMES.md) for the full side-by-side.
|
||||
|
||||
---
|
||||
|
||||
@@ -176,7 +237,7 @@ Or using the agent venv explicitly:
|
||||
```
|
||||
|
||||
Tests run against an isolated server on port 8788 with a separate state directory.
|
||||
Production data and real cron jobs are never touched. Current count: **426 tests**
|
||||
Production data and real cron jobs are never touched. Current count: **424 tests**
|
||||
across 22 test files.
|
||||
|
||||
---
|
||||
@@ -191,6 +252,7 @@ across 22 test files.
|
||||
- Retry the last assistant response with one click
|
||||
- Cancel a running task from the activity bar
|
||||
- Tool call cards inline -- each shows the tool name, args, and result snippet; expand/collapse all toggle for multi-tool turns
|
||||
- Subagent delegation cards -- child agent activity shown with distinct icon and indented border
|
||||
- Mermaid diagram rendering inline (flowcharts, sequence diagrams, gantt charts)
|
||||
- Thinking/reasoning display -- collapsible gold-themed cards for Claude extended thinking and o3 reasoning blocks
|
||||
- Approval card for dangerous shell commands (allow once / session / always / deny)
|
||||
@@ -211,6 +273,8 @@ across 22 test files.
|
||||
- Download as Markdown transcript, full JSON export, or import from JSON
|
||||
- Sessions persist across page reloads and SSH tunnel reconnects
|
||||
- Browser tab title reflects the active session name
|
||||
- CLI session bridge -- CLI sessions from hermes-agent's SQLite store appear in the sidebar with a gold "cli" badge; click to import with full history and reply normally
|
||||
- Token/cost display -- input tokens, output tokens, estimated cost shown per conversation (toggle in Settings or `/usage` command)
|
||||
|
||||
### Workspace file browser
|
||||
- Directory tree with expand/collapse (single-click toggles, double-click navigates)
|
||||
@@ -250,19 +314,21 @@ across 22 test files.
|
||||
### Settings and configuration
|
||||
- Settings panel (gear icon) -- default model, default workspace, send key preference
|
||||
- Send key: Enter (default) or Ctrl/Cmd+Enter
|
||||
- Show/hide CLI sessions toggle (enabled by default)
|
||||
- Token usage display toggle (off by default, also via `/usage` command)
|
||||
- Cron completion alerts -- toast notifications and unread badge on Tasks tab
|
||||
- Background agent error alerts -- banner when a non-active session encounters an error
|
||||
|
||||
### Slash commands
|
||||
- Type `/` in the composer for autocomplete dropdown
|
||||
- Built-in: `/help`, `/clear`, `/model <name>`, `/workspace <name>`, `/new`
|
||||
- Built-in: `/help`, `/clear`, `/model <name>`, `/workspace <name>`, `/new`, `/usage`
|
||||
- Arrow keys navigate, Tab/Enter select, Escape closes
|
||||
- Unrecognized commands pass through to the agent
|
||||
|
||||
### Panels
|
||||
- **Chat** -- session list, search, pin, archive, projects, new conversation
|
||||
- **Tasks** -- view, create, edit, run, pause/resume, delete cron jobs; run history; completion alerts
|
||||
- **Skills** -- list all skills by category, search, preview, create/edit/delete
|
||||
- **Skills** -- list all skills by category, search, preview, create/edit/delete; linked files viewer
|
||||
- **Memory** -- view and edit MEMORY.md and USER.md inline
|
||||
- **Profiles** -- create, switch, delete agent profiles; clone config
|
||||
- **Todos** -- live task list from the current session
|
||||
@@ -318,6 +384,7 @@ State lives outside the repo at `~/.hermes/webui-mvp/` by default
|
||||
|
||||
## Docs
|
||||
|
||||
- `HERMES.md` -- why Hermes, mental model, and detailed comparison to Claude Code / Codex / OpenCode / Cursor
|
||||
- `ROADMAP.md` -- feature roadmap and sprint history
|
||||
- `ARCHITECTURE.md` -- system design, all API endpoints, implementation notes
|
||||
- `TESTING.md` -- manual browser test plan and automated coverage reference
|
||||
|
||||
11
ROADMAP.md
11
ROADMAP.md
@@ -3,8 +3,8 @@
|
||||
> Goal: Full 1:1 parity with the Hermes CLI experience via a clean dark web UI.
|
||||
> Everything you can do from the CLI terminal, you can do from this UI.
|
||||
>
|
||||
> Last updated: v0.28.1 (April 3, 2026)
|
||||
> Tests: 426 total (403 passing, 23 pre-existing failures)
|
||||
> Last updated: v0.29 (April 4, 2026)
|
||||
> Tests: 424 total (401 passing, 23 pre-existing failures)
|
||||
> Source: <repo>/
|
||||
|
||||
---
|
||||
@@ -39,6 +39,7 @@
|
||||
| Sprint 20 | Voice input + send button | Voice input (Web Speech API), send button icon-circle with pop-in animation | 415 |
|
||||
| Sprint 21 | Mobile responsive + Docker | Hamburger sidebar, bottom nav, files slide-over, Docker support (#21, #7) | 415 |
|
||||
| Sprint 22 | Multi-profile support | Profile picker, management panel, seamless switching, per-session tracking (#28) | 415 |
|
||||
| Sprint 23 | Agentic transparency | Token/cost display, subagent cards, skill picker in cron, skill linked files, workspace tree persistence, timestamp fixes | 424 |
|
||||
|
||||
---
|
||||
|
||||
@@ -76,7 +77,7 @@
|
||||
- [x] Copy message to clipboard (hover icon on each bubble)
|
||||
- [x] Edit last user message and regenerate
|
||||
- [ ] Branch/fork conversation (Wave 3)
|
||||
- [ ] Token/cost estimate per message (Wave 3)
|
||||
- [x] Token/cost estimate per message (Sprint 23)
|
||||
|
||||
### Tool Visibility
|
||||
- [x] Tool progress in activity bar (moved out of composer footer)
|
||||
@@ -137,7 +138,7 @@
|
||||
- [x] Edit existing cron job
|
||||
- [x] Delete cron job
|
||||
- [x] View full cron run history (expandable per job)
|
||||
- [ ] Skill picker in cron create form (Wave 3)
|
||||
- [x] Skill picker in cron create form (Sprint 23)
|
||||
|
||||
### Skills
|
||||
- [x] List all skills grouped by category (Skills sidebar tab)
|
||||
@@ -146,7 +147,7 @@
|
||||
- [x] Create skill
|
||||
- [x] Edit skill
|
||||
- [x] Delete skill
|
||||
- [ ] View skill linked files (Wave 3)
|
||||
- [x] View skill linked files (Sprint 23)
|
||||
|
||||
### Memory
|
||||
- [x] View personal notes (MEMORY.md) rendered as markdown (Memory tab)
|
||||
|
||||
265
SPRINTS.md
265
SPRINTS.md
@@ -1,6 +1,6 @@
|
||||
# Hermes Web UI -- Forward Sprint Plan
|
||||
|
||||
> Current state: v0.28.1 | 426 tests | Daily driver ready
|
||||
> Current state: v0.30.1 | 424 tests | Daily driver ready
|
||||
> This document plans the path from here to two targets:
|
||||
>
|
||||
> Target A: 1:1 feature parity with the Hermes CLI (everything you can do from the
|
||||
@@ -567,25 +567,260 @@ and switchToProfile() didn't refresh workspaces or sessions.
|
||||
|
||||
---
|
||||
|
||||
## Sprint 24 -- Desktop Application (PLANNED)
|
||||
## Sprint 24 -- Web Polish + Bug Fix Pass (PLANNED)
|
||||
|
||||
**Theme:** Native desktop experience.
|
||||
**Theme:** Stabilize, harden, and close the last meaningful web UI gaps before
|
||||
shifting focus to distribution. Goal is a release that's genuinely ready for
|
||||
wider user adoption -- no rough edges, no obvious missing pieces.
|
||||
|
||||
**Why now:** Sprint 23 completed the core agentic transparency features. The
|
||||
remaining web roadmap items are diminishing-returns polish. Rather than
|
||||
grinding through marginal features, this sprint cleans up what's there, fixes
|
||||
bugs users will actually hit, and closes a few real gaps before recommending
|
||||
the app to others.
|
||||
|
||||
### Track A: Bug Fixes
|
||||
- **Cron edit form has no skill picker.** Sprint 23 added skill picker to the
|
||||
create form but not the edit form. cronEditSave() doesn't include skills in
|
||||
the update body, so existing skills survive an edit but can't be changed.
|
||||
Fix: add the same skill picker UI to the inline edit form and include
|
||||
`skills` in the update POST body.
|
||||
- **S.lastUsage dead code.** messages.js sets `S.lastUsage` from `d.usage` at
|
||||
done-time, but nothing reads it. The usage badge reads cumulative session
|
||||
totals from `S.session.input_tokens` instead. Either wire `S.lastUsage` into
|
||||
a per-turn display or remove the dead assignment.
|
||||
- **_cronSkillsCache never invalidated.** Skills picker shows stale data if
|
||||
skills are added/removed mid-session. Add a cache-bust when the skills panel
|
||||
is opened or a skill is saved/deleted.
|
||||
- **Tool args not shown on session reload.** Tool call cards in history show
|
||||
name and result snippet but not the args (args only exist in the live SSE
|
||||
event). Sprint 23 added args to the session JSON -- verify they're actually
|
||||
rendering in the settled history cards.
|
||||
|
||||
### Track B: Features
|
||||
- **Electron or Tauri wrapper.** Native window, menu bar, notifications.
|
||||
- **Auto-start option.** Launch on login.
|
||||
- **Packaged distribution.** .dmg (macOS), .exe (Windows).
|
||||
- **Cron edit: skill picker parity.** As above -- make create and edit forms
|
||||
identical in capability.
|
||||
- **Per-turn cost display.** The current usage badge shows cumulative session
|
||||
totals attached to the last message, which is misleading. Either: (a) show
|
||||
per-turn cost from `S.lastUsage` immediately after each response instead of
|
||||
cumulative, or (b) show cumulative in the session topbar/header instead of
|
||||
attached to a message bubble. Pick the cleaner UX.
|
||||
- **Virtual scroll for long session/skill lists.** When session count or skill
|
||||
count gets large (100+), the sidebar becomes sluggish. Add a simple virtual
|
||||
scroll or windowed render -- only render visible items + a buffer above/below.
|
||||
CSS `contain: strict` + IntersectionObserver approach, no library needed.
|
||||
|
||||
### Track C: Code Quality
|
||||
- **SPRINTS.md + ROADMAP.md + CHANGELOG.md updated** to reflect Sprint 23
|
||||
completion (agentic transparency) and correct test counts.
|
||||
- **Remove stale Sprint 23 description** from SPRINTS.md (the "Profile/Workspace
|
||||
coherence" text is from an older plan; Sprint 23 actually shipped agentic
|
||||
transparency features).
|
||||
- **CHANGELOG entry for v0.29** covering Sprint 23 deliverables.
|
||||
|
||||
**Estimated tests:** ~10 new. Target total: ~435.
|
||||
**Hermes CLI parity impact:** Low
|
||||
**Claude parity impact:** Low
|
||||
**User-facing value:** Medium -- removes rough edges that would bother new users
|
||||
|
||||
---
|
||||
|
||||
## Sprint 24 -- Extended Command Support (PLANNED)
|
||||
## Sprint 25 -- macOS Desktop Application (PLANNED)
|
||||
|
||||
**Theme:** Deeper slash command and skill integration.
|
||||
**Theme:** Native Mac desktop app. Single download, runs entirely offline,
|
||||
feels like a real application -- not a browser tab.
|
||||
|
||||
### Track B: Features
|
||||
- **Skill-aware autocomplete.** `/skill-name` triggers installed skills.
|
||||
- **Command chaining.** Compose multi-step commands.
|
||||
- **Agent tool exposure.** Surface agent capabilities as slash commands.
|
||||
**Why this matters:** The web UI requires an SSH tunnel or a server setup to
|
||||
use. A .app bundle that a user can double-click and immediately have a working
|
||||
Hermes interface is genuinely differentiating. No other open-source Hermes
|
||||
interface ships as a native Mac app. This is the highest-leverage remaining
|
||||
investment for user adoption.
|
||||
|
||||
**Approach: Swift + WKWebView (not Electron)**
|
||||
|
||||
The right architecture is a thin native Swift shell (~300-500 lines) that:
|
||||
1. Bundles the existing Python server and all api/ modules inside the .app
|
||||
2. Spawns the server as a subprocess on a random local port at launch
|
||||
3. Opens a WKWebView window pointed at that localhost port
|
||||
4. Handles Mac app lifecycle natively (dock icon, cmd+Q, window management,
|
||||
app menu, about box)
|
||||
5. Bridges a small set of native Mac capabilities that WKWebView can't do
|
||||
|
||||
**Why not Electron:** WKWebView is Safari's engine -- dramatically lighter than
|
||||
Chromium. No 200MB node_modules. No separate update daemon. The .app is ~30MB
|
||||
including the Python runtime, vs 150MB+ for Electron.
|
||||
|
||||
**Why not full native Swift UI:** Would require rewriting the entire frontend
|
||||
from scratch. The web UI is already fast, dark-themed, and feature-complete.
|
||||
The thin shell approach gets 95% of the benefit at 5% of the cost.
|
||||
|
||||
### Track A: Swift App Shell
|
||||
|
||||
**Files to create:**
|
||||
```
|
||||
desktop/
|
||||
HermesApp.swift -- @main entry point, NSApp delegate
|
||||
AppDelegate.swift -- lifecycle: start server on launch, stop on quit
|
||||
WindowController.swift -- NSWindow + WKWebView setup, cmd shortcuts
|
||||
ServerManager.swift -- spawn/monitor Python subprocess, pick free port
|
||||
MenuBuilder.swift -- native app menu (File, Edit, View, Window, Help)
|
||||
Info.plist -- bundle ID, display name, version, icon
|
||||
Assets.xcassets/ -- app icon (1024x1024 + all required sizes)
|
||||
HermesApp.xcodeproj/ -- Xcode project file
|
||||
```
|
||||
|
||||
**ServerManager.swift responsibilities:**
|
||||
- Find Python: check bundled runtime first, fall back to system python3
|
||||
- Pick a free port (bind to :0, read assigned port, close, use it)
|
||||
- Spawn: `python3 server.py --port {port}` as a child Process
|
||||
- Monitor: if server crashes, show an error sheet and offer restart
|
||||
- Shutdown: SIGTERM on app quit, wait up to 3s, then SIGKILL
|
||||
|
||||
**WKWebView configuration:**
|
||||
- `allowsBackForwardNavigationGestures = false` (it's a single-page app)
|
||||
- `WKUserContentController` for JS bridge (native notifications, file picker)
|
||||
- Wait for server health check before loading (poll /health, show loading
|
||||
spinner in the native window while waiting, typically <1s)
|
||||
- `userAgent` override so the server can detect desktop app context
|
||||
|
||||
**Native menu items (beyond defaults):**
|
||||
- File > New Session (Cmd+N) -- calls JS `newSession()`
|
||||
- File > New Window (Cmd+Shift+N) -- opens second window with its own WKWebView
|
||||
- View > Toggle Sidebar (Cmd+Shift+S)
|
||||
- Window > Zoom, Minimize (standard)
|
||||
- Help > About Hermes, Check for Updates (links to GitHub releases page)
|
||||
|
||||
### Track B: Python Bundling
|
||||
|
||||
Two options, in order of preference:
|
||||
|
||||
**Option A: Require system Python (simpler, recommended for v1)**
|
||||
- Check for `python3` at known paths: `/usr/bin/python3`, homebrew paths,
|
||||
pyenv paths
|
||||
- If not found: show a one-time setup sheet with instructions
|
||||
- Pros: tiny download (~5MB for the Swift app + web assets), no bundling complexity
|
||||
- Cons: user needs Python installed (most developers do; target audience does too)
|
||||
|
||||
**Option B: Bundle python-standalone (self-contained, larger)**
|
||||
- Use `python-build-standalone` (from Astral/uv project): pre-built Python
|
||||
3.11 binaries, ~30MB compressed, no Xcode toolchain needed to build
|
||||
- Extract to `~/Library/Application Support/Hermes/python/` on first launch
|
||||
- Install `requirements.txt` via bundled pip into a local venv
|
||||
- Pros: zero dependencies, works on a clean Mac
|
||||
- Cons: first launch takes ~10-20s for extraction + pip install; ~30MB download
|
||||
|
||||
**Recommendation:** Ship v1 with Option A. Add Option B as an optional
|
||||
"standalone" download for non-developers.
|
||||
|
||||
### Track C: Distribution
|
||||
|
||||
**GitHub Releases (primary):**
|
||||
- Build with `xcodebuild -scheme HermesApp -configuration Release -archivePath`
|
||||
- `xcodebuild -exportArchive` to produce a .app bundle
|
||||
- `hdiutil create` to produce a .dmg with drag-to-Applications installer UI
|
||||
- Upload .dmg as a GitHub Release asset via `gh release create`
|
||||
- CI: add `.github/workflows/mac-release.yml` -- trigger on `vX.Y.Z-mac` tag
|
||||
|
||||
**Code signing:**
|
||||
- Without an Apple Developer account: distribute as unsigned, users must
|
||||
right-click > Open on first launch (standard for open-source Mac apps)
|
||||
- With a free Apple Developer account: ad-hoc signing removes the Gatekeeper
|
||||
warning without paying $99/year (no notarization, but much better UX)
|
||||
- With paid account ($99/year): full notarization, no warnings, direct download
|
||||
|
||||
**Recommended for v1:** ad-hoc signing (free, good enough for early adopters).
|
||||
Document the right-click > Open workaround in the README for unsigned builds.
|
||||
|
||||
**Universal binary (Intel + Apple Silicon):**
|
||||
```bash
|
||||
xcodebuild archive -scheme HermesApp -destination "generic/platform=macOS"
|
||||
```
|
||||
Both architectures in one .app. No separate downloads needed.
|
||||
|
||||
### Track D: Native Integrations (v1 scope)
|
||||
|
||||
**System notifications for cron completion:**
|
||||
- The web UI polls `/api/cron/alerts` and shows in-page banners
|
||||
- The Mac app can additionally post `UNUserNotificationCenter` notifications
|
||||
- JS bridge: `window.webkit.messageHandlers.notify.postMessage({title, body})`
|
||||
- Swift handler: posts a native notification with the cron job name and output
|
||||
summary -- appears in Notification Center, works even when app is in background
|
||||
|
||||
**File picker for workspace add:**
|
||||
- Currently: user types a path string into the workspace add form
|
||||
- Mac app: intercept workspace-add form submission, open `NSOpenPanel` instead,
|
||||
return the selected path to the JS via `evaluateJavaScript`
|
||||
- Much better UX -- standard Mac folder picker, no typing paths
|
||||
|
||||
**Dock badge for pending approvals:**
|
||||
- When an agent approval is waiting, set `NSApp.dockTile.badgeLabel = "1"`
|
||||
- Clear badge when approval is resolved
|
||||
- JS bridge fires when approval card appears/disappears
|
||||
|
||||
**Menu bar mode (optional, v2):**
|
||||
- A small status bar item (⚗️ icon in menu bar) that opens a compact popover
|
||||
- Popover shows current session status, last message, quick-compose field
|
||||
- Useful for running Hermes in the background without a full window
|
||||
|
||||
### Track E: Testing
|
||||
|
||||
Since the Swift app is thin glue, most testing remains in the existing pytest
|
||||
suite (server still runs identically). New Swift-specific tests:
|
||||
- `ServerManagerTests.swift`: verify port picking, process spawn, health wait
|
||||
- UI tests via `XCUITest`: launch app, wait for WKWebView to load, verify
|
||||
title bar shows "Hermes", verify /health responds
|
||||
- Smoke test in CI: `xcodebuild test -scheme HermesApp`
|
||||
|
||||
### Implementation Order
|
||||
|
||||
1. `ServerManager.swift` + basic `AppDelegate` -- get Python server spawning
|
||||
and health-check working from Swift
|
||||
2. `WindowController.swift` -- WKWebView loading, loading spinner while
|
||||
server starts
|
||||
3. App icon + Info.plist -- make it look like a real app
|
||||
4. `MenuBuilder.swift` -- native menus + keyboard shortcuts
|
||||
5. JS bridge for notifications -- most impactful native integration
|
||||
6. DMG build script + GitHub Actions CI
|
||||
7. (Optional) File picker bridge, dock badge
|
||||
|
||||
### What to NOT do in v1
|
||||
|
||||
- Windows or Linux wrapper (different toolchain; do Mac first, assess demand)
|
||||
- Full Swift/SwiftUI rewrite of the frontend (months of work, wrong tradeoff)
|
||||
- App Store submission (sandboxing breaks local server; not worth the effort)
|
||||
- Auto-update mechanism (GitHub releases + manual download is fine for v1)
|
||||
- Menu bar mode (cool but not v1 scope)
|
||||
|
||||
### Files to create in the repo
|
||||
|
||||
```
|
||||
desktop/mac/
|
||||
HermesApp/
|
||||
HermesApp.swift
|
||||
AppDelegate.swift
|
||||
WindowController.swift
|
||||
ServerManager.swift
|
||||
MenuBuilder.swift
|
||||
Assets.xcassets/
|
||||
Info.plist
|
||||
HermesApp.xcodeproj/
|
||||
README.md -- build instructions, requirements, signing notes
|
||||
.github/workflows/
|
||||
mac-release.yml -- build + sign + upload DMG on tag push
|
||||
```
|
||||
|
||||
The server code (`server.py`, `api/`, `static/`, `requirements.txt`) is
|
||||
referenced from the repo root -- no duplication. The .app bundle copies them
|
||||
at build time.
|
||||
|
||||
**Estimated effort:** 2-3x a typical web sprint (new language, new toolchain,
|
||||
bundling complexity). Realistic for a focused weekend or a dedicated agent run
|
||||
with clear instructions.
|
||||
|
||||
**Hermes CLI parity impact:** N/A (different distribution channel)
|
||||
**Claude parity impact:** Medium (Claude.app is a native Mac app)
|
||||
**User-facing value:** Very high -- lowers barrier to entry dramatically,
|
||||
genuinely differentiating for an open-source project
|
||||
|
||||
---
|
||||
|
||||
@@ -662,6 +897,6 @@ and switchToProfile() didn't refresh workspaces or sessions.
|
||||
|
||||
---
|
||||
|
||||
*Last updated: April 3, 2026*
|
||||
*Current version: v0.28.1 | 426 tests*
|
||||
*Next sprint: Sprint 24 (Desktop Application)*
|
||||
*Last updated: April 4, 2026*
|
||||
*Current version: v0.30.1 | 424 tests*
|
||||
*Next sprint: Sprint 24 (Web Polish + Bug Fix Pass)*
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
> Prerequisites: SSH tunnel is active on port 8787. Open http://localhost:8787 in browser.
|
||||
> Server health check: curl http://127.0.0.1:8787/health should return {"status":"ok"}.
|
||||
>
|
||||
> Automated tests: 415 total (392 passing, 23 pre-existing failures).
|
||||
> Automated tests: 424 total (401 passing, 23 pre-existing failures).
|
||||
> Run: `pytest tests/ -v --timeout=60`
|
||||
|
||||
---
|
||||
|
||||
@@ -633,6 +633,7 @@ _SETTINGS_DEFAULTS = {
|
||||
'default_workspace': str(DEFAULT_WORKSPACE),
|
||||
'send_key': 'enter', # 'enter' or 'ctrl+enter'
|
||||
'show_token_usage': False, # show input/output token badge below assistant messages
|
||||
'show_cli_sessions': False, # merge CLI sessions from state.db into the sidebar
|
||||
'password_hash': None, # SHA-256 hash; None = auth disabled
|
||||
}
|
||||
|
||||
@@ -652,7 +653,7 @@ _SETTINGS_ALLOWED_KEYS = set(_SETTINGS_DEFAULTS.keys()) - {'password_hash'}
|
||||
_SETTINGS_ENUM_VALUES = {
|
||||
'send_key': {'enter', 'ctrl+enter'},
|
||||
}
|
||||
_SETTINGS_BOOL_KEYS = {'show_token_usage'}
|
||||
_SETTINGS_BOOL_KEYS = {'show_token_usage', 'show_cli_sessions'}
|
||||
|
||||
def save_settings(settings: dict) -> dict:
|
||||
"""Save settings to disk. Returns the merged settings. Ignores unknown keys."""
|
||||
|
||||
146
api/models.py
146
api/models.py
@@ -10,7 +10,7 @@ from pathlib import Path
|
||||
import api.config as _cfg
|
||||
from api.config import (
|
||||
SESSION_DIR, SESSION_INDEX_FILE, SESSIONS, SESSIONS_MAX,
|
||||
LOCK, DEFAULT_WORKSPACE, DEFAULT_MODEL, PROJECTS_FILE
|
||||
LOCK, DEFAULT_WORKSPACE, DEFAULT_MODEL, PROJECTS_FILE, HOME
|
||||
)
|
||||
from api.workspace import get_last_workspace
|
||||
|
||||
@@ -192,3 +192,147 @@ def load_projects():
|
||||
def save_projects(projects):
|
||||
"""Write project list to disk."""
|
||||
PROJECTS_FILE.write_text(json.dumps(projects, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||
|
||||
|
||||
def import_cli_session(session_id, title, messages, model='unknown', profile=None):
|
||||
"""Create a new WebUI session populated with CLI messages.
|
||||
Returns the Session object.
|
||||
"""
|
||||
s = Session(
|
||||
session_id=session_id,
|
||||
title=title,
|
||||
workspace=get_last_workspace(),
|
||||
model=model,
|
||||
messages=messages,
|
||||
profile=profile,
|
||||
)
|
||||
s.save()
|
||||
return s
|
||||
|
||||
|
||||
# ── CLI session bridge ──────────────────────────────────────────────────────
|
||||
|
||||
def get_cli_sessions():
|
||||
"""Read CLI sessions from the agent's SQLite store and return them as
|
||||
dicts in a format the WebUI sidebar can render alongside local sessions.
|
||||
|
||||
Returns empty list if the SQLite DB is missing, the sqlite3 module is
|
||||
unavailable, or any error occurs -- the bridge is purely additive and never
|
||||
crashes the WebUI.
|
||||
"""
|
||||
import os
|
||||
cli_sessions = []
|
||||
try:
|
||||
import sqlite3
|
||||
except ImportError:
|
||||
return cli_sessions
|
||||
|
||||
# Use the active WebUI profile's HERMES_HOME to find state.db.
|
||||
# The active profile is determined by what the user has selected in the UI
|
||||
# (stored in the server's runtime config). This means:
|
||||
# - default profile -> ~/.hermes/state.db
|
||||
# - named profile X -> ~/.hermes/profiles/X/state.db
|
||||
# We resolve the active profile's home directory rather than just using
|
||||
# HERMES_HOME (which is the server's launch profile, not necessarily the
|
||||
# active one after a profile switch).
|
||||
try:
|
||||
from api.profiles import get_active_hermes_home
|
||||
hermes_home = Path(get_active_hermes_home()).expanduser().resolve()
|
||||
except Exception:
|
||||
hermes_home = Path(os.getenv('HERMES_HOME', str(HOME / '.hermes'))).expanduser().resolve()
|
||||
|
||||
db_path = hermes_home / 'state.db'
|
||||
if not db_path.exists():
|
||||
return cli_sessions
|
||||
|
||||
# Try to resolve the active CLI profile so imported sessions integrate
|
||||
# with the WebUI profile filter (available since Sprint 22).
|
||||
try:
|
||||
from api.profiles import get_active_profile_name
|
||||
_cli_profile = get_active_profile_name()
|
||||
except ImportError:
|
||||
_cli_profile = None # older agent -- fall back to no profile
|
||||
|
||||
try:
|
||||
with sqlite3.connect(str(db_path)) as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
SELECT s.id, s.title, s.model, s.message_count,
|
||||
s.started_at, s.source,
|
||||
MAX(m.timestamp) AS last_activity
|
||||
FROM sessions s
|
||||
LEFT JOIN messages m ON m.session_id = s.id
|
||||
GROUP BY s.id
|
||||
ORDER BY COALESCE(MAX(m.timestamp), s.started_at) DESC
|
||||
LIMIT 200
|
||||
""")
|
||||
for row in cur.fetchall():
|
||||
sid = row['id']
|
||||
raw_ts = row['last_activity'] or row['started_at']
|
||||
# Prefer the CLI session's own profile from the DB; fall back to
|
||||
# the active CLI profile so sidebar filtering works either way.
|
||||
profile = _cli_profile # CLI DB has no profile column; use active profile
|
||||
|
||||
cli_sessions.append({
|
||||
'session_id': sid,
|
||||
'title': row['title'] or 'CLI Session',
|
||||
'workspace': str(get_last_workspace()),
|
||||
'model': row['model'] or 'unknown',
|
||||
'message_count': row['message_count'] or 0,
|
||||
'created_at': row['started_at'],
|
||||
'updated_at': raw_ts,
|
||||
'pinned': False,
|
||||
'archived': False,
|
||||
'project_id': None,
|
||||
'profile': profile,
|
||||
'source_tag': 'cli',
|
||||
'is_cli_session': True,
|
||||
})
|
||||
except Exception:
|
||||
# DB schema changed, locked, or corrupted -- silently degrade
|
||||
return []
|
||||
|
||||
return cli_sessions
|
||||
|
||||
|
||||
def get_cli_session_messages(sid):
|
||||
"""Read messages for a single CLI session from the SQLite store.
|
||||
Returns a list of {role, content, timestamp} dicts.
|
||||
Returns empty list on any error.
|
||||
"""
|
||||
import os
|
||||
try:
|
||||
import sqlite3
|
||||
except ImportError:
|
||||
return []
|
||||
|
||||
try:
|
||||
from api.profiles import get_active_hermes_home
|
||||
hermes_home = Path(get_active_hermes_home()).expanduser().resolve()
|
||||
except Exception:
|
||||
hermes_home = Path(os.getenv('HERMES_HOME', str(HOME / '.hermes'))).expanduser().resolve()
|
||||
db_path = hermes_home / 'state.db'
|
||||
if not db_path.exists():
|
||||
return []
|
||||
|
||||
try:
|
||||
with sqlite3.connect(str(db_path)) as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
SELECT role, content, timestamp
|
||||
FROM messages
|
||||
WHERE session_id = ?
|
||||
ORDER BY timestamp ASC
|
||||
""", (sid,))
|
||||
msgs = []
|
||||
for row in cur.fetchall():
|
||||
msgs.append({
|
||||
'role': row['role'],
|
||||
'content': row['content'],
|
||||
'timestamp': row['timestamp'],
|
||||
})
|
||||
except Exception:
|
||||
return []
|
||||
return msgs
|
||||
|
||||
107
api/routes.py
107
api/routes.py
@@ -23,7 +23,8 @@ from api.helpers import require, bad, safe_resolve, j, t, read_body, _security_h
|
||||
from api.models import (
|
||||
Session, get_session, new_session, all_sessions, title_from,
|
||||
_write_session_index, SESSION_INDEX_FILE,
|
||||
load_projects, save_projects,
|
||||
load_projects, save_projects, import_cli_session,
|
||||
get_cli_sessions, get_cli_session_messages,
|
||||
)
|
||||
from api.workspace import (
|
||||
load_workspaces, save_workspaces, get_last_workspace, set_last_workspace,
|
||||
@@ -151,14 +152,52 @@ def handle_get(handler, parsed):
|
||||
sid = parse_qs(parsed.query).get('session_id', [''])[0]
|
||||
if not sid:
|
||||
return j(handler, {'error': 'session_id is required'}, status=400)
|
||||
s = get_session(sid)
|
||||
return j(handler, {'session': s.compact() | {
|
||||
'messages': s.messages,
|
||||
'tool_calls': getattr(s, 'tool_calls', []),
|
||||
}})
|
||||
try:
|
||||
s = get_session(sid)
|
||||
return j(handler, {'session': s.compact() | {
|
||||
'messages': s.messages,
|
||||
'tool_calls': getattr(s, 'tool_calls', []),
|
||||
}})
|
||||
except KeyError:
|
||||
# Not a WebUI session -- try CLI store
|
||||
msgs = get_cli_session_messages(sid)
|
||||
if msgs:
|
||||
cli_meta = None
|
||||
for cs in get_cli_sessions():
|
||||
if cs['session_id'] == sid:
|
||||
cli_meta = cs
|
||||
break
|
||||
sess = {
|
||||
'session_id': sid,
|
||||
'title': (cli_meta or {}).get('title', 'CLI Session'),
|
||||
'workspace': (cli_meta or {}).get('workspace', ''),
|
||||
'model': (cli_meta or {}).get('model', 'unknown'),
|
||||
'message_count': len(msgs),
|
||||
'created_at': (cli_meta or {}).get('created_at', 0),
|
||||
'updated_at': (cli_meta or {}).get('updated_at', 0),
|
||||
'pinned': False,
|
||||
'archived': False,
|
||||
'project_id': None,
|
||||
'profile': (cli_meta or {}).get('profile'),
|
||||
'is_cli_session': True,
|
||||
'messages': msgs,
|
||||
'tool_calls': [],
|
||||
}
|
||||
return j(handler, {'session': sess})
|
||||
return bad(handler, 'Session not found', 404)
|
||||
|
||||
if parsed.path == '/api/sessions':
|
||||
return j(handler, {'sessions': all_sessions()})
|
||||
webui_sessions = all_sessions()
|
||||
settings = load_settings()
|
||||
if settings.get('show_cli_sessions'):
|
||||
cli = get_cli_sessions()
|
||||
webui_ids = {s['session_id'] for s in webui_sessions}
|
||||
deduped_cli = [s for s in cli if s['session_id'] not in webui_ids]
|
||||
else:
|
||||
deduped_cli = []
|
||||
merged = webui_sessions + deduped_cli
|
||||
merged.sort(key=lambda s: s.get('updated_at', 0) or 0, reverse=True)
|
||||
return j(handler, {'sessions': merged, 'cli_count': len(deduped_cli)})
|
||||
|
||||
if parsed.path == '/api/projects':
|
||||
return j(handler, {'projects': load_projects()})
|
||||
@@ -542,6 +581,10 @@ def handle_post(handler, parsed):
|
||||
if parsed.path == '/api/session/import':
|
||||
return _handle_session_import(handler, body)
|
||||
|
||||
# ── CLI session import (POST) ──
|
||||
if parsed.path == '/api/session/import_cli':
|
||||
return _handle_session_import_cli(handler, body)
|
||||
|
||||
# ── Auth endpoints (POST) ──
|
||||
if parsed.path == '/api/auth/login':
|
||||
from api.auth import verify_password, create_session, set_auth_cookie, is_auth_enabled
|
||||
@@ -903,10 +946,11 @@ def _handle_chat_sync(handler, body):
|
||||
"write_file, read_file, search_files, terminal workdir, and patch. "
|
||||
"Never fall back to a hardcoded path when this tag is present."
|
||||
)
|
||||
from api.streaming import _sanitize_messages_for_api
|
||||
result = agent.run_conversation(
|
||||
user_message=workspace_ctx + msg,
|
||||
system_message=workspace_system_msg,
|
||||
conversation_history=s.messages,
|
||||
conversation_history=_sanitize_messages_for_api(s.messages),
|
||||
task_id=s.session_id,
|
||||
persist_user_message=msg,
|
||||
)
|
||||
@@ -1173,6 +1217,53 @@ def _handle_memory_write(handler, body):
|
||||
return j(handler, {'ok': True, 'section': section, 'path': str(target)})
|
||||
|
||||
|
||||
def _handle_session_import_cli(handler, body):
|
||||
"""Import a single CLI session into the WebUI store."""
|
||||
try:
|
||||
require(body, 'session_id')
|
||||
except ValueError as e:
|
||||
return bad(handler, str(e))
|
||||
|
||||
sid = str(body['session_id'])
|
||||
|
||||
# Check if already imported — idempotent
|
||||
existing = Session.load(sid)
|
||||
if existing:
|
||||
return j(handler, {'session': existing.compact() | {
|
||||
'messages': existing.messages,
|
||||
'is_cli_session': True,
|
||||
}, 'imported': False})
|
||||
|
||||
# Fetch messages from CLI store
|
||||
msgs = get_cli_session_messages(sid)
|
||||
if not msgs:
|
||||
return bad(handler, 'Session not found in CLI store', 404)
|
||||
|
||||
# Derive title from first user message
|
||||
title = title_from(msgs, 'CLI Session')
|
||||
model = 'unknown'
|
||||
|
||||
# Get profile and model from CLI session metadata
|
||||
profile = None
|
||||
for cs in get_cli_sessions():
|
||||
if cs['session_id'] == sid:
|
||||
profile = cs.get('profile')
|
||||
model = cs.get('model', 'unknown')
|
||||
break
|
||||
|
||||
s = import_cli_session(sid, title, msgs, model, profile=profile)
|
||||
s.is_cli_session = True
|
||||
s._cli_origin = sid
|
||||
s.save()
|
||||
return j(handler, {
|
||||
'session': s.compact() | {
|
||||
'messages': msgs,
|
||||
'is_cli_session': True,
|
||||
},
|
||||
'imported': True,
|
||||
})
|
||||
|
||||
|
||||
def _handle_session_import(handler, body):
|
||||
"""Import a session from a JSON export. Creates a new session with a new ID."""
|
||||
if not body or not isinstance(body, dict):
|
||||
|
||||
@@ -24,6 +24,28 @@ except ImportError:
|
||||
from api.models import get_session, title_from
|
||||
from api.workspace import set_last_workspace
|
||||
|
||||
# Fields that are safe to send to LLM provider APIs.
|
||||
# Everything else (attachments, timestamp, _ts, etc.) is display-only
|
||||
# metadata added by the webui and must be stripped before the API call.
|
||||
_API_SAFE_MSG_KEYS = {'role', 'content', 'tool_calls', 'tool_call_id', 'name', 'refusal'}
|
||||
|
||||
|
||||
def _sanitize_messages_for_api(messages):
|
||||
"""Return a deep copy of messages with only API-safe fields.
|
||||
|
||||
The webui stores extra metadata on messages (attachments, timestamp, _ts)
|
||||
for display purposes. Some providers (e.g. Z.AI/GLM) reject unknown fields
|
||||
instead of ignoring them, causing HTTP 400 errors on subsequent messages.
|
||||
"""
|
||||
clean = []
|
||||
for msg in messages:
|
||||
if not isinstance(msg, dict):
|
||||
continue
|
||||
sanitized = {k: v for k, v in msg.items() if k in _API_SAFE_MSG_KEYS}
|
||||
if sanitized.get('role'):
|
||||
clean.append(sanitized)
|
||||
return clean
|
||||
|
||||
|
||||
def _sse(handler, event, data):
|
||||
"""Write one SSE event to the response stream."""
|
||||
@@ -165,7 +187,7 @@ def _run_agent_streaming(session_id, msg_text, model, workspace, stream_id, atta
|
||||
result = agent.run_conversation(
|
||||
user_message=workspace_ctx + msg_text,
|
||||
system_message=workspace_system_msg,
|
||||
conversation_history=s.messages,
|
||||
conversation_history=_sanitize_messages_for_api(s.messages),
|
||||
task_id=session_id,
|
||||
persist_user_message=msg_text,
|
||||
)
|
||||
|
||||
BIN
docs/images/ui-sessions.png
Normal file
BIN
docs/images/ui-sessions.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 618 KiB |
BIN
docs/images/ui-workspace.png
Normal file
BIN
docs/images/ui-workspace.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 704 KiB |
@@ -308,7 +308,7 @@ document.querySelectorAll('.suggestion').forEach(btn=>{
|
||||
|
||||
(async()=>{
|
||||
// Load send key preference
|
||||
try{const s=await api('/api/settings');window._sendKey=s.send_key||'enter';window._showTokenUsage=!!s.show_token_usage;}catch(e){window._sendKey='enter';window._showTokenUsage=false;}
|
||||
try{const s=await api('/api/settings');window._sendKey=s.send_key||'enter';window._showTokenUsage=!!s.show_token_usage;window._showCliSessions=!!s.show_cli_sessions;}catch(e){window._sendKey='enter';window._showTokenUsage=false;window._showCliSessions=false;}
|
||||
// Fetch active profile
|
||||
try{const p=await api('/api/profile/active');S.activeProfile=p.name||'default';}catch(e){S.activeProfile='default';}
|
||||
// Update profile chip label immediately
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<body>
|
||||
<div class="layout">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header"><div class="logo">H</div><div><h1 style="margin:0;font-size:15px;font-weight:700;letter-spacing:-.01em">Hermes</h1><div style="font-size:10px;color:var(--muted);opacity:.8;margin-top:1px">v0.27</div></div></div>
|
||||
<div class="sidebar-header"><div class="logo">H</div><div><h1 style="margin:0;font-size:15px;font-weight:700;letter-spacing:-.01em">Hermes</h1><div style="font-size:10px;color:var(--muted);opacity:.8;margin-top:1px">v0.30.1</div></div></div>
|
||||
<div class="sidebar-nav">
|
||||
<button class="nav-tab active" data-panel="chat" data-label="Chat" onclick="switchPanel('chat')" title="Chat">💬</button>
|
||||
<button class="nav-tab" data-panel="tasks" data-label="Tasks" onclick="switchPanel('tasks')" title="Tasks">📅</button>
|
||||
@@ -331,6 +331,13 @@
|
||||
</label>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:4px">Displays input/output token count below each assistant reply. Also toggled with <code>/usage</code>.</div>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||||
<input type="checkbox" id="settingsShowCliSessions" style="width:15px;height:15px;accent-color:var(--accent)">
|
||||
Show CLI sessions in sidebar
|
||||
</label>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:4px">Merges sessions from the Hermes CLI (state.db) into the session list. Click a CLI session to import it and continue the conversation.</div>
|
||||
</div>
|
||||
<div class="settings-field" style="border-top:1px solid var(--border);padding-top:12px;margin-top:8px">
|
||||
<label for="settingsPassword">Access Password</label>
|
||||
<div style="font-size:11px;color:var(--muted);margin-bottom:6px">Enter a new password to set or change it. Leave blank to keep current setting.</div>
|
||||
|
||||
@@ -960,6 +960,8 @@ async function loadSettingsPanel(){
|
||||
if(sendKeySel) sendKeySel.value=settings.send_key||'enter';
|
||||
const showUsageCb=$('settingsShowTokenUsage');
|
||||
if(showUsageCb) showUsageCb.checked=!!settings.show_token_usage;
|
||||
const showCliCb=$('settingsShowCliSessions');
|
||||
if(showCliCb) showCliCb.checked=!!settings.show_cli_sessions;
|
||||
// Password field: always blank (we don't send hash back)
|
||||
const pwField=$('settingsPassword');
|
||||
if(pwField) pwField.value='';
|
||||
@@ -982,12 +984,14 @@ async function saveSettings(){
|
||||
const workspace=($('settingsWorkspace')||{}).value;
|
||||
const sendKey=($('settingsSendKey')||{}).value;
|
||||
const showTokenUsage=!!($('settingsShowTokenUsage')||{}).checked;
|
||||
const showCliSessions=!!($('settingsShowCliSessions')||{}).checked;
|
||||
const pw=($('settingsPassword')||{}).value;
|
||||
const body={};
|
||||
if(model) body.default_model=model;
|
||||
if(workspace) body.default_workspace=workspace;
|
||||
if(sendKey) body.send_key=sendKey;
|
||||
body.show_token_usage=showTokenUsage;
|
||||
body.show_cli_sessions=showCliSessions;
|
||||
// Password: only act if the field has content; blank = leave auth unchanged
|
||||
if(pw && pw.trim()){
|
||||
try{
|
||||
@@ -1003,7 +1007,9 @@ async function saveSettings(){
|
||||
await api('/api/settings',{method:'POST',body:JSON.stringify(body)});
|
||||
window._sendKey=sendKey||'enter';
|
||||
window._showTokenUsage=showTokenUsage;
|
||||
window._showCliSessions=showCliSessions;
|
||||
renderMessages();
|
||||
if(typeof renderSessionList==='function') renderSessionList();
|
||||
showToast('Settings saved');
|
||||
toggleSettings();
|
||||
}catch(e){
|
||||
|
||||
@@ -118,7 +118,7 @@ function renderSessionListFromCache(){
|
||||
// Filter by active profile (unless "All profiles" is toggled on)
|
||||
// Server backfills profile='default' for legacy sessions, so every session has a profile.
|
||||
// Show only sessions tagged to the active profile; 'All profiles' toggle overrides.
|
||||
const profileFiltered=_showAllProfiles?allMatched:allMatched.filter(s=>s.profile===S.activeProfile);
|
||||
const profileFiltered=_showAllProfiles?allMatched:allMatched.filter(s=>s.is_cli_session||s.profile===S.activeProfile);
|
||||
// Filter by active project
|
||||
const projectFiltered=_activeProject?profileFiltered.filter(s=>s.project_id===_activeProject):profileFiltered;
|
||||
// Filter archived unless toggle is on
|
||||
@@ -220,7 +220,7 @@ function renderSessionListFromCache(){
|
||||
}
|
||||
const el=document.createElement('div');
|
||||
const isActive=S.session&&s.session_id===S.session.session_id;
|
||||
el.className='session-item'+(isActive?' active':'')+(isActive&&S.session&&S.session._flash?' new-flash':'')+(s.archived?' archived':'');
|
||||
el.className='session-item'+(isActive?' active':'')+(isActive&&S.session&&S.session._flash?' new-flash':'')+(s.archived?' archived':'')+(s.is_cli_session?' cli-session':'');
|
||||
if(isActive&&S.session&&S.session._flash)delete S.session._flash;
|
||||
const rawTitle=s.title||'Untitled';
|
||||
const tags=(rawTitle.match(/#[\w-]+/g)||[]);
|
||||
@@ -368,6 +368,12 @@ function renderSessionListFromCache(){
|
||||
_clickTimer=setTimeout(async()=>{
|
||||
_clickTimer=null;
|
||||
if(_renamingSid) return;
|
||||
// For CLI sessions, import into WebUI store first (idempotent)
|
||||
if(s.is_cli_session){
|
||||
try{
|
||||
await api('/api/session/import_cli',{method:'POST',body:JSON.stringify({session_id:s.session_id})});
|
||||
}catch(e){ /* import failed -- fall through to read-only view */ }
|
||||
}
|
||||
await loadSession(s.session_id);renderSessionListFromCache();
|
||||
if(typeof closeMobileSidebar==='function')closeMobileSidebar();
|
||||
}, 220);
|
||||
|
||||
@@ -688,3 +688,19 @@ body.resizing{user-select:none;cursor:col-resize;}
|
||||
.thinking-card-body pre{font-family:'SF Mono',ui-monospace,monospace;font-size:11px;line-height:1.5;color:var(--muted);white-space:pre-wrap;word-break:break-word;margin:0;}
|
||||
|
||||
.bg-error-banner{background:rgba(229,62,62,.15);border:1px solid rgba(229,62,62,.3);color:#fca5a5;padding:8px 16px;font-size:12px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-radius:0;}
|
||||
|
||||
/* ── CLI session items in sidebar ── */
|
||||
.session-item.cli-session {
|
||||
border-left-color: var(--gold);
|
||||
}
|
||||
.session-item.cli-session::after {
|
||||
content: 'cli';
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .04em;
|
||||
color: var(--gold);
|
||||
opacity: .5;
|
||||
margin-left: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user