mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-27 04:00:37 +00:00
refine session attention indicators
This commit is contained in:
+2
-14
@@ -6,24 +6,12 @@
|
||||
- **Nous static models now use explicit `@nous:` prefix** — the four hardcoded "(via Nous)" models (`Claude Opus 4.6`, `Claude Sonnet 4.6`, `GPT-5.4 Mini`, `Gemini 3.1 Pro Preview`) now carry `@nous:` prefix IDs, matching the format of live-fetched Nous models. Previously they used slash-only IDs that relied on the portal provider guard; the explicit prefix routes them through the same bulletproof `@provider:model` branch and eliminates 404 errors on those entries. (`api/config.py`, `tests/test_nous_portal_routing.py`)
|
||||
|
||||
### Added
|
||||
- **Workspace path autocomplete in Spaces** — the "Add workspace path" field in
|
||||
the Spaces panel now suggests trusted directories as you type, supports
|
||||
keyboard navigation plus `Tab` completion, and keeps hidden directories out of
|
||||
the list unless the current path segment starts with `.`. Suggestions are
|
||||
limited to trusted roots (home, saved workspaces, and the boot default
|
||||
workspace subtree) and never enumerate blocked system roots. (`api/routes.py`,
|
||||
`api/workspace.py`, `static/panels.js`, `static/style.css`) (partial for #616)
|
||||
- **Workspace path autocomplete in Spaces** — the "Add workspace path" field in the Spaces panel now suggests trusted directories as you type, supports keyboard navigation plus `Tab` completion, and keeps hidden directories out of the list unless the current path segment starts with `.`. Suggestions are limited to trusted roots (home, saved workspaces, and the boot default workspace subtree) and never enumerate blocked system roots. (`api/routes.py`, `api/workspace.py`, `static/panels.js`, `static/style.css`) (partial for #616)
|
||||
|
||||
## [v0.50.163] — 2026-04-23
|
||||
|
||||
### Fixed
|
||||
- **Message ordering after task cancellation** — cancelling a stream while the
|
||||
agent is responding no longer causes subsequent responses to appear above the
|
||||
"Task cancelled." marker. The cancel handler now fetches the authoritative
|
||||
message list from the server (same as the done event), and the server persists
|
||||
the cancel message to the session so both paths stay in sync. Falls back to
|
||||
the previous local-push behaviour if the API call fails. (`api/streaming.py`,
|
||||
`static/messages.js`) (@mittyok, #882)
|
||||
- **Message ordering after task cancellation** — cancelling a stream while the agent is responding no longer causes subsequent responses to appear above the "Task cancelled." marker. The cancel handler now fetches the authoritative message list from the server (same as the done event), and the server persists the cancel message to the session so both paths stay in sync. Falls back to the previous local-push behaviour if the API call fails. (`api/streaming.py`, `static/messages.js`) (@mittyok, #882)
|
||||
|
||||
## [v0.50.161] — 2026-04-23
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ terminal, a browser, or a messaging app — and it's the same agent with the sam
|
||||
This document explains the mental model, how Hermes compares to other tools honestly, and where
|
||||
it is and is not the right choice.
|
||||
|
||||
For repository work, start with `AGENTS.md` at the repo root. It contains the current working
|
||||
rules for Codex and Hermes on this codebase.
|
||||
|
||||
---
|
||||
|
||||
## The real problem: most tools are excellent in the moment and weak over time
|
||||
|
||||
@@ -10,6 +10,10 @@ if [[ -f "${REPO_ROOT}/.env" ]]; then
|
||||
set +a
|
||||
fi
|
||||
|
||||
if [[ -x "$HOME/.hermes/bin/hermes-system-proxy-env.py" ]]; then
|
||||
eval "$($HOME/.hermes/bin/hermes-system-proxy-env.py)"
|
||||
fi
|
||||
|
||||
PYTHON="${HERMES_WEBUI_PYTHON:-}"
|
||||
if [[ -z "${PYTHON}" ]]; then
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
|
||||
+12
-11
@@ -404,6 +404,7 @@ async function renderSessionList(){
|
||||
} else {
|
||||
stopStreamingPoll();
|
||||
}
|
||||
ensureSessionTimeRefreshPoll();
|
||||
renderSessionListFromCache(); // no-ops if rename is in progress
|
||||
}catch(e){console.warn('renderSessionList',e);}
|
||||
}
|
||||
@@ -415,7 +416,9 @@ let _gatewayProbeInFlight = false;
|
||||
let _gatewaySSEWarningShown = false;
|
||||
const _gatewayFallbackPollMs = 30000;
|
||||
const _streamingPollMs = 5000;
|
||||
const _sessionTimeRefreshMs = 60000;
|
||||
let _streamingPollTimer = null;
|
||||
let _sessionTimeRefreshTimer = null;
|
||||
|
||||
function startStreamingPoll(){
|
||||
if(_streamingPollTimer) return;
|
||||
@@ -430,6 +433,13 @@ function stopStreamingPoll(){
|
||||
_streamingPollTimer = null;
|
||||
}
|
||||
|
||||
function ensureSessionTimeRefreshPoll(){
|
||||
if(_sessionTimeRefreshTimer) return;
|
||||
_sessionTimeRefreshTimer = setInterval(() => {
|
||||
renderSessionListFromCache();
|
||||
}, _sessionTimeRefreshMs);
|
||||
}
|
||||
|
||||
function startGatewayPollFallback(ms){
|
||||
const intervalMs = Math.max(5000, Number(ms) || _gatewayFallbackPollMs);
|
||||
if(_gatewayPollTimer) clearInterval(_gatewayPollTimer);
|
||||
@@ -773,17 +783,8 @@ function renderSessionListFromCache(){
|
||||
const titleRow=document.createElement('div');
|
||||
titleRow.className='session-title-row';
|
||||
const state=document.createElement('span');
|
||||
state.className='session-state-indicator';
|
||||
if(isStreaming){
|
||||
const spinner=document.createElement('span');
|
||||
spinner.className='session-spinner';
|
||||
state.appendChild(spinner);
|
||||
}else if(hasUnread){
|
||||
const dot=document.createElement('span');
|
||||
dot.className='session-unread-dot';
|
||||
state.appendChild(dot);
|
||||
}
|
||||
if(state.childElementCount) titleRow.appendChild(state);
|
||||
state.className='session-state-indicator'+(isStreaming?' is-streaming':(hasUnread?' is-unread':''));
|
||||
if(isStreaming||hasUnread) titleRow.appendChild(state);
|
||||
const title=document.createElement('span');
|
||||
title.className='session-title';
|
||||
title.textContent=cleanTitle||'Untitled';
|
||||
|
||||
+13
-13
@@ -119,8 +119,8 @@
|
||||
:root:not(.dark) .session-item:hover{background:rgba(0,0,0,.06);color:#2c2825;}
|
||||
:root:not(.dark) .session-item.active{background:var(--accent-bg);color:var(--accent-text);}
|
||||
:root:not(.dark) .session-item.active .session-title{color:var(--accent-text);}
|
||||
:root:not(.dark) .session-pin-indicator{color:#996b15;}
|
||||
:root:not(.dark) .session-date-header.pinned{color:#996b15;}
|
||||
:root:not(.dark) .session-pin-indicator{color:var(--accent-text);}
|
||||
:root:not(.dark) .session-date-header.pinned{color:var(--accent-text);}
|
||||
:root:not(.dark) .session-actions-trigger.active,
|
||||
:root:not(.dark) .session-item.menu-open .session-actions-trigger{background:var(--accent-bg);border-color:var(--accent-bg-strong);color:var(--accent-text);}
|
||||
:root:not(.dark) .session-action-opt.is-active{background:var(--accent-bg);}
|
||||
@@ -232,22 +232,22 @@
|
||||
.session-item.active .session-title{color:var(--accent-text);}
|
||||
.session-meta{font-size:11px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||||
.session-item.active .session-meta{color:var(--accent-text);opacity:.8;}
|
||||
.session-state-indicator{display:inline-flex;align-items:center;flex-shrink:0;}
|
||||
.session-spinner{
|
||||
width:10px;
|
||||
height:10px;
|
||||
.session-state-indicator{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:10px;height:10px;color:var(--accent);}
|
||||
.session-state-indicator::before{content:"";display:block;flex-shrink:0;}
|
||||
.session-state-indicator.is-streaming::before{
|
||||
width:100%;
|
||||
height:100%;
|
||||
border:2px solid transparent;
|
||||
border-top-color:var(--blue);
|
||||
border-right-color:var(--blue);
|
||||
border-top-color:currentColor;
|
||||
border-right-color:currentColor;
|
||||
border-radius:50%;
|
||||
animation:spin 1s linear infinite;
|
||||
}
|
||||
.session-unread-dot{
|
||||
.session-state-indicator.is-unread::before{
|
||||
width:8px;
|
||||
height:8px;
|
||||
border-radius:50%;
|
||||
background:var(--blue);
|
||||
flex-shrink:0;
|
||||
background:currentColor;
|
||||
}
|
||||
.session-time{
|
||||
display:inline-flex;
|
||||
@@ -283,7 +283,7 @@
|
||||
/* Collapsible date group headers */
|
||||
.session-date-header{display:flex;align-items:center;gap:5px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);padding:8px 10px 4px;cursor:pointer;user-select:none;opacity:.8;transition:opacity .15s;}
|
||||
.session-date-header:hover{opacity:1;}
|
||||
.session-date-header.pinned{color:#f5c542;}
|
||||
.session-date-header.pinned{color:var(--accent);}
|
||||
.session-date-caret{font-size:9px;transition:transform .2s;flex-shrink:0;display:inline-block;}
|
||||
.session-date-caret.collapsed{transform:rotate(-90deg);}
|
||||
.app-dialog-overlay{position:fixed;inset:0;background:rgba(7,12,19,.62);backdrop-filter:blur(6px);z-index:1100;display:none;align-items:center;justify-content:center;padding:24px;}
|
||||
@@ -1508,7 +1508,7 @@ body.resizing{user-select:none;cursor:col-resize;}
|
||||
.provider-card .sm-btn:disabled{opacity:.4;cursor:not-allowed;}
|
||||
|
||||
/* ── Session pin indicator (inline, only when pinned) ── */
|
||||
.session-pin-indicator{flex-shrink:0;color:#f5c542;line-height:1;display:flex;align-items:center;}
|
||||
.session-pin-indicator{flex-shrink:0;color:var(--accent);line-height:1;display:flex;align-items:center;}
|
||||
.session-pin-indicator svg{width:10px;height:10px;}
|
||||
|
||||
/* ── Cron alert badge ── */
|
||||
|
||||
@@ -76,3 +76,18 @@ def test_all_sessions_marks_streaming_false_when_stream_is_not_active():
|
||||
|
||||
models.STREAMS.pop("stale-stream", None)
|
||||
assert all_sessions()[0]["is_streaming"] is False
|
||||
|
||||
|
||||
def test_all_sessions_does_not_report_streaming_after_restart_without_active_registry():
|
||||
"""Server restarts should not resurrect sidebar streaming state from disk alone."""
|
||||
s = _make_session("restart_session", stream_id="old-stream")
|
||||
s.save()
|
||||
|
||||
models.SESSIONS.clear()
|
||||
reloaded = Session.load("restart_session")
|
||||
assert reloaded is not None
|
||||
assert reloaded.active_stream_id == "old-stream"
|
||||
|
||||
listed = all_sessions()
|
||||
assert listed[0]["active_stream_id"] == "old-stream"
|
||||
assert listed[0]["is_streaming"] is False
|
||||
|
||||
Reference in New Issue
Block a user