Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@

## [Unreleased]


## [v0.51.92] — 2026-05-19 — Release BP (stage-385 — 7-PR full sweep batch — RFC Slice 3c clarification + workspace tree icon alignment + project move cache refresh + auto-compression handoff metadata + Grok OAuth provider catalog + anonymous custom endpoint picker fallback + PWA standalone reload + pull-to-refresh)

### Fixed

- **PR #2563** by @Michaelyklam (closes #2554) — Align workspace-tree file rows with sibling directory rows by reserving the same expand/collapse toggle slot for files via a new `.file-tree-toggle-placeholder` element. Expanded directories now show child files stepped in at the same icon column as child folders. Directory toggles and file interactions are unchanged; source-level regression coverage and before/after PNGs included.
- **PR #2561** by @nanookclaw (closes #2551) — Refresh the authoritative `_allSessions` cache when the project picker moves a session to/from a project. Previous code mutated only the shallow sidebar row copy, so `renderSessionListFromCache()` re-read the unchanged cache and repainted a stale project dot until the next `/api/sessions` poll healed the UI. Both the "Removed from project" and "Moved to <project>" branches now write the new `project_id` into `_allSessions[idx]` before re-rendering.
- **PR #2567** by @dso2ng (refs #2477) — Surface automatic-compression handoff metadata through the `compressed` SSE event so the active browser stream keeps its completion card even after the backend rotates the session id from the origin to a compressed continuation. The event now carries both `old_session_id` and `new_session_id`/`continuation_session_id`; the frontend `compressed` listener accepts either, and the automatic-compression detail line names the compressed continuation session so the done state isn't silently dropped.
- **PR #2568** by @Michaelyklam (closes #2545) — Add the Hermes Agent `xai-oauth` provider to the WebUI's OAuth provider catalog so Grok OAuth accounts authenticated via the Hermes CLI appear in Settings → Providers and the `/api/models` picker. The provider is treated as CLI-managed OAuth (no WebUI API-key form) and uses the live Hermes CLI model catalog when available with a Grok 4.20 static fallback.
- **PR #2550** by @espokaos-ops (refs #2542) — Keep anonymous custom OpenAI-compatible endpoints in the model picker even when the configured `/v1/models` probe fails. Lightweight relays and llama-server-style deployments that authenticate `/v1/chat/completions` but not `/v1/models` no longer have their provider group silently dropped from the picker. Users can type a model id manually in the free-form input when no live catalog is available.

### Added

- **PR #2548** by @espokaos-ops — Add a PWA-standalone reload affordance. A small refresh button appears in the app titlebar (visible only under `@media (display-mode: standalone), (display-mode: fullscreen)`) so users running the WebUI as an installed home-screen PWA can reload without re-launching the app. Adds a complementary pull-to-refresh gesture on the messages container with an 80px threshold and a smooth-scroll-to-top guard so accidental triggers while reading history feel intentional. 4-viewport screenshots (390/1280/1440/1920, light/dark, hover/idle) included under `docs/pr-media/2548/`.

### Documentation

- **PR #2560** by @Michaelyklam (refs #1925) — Clarify the RuntimeAdapter Slice 3c state after #2544 shipped. The RFC now distinguishes shipped `/api/goal` routing through `RuntimeAdapter.update_goal(...)` from the still-staged `queue_message(...)` protocol method, and explicitly warns not to add a new server-side queue endpoint or queue scheduler merely for adapter symmetry while `/queue` remains browser-side queue/drain behavior.

## [v0.51.91] — 2026-05-18 — Release BO (stage-384 — 5-PR full sweep batch — reasoning-replay history fix + archive-extract per-session inbox + fallback streaming warnings + sanitized custom-provider env hints + Slice 3c queue/goal adapter routing)

### Fixed
Expand Down
16 changes: 16 additions & 0 deletions api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,7 @@ def _resolve_cli_toolsets(cfg=None):
"anthropic": "Anthropic",
"openai": "OpenAI",
"openai-codex": "OpenAI Codex",
"xai-oauth": "xAI Grok OAuth",
"copilot": "GitHub Copilot",
"zai": "Z.AI / GLM",
"kimi-coding": "Kimi / Moonshot",
Expand Down Expand Up @@ -1209,6 +1210,9 @@ def _named_custom_provider_slug_for_base_url(
"x-ai": [
{"id": "grok-4.20", "label": "Grok 4.20"},
],
"xai-oauth": [
{"id": "grok-4.20", "label": "Grok 4.20"},
],
}


Expand Down Expand Up @@ -3852,6 +3856,18 @@ def _read_custom_endpoint_models(
"models": models_for_group,
}
)
elif pid == "custom" and cfg_base_url:
# Anonymous custom endpoint: /v1/models probe may have
# failed (e.g. llama-server, lightweight relay), but the
# chat endpoint itself may still work. Add the group
# with an empty model list so the user can type a model
# ID manually rather than being blocked by a silent
# probe failure (#2542).
groups.append({
"provider": provider_name,
"provider_id": pid,
"models": [],
})
else:
if default_model:
label = _get_label_for_model(default_model, groups)
Expand Down
9 changes: 9 additions & 0 deletions api/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,7 @@ def _fetch_codex_account_usage_from_pool():
"nous",
"openai-codex",
"qwen-oauth",
"xai-oauth",
})

# SECTION: Helper functions
Expand Down Expand Up @@ -1866,6 +1867,14 @@ def get_providers() -> dict[str, Any]:
if live_models:
models = live_models
models_total = len(models)
if pid == "xai-oauth":
live_models = _models_from_live_provider_ids(
pid,
_read_live_provider_model_ids("xai-oauth"),
)
if live_models:
models = live_models
models_total = len(models)
# Nous Portal: prefer the live catalog so the providers card matches
# the dropdown picker (#1538). Same fallback shape as the static-only
# case below — when hermes_cli is unavailable or its lookup raises,
Expand Down
11 changes: 10 additions & 1 deletion api/streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -4345,11 +4345,15 @@ def _periodic_checkpoint():
# reference stays alive even after the dict entry is removed.
# Concurrent readers that already looked up the old ID will still
# see the same Lock object until they release it.
_compression_origin_session_id = session_id
_compression_continuation_session_id = None
_agent_sid = getattr(agent, 'session_id', None)
_compressed = False
if _agent_sid and _agent_sid != session_id:
old_sid = session_id
new_sid = _agent_sid
_compression_origin_session_id = old_sid
_compression_continuation_session_id = new_sid
s.session_id = new_sid
# Carry profile identity across the compression boundary.
# Without this, s.profile stays None on the continuation
Expand Down Expand Up @@ -4426,8 +4430,13 @@ def _periodic_checkpoint():
_compression_summary_from_messages(s.messages)
or _compression_summary_from_messages(s.context_messages)
)
if _compression_continuation_session_id is None:
_compression_continuation_session_id = s.session_id
put('compressed', {
'session_id': s.session_id,
'session_id': _compression_origin_session_id,
'old_session_id': _compression_origin_session_id,
'new_session_id': _compression_continuation_session_id,
'continuation_session_id': _compression_continuation_session_id,
'message': 'Context auto-compressed to continue the conversation',
'usage': _live_usage_snapshot(),
})
Expand Down
Binary file added docs/pr-media/2548/fullpage_idle_dark_390.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/pr-media/2548/ptr_active_dark_1280.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/pr-media/2548/ptr_active_dark_1440.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/pr-media/2548/ptr_active_dark_1920.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/pr-media/2548/ptr_active_dark_390.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/pr-media/2548/ptr_active_light_1280.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/pr-media/2548/ptr_active_light_1440.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/pr-media/2548/ptr_active_light_1920.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/pr-media/2548/ptr_active_light_390.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/pr-media/2548/reload-btn_dark-hover_1280.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/pr-media/2548/reload-btn_dark-hover_1440.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/pr-media/2548/reload-btn_dark-hover_1920.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/pr-media/2548/reload-btn_dark-hover_390.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/pr-media/2548/reload-btn_dark_1280.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/pr-media/2548/reload-btn_dark_1440.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/pr-media/2548/reload-btn_dark_1920.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/pr-media/2548/reload-btn_dark_390.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/pr-media/2548/reload-btn_light-hover_390.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/pr-media/2548/reload-btn_light_1280.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/pr-media/2548/reload-btn_light_1440.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/pr-media/2548/reload-btn_light_1920.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/pr-media/2548/reload-btn_light_390.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/pr-media/2554/tree-indent-after.png
Binary file added docs/pr-media/2554/tree-indent-before.png
32 changes: 20 additions & 12 deletions docs/rfcs/hermes-run-adapter-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ The immediate goal is not to build a sidecar. The immediate goal is to define th
browser contract, classify current runtime state, and gate the first reversible
journal slice.

## Current Gate State — 2026-05-17
## Current Gate State — 2026-05-18

Slice 1 is now past the first active validation gate:

Expand Down Expand Up @@ -85,11 +85,19 @@ adapter-seam work:
- #2487 shipped the Slice 3b approval/clarify gate, and #2496 shipped approval /
clarify response routing through the adapter seam in v0.51.89.
- #2509 shipped the Slice 3c queue/continue + goal gate in v0.51.90.

The next gate is not the runner/sidecar yet. It is the Slice 3c implementation:
add the queue/goal adapter methods and route accepted legacy control paths
through them without moving goal evaluation, continuation scheduling, or
execution ownership out of the existing agent loop.
- #2544 shipped the first Slice 3c implementation in v0.51.91. The goal
route now uses `RuntimeAdapter.update_goal(...)` only when
`HERMES_WEBUI_RUNTIME_ADAPTER=legacy-journal` is enabled, while preserving the
legacy-direct response shape and leaving post-turn goal evaluation in the
existing agent loop.

The next gate is still not the runner/sidecar by default. Slice 3c's goal route
is shipped, and `queue_message(...)` remains a staged protocol method. Queue /
continue routing needs an explicit follow-up contract because the legacy `/queue`
path is browser-side queue/drain behavior today; no new server-side queue endpoint
or queue scheduler should be added just for adapter symmetry. If maintainers want
queue/continue to move before Slice 4, that follow-up should specify the exact
legacy entry point, response shape, and ordering/idempotency contract first.

## Goals

Expand Down Expand Up @@ -354,12 +362,12 @@ class RuntimeAdapter:
) -> ControlResult: ...
```

`queue_message` is named for the legacy `/api/session/queue` payload: it
accepts follow-up chat text rather than arbitrary runtime input. The method name
does not require the HTTP route to change; it documents the adapter-level control
semantics that a later Slice 3c implementation should preserve. The method enters
the protocol before route wiring so queue/continue can land as a separate, small
control-routing follow-up instead of being coupled to goal routing.
`queue_message` is named for the legacy queued-message payload shape: it accepts
follow-up chat text rather than arbitrary runtime input. The method name does not
require a new HTTP route. Today `/queue` is primarily browser-side queue/drain
behavior; the adapter method enters the protocol so a later queue/continue slice
has a typed control surface, but route wiring remains deliberately staged until
the exact legacy entry point and ordering/idempotency contract are explicit.

For `update_goal`, the `action` argument is the bounded adapter capability label.
During the legacy-journal slice, the legacy goal parser still receives the full
Expand Down
6 changes: 6 additions & 0 deletions static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,12 @@
<span class="app-titlebar-sub" id="appTitlebarSub" hidden></span>
</div>
<div class="app-titlebar-spacer" aria-hidden="true"></div>
<button class="app-titlebar-reload" id="btnReload" onclick="window.location.reload()" type="button" aria-label="Reload" title="Reload page">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" width="16" height="16">
<polyline points="23 4 23 10 17 10"/>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
</svg>
</button>
</header>
<div class="layout">
<nav class="rail" aria-label="Primary navigation">
Expand Down
5 changes: 4 additions & 1 deletion static/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -1809,7 +1809,9 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
if(!S.session||S.session.session_id!==activeSid) return;
let d={};
try{ d=JSON.parse(e.data||'{}')||{}; }catch(_){ d={}; }
if(d.session_id&&d.session_id!==activeSid) return;
const eventSid=d.old_session_id||d.session_id||activeSid;
if(eventSid!==activeSid && d.new_session_id!==activeSid && d.continuation_session_id!==activeSid) return;
const continuationSid=d.new_session_id||d.continuation_session_id||'';
const message=String(d.message||'Context auto-compressed to continue the conversation').trim();
if(d.usage&&typeof _syncCtxIndicator==='function'){
S.lastUsage={...(S.lastUsage||{}),...d.usage};
Expand All @@ -1822,6 +1824,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
automatic:true,
message,
summary:{headline:message},
continuationSessionId:continuationSid,
};
setCompressionUi(state);
const appended=typeof appendLiveCompressionCard==='function'&&appendLiveCompressionCard(state);
Expand Down
11 changes: 9 additions & 2 deletions static/sessions.js
Original file line number Diff line number Diff line change
Expand Up @@ -3569,7 +3569,12 @@ function _showProjectPicker(session, anchorEl){
picker.remove();
document.removeEventListener('click',close);
await api('/api/session/move',{method:'POST',body:JSON.stringify({session_id:session.session_id,project_id:null})});
session.project_id=null;
// Sidebar rows are shallow copies of _allSessions entries (see
// _attachChildSessionsToSidebarRows), so mutating `session` only updates
// the discarded copy. Write into the authoritative cache so the next
// renderSessionListFromCache() reflects the move. (#2551)
const idx=_allSessions.findIndex(s=>s&&s.session_id===session.session_id);
if(idx>=0) _allSessions[idx].project_id=null;
renderSessionListFromCache();
showToast('Removed from project');
};
Expand All @@ -3591,7 +3596,9 @@ function _showProjectPicker(session, anchorEl){
picker.remove();
document.removeEventListener('click',close);
await api('/api/session/move',{method:'POST',body:JSON.stringify({session_id:session.session_id,project_id:p.project_id})});
session.project_id=p.project_id;
// See #2551 — write to _allSessions, not the shallow sidebar copy.
const idx=_allSessions.findIndex(s=>s&&s.session_id===session.session_id);
if(idx>=0) _allSessions[idx].project_id=p.project_id;
renderSessionListFromCache();
showToast('Moved to '+p.name);
};
Expand Down
13 changes: 13 additions & 0 deletions static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,18 @@
@media (display-mode: standalone), (display-mode: fullscreen){
:root{--app-titlebar-safe-top:env(safe-area-inset-top,0px);}
}
/* Pull-to-refresh indicator for PWA standalone mode */
.pull-to-refresh-indicator{position:absolute;top:0;left:0;right:0;z-index:100;display:flex;align-items:center;justify-content:center;gap:8px;padding:12px;font-size:13px;color:var(--muted);pointer-events:none;opacity:0;transform:translateY(-100%);transition:opacity .2s,transform .2s;background:var(--bg);border-bottom:1px solid var(--border);}
.pull-to-refresh-indicator.active{opacity:1;transform:translateY(0);}
.pull-to-refresh-indicator .ptr-icon{display:inline-block;transition:transform .2s;font-size:16px;}
.pull-to-refresh-indicator .ptr-icon.ready{transform:rotate(180deg);color:var(--accent);}
/* Reload button — visible only in PWA standalone / fullscreen */
.app-titlebar-reload{display:none;align-items:center;justify-content:center;width:32px;height:32px;flex-shrink:0;background:none;border:none;color:var(--muted);border-radius:8px;cursor:pointer;padding:0;-webkit-tap-highlight-color:transparent;transition:background-color .15s,color .15s;}
.app-titlebar-reload:hover{background:var(--hover-bg);color:var(--text);}
@media (display-mode: standalone), (display-mode: fullscreen){
:root{--app-titlebar-safe-top:env(safe-area-inset-top,0px);}
.app-titlebar-reload{display:inline-flex;}
}
}
html{background:var(--sidebar);}
body{background:var(--bg);color:var(--text);height:100vh;height:100dvh;overflow:hidden;display:flex;flex-direction:column;}
Expand Down Expand Up @@ -1423,6 +1435,7 @@
.file-item:hover{background:var(--hover-bg);color:var(--text);}
.file-item.active{background:var(--accent-bg);color:var(--accent-text);}
.file-tree-toggle{font-size:10px;color:var(--muted);flex-shrink:0;width:10px;text-align:center;line-height:1;}
.file-tree-toggle-placeholder{display:inline-block;flex:0 0 10px;width:10px;line-height:1;}
.file-item.file-empty{color:var(--muted);opacity:.5;font-style:italic;cursor:default;font-size:11px;}
.file-item.file-empty:hover{background:none;color:var(--muted);}
.preview-area{flex:1;overflow:auto;padding:14px;flex-direction:column;gap:8px;display:none;opacity:0;transition:opacity .15s;}
Expand Down
Loading
Loading