refine session attention indicators

This commit is contained in:
Frank Song
2026-04-23 18:51:34 +08:00
parent 84371d11c3
commit 2f85657c48
6 changed files with 49 additions and 38 deletions
+2 -14
View File
@@ -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
+3
View File
@@ -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
+4
View File
@@ -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
View File
@@ -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
View File
@@ -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