feat: multi-profile support -- create, switch, delete profiles from web UI (Issue #28)
Add full profile management to the web UI, matching the hermes-agent CLI
profile system. Profiles are isolated HERMES_HOME instances with their own
config, skills, memory, cron, and API keys.
Backend: new api/profiles.py wrapping hermes_cli.profiles, dynamic config
reloading, 5 new API endpoints, profile-aware path resolution, HERMES_HOME
env save/restore in streaming, module-level cache patching for skills_tool
and cron/jobs.
Frontend: profile chip in topbar with dropdown, Profiles sidebar panel with
CRUD UI, boot-time profile fetch, cascade refresh on switch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 17:50:21 +00:00
|
|
|
|
const S={session:null,messages:[],entries:[],busy:false,pendingFiles:[],toolCalls:[],activeStreamId:null,currentDir:'.',activeProfile:'default'};
|
2026-03-31 03:40:19 +00:00
|
|
|
|
const INFLIGHT={}; // keyed by session_id while request in-flight
|
2026-04-13 23:18:15 +00:00
|
|
|
|
const SESSION_QUEUES={}; // keyed by session_id for queued follow-up turns
|
2026-04-24 19:33:56 +00:00
|
|
|
|
// Tracks which session's queue to drain in setBusy(false).
|
|
|
|
|
|
// Set to activeSid just before setBusy(false) in done/error handlers so the
|
|
|
|
|
|
// queue drains the session that *finished*, not the one currently viewed.
|
|
|
|
|
|
// Single-shot: setBusy() reads and clears this on every call. Concurrent
|
|
|
|
|
|
// back-to-back stream completions would overwrite it, but HTTPServer is
|
|
|
|
|
|
// single-threaded so only one done event fires at a time in practice.
|
|
|
|
|
|
let _queueDrainSid=null;
|
2026-03-31 03:40:19 +00:00
|
|
|
|
const $=id=>document.getElementById(id);
|
2026-04-13 23:18:15 +00:00
|
|
|
|
function _getSessionQueue(sid, create=false){
|
|
|
|
|
|
if(!sid) return [];
|
|
|
|
|
|
if(!SESSION_QUEUES[sid]&&create) SESSION_QUEUES[sid]=[];
|
|
|
|
|
|
return SESSION_QUEUES[sid]||[];
|
|
|
|
|
|
}
|
|
|
|
|
|
function queueSessionMessage(sid, payload){
|
|
|
|
|
|
if(!sid||!payload) return 0;
|
|
|
|
|
|
const q=_getSessionQueue(sid,true);
|
2026-04-20 23:04:09 +00:00
|
|
|
|
// Stamp created_at so the restore path can detect stale entries (agent already responded)
|
|
|
|
|
|
const entry={...payload, _queued_at: Date.now()};
|
|
|
|
|
|
q.push(entry);
|
|
|
|
|
|
// Persist to sessionStorage so the queue survives page refresh
|
|
|
|
|
|
try{ sessionStorage.setItem('hermes-queue-'+sid, JSON.stringify(q)); }catch(_){}
|
2026-04-13 23:18:15 +00:00
|
|
|
|
return q.length;
|
|
|
|
|
|
}
|
|
|
|
|
|
function shiftQueuedSessionMessage(sid){
|
|
|
|
|
|
const q=_getSessionQueue(sid,false);
|
|
|
|
|
|
if(!q.length) return null;
|
|
|
|
|
|
const next=q.shift();
|
2026-04-20 23:04:09 +00:00
|
|
|
|
if(!q.length){
|
|
|
|
|
|
delete SESSION_QUEUES[sid];
|
|
|
|
|
|
try{ sessionStorage.removeItem('hermes-queue-'+sid); }catch(_){}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
try{ sessionStorage.setItem('hermes-queue-'+sid, JSON.stringify(q)); }catch(_){}
|
|
|
|
|
|
}
|
2026-04-13 23:18:15 +00:00
|
|
|
|
return next;
|
|
|
|
|
|
}
|
|
|
|
|
|
function getQueuedSessionCount(sid){
|
|
|
|
|
|
return _getSessionQueue(sid,false).length;
|
|
|
|
|
|
}
|
2026-04-18 06:55:04 +00:00
|
|
|
|
function _compressionSessionLock(){
|
|
|
|
|
|
return window._compressionLockSid||null;
|
|
|
|
|
|
}
|
|
|
|
|
|
function _setCompressionSessionLock(sid){
|
|
|
|
|
|
window._compressionLockSid=sid||null;
|
|
|
|
|
|
}
|
2026-03-31 03:40:19 +00:00
|
|
|
|
const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
|
|
|
|
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
// Dynamic model labels -- populated by populateModelDropdown(), fallback to static map
|
|
|
|
|
|
let _dynamicModelLabels={};
|
|
|
|
|
|
|
fix: model picker correctly updates on profile switch without flicker or raw injection
Root cause: three interacting bugs caused the model picker to show the wrong
model or flicker after a profile switch.
Bug 1 — syncTopbar() fought switchToProfile().
After switchToProfile() set the picker to the profile's model, syncTopbar()
was called (via renderSessionList -> loadSession, then explicitly at the end)
and overwrote it with S.session.model -- the old session's model.
Fix: added S._pendingProfileModel flag. switchToProfile() sets it;
syncTopbar() checks it first, applies the override, then clears it.
S.session.model is also updated to the resolved value so subsequent
syncTopbar() calls are consistent.
Bug 2 — Raw option injected at top of list for mismatched model IDs.
Profile configs store model IDs like 'claude-sonnet-4-6' (hermes-agent
format: hyphens, no namespace prefix) but the dropdown has
'anthropic/claude-sonnet-4.6' (OpenRouter format: dots, with prefix).
The old code did sel.value = id, found no match, then injected a new
<option> at the top of the list -- creating a lowercase duplicate that
didn't match any real provider group entry.
Fix: _findModelInDropdown() normalises both sides (strip prefix, hyphens->dots,
lowercase) and finds the best matching existing option. No new options are ever
injected for profile switching.
Bug 3 — populateModelDropdown() injected raw option on cold load.
Same issue: if default_model from /api/models didn't exactly match a dropdown
value, an extra option was added. Fixed by using _applyModelToDropdown()
which only selects existing options.
New helpers in ui.js:
_findModelInDropdown(modelId, sel) -- smart fuzzy match, returns matched value
_applyModelToDropdown(modelId, sel) -- sets picker, returns resolved value
Tests: 426 passed, 0 failed.
2026-04-03 20:10:47 +00:00
|
|
|
|
// ── Smart model resolver ────────────────────────────────────────────────────
|
|
|
|
|
|
// Finds the best matching option value in a <select> for a given model ID.
|
|
|
|
|
|
// Handles mismatches like 'claude-sonnet-4-6' vs 'anthropic/claude-sonnet-4.6'.
|
|
|
|
|
|
// Returns the matched option's value (already in the list), or null if no match.
|
|
|
|
|
|
function _findModelInDropdown(modelId, sel){
|
|
|
|
|
|
if(!modelId||!sel) return null;
|
|
|
|
|
|
const opts=Array.from(sel.options).map(o=>o.value);
|
|
|
|
|
|
// 1. Exact match
|
|
|
|
|
|
if(opts.includes(modelId)) return modelId;
|
|
|
|
|
|
// 2. Normalize: lowercase, strip namespace prefix, replace hyphens→dots
|
|
|
|
|
|
const norm=s=>s.toLowerCase().replace(/^[^/]+\//,'').replace(/-/g,'.');
|
|
|
|
|
|
const target=norm(modelId);
|
|
|
|
|
|
const exact=opts.find(o=>norm(o)===target);
|
|
|
|
|
|
if(exact) return exact;
|
|
|
|
|
|
// 3. Prefix/substring: target starts with or contains a significant chunk
|
|
|
|
|
|
const base=target.replace(/\.\d+$/,''); // strip trailing version number
|
|
|
|
|
|
const partial=opts.find(o=>norm(o).startsWith(base)||norm(o).includes(base));
|
|
|
|
|
|
return partial||null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Set the model picker to the best match for modelId.
|
|
|
|
|
|
// Returns the resolved value that was actually set, or null if nothing matched.
|
|
|
|
|
|
function _applyModelToDropdown(modelId, sel){
|
|
|
|
|
|
if(!modelId||!sel) return null;
|
|
|
|
|
|
const resolved=_findModelInDropdown(modelId,sel);
|
|
|
|
|
|
if(resolved){
|
|
|
|
|
|
sel.value=resolved;
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
if(sel.id==='modelSelect' && typeof syncModelChip==='function') syncModelChip();
|
fix: model picker correctly updates on profile switch without flicker or raw injection
Root cause: three interacting bugs caused the model picker to show the wrong
model or flicker after a profile switch.
Bug 1 — syncTopbar() fought switchToProfile().
After switchToProfile() set the picker to the profile's model, syncTopbar()
was called (via renderSessionList -> loadSession, then explicitly at the end)
and overwrote it with S.session.model -- the old session's model.
Fix: added S._pendingProfileModel flag. switchToProfile() sets it;
syncTopbar() checks it first, applies the override, then clears it.
S.session.model is also updated to the resolved value so subsequent
syncTopbar() calls are consistent.
Bug 2 — Raw option injected at top of list for mismatched model IDs.
Profile configs store model IDs like 'claude-sonnet-4-6' (hermes-agent
format: hyphens, no namespace prefix) but the dropdown has
'anthropic/claude-sonnet-4.6' (OpenRouter format: dots, with prefix).
The old code did sel.value = id, found no match, then injected a new
<option> at the top of the list -- creating a lowercase duplicate that
didn't match any real provider group entry.
Fix: _findModelInDropdown() normalises both sides (strip prefix, hyphens->dots,
lowercase) and finds the best matching existing option. No new options are ever
injected for profile switching.
Bug 3 — populateModelDropdown() injected raw option on cold load.
Same issue: if default_model from /api/models didn't exactly match a dropdown
value, an extra option was added. Fixed by using _applyModelToDropdown()
which only selects existing options.
New helpers in ui.js:
_findModelInDropdown(modelId, sel) -- smart fuzzy match, returns matched value
_applyModelToDropdown(modelId, sel) -- sets picker, returns resolved value
Tests: 426 passed, 0 failed.
2026-04-03 20:10:47 +00:00
|
|
|
|
return resolved;
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
async function populateModelDropdown(){
|
|
|
|
|
|
const sel=$('modelSelect');
|
|
|
|
|
|
if(!sel) return;
|
|
|
|
|
|
try{
|
2026-04-16 18:20:08 +00:00
|
|
|
|
const data=await fetch(new URL('api/models',location.href).href,{credentials:'include'}).then(r=>r.json());
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
if(!data.groups||!data.groups.length) return; // keep HTML defaults
|
fix: warn on provider/model mismatch, surface auth errors (#266)
* fix: warn on provider/model mismatch, surface auth errors (#266)
Fixes #266 — WebUI silently ignores provider/model selection mismatch.
The problem: selecting an OpenRouter (or Anthropic/OpenAI) model while
Hermes is configured for a different provider (e.g. local Ollama) sends
the request to the wrong endpoint, which returns a 401 Unauthorized error
with no UI indication of why.
Three-layer fix:
1. api/streaming.py — detect 401/auth errors explicitly
Added is_auth_error detection covering '401', 'AuthenticationError',
'authentication', 'unauthorized', 'invalid api key', and the specific
Ollama error string 'no cookie auth credentials'. Auth errors emit
apperror with type='auth_mismatch' and a hint pointing to 'hermes model'.
2. static/ui.js — expose active_provider and warn on selection
- populateModelDropdown() stores data.active_provider from /api/models
as window._activeProvider (the field was already in the response but
the frontend never used it)
- New _checkProviderMismatch(modelId) helper: compares the selected
model's slash-prefix (e.g. 'openai/' from 'openai/gpt-4o') against
the active provider. Skips the check for 'openrouter' and 'custom'
to avoid false positives on configs that legitimately route any model.
3. static/boot.js — warn on model dropdown change
modelSelect.onchange calls _checkProviderMismatch() and shows a toast
when the selected model looks incompatible with the configured provider.
4. static/messages.js — distinct UI label for auth errors
apperror handler now distinguishes type='auth_mismatch' and shows
'Provider mismatch' as the error label instead of 'Error'.
5. static/i18n.js — provider_mismatch_warning and provider_mismatch_label
keys added to all 5 locales (en, es, de, zh-Hans, zh-Hant).
Tests: 21 new tests in tests/test_provider_mismatch.py covering all
five change areas. 679/679 total pass (658 baseline + 21 new).
* fix: t() call args spread + use i18n label for auth mismatch
1. ui.js: _checkProviderMismatch passed [modelId, ap] as a single
array arg to t(). Since t(key, ...args) spreads, the function
received the array as m and undefined as p. Fixed to pass as
separate args: t('provider_mismatch_warning', modelId, ap).
2. messages.js: 'Provider mismatch' label was hardcoded instead of
using t('provider_mismatch_label'). Now uses the i18n key with
fallback for when t() isn't available.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 04:25:18 +00:00
|
|
|
|
// Store active provider globally so the send path can warn on mismatch
|
|
|
|
|
|
window._activeProvider=data.active_provider||null;
|
2026-04-23 16:58:15 +00:00
|
|
|
|
// Store default model so newSession() can apply it (#872).
|
|
|
|
|
|
// Per-page-load — not synced across browser tabs.
|
|
|
|
|
|
window._defaultModel=data.default_model||null;
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
// Clear existing options
|
|
|
|
|
|
sel.innerHTML='';
|
|
|
|
|
|
_dynamicModelLabels={};
|
|
|
|
|
|
for(const g of data.groups){
|
|
|
|
|
|
const og=document.createElement('optgroup');
|
|
|
|
|
|
og.label=g.provider;
|
2026-04-22 00:24:54 +00:00
|
|
|
|
if(g.provider_id) og.dataset.provider=g.provider_id;
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
for(const m of g.models){
|
|
|
|
|
|
const opt=document.createElement('option');
|
|
|
|
|
|
opt.value=m.id;
|
|
|
|
|
|
opt.textContent=m.label;
|
|
|
|
|
|
og.appendChild(opt);
|
|
|
|
|
|
_dynamicModelLabels[m.id]=m.label;
|
|
|
|
|
|
}
|
|
|
|
|
|
sel.appendChild(og);
|
|
|
|
|
|
}
|
|
|
|
|
|
// Set default model from server if no localStorage preference
|
|
|
|
|
|
if(data.default_model && !localStorage.getItem('hermes-webui-model')){
|
fix: model picker correctly updates on profile switch without flicker or raw injection
Root cause: three interacting bugs caused the model picker to show the wrong
model or flicker after a profile switch.
Bug 1 — syncTopbar() fought switchToProfile().
After switchToProfile() set the picker to the profile's model, syncTopbar()
was called (via renderSessionList -> loadSession, then explicitly at the end)
and overwrote it with S.session.model -- the old session's model.
Fix: added S._pendingProfileModel flag. switchToProfile() sets it;
syncTopbar() checks it first, applies the override, then clears it.
S.session.model is also updated to the resolved value so subsequent
syncTopbar() calls are consistent.
Bug 2 — Raw option injected at top of list for mismatched model IDs.
Profile configs store model IDs like 'claude-sonnet-4-6' (hermes-agent
format: hyphens, no namespace prefix) but the dropdown has
'anthropic/claude-sonnet-4.6' (OpenRouter format: dots, with prefix).
The old code did sel.value = id, found no match, then injected a new
<option> at the top of the list -- creating a lowercase duplicate that
didn't match any real provider group entry.
Fix: _findModelInDropdown() normalises both sides (strip prefix, hyphens->dots,
lowercase) and finds the best matching existing option. No new options are ever
injected for profile switching.
Bug 3 — populateModelDropdown() injected raw option on cold load.
Same issue: if default_model from /api/models didn't exactly match a dropdown
value, an extra option was added. Fixed by using _applyModelToDropdown()
which only selects existing options.
New helpers in ui.js:
_findModelInDropdown(modelId, sel) -- smart fuzzy match, returns matched value
_applyModelToDropdown(modelId, sel) -- sets picker, returns resolved value
Tests: 426 passed, 0 failed.
2026-04-03 20:10:47 +00:00
|
|
|
|
_applyModelToDropdown(data.default_model, sel);
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
}
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
if(typeof syncModelChip==='function') syncModelChip();
|
fix: silent agent errors, stale model list, live model fetching (#377)
* fix: silent errors, stale models, live model fetching (#373, #374, #375)
- api/streaming.py: detect empty agent response (_assistant_added check),
emit apperror(type='no_response' or 'auth_mismatch') instead of silent done
- api/streaming.py: add _token_sent flag so guard works for streaming agents
- static/messages.js: done handler belt-and-suspenders guard for zero replies
- static/messages.js: apperror handler labels 'no_response' type distinctly
- api/config.py: remove gpt-4o and o3 from _FALLBACK_MODELS and
_PROVIDER_MODELS['openai'] (superseded by gpt-5.4-mini and o4-mini)
- api/routes.py: new /api/models/live?provider= endpoint, fetches /v1/models
from provider API with B310 scheme check + SSRF guard
- static/ui.js: _fetchLiveModels() background fetch after static list loads,
appends new models to dropdown, caches per session, skips unsupported providers
Other:
- tests/test_issues_373_374_375.py: 25 new structural tests
- tests/test_regressions.py: extend done-handler window 1500->2500 chars
- CHANGELOG.md: v0.50.19 entry; 947 tests (up from 922)
* fix: SSRF hostname bypass + auth detection operator precedence
1. routes.py: SSRF guard used substring matching (any(k in hostname))
which allows bypass via hostnames like evil-ollama.attacker.com.
Changed to exact hostname matching against a fixed set of known
local hostnames (localhost, 127.0.0.1, 0.0.0.0, ::1).
2. streaming.py: _is_auth detection had a Python operator precedence
bug on the ternary expression. The line:
'AuthenticationError' in type(...).__name__ if _last_err else False
parsed as the ternary absorbing the rest of the or-chain when
_last_err was falsy. Fixed to: (_last_err and 'AuthenticationError' in ...)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: fix v0.50.20 CHANGELOG version number and test count (949 tests)
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:52:35 +00:00
|
|
|
|
// Kick off a background live-model fetch for the active provider.
|
|
|
|
|
|
// This runs after the static list is already shown (no blocking flicker).
|
|
|
|
|
|
if(data.active_provider) _fetchLiveModels(data.active_provider, sel);
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
}catch(e){
|
|
|
|
|
|
// API unavailable -- keep the hardcoded HTML options as fallback
|
|
|
|
|
|
console.warn('Failed to load models from server:',e.message);
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
if(typeof syncModelChip==='function') syncModelChip();
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
fix: silent agent errors, stale model list, live model fetching (#377)
* fix: silent errors, stale models, live model fetching (#373, #374, #375)
- api/streaming.py: detect empty agent response (_assistant_added check),
emit apperror(type='no_response' or 'auth_mismatch') instead of silent done
- api/streaming.py: add _token_sent flag so guard works for streaming agents
- static/messages.js: done handler belt-and-suspenders guard for zero replies
- static/messages.js: apperror handler labels 'no_response' type distinctly
- api/config.py: remove gpt-4o and o3 from _FALLBACK_MODELS and
_PROVIDER_MODELS['openai'] (superseded by gpt-5.4-mini and o4-mini)
- api/routes.py: new /api/models/live?provider= endpoint, fetches /v1/models
from provider API with B310 scheme check + SSRF guard
- static/ui.js: _fetchLiveModels() background fetch after static list loads,
appends new models to dropdown, caches per session, skips unsupported providers
Other:
- tests/test_issues_373_374_375.py: 25 new structural tests
- tests/test_regressions.py: extend done-handler window 1500->2500 chars
- CHANGELOG.md: v0.50.19 entry; 947 tests (up from 922)
* fix: SSRF hostname bypass + auth detection operator precedence
1. routes.py: SSRF guard used substring matching (any(k in hostname))
which allows bypass via hostnames like evil-ollama.attacker.com.
Changed to exact hostname matching against a fixed set of known
local hostnames (localhost, 127.0.0.1, 0.0.0.0, ::1).
2. streaming.py: _is_auth detection had a Python operator precedence
bug on the ternary expression. The line:
'AuthenticationError' in type(...).__name__ if _last_err else False
parsed as the ternary absorbing the rest of the or-chain when
_last_err was falsy. Fixed to: (_last_err and 'AuthenticationError' in ...)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: fix v0.50.20 CHANGELOG version number and test count (949 tests)
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:52:35 +00:00
|
|
|
|
// Cache so we don't re-fetch on every page load
|
|
|
|
|
|
const _liveModelCache={};
|
|
|
|
|
|
|
2026-04-23 16:58:15 +00:00
|
|
|
|
function _addLiveModelsToSelect(provider, models, sel){
|
|
|
|
|
|
if(!provider||!models||!models.length||!sel) return 0;
|
|
|
|
|
|
const currentVal=sel.value;
|
|
|
|
|
|
let providerGroup=null;
|
|
|
|
|
|
for(const og of sel.querySelectorAll('optgroup')){
|
|
|
|
|
|
if(og.dataset.provider&&og.dataset.provider===provider){
|
|
|
|
|
|
providerGroup=og; break;
|
|
|
|
|
|
}
|
|
|
|
|
|
if(og.label&&og.label.toLowerCase().includes(provider.toLowerCase())){
|
|
|
|
|
|
providerGroup=og; break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if(!providerGroup){
|
|
|
|
|
|
providerGroup=document.createElement('optgroup');
|
|
|
|
|
|
providerGroup.label=provider.charAt(0).toUpperCase()+provider.slice(1)+' (live)';
|
|
|
|
|
|
sel.appendChild(providerGroup);
|
|
|
|
|
|
}
|
|
|
|
|
|
const existingIds=new Set([...sel.options].map(o=>o.value));
|
2026-04-23 21:41:06 +00:00
|
|
|
|
// Normalized dedup: strip @provider: prefix and unify separators so
|
|
|
|
|
|
// 'minimax/minimax-m2.7' matches '@nous:minimax/minimax-m2.7' (#907).
|
|
|
|
|
|
// Strip ONLY the first colon — Ollama tag IDs are multi-colon
|
|
|
|
|
|
// (e.g. '@ollama-cloud:qwen3-vl:235b-instruct') and split(':',2) would
|
|
|
|
|
|
// truncate the tag suffix in JS (the limit arg discards extras, unlike Python).
|
|
|
|
|
|
const _normId=id=>{
|
|
|
|
|
|
let s=String(id||'');
|
|
|
|
|
|
if(s.startsWith('@')&&s.includes(':')) s=s.substring(s.indexOf(':')+1); // strip only @provider:
|
|
|
|
|
|
s=s.split('/').pop(); // strip namespace prefix
|
|
|
|
|
|
return s.replace(/-/g,'.').toLowerCase();
|
|
|
|
|
|
};
|
|
|
|
|
|
const existingNorm=new Set([...sel.options].map(o=>_normId(o.value)));
|
2026-04-23 16:58:15 +00:00
|
|
|
|
let added=0;
|
|
|
|
|
|
const _ap=(window._activeProvider||'').toLowerCase();
|
|
|
|
|
|
const _isPortalFetch=_ap && _ap!=='openrouter' && _ap!=='custom' && provider===_ap;
|
|
|
|
|
|
for(const m of models){
|
|
|
|
|
|
let mid=m.id;
|
|
|
|
|
|
if(_isPortalFetch && !mid.startsWith('@')){
|
|
|
|
|
|
mid=`@${provider}:${mid}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
if(existingIds.has(mid)) continue;
|
2026-04-23 21:41:06 +00:00
|
|
|
|
if(existingNorm.has(_normId(mid))) continue; // dedup cross-prefix duplicates (#907)
|
2026-04-23 16:58:15 +00:00
|
|
|
|
const opt=document.createElement('option');
|
|
|
|
|
|
opt.value=mid;
|
|
|
|
|
|
opt.textContent=m.label||m.id;
|
|
|
|
|
|
opt.title='Live model — fetched from provider';
|
|
|
|
|
|
providerGroup.appendChild(opt);
|
|
|
|
|
|
_dynamicModelLabels[mid]=m.label||m.id;
|
|
|
|
|
|
added++;
|
|
|
|
|
|
}
|
|
|
|
|
|
if(added>0 && currentVal) _applyModelToDropdown(currentVal, sel);
|
|
|
|
|
|
return added;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
fix: silent agent errors, stale model list, live model fetching (#377)
* fix: silent errors, stale models, live model fetching (#373, #374, #375)
- api/streaming.py: detect empty agent response (_assistant_added check),
emit apperror(type='no_response' or 'auth_mismatch') instead of silent done
- api/streaming.py: add _token_sent flag so guard works for streaming agents
- static/messages.js: done handler belt-and-suspenders guard for zero replies
- static/messages.js: apperror handler labels 'no_response' type distinctly
- api/config.py: remove gpt-4o and o3 from _FALLBACK_MODELS and
_PROVIDER_MODELS['openai'] (superseded by gpt-5.4-mini and o4-mini)
- api/routes.py: new /api/models/live?provider= endpoint, fetches /v1/models
from provider API with B310 scheme check + SSRF guard
- static/ui.js: _fetchLiveModels() background fetch after static list loads,
appends new models to dropdown, caches per session, skips unsupported providers
Other:
- tests/test_issues_373_374_375.py: 25 new structural tests
- tests/test_regressions.py: extend done-handler window 1500->2500 chars
- CHANGELOG.md: v0.50.19 entry; 947 tests (up from 922)
* fix: SSRF hostname bypass + auth detection operator precedence
1. routes.py: SSRF guard used substring matching (any(k in hostname))
which allows bypass via hostnames like evil-ollama.attacker.com.
Changed to exact hostname matching against a fixed set of known
local hostnames (localhost, 127.0.0.1, 0.0.0.0, ::1).
2. streaming.py: _is_auth detection had a Python operator precedence
bug on the ternary expression. The line:
'AuthenticationError' in type(...).__name__ if _last_err else False
parsed as the ternary absorbing the rest of the or-chain when
_last_err was falsy. Fixed to: (_last_err and 'AuthenticationError' in ...)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: fix v0.50.20 CHANGELOG version number and test count (949 tests)
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:52:35 +00:00
|
|
|
|
async function _fetchLiveModels(provider, sel){
|
|
|
|
|
|
if(!provider||!sel) return;
|
2026-04-23 16:58:15 +00:00
|
|
|
|
// Already fetched — apply cached models to this select element (#872)
|
|
|
|
|
|
if(_liveModelCache[provider]){
|
|
|
|
|
|
const added=_addLiveModelsToSelect(provider,_liveModelCache[provider],sel);
|
|
|
|
|
|
if(added>0 && typeof syncModelChip==='function') syncModelChip();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
fix: silent agent errors, stale model list, live model fetching (#377)
* fix: silent errors, stale models, live model fetching (#373, #374, #375)
- api/streaming.py: detect empty agent response (_assistant_added check),
emit apperror(type='no_response' or 'auth_mismatch') instead of silent done
- api/streaming.py: add _token_sent flag so guard works for streaming agents
- static/messages.js: done handler belt-and-suspenders guard for zero replies
- static/messages.js: apperror handler labels 'no_response' type distinctly
- api/config.py: remove gpt-4o and o3 from _FALLBACK_MODELS and
_PROVIDER_MODELS['openai'] (superseded by gpt-5.4-mini and o4-mini)
- api/routes.py: new /api/models/live?provider= endpoint, fetches /v1/models
from provider API with B310 scheme check + SSRF guard
- static/ui.js: _fetchLiveModels() background fetch after static list loads,
appends new models to dropdown, caches per session, skips unsupported providers
Other:
- tests/test_issues_373_374_375.py: 25 new structural tests
- tests/test_regressions.py: extend done-handler window 1500->2500 chars
- CHANGELOG.md: v0.50.19 entry; 947 tests (up from 922)
* fix: SSRF hostname bypass + auth detection operator precedence
1. routes.py: SSRF guard used substring matching (any(k in hostname))
which allows bypass via hostnames like evil-ollama.attacker.com.
Changed to exact hostname matching against a fixed set of known
local hostnames (localhost, 127.0.0.1, 0.0.0.0, ::1).
2. streaming.py: _is_auth detection had a Python operator precedence
bug on the ternary expression. The line:
'AuthenticationError' in type(...).__name__ if _last_err else False
parsed as the ternary absorbing the rest of the or-chain when
_last_err was falsy. Fixed to: (_last_err and 'AuthenticationError' in ...)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: fix v0.50.20 CHANGELOG version number and test count (949 tests)
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:52:35 +00:00
|
|
|
|
try{
|
2026-04-16 18:20:08 +00:00
|
|
|
|
const url=new URL('api/models/live',location.href);
|
fix: silent agent errors, stale model list, live model fetching (#377)
* fix: silent errors, stale models, live model fetching (#373, #374, #375)
- api/streaming.py: detect empty agent response (_assistant_added check),
emit apperror(type='no_response' or 'auth_mismatch') instead of silent done
- api/streaming.py: add _token_sent flag so guard works for streaming agents
- static/messages.js: done handler belt-and-suspenders guard for zero replies
- static/messages.js: apperror handler labels 'no_response' type distinctly
- api/config.py: remove gpt-4o and o3 from _FALLBACK_MODELS and
_PROVIDER_MODELS['openai'] (superseded by gpt-5.4-mini and o4-mini)
- api/routes.py: new /api/models/live?provider= endpoint, fetches /v1/models
from provider API with B310 scheme check + SSRF guard
- static/ui.js: _fetchLiveModels() background fetch after static list loads,
appends new models to dropdown, caches per session, skips unsupported providers
Other:
- tests/test_issues_373_374_375.py: 25 new structural tests
- tests/test_regressions.py: extend done-handler window 1500->2500 chars
- CHANGELOG.md: v0.50.19 entry; 947 tests (up from 922)
* fix: SSRF hostname bypass + auth detection operator precedence
1. routes.py: SSRF guard used substring matching (any(k in hostname))
which allows bypass via hostnames like evil-ollama.attacker.com.
Changed to exact hostname matching against a fixed set of known
local hostnames (localhost, 127.0.0.1, 0.0.0.0, ::1).
2. streaming.py: _is_auth detection had a Python operator precedence
bug on the ternary expression. The line:
'AuthenticationError' in type(...).__name__ if _last_err else False
parsed as the ternary absorbing the rest of the or-chain when
_last_err was falsy. Fixed to: (_last_err and 'AuthenticationError' in ...)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: fix v0.50.20 CHANGELOG version number and test count (949 tests)
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:52:35 +00:00
|
|
|
|
url.searchParams.set('provider',provider);
|
|
|
|
|
|
const data=await fetch(url.href,{credentials:'include'}).then(r=>r.json());
|
|
|
|
|
|
if(!data.models||!data.models.length) return;
|
|
|
|
|
|
_liveModelCache[provider]=data.models;
|
2026-04-23 16:58:15 +00:00
|
|
|
|
const added=_addLiveModelsToSelect(provider,data.models,sel);
|
fix: silent agent errors, stale model list, live model fetching (#377)
* fix: silent errors, stale models, live model fetching (#373, #374, #375)
- api/streaming.py: detect empty agent response (_assistant_added check),
emit apperror(type='no_response' or 'auth_mismatch') instead of silent done
- api/streaming.py: add _token_sent flag so guard works for streaming agents
- static/messages.js: done handler belt-and-suspenders guard for zero replies
- static/messages.js: apperror handler labels 'no_response' type distinctly
- api/config.py: remove gpt-4o and o3 from _FALLBACK_MODELS and
_PROVIDER_MODELS['openai'] (superseded by gpt-5.4-mini and o4-mini)
- api/routes.py: new /api/models/live?provider= endpoint, fetches /v1/models
from provider API with B310 scheme check + SSRF guard
- static/ui.js: _fetchLiveModels() background fetch after static list loads,
appends new models to dropdown, caches per session, skips unsupported providers
Other:
- tests/test_issues_373_374_375.py: 25 new structural tests
- tests/test_regressions.py: extend done-handler window 1500->2500 chars
- CHANGELOG.md: v0.50.19 entry; 947 tests (up from 922)
* fix: SSRF hostname bypass + auth detection operator precedence
1. routes.py: SSRF guard used substring matching (any(k in hostname))
which allows bypass via hostnames like evil-ollama.attacker.com.
Changed to exact hostname matching against a fixed set of known
local hostnames (localhost, 127.0.0.1, 0.0.0.0, ::1).
2. streaming.py: _is_auth detection had a Python operator precedence
bug on the ternary expression. The line:
'AuthenticationError' in type(...).__name__ if _last_err else False
parsed as the ternary absorbing the rest of the or-chain when
_last_err was falsy. Fixed to: (_last_err and 'AuthenticationError' in ...)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: fix v0.50.20 CHANGELOG version number and test count (949 tests)
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:52:35 +00:00
|
|
|
|
if(added>0){
|
|
|
|
|
|
if(typeof syncModelChip==='function') syncModelChip();
|
|
|
|
|
|
console.log('[hermes] Live models loaded for',provider+':',added,'new models added');
|
|
|
|
|
|
}
|
|
|
|
|
|
}catch(e){
|
|
|
|
|
|
console.debug('[hermes] Live model fetch failed for',provider,e.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
fix: warn on provider/model mismatch, surface auth errors (#266)
* fix: warn on provider/model mismatch, surface auth errors (#266)
Fixes #266 — WebUI silently ignores provider/model selection mismatch.
The problem: selecting an OpenRouter (or Anthropic/OpenAI) model while
Hermes is configured for a different provider (e.g. local Ollama) sends
the request to the wrong endpoint, which returns a 401 Unauthorized error
with no UI indication of why.
Three-layer fix:
1. api/streaming.py — detect 401/auth errors explicitly
Added is_auth_error detection covering '401', 'AuthenticationError',
'authentication', 'unauthorized', 'invalid api key', and the specific
Ollama error string 'no cookie auth credentials'. Auth errors emit
apperror with type='auth_mismatch' and a hint pointing to 'hermes model'.
2. static/ui.js — expose active_provider and warn on selection
- populateModelDropdown() stores data.active_provider from /api/models
as window._activeProvider (the field was already in the response but
the frontend never used it)
- New _checkProviderMismatch(modelId) helper: compares the selected
model's slash-prefix (e.g. 'openai/' from 'openai/gpt-4o') against
the active provider. Skips the check for 'openrouter' and 'custom'
to avoid false positives on configs that legitimately route any model.
3. static/boot.js — warn on model dropdown change
modelSelect.onchange calls _checkProviderMismatch() and shows a toast
when the selected model looks incompatible with the configured provider.
4. static/messages.js — distinct UI label for auth errors
apperror handler now distinguishes type='auth_mismatch' and shows
'Provider mismatch' as the error label instead of 'Error'.
5. static/i18n.js — provider_mismatch_warning and provider_mismatch_label
keys added to all 5 locales (en, es, de, zh-Hans, zh-Hant).
Tests: 21 new tests in tests/test_provider_mismatch.py covering all
five change areas. 679/679 total pass (658 baseline + 21 new).
* fix: t() call args spread + use i18n label for auth mismatch
1. ui.js: _checkProviderMismatch passed [modelId, ap] as a single
array arg to t(). Since t(key, ...args) spreads, the function
received the array as m and undefined as p. Fixed to pass as
separate args: t('provider_mismatch_warning', modelId, ap).
2. messages.js: 'Provider mismatch' label was hardcoded instead of
using t('provider_mismatch_label'). Now uses the i18n key with
fallback for when t() isn't available.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 04:25:18 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* Check if the given model ID belongs to a different provider than the one
|
|
|
|
|
|
* currently configured in Hermes. Returns a warning string if mismatched,
|
|
|
|
|
|
* or null if the selection looks compatible.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Provider detection is intentionally loose — we compare the model's slash
|
|
|
|
|
|
* prefix (e.g. "openai/" from "openai/gpt-4o") against the active provider
|
|
|
|
|
|
* name. Custom/local endpoints report active_provider='custom' or the
|
|
|
|
|
|
* base_url hostname and we skip the check to avoid false positives.
|
|
|
|
|
|
*/
|
|
|
|
|
|
function _checkProviderMismatch(modelId){
|
|
|
|
|
|
const ap=(window._activeProvider||'').toLowerCase();
|
|
|
|
|
|
if(!ap||ap==='custom'||ap==='openrouter') return null; // can't reliably check
|
2026-04-22 20:21:02 +00:00
|
|
|
|
// @provider: prefixed IDs came from that provider's live model list — no mismatch possible
|
|
|
|
|
|
if(modelId.startsWith('@')) return null;
|
fix: warn on provider/model mismatch, surface auth errors (#266)
* fix: warn on provider/model mismatch, surface auth errors (#266)
Fixes #266 — WebUI silently ignores provider/model selection mismatch.
The problem: selecting an OpenRouter (or Anthropic/OpenAI) model while
Hermes is configured for a different provider (e.g. local Ollama) sends
the request to the wrong endpoint, which returns a 401 Unauthorized error
with no UI indication of why.
Three-layer fix:
1. api/streaming.py — detect 401/auth errors explicitly
Added is_auth_error detection covering '401', 'AuthenticationError',
'authentication', 'unauthorized', 'invalid api key', and the specific
Ollama error string 'no cookie auth credentials'. Auth errors emit
apperror with type='auth_mismatch' and a hint pointing to 'hermes model'.
2. static/ui.js — expose active_provider and warn on selection
- populateModelDropdown() stores data.active_provider from /api/models
as window._activeProvider (the field was already in the response but
the frontend never used it)
- New _checkProviderMismatch(modelId) helper: compares the selected
model's slash-prefix (e.g. 'openai/' from 'openai/gpt-4o') against
the active provider. Skips the check for 'openrouter' and 'custom'
to avoid false positives on configs that legitimately route any model.
3. static/boot.js — warn on model dropdown change
modelSelect.onchange calls _checkProviderMismatch() and shows a toast
when the selected model looks incompatible with the configured provider.
4. static/messages.js — distinct UI label for auth errors
apperror handler now distinguishes type='auth_mismatch' and shows
'Provider mismatch' as the error label instead of 'Error'.
5. static/i18n.js — provider_mismatch_warning and provider_mismatch_label
keys added to all 5 locales (en, es, de, zh-Hans, zh-Hant).
Tests: 21 new tests in tests/test_provider_mismatch.py covering all
five change areas. 679/679 total pass (658 baseline + 21 new).
* fix: t() call args spread + use i18n label for auth mismatch
1. ui.js: _checkProviderMismatch passed [modelId, ap] as a single
array arg to t(). Since t(key, ...args) spreads, the function
received the array as m and undefined as p. Fixed to pass as
separate args: t('provider_mismatch_warning', modelId, ap).
2. messages.js: 'Provider mismatch' label was hardcoded instead of
using t('provider_mismatch_label'). Now uses the i18n key with
fallback for when t() isn't available.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 04:25:18 +00:00
|
|
|
|
const slash=modelId.indexOf('/');
|
|
|
|
|
|
if(slash<0) return null; // bare model name, no provider prefix
|
|
|
|
|
|
const modelProvider=modelId.substring(0,slash).toLowerCase();
|
|
|
|
|
|
// Normalise common aliases
|
|
|
|
|
|
const aliases={'claude':'anthropic','gpt':'openai','gemini':'google'};
|
|
|
|
|
|
const norm=p=>aliases[p]||p;
|
|
|
|
|
|
if(norm(modelProvider)!==norm(ap)){
|
|
|
|
|
|
return (window.t?window.t('provider_mismatch_warning',modelId,ap):
|
|
|
|
|
|
`"${modelId}" may not work with your configured provider (${ap}). Send anyway or run \`hermes model\` to switch.`);
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
function _selectedModelOption(){
|
|
|
|
|
|
const sel=$('modelSelect');
|
|
|
|
|
|
if(!sel) return null;
|
|
|
|
|
|
return sel.options[sel.selectedIndex]||null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function syncModelChip(){
|
|
|
|
|
|
const sel=$('modelSelect');
|
|
|
|
|
|
const chip=$('composerModelChip');
|
|
|
|
|
|
const label=$('composerModelLabel');
|
|
|
|
|
|
const dd=$('composerModelDropdown');
|
|
|
|
|
|
if(!sel||!chip||!label) return;
|
2026-04-18 06:37:09 +00:00
|
|
|
|
// Don't show a model label until boot has finished loading to prevent flash of wrong default
|
|
|
|
|
|
if(!S._bootReady){ label.textContent=''; chip.title='Conversation model'; return; }
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
const opt=_selectedModelOption();
|
|
|
|
|
|
label.textContent=opt?opt.textContent:getModelLabel(sel.value||'');
|
|
|
|
|
|
chip.title=sel.value||'Conversation model';
|
|
|
|
|
|
chip.classList.toggle('active',!!(dd&&dd.classList.contains('open')));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function _positionModelDropdown(){
|
|
|
|
|
|
const dd=$('composerModelDropdown');
|
|
|
|
|
|
const chip=$('composerModelChip');
|
|
|
|
|
|
const footer=document.querySelector('.composer-footer');
|
|
|
|
|
|
if(!dd||!chip||!footer) return;
|
|
|
|
|
|
const chipRect=chip.getBoundingClientRect();
|
|
|
|
|
|
const footerRect=footer.getBoundingClientRect();
|
|
|
|
|
|
let left=chipRect.left-footerRect.left;
|
|
|
|
|
|
const maxLeft=Math.max(0, footer.clientWidth-dd.offsetWidth);
|
|
|
|
|
|
left=Math.max(0, Math.min(left, maxLeft));
|
|
|
|
|
|
dd.style.left=`${left}px`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderModelDropdown(){
|
|
|
|
|
|
const dd=$('composerModelDropdown');
|
|
|
|
|
|
const sel=$('modelSelect');
|
|
|
|
|
|
if(!dd||!sel) return;
|
2026-04-18 16:27:36 +00:00
|
|
|
|
// Store model data for filtering
|
|
|
|
|
|
const _modelData=[];
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
for(const child of Array.from(sel.children)){
|
|
|
|
|
|
if(child.tagName==='OPTGROUP'){
|
|
|
|
|
|
for(const opt of Array.from(child.children)){
|
2026-04-18 16:27:36 +00:00
|
|
|
|
_modelData.push({value:opt.value,name:esc(opt.textContent||getModelLabel(opt.value)),id:esc(opt.value),group:child.label||''});
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if(child.tagName==='OPTION'){
|
2026-04-18 16:27:36 +00:00
|
|
|
|
_modelData.push({value:child.value,name:esc(child.textContent||getModelLabel(child.value)),id:esc(child.value),group:''});
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-18 16:27:36 +00:00
|
|
|
|
// Create search input FIRST before filterModels definition
|
|
|
|
|
|
const _searchRow=document.createElement('div');
|
|
|
|
|
|
_searchRow.className='model-search-row';
|
|
|
|
|
|
_searchRow.innerHTML=`<input class="model-search-input" type="text" placeholder="${esc(t('model_search_placeholder')||'Search models…')}" spellcheck="false" autocomplete="off"><button class="model-search-clear" title="Clear search">${li('x',10)}</button>`;
|
|
|
|
|
|
const _si=_searchRow.querySelector('.model-search-input');
|
|
|
|
|
|
const _sc=_searchRow.querySelector('.model-search-clear');
|
|
|
|
|
|
// Create custom model section elements
|
2026-04-14 20:56:56 +00:00
|
|
|
|
const _custSep=document.createElement('div');
|
|
|
|
|
|
_custSep.className='model-group model-custom-sep';
|
|
|
|
|
|
_custSep.textContent=t('model_custom_label')||'Custom model ID';
|
|
|
|
|
|
const _custRow=document.createElement('div');
|
|
|
|
|
|
_custRow.className='model-custom-row';
|
|
|
|
|
|
_custRow.innerHTML=`<input class="model-custom-input" type="text" placeholder="${esc(t('model_custom_placeholder')||'e.g. openai/gpt-5.4')}" spellcheck="false" autocomplete="off"><button class="model-custom-btn" title="Use this model">${li('plus',12)}</button>`;
|
|
|
|
|
|
const _ci=_custRow.querySelector('.model-custom-input');
|
|
|
|
|
|
const _cb=_custRow.querySelector('.model-custom-btn');
|
2026-04-18 16:27:36 +00:00
|
|
|
|
// Filter function (defined AFTER _searchRow and _cust* are created)
|
|
|
|
|
|
const _filterModels=(term)=>{
|
|
|
|
|
|
term=term.trim().toLowerCase();
|
|
|
|
|
|
const found=new Set();
|
|
|
|
|
|
for(const m of _modelData){
|
|
|
|
|
|
const name=m.name.toLowerCase();
|
|
|
|
|
|
const id=m.id.toLowerCase();
|
|
|
|
|
|
if(name.includes(term)||id.includes(term)){
|
|
|
|
|
|
found.add(m.value);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Clear and rebuild
|
|
|
|
|
|
dd.innerHTML='';
|
|
|
|
|
|
// Add search and custom elements first (CRITICAL: must be before models)
|
|
|
|
|
|
dd.appendChild(_searchRow);
|
|
|
|
|
|
dd.appendChild(_custSep);
|
|
|
|
|
|
dd.appendChild(_custRow);
|
|
|
|
|
|
// Add models matching filter
|
|
|
|
|
|
let _lastGroup=null;
|
|
|
|
|
|
for(const m of _modelData){
|
|
|
|
|
|
if(!term||found.has(m.value)){
|
|
|
|
|
|
if(m.group&&m.group!==_lastGroup){
|
|
|
|
|
|
const heading=document.createElement('div');
|
|
|
|
|
|
heading.className='model-group';
|
|
|
|
|
|
heading.textContent=m.group;
|
|
|
|
|
|
dd.appendChild(heading);
|
|
|
|
|
|
_lastGroup=m.group;
|
|
|
|
|
|
}
|
|
|
|
|
|
const row=document.createElement('div');
|
|
|
|
|
|
row.className='model-opt'+(m.value===sel.value?' active':'');
|
|
|
|
|
|
row.innerHTML=`<span class="model-opt-name">${m.name}</span><span class="model-opt-id">${m.id}</span>`;
|
|
|
|
|
|
row.onclick=()=>selectModelFromDropdown(m.value);
|
|
|
|
|
|
dd.appendChild(row);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Show "No results" if filtered and nothing matched
|
|
|
|
|
|
if(term&&found.size===0){
|
|
|
|
|
|
const noResult=document.createElement('div');
|
|
|
|
|
|
noResult.className='model-search-no-results';
|
|
|
|
|
|
noResult.textContent=t('model_search_no_results')||'No models found';
|
|
|
|
|
|
noResult.style.padding='12px 14px';
|
|
|
|
|
|
noResult.style.color='var(--muted)';
|
|
|
|
|
|
noResult.style.textAlign='center';
|
|
|
|
|
|
dd.appendChild(noResult);
|
|
|
|
|
|
}
|
|
|
|
|
|
// Restore focus to search input
|
|
|
|
|
|
_si.focus();
|
|
|
|
|
|
};
|
|
|
|
|
|
// Event handlers for search input
|
|
|
|
|
|
_si.addEventListener('input',()=>_filterModels(_si.value));
|
|
|
|
|
|
_si.addEventListener('keydown',e=>{if(e.key==='Enter') {e.preventDefault();}if(e.key==='Escape') {closeModelDropdown();}});
|
|
|
|
|
|
_si.addEventListener('click',e=>e.stopPropagation());
|
|
|
|
|
|
// Event handlers for clear button
|
|
|
|
|
|
_sc.onclick=()=>{ _si.value=''; _filterModels(''); _si.focus(); };
|
|
|
|
|
|
_sc.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){ _si.value=''; _filterModels(''); _si.focus(); e.preventDefault(); }});
|
|
|
|
|
|
// Event handlers for custom input
|
2026-04-14 20:56:56 +00:00
|
|
|
|
const _applyCustom=()=>{const v=_ci.value.trim();if(!v)return;selectModelFromDropdown(v);_ci.value='';};
|
|
|
|
|
|
_cb.onclick=_applyCustom;
|
|
|
|
|
|
_ci.addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();_applyCustom();}if(e.key==='Escape'){closeModelDropdown();}});
|
|
|
|
|
|
_ci.addEventListener('click',e=>e.stopPropagation());
|
2026-04-18 16:27:36 +00:00
|
|
|
|
// Add search and custom elements to dropdown (initial render)
|
|
|
|
|
|
dd.appendChild(_searchRow);
|
|
|
|
|
|
dd.appendChild(_custSep);
|
2026-04-14 20:56:56 +00:00
|
|
|
|
dd.appendChild(_custRow);
|
2026-04-18 16:27:36 +00:00
|
|
|
|
// Apply initial filter (empty shows all)
|
|
|
|
|
|
_filterModels('');
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function selectModelFromDropdown(value){
|
|
|
|
|
|
const sel=$('modelSelect');
|
|
|
|
|
|
if(!sel||sel.value===value) { closeModelDropdown(); return; }
|
2026-04-14 21:06:23 +00:00
|
|
|
|
// If the value isn't in the option list (custom model ID), add a temporary option
|
|
|
|
|
|
// so sel.value assignment succeeds and the model chip shows the custom ID.
|
|
|
|
|
|
if(!Array.from(sel.options).some(o=>o.value===value)){
|
|
|
|
|
|
const opt=document.createElement('option');
|
|
|
|
|
|
opt.value=value;
|
2026-04-22 20:18:02 +00:00
|
|
|
|
opt.textContent=getModelLabel(value);
|
2026-04-14 21:06:23 +00:00
|
|
|
|
opt.dataset.custom='1';
|
|
|
|
|
|
// Remove any previous custom option before adding new one
|
|
|
|
|
|
sel.querySelectorAll('option[data-custom]').forEach(o=>o.remove());
|
|
|
|
|
|
sel.appendChild(opt);
|
|
|
|
|
|
}
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
sel.value=value;
|
|
|
|
|
|
syncModelChip();
|
|
|
|
|
|
closeModelDropdown();
|
|
|
|
|
|
if(typeof sel.onchange==='function') await sel.onchange();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toggleModelDropdown(){
|
|
|
|
|
|
const dd=$('composerModelDropdown');
|
|
|
|
|
|
const chip=$('composerModelChip');
|
|
|
|
|
|
const sel=$('modelSelect');
|
|
|
|
|
|
if(!dd||!chip||!sel) return;
|
|
|
|
|
|
const open=dd.classList.contains('open');
|
|
|
|
|
|
if(open){closeModelDropdown(); return;}
|
|
|
|
|
|
if(typeof closeProfileDropdown==='function') closeProfileDropdown();
|
|
|
|
|
|
if(typeof closeWsDropdown==='function') closeWsDropdown();
|
2026-04-24 01:24:51 +00:00
|
|
|
|
if(typeof closeReasoningDropdown==='function') closeReasoningDropdown();
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
renderModelDropdown();
|
|
|
|
|
|
dd.classList.add('open');
|
|
|
|
|
|
_positionModelDropdown();
|
|
|
|
|
|
chip.classList.add('active');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeModelDropdown(){
|
|
|
|
|
|
const dd=$('composerModelDropdown');
|
|
|
|
|
|
const chip=$('composerModelChip');
|
|
|
|
|
|
if(dd) dd.classList.remove('open');
|
|
|
|
|
|
if(chip) chip.classList.remove('active');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('click',e=>{
|
|
|
|
|
|
if(!e.target.closest('#composerModelChip') && !e.target.closest('#composerModelDropdown')) closeModelDropdown();
|
|
|
|
|
|
});
|
|
|
|
|
|
window.addEventListener('resize',()=>{
|
|
|
|
|
|
const dd=$('composerModelDropdown');
|
|
|
|
|
|
if(dd&&dd.classList.contains('open')) _positionModelDropdown();
|
2026-04-24 02:18:51 +00:00
|
|
|
|
// Keep the reasoning dropdown aligned under its chip when the window
|
|
|
|
|
|
// resizes while open — same pattern as the model dropdown above.
|
|
|
|
|
|
const rdd=$('composerReasoningDropdown');
|
|
|
|
|
|
if(rdd&&rdd.classList.contains('open')&&typeof _positionReasoningDropdown==='function'){
|
|
|
|
|
|
_positionReasoningDropdown();
|
|
|
|
|
|
}
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-24 01:24:51 +00:00
|
|
|
|
// ── Reasoning effort chip ────────────────────────────────────────────────────
|
|
|
|
|
|
let _currentReasoningEffort=null;
|
|
|
|
|
|
|
|
|
|
|
|
function _applyReasoningChip(eff){
|
|
|
|
|
|
_currentReasoningEffort=eff;
|
|
|
|
|
|
const wrap=$('composerReasoningWrap');
|
|
|
|
|
|
const label=$('composerReasoningLabel');
|
|
|
|
|
|
if(!wrap||!label) return;
|
|
|
|
|
|
if(!eff||eff==='none'){wrap.style.display='none';return;}
|
|
|
|
|
|
wrap.style.display='';
|
2026-04-24 02:18:51 +00:00
|
|
|
|
label.textContent=eff;
|
2026-04-24 01:24:51 +00:00
|
|
|
|
_highlightReasoningOption(eff);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function fetchReasoningChip(){
|
|
|
|
|
|
api('/api/reasoning').then(function(st){
|
|
|
|
|
|
_applyReasoningChip((st&&st.reasoning_effort)||'');
|
|
|
|
|
|
}).catch(function(){_applyReasoningChip('');});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function syncReasoningChip(){
|
|
|
|
|
|
if(_currentReasoningEffort===null){fetchReasoningChip();return;}
|
|
|
|
|
|
_applyReasoningChip(_currentReasoningEffort);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function _highlightReasoningOption(effort){
|
|
|
|
|
|
const dd=$('composerReasoningDropdown');
|
|
|
|
|
|
if(!dd) return;
|
|
|
|
|
|
dd.querySelectorAll('.reasoning-option').forEach(function(opt){
|
|
|
|
|
|
opt.classList.toggle('selected',opt.dataset.effort===effort);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toggleReasoningDropdown(){
|
|
|
|
|
|
const dd=$('composerReasoningDropdown');
|
|
|
|
|
|
const chip=$('composerReasoningChip');
|
|
|
|
|
|
if(!dd||!chip) return;
|
|
|
|
|
|
const open=dd.classList.contains('open');
|
|
|
|
|
|
if(open){closeReasoningDropdown();return;}
|
|
|
|
|
|
if(typeof closeProfileDropdown==='function') closeProfileDropdown();
|
|
|
|
|
|
if(typeof closeWsDropdown==='function') closeWsDropdown();
|
|
|
|
|
|
closeModelDropdown();
|
|
|
|
|
|
_highlightReasoningOption(_currentReasoningEffort);
|
|
|
|
|
|
dd.classList.add('open');
|
2026-04-24 02:18:51 +00:00
|
|
|
|
_positionReasoningDropdown();
|
2026-04-24 01:24:51 +00:00
|
|
|
|
chip.classList.add('active');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-24 02:18:51 +00:00
|
|
|
|
function _positionReasoningDropdown(){
|
|
|
|
|
|
const dd=$('composerReasoningDropdown');
|
|
|
|
|
|
const chip=$('composerReasoningChip');
|
|
|
|
|
|
const footer=document.querySelector('.composer-footer');
|
|
|
|
|
|
if(!dd||!chip||!footer) return;
|
|
|
|
|
|
const chipRect=chip.getBoundingClientRect();
|
|
|
|
|
|
const footerRect=footer.getBoundingClientRect();
|
|
|
|
|
|
let left=chipRect.left-footerRect.left;
|
|
|
|
|
|
const maxLeft=Math.max(0,footer.clientWidth-dd.offsetWidth);
|
|
|
|
|
|
left=Math.max(0,Math.min(left,maxLeft));
|
|
|
|
|
|
dd.style.left=`${left}px`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-24 01:24:51 +00:00
|
|
|
|
function closeReasoningDropdown(){
|
|
|
|
|
|
const dd=$('composerReasoningDropdown');
|
|
|
|
|
|
const chip=$('composerReasoningChip');
|
|
|
|
|
|
if(dd) dd.classList.remove('open');
|
|
|
|
|
|
if(chip) chip.classList.remove('active');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('click',function(e){
|
|
|
|
|
|
if(!e.target.closest('#composerReasoningChip')&&!e.target.closest('#composerReasoningDropdown')) closeReasoningDropdown();
|
|
|
|
|
|
if(e.target.closest('.reasoning-option')){
|
|
|
|
|
|
const opt=e.target.closest('.reasoning-option');
|
|
|
|
|
|
const effort=opt&&opt.dataset.effort;
|
|
|
|
|
|
if(effort){
|
|
|
|
|
|
api('/api/reasoning',{method:'POST',body:JSON.stringify({effort:effort})})
|
|
|
|
|
|
.then(function(st){
|
|
|
|
|
|
_applyReasoningChip((st&&st.reasoning_effort)||effort);
|
|
|
|
|
|
showToast('🧠 Reasoning effort set to '+((st&&st.reasoning_effort)||effort));
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(function(){showToast('🧠 Failed to set effort');});
|
|
|
|
|
|
closeReasoningDropdown();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
// ── Scroll pinning ──────────────────────────────────────────────────────────
|
|
|
|
|
|
// When streaming, auto-scroll only if the user hasn't manually scrolled up.
|
2026-04-18 17:09:59 +00:00
|
|
|
|
// Once the user scrolls back to within 150px of the bottom, re-pin.
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
let _scrollPinned=true;
|
|
|
|
|
|
(function(){
|
|
|
|
|
|
const el=document.getElementById('messages');
|
|
|
|
|
|
if(!el) return;
|
|
|
|
|
|
el.addEventListener('scroll',()=>{
|
2026-04-18 17:09:59 +00:00
|
|
|
|
const nearBottom=el.scrollHeight-el.scrollTop-el.clientHeight<150;
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
_scrollPinned=nearBottom;
|
2026-04-18 17:09:59 +00:00
|
|
|
|
const btn=$('scrollToBottomBtn');
|
|
|
|
|
|
if(btn) btn.style.display=_scrollPinned?'none':'flex';
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
});
|
|
|
|
|
|
})();
|
2026-04-04 01:54:53 +00:00
|
|
|
|
function _fmtTokens(n){if(!n||n<0)return'0';if(n>=1e6)return(n/1e6).toFixed(1)+'M';if(n>=1e3)return(n/1e3).toFixed(1)+'k';return String(n);}
|
feat: Sprint 23 — agentic transparency + polish
Track A: Token/cost display
- Read agent usage attrs (session_prompt_tokens, session_completion_tokens,
session_estimated_cost_usd) after run_conversation in streaming.py
- Add input_tokens, output_tokens, estimated_cost fields to Session model
- Include usage in done SSE event payload
- Store usage on S.lastUsage in messages.js done handler
- Render usage badge below last assistant message (input/output/cost)
Track B: Subagent delegation cards
- Add subagent_progress to toolIcon map with shuffle emoji
- Special-case subagent_progress in buildToolCard: "Subagent" label,
strip double emoji from preview, add tool-card-subagent CSS class
- Indented border-left styling for subagent cards
- Clean delegate_task display name
Track C: Skill picker in cron create form
- Add skill search input + tag chips to cron create form HTML
- Skill picker JS in panels.js: search/filter, click-to-add tags,
remove tag chips, pre-fetch skill list on form open
- submitCronCreate sends skills array in POST body
- Skill picker dropdown + tag CSS
Track D: Skill linked files viewer
- Add file query param to /api/skills/content endpoint
- Serve linked files from skill directory with path traversal protection
- Ensure linked_files key always present in skill content response
- Render linked files section below SKILL.md content in preview panel
- openSkillFile function for viewing individual linked files
Track E: Bug fixes and code quality
- Expand Session.__init__ and compact() to readable multi-line format
- Remove inline import json as _j2 inside loop in streaming.py
- Fix tool_calls: capture args from assistant messages, skip unresolved names
- Store args snapshot in persisted tool_calls for reload display
6 new tests. Total: 421 (409 passing).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:33:49 +00:00
|
|
|
|
|
2026-04-04 21:11:28 +00:00
|
|
|
|
// Context usage indicator in composer footer
|
|
|
|
|
|
function _syncCtxIndicator(usage){
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
const wrap=$('ctxIndicatorWrap');
|
2026-04-04 21:11:28 +00:00
|
|
|
|
const el=$('ctxIndicator');
|
|
|
|
|
|
if(!el)return;
|
2026-04-05 01:50:17 +00:00
|
|
|
|
const promptTok=usage.last_prompt_tokens||usage.input_tokens||0;
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
const totalTok=(usage.input_tokens||0)+(usage.output_tokens||0);
|
2026-04-05 01:50:17 +00:00
|
|
|
|
const ctxWindow=usage.context_length||0;
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
const cost=usage.estimated_cost;
|
|
|
|
|
|
// Show indicator whenever we have any usage data (tokens or cost)
|
|
|
|
|
|
if(!promptTok&&!totalTok&&!cost){
|
|
|
|
|
|
if(wrap) wrap.style.display='none';
|
|
|
|
|
|
return;
|
2026-04-04 21:11:28 +00:00
|
|
|
|
}
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
if(wrap) wrap.style.display='';
|
|
|
|
|
|
const hasCtxWindow=!!(promptTok&&ctxWindow);
|
|
|
|
|
|
const pct=hasCtxWindow?Math.min(100,Math.round((promptTok/ctxWindow)*100)):0;
|
|
|
|
|
|
const ring=$('ctxRingValue');
|
|
|
|
|
|
const center=$('ctxPercent');
|
|
|
|
|
|
const usageLine=$('ctxTooltipUsage');
|
|
|
|
|
|
const tokensLine=$('ctxTooltipTokens');
|
|
|
|
|
|
const thresholdLine=$('ctxTooltipThreshold');
|
|
|
|
|
|
const costLine=$('ctxTooltipCost');
|
|
|
|
|
|
if(ring){
|
|
|
|
|
|
const circumference=61.261056745;
|
|
|
|
|
|
ring.style.strokeDasharray=String(circumference);
|
|
|
|
|
|
ring.style.strokeDashoffset=String(circumference*(1-pct/100));
|
2026-04-04 21:11:28 +00:00
|
|
|
|
}
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
if(center) center.textContent=hasCtxWindow?String(pct):'\u00b7';
|
|
|
|
|
|
el.classList.toggle('ctx-mid',pct>50&&pct<=75);
|
|
|
|
|
|
el.classList.toggle('ctx-high',pct>75);
|
|
|
|
|
|
let label=hasCtxWindow?`Context window ${pct}% used`:`${_fmtTokens(totalTok)} tokens used`;
|
|
|
|
|
|
if(cost) label+=` \u00b7 $${cost<0.01?cost.toFixed(4):cost.toFixed(2)}`;
|
|
|
|
|
|
el.setAttribute('aria-label',label);
|
|
|
|
|
|
if(usageLine) usageLine.textContent=hasCtxWindow?`${pct}% used (${Math.max(0,100-pct)}% left)`:`${_fmtTokens(totalTok)} tokens used`;
|
|
|
|
|
|
if(tokensLine) tokensLine.textContent=hasCtxWindow?`${_fmtTokens(promptTok)} / ${_fmtTokens(ctxWindow)} tokens used`:`In: ${_fmtTokens(usage.input_tokens||0)} \u00b7 Out: ${_fmtTokens(usage.output_tokens||0)}`;
|
2026-04-05 01:50:17 +00:00
|
|
|
|
const threshold=usage.threshold_tokens||0;
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
if(thresholdLine){
|
|
|
|
|
|
if(threshold&&ctxWindow){
|
|
|
|
|
|
thresholdLine.style.display='';
|
|
|
|
|
|
thresholdLine.textContent=`Auto-compress at ${_fmtTokens(threshold)} (${Math.round(threshold/ctxWindow*100)}%)`;
|
|
|
|
|
|
}else{
|
|
|
|
|
|
thresholdLine.style.display='none';
|
|
|
|
|
|
thresholdLine.textContent='';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if(costLine){
|
|
|
|
|
|
if(cost){
|
|
|
|
|
|
costLine.style.display='';
|
|
|
|
|
|
costLine.textContent=`Estimated cost: $${cost<0.01?cost.toFixed(4):cost.toFixed(2)}`;
|
|
|
|
|
|
}else{
|
|
|
|
|
|
costLine.style.display='none';
|
|
|
|
|
|
costLine.textContent='';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-04 21:11:28 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
function scrollIfPinned(){
|
|
|
|
|
|
if(!_scrollPinned) return;
|
|
|
|
|
|
const el=$('messages');
|
|
|
|
|
|
if(el) el.scrollTop=el.scrollHeight;
|
|
|
|
|
|
}
|
|
|
|
|
|
function scrollToBottom(){
|
|
|
|
|
|
_scrollPinned=true;
|
|
|
|
|
|
const el=$('messages');
|
|
|
|
|
|
if(el) el.scrollTop=el.scrollHeight;
|
2026-04-18 17:09:59 +00:00
|
|
|
|
const btn=$('scrollToBottomBtn');
|
|
|
|
|
|
if(btn) btn.style.display='none';
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-22 20:18:02 +00:00
|
|
|
|
function _fmtOllamaLabel(mid){
|
|
|
|
|
|
const [namePart, ...variantParts] = mid.split(':');
|
|
|
|
|
|
const variant = variantParts.join(':');
|
|
|
|
|
|
const _fmt = (s) => {
|
|
|
|
|
|
const tokens = s.replace(/[-_]/g, ' ').split(' ');
|
|
|
|
|
|
return tokens.map(t => {
|
|
|
|
|
|
const alphaOnly = t.replace(/\./g, '');
|
|
|
|
|
|
if (t.length <= 3 && /^[a-zA-Z.]+$/.test(t)) return t.toUpperCase();
|
|
|
|
|
|
if (/^\d/.test(alphaOnly)) return t.toUpperCase();
|
|
|
|
|
|
return t.charAt(0).toUpperCase() + t.slice(1);
|
|
|
|
|
|
}).join(' ');
|
|
|
|
|
|
};
|
|
|
|
|
|
let label = _fmt(namePart);
|
|
|
|
|
|
if (variant) label += ' (' + _fmt(variant) + ')';
|
|
|
|
|
|
return label;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
function getModelLabel(modelId){
|
|
|
|
|
|
if(!modelId) return 'Unknown';
|
|
|
|
|
|
// Check dynamic labels first, then fall back to splitting the ID
|
|
|
|
|
|
if(_dynamicModelLabels[modelId]) return _dynamicModelLabels[modelId];
|
|
|
|
|
|
// Static fallback for common models
|
2026-04-18 17:09:59 +00:00
|
|
|
|
const STATIC_LABELS={'openai/gpt-5.4-mini':'GPT-5.4 Mini','openai/gpt-4o':'GPT-4o','openai/o3':'o3','openai/o4-mini':'o4-mini','anthropic/claude-sonnet-4.6':'Sonnet 4.6','anthropic/claude-sonnet-4-5':'Sonnet 4.5','anthropic/claude-haiku-3-5':'Haiku 3.5','google/gemini-3.1-pro-preview':'Gemini 3.1 Pro','google/gemini-3-flash-preview':'Gemini 3 Flash','google/gemini-3.1-flash-lite-preview':'Gemini 3.1 Flash Lite','google/gemini-2.5-pro':'Gemini 2.5 Pro','google/gemini-2.5-flash':'Gemini 2.5 Flash','deepseek/deepseek-chat-v3-0324':'DeepSeek V3','meta-llama/llama-4-scout':'Llama 4 Scout'};
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
if(STATIC_LABELS[modelId]) return STATIC_LABELS[modelId];
|
2026-04-22 20:18:02 +00:00
|
|
|
|
// Safe Ollama-tag fallback formatter before generic split('/').pop()
|
|
|
|
|
|
let _last = modelId.split('/').pop() || modelId;
|
|
|
|
|
|
// Strip @provider: prefix if present (e.g. @ollama-cloud:kimi-k2.6)
|
|
|
|
|
|
if (_last.startsWith('@') && _last.includes(':')) _last = _last.split(':').slice(1).join(':');
|
|
|
|
|
|
const looksLikeOllamaTag = /^[a-z0-9][\w.-]*:[\w.-]+$/i.test(_last);
|
|
|
|
|
|
// Narrow: only apply Ollama formatter to IDs with explicit @ollama prefix or colon-tag format.
|
|
|
|
|
|
// Avoids reformatting bare provider model IDs like claude-sonnet-4-6 or gpt-4o.
|
|
|
|
|
|
const looksLikeBareOllamaId = modelId.startsWith('@ollama') || looksLikeOllamaTag;
|
|
|
|
|
|
const ollamaLabel = _fmtOllamaLabel(_last);
|
|
|
|
|
|
if ((modelId.startsWith('ollama/') || modelId.startsWith('@ollama') || looksLikeOllamaTag || looksLikeBareOllamaId) && ollamaLabel !== _last) {
|
|
|
|
|
|
return ollamaLabel;
|
|
|
|
|
|
}
|
|
|
|
|
|
return _last || 'Unknown';
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-19 05:40:37 +00:00
|
|
|
|
function _stripXmlToolCallsDisplay(s){
|
|
|
|
|
|
// Strip <function_calls>...</function_calls> blocks emitted by DeepSeek and
|
|
|
|
|
|
// similar models in their raw response text. These are processed separately
|
|
|
|
|
|
// as tool calls; leaving them in the content causes them to render visibly
|
|
|
|
|
|
// in the settled chat bubble. (#702)
|
2026-04-24 18:04:16 +00:00
|
|
|
|
// Also handles DSML-prefixed variants from DeepSeek/Bedrock, including
|
|
|
|
|
|
// spacing variants like "<|DSML |function_calls" and truncated prefixes.
|
|
|
|
|
|
if(!s) return s;
|
|
|
|
|
|
const lo=String(s).toLowerCase();
|
|
|
|
|
|
if(lo.indexOf('function_calls')===-1 && lo.indexOf('dsml')===-1) return s;
|
|
|
|
|
|
// Support both plain <function_calls> and DSML-prefixed variants.
|
|
|
|
|
|
s=s.replace(/<(?:\s*|\s*DSML\s*[||]\s*)?function_calls>[\s\S]*?<\/(?:\s*|\s*DSML\s*[||]\s*)?function_calls>/gi,'');
|
|
|
|
|
|
// Also remove truncated opening tags (missing closing ">" at stream tail).
|
|
|
|
|
|
s=s.replace(/<(?:\s*|\s*DSML\s*[||]\s*)?function_calls(?:>|$)[\s\S]*$/i,'');
|
|
|
|
|
|
// Remove malformed DSML tag fragments like "<|DSML |" that can leak in tokens.
|
|
|
|
|
|
s=s.replace(/<\s*|\s*DSML\s*[||]\s*/gi,'');
|
2026-04-19 05:40:37 +00:00
|
|
|
|
return s.trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-24 18:04:16 +00:00
|
|
|
|
function _sanitizeThinkingDisplayText(text){
|
|
|
|
|
|
const stripped=_stripXmlToolCallsDisplay(String(text||''));
|
|
|
|
|
|
return stripped.trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 03:40:19 +00:00
|
|
|
|
function renderMd(raw){
|
|
|
|
|
|
let s=raw||'';
|
2026-04-14 19:35:52 +00:00
|
|
|
|
// ── MEDIA: token stash (must run first, before any other processing) ───────
|
|
|
|
|
|
// Detect MEDIA:<path-or-url> tokens emitted by the agent (e.g. screenshots,
|
|
|
|
|
|
// generated images) and replace them with inline <img> or download links.
|
|
|
|
|
|
// Stashed so the path/URL is never processed as markdown.
|
|
|
|
|
|
const _IMAGE_EXTS=/\.(png|jpg|jpeg|gif|webp|bmp|ico)$/i;
|
|
|
|
|
|
const media_stash=[];
|
|
|
|
|
|
s=s.replace(/MEDIA:([^\s\)\]]+)/g,(_,raw_ref)=>{
|
|
|
|
|
|
media_stash.push(raw_ref);
|
|
|
|
|
|
return '\x00D'+(media_stash.length-1)+'\x00';
|
|
|
|
|
|
});
|
|
|
|
|
|
// ── End MEDIA stash ─────────────────────────────────────────────────────────
|
v0.46.0: security, Docker UID/GID, model discovery, i18n, cancel fix
* fix: decode HTML entities before markdown processing + zh/zh-Hant translations (#239)
Adds decode() helper in renderMd() to fix double-escaping of HTML entities
from LLM output (e.g. <code> becoming &lt;code&gt; instead
of rendering). XSS-safe: decode runs before esc(), only 5 entity patterns.
Also adds 40+ missing zh (Simplified Chinese) translation keys and a new
zh-Hant (Traditional Chinese) locale with 163 keys.
Fix applied: removed duplicate settings_label_notifications key in both
zh and zh-Hant locales.
Fixes #240
* fix: restore custom model list discovery with config api key (#238)
get_available_models() now reads api_key from config.yaml before env vars:
1. model.api_key
2. providers.<active>.api_key / providers.custom.api_key
3. env var fallbacks (HERMES_API_KEY, OPENAI_API_KEY, etc.)
Also adds OpenAI/Python User-Agent header and a regression test covering
authenticated /v1/models discovery.
Fixes users with LM Studio / Ollama custom endpoints configured in
config.yaml whose model picker silently collapsed to the default model.
* feat: Docker UID/GID matching to avoid root-owned .hermes files (#237)
Adds docker_init.bash with hermeswebuitoo/hermeswebui user pattern so
container files match the host user UID/GID. Prevents .hermes volume
mounts from being owned by root when using a non-root host user.
Configure via WANTED_UID and WANTED_GID env vars (default 1000/1000).
Readme updated with setup instructions.
Fix applied: removed duplicate WANTED_GID=1000 line in docker-compose.yml
that was overriding the ${GID:-1000} variable expansion.
* security: redact credentials from API responses and fix credential file permissions (#243)
Adds response-layer credential redaction to three endpoints:
- GET /api/session — messages[], tool_calls[], and title
- GET /api/session/export — download also redacted
- SSE done event — session payload in stream
- GET /api/memory — MEMORY.md and USER.md content
Adds api/startup.py with fix_credential_permissions() at server startup.
Adds 13 tests in tests/test_security_redaction.py.
Merged with #237 container detection changes in server.py.
* fix: cancel button now interrupts agent and cleans up UI state (#244)
Wires agent.interrupt() into cancel_stream() so the backend actually
stops tool execution when the user clicks Cancel, rather than only
stopping the SSE stream while the agent keeps running.
Changes:
- api/config.py: adds AGENT_INSTANCES dict (stream_id -> AIAgent)
- api/streaming.py: stores agent in AGENT_INSTANCES after creation,
checks CANCEL_FLAGS immediately after store (race condition fix),
calls agent.interrupt() in cancel_stream(), cleans up in finally block
- static/boot.js: removes stale setStatus(cancelling) call
- static/messages.js: setBusy(false)/setStatus('') unconditionally on cancel
Race condition fix: after storing agent in AGENT_INSTANCES, immediately
checks if CANCEL_FLAGS[stream_id] is already set (cancel arrived during
agent init) and interrupts before starting. Check is inside the same
STREAMS_LOCK acquisition, making it atomic.
New test file: tests/test_cancel_interrupt.py with 6 unit tests.
* docs: v0.46.0 release notes, bump version, update test counts
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 17:17:52 +00:00
|
|
|
|
// Pre-pass: decode HTML entities first so markdown processing works correctly.
|
|
|
|
|
|
// This prevents double-escaping when LLM outputs entities like < > &
|
|
|
|
|
|
const decode=s=>s.replace(/</g,'<').replace(/>/g,'>').replace(/&/g,'&').replace(/"/g,'"').replace(/'/g,"'");
|
|
|
|
|
|
s=decode(s);
|
feat: safe HTML rendering in AI responses + active session gold style + Sprint 16 tests
renderMd() now correctly renders safe inline HTML tags that AI models
emit in their responses:
Pre-pass (ui.js):
Converts <strong>, <b>, <em>, <i>, <code>, <br> to their markdown
equivalents (**text**, *text*, `text`, newline) before the pipeline
runs. Code blocks and backtick spans are stashed first so their content
is never modified.
inlineMd() helper (ui.js):
New helper for processing inline formatting inside list items,
blockquotes, and headings. Previously these used esc() directly, which
escaped <strong>/<code> tags that had already been converted from HTML
by the pre-pass — causing them to appear as literal <strong> text
instead of rendering as bold. inlineMd() applies bold/italic/code
processing and then escapes only unknown tags.
Safety net (ui.js):
After the full pipeline, any HTML tags NOT emitted by our own renderer
(i.e. <img>, <script>, <iframe>, <svg>, <object>, etc.) are escaped
via esc(). The SAFE_TAGS allowlist covers every tag the pipeline itself
produces. XSS is fully blocked.
Active session gold style (sessions.js, style.css):
Active session item now uses gold/amber (#e8a030) instead of blue,
matching the logo gradient color for better visual hierarchy.
Project color border-left is skipped when the session is active
(gold always wins). Session items get border-radius: 0 8px 8px 0
to complement the left border indicator.
Tests (tests/test_sprint16.py — 74 tests):
- Static analysis: pre-pass, SAFE_TAGS, SAFE_INLINE, inlineMd present
- Behavioural: all safe tags render in paragraphs, list items (ul+ol),
blockquotes, headings (h1/h2/h3)
- Exact screenshot regression: the 4-item list with <strong> labels
and <code> values that was showing as literal text
- XSS: 7 attack vectors blocked (<img>, <script>, <iframe>, <svg>,
<object>, XSS inside bold, XSS nested inside <strong>)
- Edge cases: code block protection, double-escaping guards, br tag,
mixed markdown+HTML, inlineMd called in list/blockquote handlers
Tests: 312 passed, 0 failed.
2026-04-03 00:27:43 +00:00
|
|
|
|
// Pre-pass: convert safe inline HTML tags the model may emit into their
|
|
|
|
|
|
// markdown equivalents so the pipeline can render them correctly.
|
|
|
|
|
|
// Only runs OUTSIDE fenced code blocks and backtick spans (stash + restore).
|
|
|
|
|
|
// Unsafe tags (anything not in the allowlist) are left as-is and will be
|
|
|
|
|
|
// HTML-escaped by esc() when they reach an innerHTML assignment -- no XSS risk.
|
feat: KaTeX math rendering for LaTeX in chat + workspace previews (#352)
* feat: KaTeX math rendering for $..$ and $$..$$ in chat and previews (fixes #347)
- Stash math delimiters before markdown pipeline, restore as .katex-block/.katex-inline elements
- KaTeX JS lazy-loaded from CDN on first math block (mirrors mermaid pattern)
- KaTeX CSS loaded eagerly in <head> to prevent layout shift
- SRI hashes on both CDN tags
- throwOnError:false — bad LaTeX degrades to code span
- Supports $$, $, \\(...\\), \\[...\\] delimiters
- 18 new tests, 831/831 passing
* fix: remove invalid \' escape sequences in math stash lines
Lines 311, 314, 316, 317 had \' (backslash-quote) instead of plain '
in the arrow function bodies. This is a JS syntax error — node --check
fails with 'Invalid or unexpected token'. Likely caused by a
serialization artifact during code generation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: swap stash order (fence before math) to protect code spans; add renderKatexBlocks to workspace preview
- static/ui.js: fence_stash now runs BEFORE math_stash so dollar signs
inside backtick code spans are not extracted as math. Previously
`$x$` would render as KaTeX inside a <code> tag instead of
showing the literal string $x$.
- static/workspace.js: add requestAnimationFrame(renderKatexBlocks)
after markdown preview renders so math works in workspace file
previews, not only in chat messages.
* feat: KaTeX math rendering + stash order fix + workspace wiring (#352)
- tests/test_issue347.py: 11 new tests (29 total) covering fence-before-math
ordering, workspace.js renderKatexBlocks call, stash token distinctness,
false-positive prevention, safe-tags boundary check
- CHANGELOG.md: v0.50.15 entry; 870 tests total (up from 841)
* fix: use literal null byte (\x00M) in math stash token — matches restore regex
The original PR's second commit (fix: remove invalid \' escapes) accidentally
doubled the backslash in the math stash tokens: '\\x00M' is a 5-char string
(backslash + x + 0 + 0 + M) but the restore regex /\x00M/ expects a null byte.
Result: $...$ in messages produced visible \x00M0\x00 tokens instead of
KaTeX spans.
Changed all 4 math stash return statements to use '\x00M' (single backslash =
null byte, same convention as fence_stash's '\x00F').
Also updates test_stash_tokens_distinct to check for the correct pattern.
* fix: add null-byte token test; update CHANGELOG to v0.50.15 with fixes
- tests/test_issue347.py: add test_math_stash_token_uses_single_backslash_null_byte
to catch the \\x00M double-backslash regression; 30 tests total (up from 29)
- CHANGELOG.md: v0.50.15 entry documents all fixes including the token bug
and workspace preview wiring; 871 tests total
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:40:15 +00:00
|
|
|
|
// Fence stash: protect code blocks and backtick spans from all further processing
|
|
|
|
|
|
// Must run BEFORE math_stash so $..$ inside code spans is not extracted as math
|
feat: safe HTML rendering in AI responses + active session gold style + Sprint 16 tests
renderMd() now correctly renders safe inline HTML tags that AI models
emit in their responses:
Pre-pass (ui.js):
Converts <strong>, <b>, <em>, <i>, <code>, <br> to their markdown
equivalents (**text**, *text*, `text`, newline) before the pipeline
runs. Code blocks and backtick spans are stashed first so their content
is never modified.
inlineMd() helper (ui.js):
New helper for processing inline formatting inside list items,
blockquotes, and headings. Previously these used esc() directly, which
escaped <strong>/<code> tags that had already been converted from HTML
by the pre-pass — causing them to appear as literal <strong> text
instead of rendering as bold. inlineMd() applies bold/italic/code
processing and then escapes only unknown tags.
Safety net (ui.js):
After the full pipeline, any HTML tags NOT emitted by our own renderer
(i.e. <img>, <script>, <iframe>, <svg>, <object>, etc.) are escaped
via esc(). The SAFE_TAGS allowlist covers every tag the pipeline itself
produces. XSS is fully blocked.
Active session gold style (sessions.js, style.css):
Active session item now uses gold/amber (#e8a030) instead of blue,
matching the logo gradient color for better visual hierarchy.
Project color border-left is skipped when the session is active
(gold always wins). Session items get border-radius: 0 8px 8px 0
to complement the left border indicator.
Tests (tests/test_sprint16.py — 74 tests):
- Static analysis: pre-pass, SAFE_TAGS, SAFE_INLINE, inlineMd present
- Behavioural: all safe tags render in paragraphs, list items (ul+ol),
blockquotes, headings (h1/h2/h3)
- Exact screenshot regression: the 4-item list with <strong> labels
and <code> values that was showing as literal text
- XSS: 7 attack vectors blocked (<img>, <script>, <iframe>, <svg>,
<object>, XSS inside bold, XSS nested inside <strong>)
- Edge cases: code block protection, double-escaping guards, br tag,
mixed markdown+HTML, inlineMd called in list/blockquote handlers
Tests: 312 passed, 0 failed.
2026-04-03 00:27:43 +00:00
|
|
|
|
const fence_stash=[];
|
|
|
|
|
|
s=s.replace(/(```[\s\S]*?```|`[^`\n]+`)/g,m=>{fence_stash.push(m);return '\x00F'+(fence_stash.length-1)+'\x00';});
|
feat: KaTeX math rendering for LaTeX in chat + workspace previews (#352)
* feat: KaTeX math rendering for $..$ and $$..$$ in chat and previews (fixes #347)
- Stash math delimiters before markdown pipeline, restore as .katex-block/.katex-inline elements
- KaTeX JS lazy-loaded from CDN on first math block (mirrors mermaid pattern)
- KaTeX CSS loaded eagerly in <head> to prevent layout shift
- SRI hashes on both CDN tags
- throwOnError:false — bad LaTeX degrades to code span
- Supports $$, $, \\(...\\), \\[...\\] delimiters
- 18 new tests, 831/831 passing
* fix: remove invalid \' escape sequences in math stash lines
Lines 311, 314, 316, 317 had \' (backslash-quote) instead of plain '
in the arrow function bodies. This is a JS syntax error — node --check
fails with 'Invalid or unexpected token'. Likely caused by a
serialization artifact during code generation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: swap stash order (fence before math) to protect code spans; add renderKatexBlocks to workspace preview
- static/ui.js: fence_stash now runs BEFORE math_stash so dollar signs
inside backtick code spans are not extracted as math. Previously
`$x$` would render as KaTeX inside a <code> tag instead of
showing the literal string $x$.
- static/workspace.js: add requestAnimationFrame(renderKatexBlocks)
after markdown preview renders so math works in workspace file
previews, not only in chat messages.
* feat: KaTeX math rendering + stash order fix + workspace wiring (#352)
- tests/test_issue347.py: 11 new tests (29 total) covering fence-before-math
ordering, workspace.js renderKatexBlocks call, stash token distinctness,
false-positive prevention, safe-tags boundary check
- CHANGELOG.md: v0.50.15 entry; 870 tests total (up from 841)
* fix: use literal null byte (\x00M) in math stash token — matches restore regex
The original PR's second commit (fix: remove invalid \' escapes) accidentally
doubled the backslash in the math stash tokens: '\\x00M' is a 5-char string
(backslash + x + 0 + 0 + M) but the restore regex /\x00M/ expects a null byte.
Result: $...$ in messages produced visible \x00M0\x00 tokens instead of
KaTeX spans.
Changed all 4 math stash return statements to use '\x00M' (single backslash =
null byte, same convention as fence_stash's '\x00F').
Also updates test_stash_tokens_distinct to check for the correct pattern.
* fix: add null-byte token test; update CHANGELOG to v0.50.15 with fixes
- tests/test_issue347.py: add test_math_stash_token_uses_single_backslash_null_byte
to catch the \\x00M double-backslash regression; 30 tests total (up from 29)
- CHANGELOG.md: v0.50.15 entry documents all fixes including the token bug
and workspace preview wiring; 871 tests total
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:40:15 +00:00
|
|
|
|
// Math stash: protect $$..$$ and $..$ from markdown processing
|
|
|
|
|
|
// Runs AFTER fence_stash so backtick code spans protect their dollar-sign contents
|
|
|
|
|
|
const math_stash=[];
|
|
|
|
|
|
// Display math: $$...$$ (must come before inline to avoid mis-parsing)
|
|
|
|
|
|
s=s.replace(/\$\$([\s\S]+?)\$\$/g,(_,m)=>{math_stash.push({type:'display',src:m});return '\x00M'+(math_stash.length-1)+'\x00';});
|
|
|
|
|
|
// Inline math: $...$ — require non-space at boundaries to avoid false positives
|
|
|
|
|
|
// e.g. "costs $5 and $10" should not trigger (space after opening $)
|
|
|
|
|
|
s=s.replace(/\$([^\s$\n][^$\n]*?[^\s$\n]|\S)\$/g,(_,m)=>{math_stash.push({type:'inline',src:m});return '\x00M'+(math_stash.length-1)+'\x00';});
|
|
|
|
|
|
// Also stash \(...\) and \[...\] LaTeX delimiters
|
|
|
|
|
|
s=s.replace(/\\\\\((.+?)\\\\\)/g,(_,m)=>{math_stash.push({type:'inline',src:m});return '\x00M'+(math_stash.length-1)+'\x00';});
|
|
|
|
|
|
s=s.replace(/\\\\\[(.+?)\\\\\]/gs,(_,m)=>{math_stash.push({type:'display',src:m});return '\x00M'+(math_stash.length-1)+'\x00';});
|
feat: safe HTML rendering in AI responses + active session gold style + Sprint 16 tests
renderMd() now correctly renders safe inline HTML tags that AI models
emit in their responses:
Pre-pass (ui.js):
Converts <strong>, <b>, <em>, <i>, <code>, <br> to their markdown
equivalents (**text**, *text*, `text`, newline) before the pipeline
runs. Code blocks and backtick spans are stashed first so their content
is never modified.
inlineMd() helper (ui.js):
New helper for processing inline formatting inside list items,
blockquotes, and headings. Previously these used esc() directly, which
escaped <strong>/<code> tags that had already been converted from HTML
by the pre-pass — causing them to appear as literal <strong> text
instead of rendering as bold. inlineMd() applies bold/italic/code
processing and then escapes only unknown tags.
Safety net (ui.js):
After the full pipeline, any HTML tags NOT emitted by our own renderer
(i.e. <img>, <script>, <iframe>, <svg>, <object>, etc.) are escaped
via esc(). The SAFE_TAGS allowlist covers every tag the pipeline itself
produces. XSS is fully blocked.
Active session gold style (sessions.js, style.css):
Active session item now uses gold/amber (#e8a030) instead of blue,
matching the logo gradient color for better visual hierarchy.
Project color border-left is skipped when the session is active
(gold always wins). Session items get border-radius: 0 8px 8px 0
to complement the left border indicator.
Tests (tests/test_sprint16.py — 74 tests):
- Static analysis: pre-pass, SAFE_TAGS, SAFE_INLINE, inlineMd present
- Behavioural: all safe tags render in paragraphs, list items (ul+ol),
blockquotes, headings (h1/h2/h3)
- Exact screenshot regression: the 4-item list with <strong> labels
and <code> values that was showing as literal text
- XSS: 7 attack vectors blocked (<img>, <script>, <iframe>, <svg>,
<object>, XSS inside bold, XSS nested inside <strong>)
- Edge cases: code block protection, double-escaping guards, br tag,
mixed markdown+HTML, inlineMd called in list/blockquote handlers
Tests: 312 passed, 0 failed.
2026-04-03 00:27:43 +00:00
|
|
|
|
// Safe tag → markdown equivalent (these produce the same output as **text** etc.)
|
|
|
|
|
|
s=s.replace(/<strong>([\s\S]*?)<\/strong>/gi,(_,t)=>'**'+t+'**');
|
|
|
|
|
|
s=s.replace(/<b>([\s\S]*?)<\/b>/gi,(_,t)=>'**'+t+'**');
|
|
|
|
|
|
s=s.replace(/<em>([\s\S]*?)<\/em>/gi,(_,t)=>'*'+t+'*');
|
|
|
|
|
|
s=s.replace(/<i>([\s\S]*?)<\/i>/gi,(_,t)=>'*'+t+'*');
|
|
|
|
|
|
s=s.replace(/<code>([^<]*?)<\/code>/gi,(_,t)=>'`'+t+'`');
|
|
|
|
|
|
s=s.replace(/<br\s*\/?>/gi,'\n');
|
|
|
|
|
|
// Restore stashed code blocks
|
|
|
|
|
|
s=s.replace(/\x00F(\d+)\x00/g,(_,i)=>fence_stash[+i]);
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
// Mermaid blocks: render as diagram containers (processed after DOM insertion)
|
|
|
|
|
|
s=s.replace(/```mermaid\n?([\s\S]*?)```/g,(_,code)=>{
|
|
|
|
|
|
const id='mermaid-'+Math.random().toString(36).slice(2,10);
|
|
|
|
|
|
return `<div class="mermaid-block" data-mermaid-id="${id}">${esc(code.trim())}</div>`;
|
|
|
|
|
|
});
|
2026-04-14 17:13:04 +00:00
|
|
|
|
s=s.replace(/```([\w+-]*)\n?([\s\S]*?)```/g,(_,lang,code)=>{
|
|
|
|
|
|
const normalizedLang=(lang||'').trim().toLowerCase();
|
|
|
|
|
|
const h=normalizedLang?`<div class="pre-header">${esc(normalizedLang)}</div>`:'';
|
|
|
|
|
|
const langAttr=normalizedLang?` class="language-${esc(normalizedLang)}"`:'';
|
|
|
|
|
|
return `${h}<pre><code${langAttr}>${esc(code.replace(/\n$/,''))}</code></pre>`;
|
|
|
|
|
|
});
|
2026-03-31 03:40:19 +00:00
|
|
|
|
s=s.replace(/`([^`\n]+)`/g,(_,c)=>`<code>${esc(c)}</code>`);
|
feat: safe HTML rendering in AI responses + active session gold style + Sprint 16 tests
renderMd() now correctly renders safe inline HTML tags that AI models
emit in their responses:
Pre-pass (ui.js):
Converts <strong>, <b>, <em>, <i>, <code>, <br> to their markdown
equivalents (**text**, *text*, `text`, newline) before the pipeline
runs. Code blocks and backtick spans are stashed first so their content
is never modified.
inlineMd() helper (ui.js):
New helper for processing inline formatting inside list items,
blockquotes, and headings. Previously these used esc() directly, which
escaped <strong>/<code> tags that had already been converted from HTML
by the pre-pass — causing them to appear as literal <strong> text
instead of rendering as bold. inlineMd() applies bold/italic/code
processing and then escapes only unknown tags.
Safety net (ui.js):
After the full pipeline, any HTML tags NOT emitted by our own renderer
(i.e. <img>, <script>, <iframe>, <svg>, <object>, etc.) are escaped
via esc(). The SAFE_TAGS allowlist covers every tag the pipeline itself
produces. XSS is fully blocked.
Active session gold style (sessions.js, style.css):
Active session item now uses gold/amber (#e8a030) instead of blue,
matching the logo gradient color for better visual hierarchy.
Project color border-left is skipped when the session is active
(gold always wins). Session items get border-radius: 0 8px 8px 0
to complement the left border indicator.
Tests (tests/test_sprint16.py — 74 tests):
- Static analysis: pre-pass, SAFE_TAGS, SAFE_INLINE, inlineMd present
- Behavioural: all safe tags render in paragraphs, list items (ul+ol),
blockquotes, headings (h1/h2/h3)
- Exact screenshot regression: the 4-item list with <strong> labels
and <code> values that was showing as literal text
- XSS: 7 attack vectors blocked (<img>, <script>, <iframe>, <svg>,
<object>, XSS inside bold, XSS nested inside <strong>)
- Edge cases: code block protection, double-escaping guards, br tag,
mixed markdown+HTML, inlineMd called in list/blockquote handlers
Tests: 312 passed, 0 failed.
2026-04-03 00:27:43 +00:00
|
|
|
|
// inlineMd: process bold/italic/code/links within a single line of text.
|
|
|
|
|
|
// Used inside list items and blockquotes where the text may already contain
|
|
|
|
|
|
// HTML from the pre-pass → bold pipeline, so we cannot call esc() directly.
|
|
|
|
|
|
function inlineMd(t){
|
2026-04-14 21:14:00 +00:00
|
|
|
|
// Stash backtick code spans first so bold/italic never esc() their content
|
|
|
|
|
|
const _code_stash=[];
|
|
|
|
|
|
t=t.replace(/`([^`\n]+)`/g,(_,x)=>{_code_stash.push(`<code>${esc(x)}</code>`);return `\x00C${_code_stash.length-1}\x00`;});
|
feat: safe HTML rendering in AI responses + active session gold style + Sprint 16 tests
renderMd() now correctly renders safe inline HTML tags that AI models
emit in their responses:
Pre-pass (ui.js):
Converts <strong>, <b>, <em>, <i>, <code>, <br> to their markdown
equivalents (**text**, *text*, `text`, newline) before the pipeline
runs. Code blocks and backtick spans are stashed first so their content
is never modified.
inlineMd() helper (ui.js):
New helper for processing inline formatting inside list items,
blockquotes, and headings. Previously these used esc() directly, which
escaped <strong>/<code> tags that had already been converted from HTML
by the pre-pass — causing them to appear as literal <strong> text
instead of rendering as bold. inlineMd() applies bold/italic/code
processing and then escapes only unknown tags.
Safety net (ui.js):
After the full pipeline, any HTML tags NOT emitted by our own renderer
(i.e. <img>, <script>, <iframe>, <svg>, <object>, etc.) are escaped
via esc(). The SAFE_TAGS allowlist covers every tag the pipeline itself
produces. XSS is fully blocked.
Active session gold style (sessions.js, style.css):
Active session item now uses gold/amber (#e8a030) instead of blue,
matching the logo gradient color for better visual hierarchy.
Project color border-left is skipped when the session is active
(gold always wins). Session items get border-radius: 0 8px 8px 0
to complement the left border indicator.
Tests (tests/test_sprint16.py — 74 tests):
- Static analysis: pre-pass, SAFE_TAGS, SAFE_INLINE, inlineMd present
- Behavioural: all safe tags render in paragraphs, list items (ul+ol),
blockquotes, headings (h1/h2/h3)
- Exact screenshot regression: the 4-item list with <strong> labels
and <code> values that was showing as literal text
- XSS: 7 attack vectors blocked (<img>, <script>, <iframe>, <svg>,
<object>, XSS inside bold, XSS nested inside <strong>)
- Edge cases: code block protection, double-escaping guards, br tag,
mixed markdown+HTML, inlineMd called in list/blockquote handlers
Tests: 312 passed, 0 failed.
2026-04-03 00:27:43 +00:00
|
|
|
|
t=t.replace(/\*\*\*(.+?)\*\*\*/g,(_,x)=>`<strong><em>${esc(x)}</em></strong>`);
|
|
|
|
|
|
t=t.replace(/\*\*(.+?)\*\*/g,(_,x)=>`<strong>${esc(x)}</strong>`);
|
|
|
|
|
|
t=t.replace(/\*([^*\n]+)\*/g,(_,x)=>`<em>${esc(x)}</em>`);
|
fix: code-in-table CSS sizing + markdown image rendering (#486, #487)
- static/style.css: add td code / th code rules (font-size 0.85em,
padding 1px 4px, vertical-align baseline) for both .msg-body and
.preview-md to fix cramped inline code in table cells (#486)
- static/ui.js inlineMd(): add image pass ( → <img
class=msg-media-img>) running while _code_stash is active (protects
image syntax inside backticks), add _img_stash (\x00G) to shield
rendered <img> src= from autolink, add img to SAFE_INLINE (#487)
- static/ui.js renderMd() outer: add image pass before outer link pass
for images in plain paragraphs, add img to SAFE_TAGS allowlist (#487)
- tests/test_issue486_487.py: 45 new tests covering CSS source checks,
JS source structure, rendering behaviour, and combination edge cases
(code + image + link in same table cell, image inside code span, etc.)
Closes #486, closes #487
2026-04-14 21:52:34 +00:00
|
|
|
|
// #487: Image pass — runs while code stash is active so  inside
|
|
|
|
|
|
// backticks stays protected as a \x00C token and is never rendered as <img>.
|
|
|
|
|
|
// Must run before _code_stash restore and before _link_stash so the image
|
|
|
|
|
|
// is not consumed by the [label](url) link regex.
|
|
|
|
|
|
t=t.replace(/!\[([^\]]*)\]\((https?:\/\/[^\)]+)\)/g,(_,alt,url)=>`<img src="${url.replace(/"/g,'%22')}" alt="${esc(alt)}" class="msg-media-img" loading="lazy" onclick="this.classList.toggle('msg-media-img--full')">`);
|
|
|
|
|
|
// Stash rendered <img> tags so autolink never matches URLs inside src=
|
|
|
|
|
|
const _img_stash=[];
|
|
|
|
|
|
t=t.replace(/(<img\b[^>]*>)/g,m=>{_img_stash.push(m);return `\x00G${_img_stash.length-1}\x00`;});
|
2026-04-14 21:14:00 +00:00
|
|
|
|
t=t.replace(/\x00C(\d+)\x00/g,(_,i)=>_code_stash[+i]);
|
2026-04-14 21:13:33 +00:00
|
|
|
|
// Stash [label](url) links before autolink so the URL in href= is not re-linked
|
|
|
|
|
|
const _link_stash=[];
|
2026-04-14 21:13:53 +00:00
|
|
|
|
t=t.replace(/\[([^\]]+)\]\((https?:\/\/[^\)]+)\)/g,(_,lb,u)=>{_link_stash.push(`<a href="${u.replace(/"/g,'%22')}" target="_blank" rel="noopener">${esc(lb)}</a>`);return `\x00L${_link_stash.length-1}\x00`;});
|
2026-04-14 21:13:33 +00:00
|
|
|
|
t=t.replace(/(https?:\/\/[^\s<>"')\]]+)/g,(url)=>{const trail=url.match(/[.,;:!?)]$/)?url.slice(-1):'';const clean=trail?url.slice(0,-1):url;return `<a href="${clean}" target="_blank" rel="noopener">${esc(clean)}</a>${trail}`;});
|
2026-04-14 21:13:34 +00:00
|
|
|
|
t=t.replace(/\x00L(\d+)\x00/g,(_,i)=>_link_stash[+i]);
|
fix: code-in-table CSS sizing + markdown image rendering (#486, #487)
- static/style.css: add td code / th code rules (font-size 0.85em,
padding 1px 4px, vertical-align baseline) for both .msg-body and
.preview-md to fix cramped inline code in table cells (#486)
- static/ui.js inlineMd(): add image pass ( → <img
class=msg-media-img>) running while _code_stash is active (protects
image syntax inside backticks), add _img_stash (\x00G) to shield
rendered <img> src= from autolink, add img to SAFE_INLINE (#487)
- static/ui.js renderMd() outer: add image pass before outer link pass
for images in plain paragraphs, add img to SAFE_TAGS allowlist (#487)
- tests/test_issue486_487.py: 45 new tests covering CSS source checks,
JS source structure, rendering behaviour, and combination edge cases
(code + image + link in same table cell, image inside code span, etc.)
Closes #486, closes #487
2026-04-14 21:52:34 +00:00
|
|
|
|
t=t.replace(/\x00G(\d+)\x00/g,(_,i)=>_img_stash[+i]);
|
feat: safe HTML rendering in AI responses + active session gold style + Sprint 16 tests
renderMd() now correctly renders safe inline HTML tags that AI models
emit in their responses:
Pre-pass (ui.js):
Converts <strong>, <b>, <em>, <i>, <code>, <br> to their markdown
equivalents (**text**, *text*, `text`, newline) before the pipeline
runs. Code blocks and backtick spans are stashed first so their content
is never modified.
inlineMd() helper (ui.js):
New helper for processing inline formatting inside list items,
blockquotes, and headings. Previously these used esc() directly, which
escaped <strong>/<code> tags that had already been converted from HTML
by the pre-pass — causing them to appear as literal <strong> text
instead of rendering as bold. inlineMd() applies bold/italic/code
processing and then escapes only unknown tags.
Safety net (ui.js):
After the full pipeline, any HTML tags NOT emitted by our own renderer
(i.e. <img>, <script>, <iframe>, <svg>, <object>, etc.) are escaped
via esc(). The SAFE_TAGS allowlist covers every tag the pipeline itself
produces. XSS is fully blocked.
Active session gold style (sessions.js, style.css):
Active session item now uses gold/amber (#e8a030) instead of blue,
matching the logo gradient color for better visual hierarchy.
Project color border-left is skipped when the session is active
(gold always wins). Session items get border-radius: 0 8px 8px 0
to complement the left border indicator.
Tests (tests/test_sprint16.py — 74 tests):
- Static analysis: pre-pass, SAFE_TAGS, SAFE_INLINE, inlineMd present
- Behavioural: all safe tags render in paragraphs, list items (ul+ol),
blockquotes, headings (h1/h2/h3)
- Exact screenshot regression: the 4-item list with <strong> labels
and <code> values that was showing as literal text
- XSS: 7 attack vectors blocked (<img>, <script>, <iframe>, <svg>,
<object>, XSS inside bold, XSS nested inside <strong>)
- Edge cases: code block protection, double-escaping guards, br tag,
mixed markdown+HTML, inlineMd called in list/blockquote handlers
Tests: 312 passed, 0 failed.
2026-04-03 00:27:43 +00:00
|
|
|
|
// Escape any plain text that isn't already wrapped in a tag we produced
|
fix: code-in-table CSS sizing + markdown image rendering (#486, #487)
- static/style.css: add td code / th code rules (font-size 0.85em,
padding 1px 4px, vertical-align baseline) for both .msg-body and
.preview-md to fix cramped inline code in table cells (#486)
- static/ui.js inlineMd(): add image pass ( → <img
class=msg-media-img>) running while _code_stash is active (protects
image syntax inside backticks), add _img_stash (\x00G) to shield
rendered <img> src= from autolink, add img to SAFE_INLINE (#487)
- static/ui.js renderMd() outer: add image pass before outer link pass
for images in plain paragraphs, add img to SAFE_TAGS allowlist (#487)
- tests/test_issue486_487.py: 45 new tests covering CSS source checks,
JS source structure, rendering behaviour, and combination edge cases
(code + image + link in same table cell, image inside code span, etc.)
Closes #486, closes #487
2026-04-14 21:52:34 +00:00
|
|
|
|
// by escaping bare < > that are not part of our own tags
|
|
|
|
|
|
const SAFE_INLINE=/^<\/?(strong|em|code|a|img)([\s>]|$)/i;
|
feat: safe HTML rendering in AI responses + active session gold style + Sprint 16 tests
renderMd() now correctly renders safe inline HTML tags that AI models
emit in their responses:
Pre-pass (ui.js):
Converts <strong>, <b>, <em>, <i>, <code>, <br> to their markdown
equivalents (**text**, *text*, `text`, newline) before the pipeline
runs. Code blocks and backtick spans are stashed first so their content
is never modified.
inlineMd() helper (ui.js):
New helper for processing inline formatting inside list items,
blockquotes, and headings. Previously these used esc() directly, which
escaped <strong>/<code> tags that had already been converted from HTML
by the pre-pass — causing them to appear as literal <strong> text
instead of rendering as bold. inlineMd() applies bold/italic/code
processing and then escapes only unknown tags.
Safety net (ui.js):
After the full pipeline, any HTML tags NOT emitted by our own renderer
(i.e. <img>, <script>, <iframe>, <svg>, <object>, etc.) are escaped
via esc(). The SAFE_TAGS allowlist covers every tag the pipeline itself
produces. XSS is fully blocked.
Active session gold style (sessions.js, style.css):
Active session item now uses gold/amber (#e8a030) instead of blue,
matching the logo gradient color for better visual hierarchy.
Project color border-left is skipped when the session is active
(gold always wins). Session items get border-radius: 0 8px 8px 0
to complement the left border indicator.
Tests (tests/test_sprint16.py — 74 tests):
- Static analysis: pre-pass, SAFE_TAGS, SAFE_INLINE, inlineMd present
- Behavioural: all safe tags render in paragraphs, list items (ul+ol),
blockquotes, headings (h1/h2/h3)
- Exact screenshot regression: the 4-item list with <strong> labels
and <code> values that was showing as literal text
- XSS: 7 attack vectors blocked (<img>, <script>, <iframe>, <svg>,
<object>, XSS inside bold, XSS nested inside <strong>)
- Edge cases: code block protection, double-escaping guards, br tag,
mixed markdown+HTML, inlineMd called in list/blockquote handlers
Tests: 312 passed, 0 failed.
2026-04-03 00:27:43 +00:00
|
|
|
|
t=t.replace(/<\/?[a-z][^>]*>/gi,tag=>SAFE_INLINE.test(tag)?tag:esc(tag));
|
|
|
|
|
|
return t;
|
|
|
|
|
|
}
|
2026-04-14 21:22:20 +00:00
|
|
|
|
// Stash <code> tags from the backtick pass above so the outer bold/italic
|
|
|
|
|
|
// regexes don't esc() their content (e.g. **`code`** → <strong><code>code</code></strong>)
|
|
|
|
|
|
const _ob_stash=[];
|
2026-04-23 16:45:20 +00:00
|
|
|
|
s=s.replace(/(<code\b[^>]*>[\s\S]*?<\/code>)/g,m=>{_ob_stash.push(m);return `\x00O${_ob_stash.length-1}\x00`;});
|
2026-04-02 06:46:40 +00:00
|
|
|
|
s=s.replace(/\*\*\*(.+?)\*\*\*/g,(_,t)=>`<strong><em>${esc(t)}</em></strong>`);
|
|
|
|
|
|
s=s.replace(/\*\*(.+?)\*\*/g,(_,t)=>`<strong>${esc(t)}</strong>`);
|
|
|
|
|
|
s=s.replace(/\*([^*\n]+)\*/g,(_,t)=>`<em>${esc(t)}</em>`);
|
2026-04-14 21:22:20 +00:00
|
|
|
|
s=s.replace(/\x00O(\d+)\x00/g,(_,i)=>_ob_stash[+i]);
|
feat: safe HTML rendering in AI responses + active session gold style + Sprint 16 tests
renderMd() now correctly renders safe inline HTML tags that AI models
emit in their responses:
Pre-pass (ui.js):
Converts <strong>, <b>, <em>, <i>, <code>, <br> to their markdown
equivalents (**text**, *text*, `text`, newline) before the pipeline
runs. Code blocks and backtick spans are stashed first so their content
is never modified.
inlineMd() helper (ui.js):
New helper for processing inline formatting inside list items,
blockquotes, and headings. Previously these used esc() directly, which
escaped <strong>/<code> tags that had already been converted from HTML
by the pre-pass — causing them to appear as literal <strong> text
instead of rendering as bold. inlineMd() applies bold/italic/code
processing and then escapes only unknown tags.
Safety net (ui.js):
After the full pipeline, any HTML tags NOT emitted by our own renderer
(i.e. <img>, <script>, <iframe>, <svg>, <object>, etc.) are escaped
via esc(). The SAFE_TAGS allowlist covers every tag the pipeline itself
produces. XSS is fully blocked.
Active session gold style (sessions.js, style.css):
Active session item now uses gold/amber (#e8a030) instead of blue,
matching the logo gradient color for better visual hierarchy.
Project color border-left is skipped when the session is active
(gold always wins). Session items get border-radius: 0 8px 8px 0
to complement the left border indicator.
Tests (tests/test_sprint16.py — 74 tests):
- Static analysis: pre-pass, SAFE_TAGS, SAFE_INLINE, inlineMd present
- Behavioural: all safe tags render in paragraphs, list items (ul+ol),
blockquotes, headings (h1/h2/h3)
- Exact screenshot regression: the 4-item list with <strong> labels
and <code> values that was showing as literal text
- XSS: 7 attack vectors blocked (<img>, <script>, <iframe>, <svg>,
<object>, XSS inside bold, XSS nested inside <strong>)
- Edge cases: code block protection, double-escaping guards, br tag,
mixed markdown+HTML, inlineMd called in list/blockquote handlers
Tests: 312 passed, 0 failed.
2026-04-03 00:27:43 +00:00
|
|
|
|
s=s.replace(/^### (.+)$/gm,(_,t)=>`<h3>${inlineMd(t)}</h3>`).replace(/^## (.+)$/gm,(_,t)=>`<h2>${inlineMd(t)}</h2>`).replace(/^# (.+)$/gm,(_,t)=>`<h1>${inlineMd(t)}</h1>`);
|
2026-03-31 03:40:19 +00:00
|
|
|
|
s=s.replace(/^---+$/gm,'<hr>');
|
feat: safe HTML rendering in AI responses + active session gold style + Sprint 16 tests
renderMd() now correctly renders safe inline HTML tags that AI models
emit in their responses:
Pre-pass (ui.js):
Converts <strong>, <b>, <em>, <i>, <code>, <br> to their markdown
equivalents (**text**, *text*, `text`, newline) before the pipeline
runs. Code blocks and backtick spans are stashed first so their content
is never modified.
inlineMd() helper (ui.js):
New helper for processing inline formatting inside list items,
blockquotes, and headings. Previously these used esc() directly, which
escaped <strong>/<code> tags that had already been converted from HTML
by the pre-pass — causing them to appear as literal <strong> text
instead of rendering as bold. inlineMd() applies bold/italic/code
processing and then escapes only unknown tags.
Safety net (ui.js):
After the full pipeline, any HTML tags NOT emitted by our own renderer
(i.e. <img>, <script>, <iframe>, <svg>, <object>, etc.) are escaped
via esc(). The SAFE_TAGS allowlist covers every tag the pipeline itself
produces. XSS is fully blocked.
Active session gold style (sessions.js, style.css):
Active session item now uses gold/amber (#e8a030) instead of blue,
matching the logo gradient color for better visual hierarchy.
Project color border-left is skipped when the session is active
(gold always wins). Session items get border-radius: 0 8px 8px 0
to complement the left border indicator.
Tests (tests/test_sprint16.py — 74 tests):
- Static analysis: pre-pass, SAFE_TAGS, SAFE_INLINE, inlineMd present
- Behavioural: all safe tags render in paragraphs, list items (ul+ol),
blockquotes, headings (h1/h2/h3)
- Exact screenshot regression: the 4-item list with <strong> labels
and <code> values that was showing as literal text
- XSS: 7 attack vectors blocked (<img>, <script>, <iframe>, <svg>,
<object>, XSS inside bold, XSS nested inside <strong>)
- Edge cases: code block protection, double-escaping guards, br tag,
mixed markdown+HTML, inlineMd called in list/blockquote handlers
Tests: 312 passed, 0 failed.
2026-04-03 00:27:43 +00:00
|
|
|
|
s=s.replace(/^> (.+)$/gm,(_,t)=>`<blockquote>${inlineMd(t)}</blockquote>`);
|
2026-03-31 03:40:19 +00:00
|
|
|
|
// B8: improved list handling supporting up to 2 levels of indentation
|
|
|
|
|
|
s=s.replace(/((?:^(?: )?[-*+] .+\n?)+)/gm,block=>{
|
|
|
|
|
|
const lines=block.trimEnd().split('\n');
|
|
|
|
|
|
let html='<ul>';
|
|
|
|
|
|
for(const l of lines){
|
|
|
|
|
|
const indent=/^ {2,}/.test(l);
|
|
|
|
|
|
const text=l.replace(/^ {0,4}[-*+] /,'');
|
feat: safe HTML rendering in AI responses + active session gold style + Sprint 16 tests
renderMd() now correctly renders safe inline HTML tags that AI models
emit in their responses:
Pre-pass (ui.js):
Converts <strong>, <b>, <em>, <i>, <code>, <br> to their markdown
equivalents (**text**, *text*, `text`, newline) before the pipeline
runs. Code blocks and backtick spans are stashed first so their content
is never modified.
inlineMd() helper (ui.js):
New helper for processing inline formatting inside list items,
blockquotes, and headings. Previously these used esc() directly, which
escaped <strong>/<code> tags that had already been converted from HTML
by the pre-pass — causing them to appear as literal <strong> text
instead of rendering as bold. inlineMd() applies bold/italic/code
processing and then escapes only unknown tags.
Safety net (ui.js):
After the full pipeline, any HTML tags NOT emitted by our own renderer
(i.e. <img>, <script>, <iframe>, <svg>, <object>, etc.) are escaped
via esc(). The SAFE_TAGS allowlist covers every tag the pipeline itself
produces. XSS is fully blocked.
Active session gold style (sessions.js, style.css):
Active session item now uses gold/amber (#e8a030) instead of blue,
matching the logo gradient color for better visual hierarchy.
Project color border-left is skipped when the session is active
(gold always wins). Session items get border-radius: 0 8px 8px 0
to complement the left border indicator.
Tests (tests/test_sprint16.py — 74 tests):
- Static analysis: pre-pass, SAFE_TAGS, SAFE_INLINE, inlineMd present
- Behavioural: all safe tags render in paragraphs, list items (ul+ol),
blockquotes, headings (h1/h2/h3)
- Exact screenshot regression: the 4-item list with <strong> labels
and <code> values that was showing as literal text
- XSS: 7 attack vectors blocked (<img>, <script>, <iframe>, <svg>,
<object>, XSS inside bold, XSS nested inside <strong>)
- Edge cases: code block protection, double-escaping guards, br tag,
mixed markdown+HTML, inlineMd called in list/blockquote handlers
Tests: 312 passed, 0 failed.
2026-04-03 00:27:43 +00:00
|
|
|
|
if(indent) html+=`<li style="margin-left:16px">${inlineMd(text)}</li>`;
|
|
|
|
|
|
else html+=`<li>${inlineMd(text)}</li>`;
|
2026-03-31 03:40:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
return html+'</ul>';
|
|
|
|
|
|
});
|
2026-04-23 19:15:56 +00:00
|
|
|
|
// Ordered lists: use value= on each <li> so the correct number is preserved
|
|
|
|
|
|
// even when blank lines between items cause the paragraph splitter to place
|
|
|
|
|
|
// each item in its own <ol> container — without value= every <ol> restarts
|
|
|
|
|
|
// at 1, producing "1. 1. 1." instead of "1. 2. 3." (#886).
|
2026-03-31 03:40:19 +00:00
|
|
|
|
s=s.replace(/((?:^(?: )?\d+\. .+\n?)+)/gm,block=>{
|
|
|
|
|
|
const lines=block.trimEnd().split('\n');
|
|
|
|
|
|
let html='<ol>';
|
|
|
|
|
|
for(const l of lines){
|
2026-04-23 19:15:56 +00:00
|
|
|
|
const numMatch=l.match(/^\s*(\d+)\. /);
|
|
|
|
|
|
const num=numMatch?parseInt(numMatch[1],10):null;
|
2026-03-31 03:40:19 +00:00
|
|
|
|
const text=l.replace(/^ {0,4}\d+\. /,'');
|
2026-04-23 19:15:56 +00:00
|
|
|
|
const valAttr=num!==null?` value="${num}"`:'';
|
|
|
|
|
|
html+=`<li${valAttr}>${inlineMd(text)}</li>`;
|
2026-03-31 03:40:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
return html+'</ol>';
|
|
|
|
|
|
});
|
|
|
|
|
|
// Tables: | col | col | header row followed by | --- | --- | separator then data rows
|
2026-04-14 21:22:20 +00:00
|
|
|
|
// NOTE: table pass runs BEFORE outer link pass so [label](url) in table cells
|
|
|
|
|
|
// is handled by inlineMd() only — prevents double-linking.
|
2026-03-31 03:40:19 +00:00
|
|
|
|
s=s.replace(/((?:^\|.+\|\n?)+)/gm,block=>{
|
|
|
|
|
|
const rows=block.trim().split('\n').filter(r=>r.trim());
|
|
|
|
|
|
if(rows.length<2)return block;
|
|
|
|
|
|
const isSep=r=>/^\|[\s|:-]+\|$/.test(r.trim());
|
|
|
|
|
|
if(!isSep(rows[1]))return block;
|
2026-04-12 04:03:01 +00:00
|
|
|
|
const parseRow=r=>r.trim().replace(/^\|/,'').replace(/\|$/,'').split('|').map(c=>`<td>${inlineMd(c.trim())}</td>`).join('');
|
|
|
|
|
|
const parseHeader=r=>r.trim().replace(/^\|/,'').replace(/\|$/,'').split('|').map(c=>`<th>${inlineMd(c.trim())}</th>`).join('');
|
2026-03-31 03:40:19 +00:00
|
|
|
|
const header=`<tr>${parseHeader(rows[0])}</tr>`;
|
|
|
|
|
|
const body=rows.slice(2).map(r=>`<tr>${parseRow(r)}</tr>`).join('');
|
|
|
|
|
|
return `<table><thead>${header}</thead><tbody>${body}</tbody></table>`;
|
|
|
|
|
|
});
|
fix: code-in-table CSS sizing + markdown image rendering (#486, #487)
- static/style.css: add td code / th code rules (font-size 0.85em,
padding 1px 4px, vertical-align baseline) for both .msg-body and
.preview-md to fix cramped inline code in table cells (#486)
- static/ui.js inlineMd(): add image pass ( → <img
class=msg-media-img>) running while _code_stash is active (protects
image syntax inside backticks), add _img_stash (\x00G) to shield
rendered <img> src= from autolink, add img to SAFE_INLINE (#487)
- static/ui.js renderMd() outer: add image pass before outer link pass
for images in plain paragraphs, add img to SAFE_TAGS allowlist (#487)
- tests/test_issue486_487.py: 45 new tests covering CSS source checks,
JS source structure, rendering behaviour, and combination edge cases
(code + image + link in same table cell, image inside code span, etc.)
Closes #486, closes #487
2026-04-14 21:52:34 +00:00
|
|
|
|
// #487: Outer image pass — handles  in plain paragraphs (outside tables/lists).
|
|
|
|
|
|
// Runs AFTER the table pass (images in table cells are handled by inlineMd() above).
|
|
|
|
|
|
// Runs BEFORE the outer [label](url) link pass so the image is not consumed as a plain link.
|
|
|
|
|
|
s=s.replace(/!\[([^\]]*)\]\((https?:\/\/[^\)]+)\)/g,(_,alt,url)=>`<img src="${url.replace(/"/g,'%22')}" alt="${esc(alt)}" class="msg-media-img" loading="lazy" onclick="this.classList.toggle('msg-media-img--full')">`);
|
2026-04-14 21:22:20 +00:00
|
|
|
|
// Outer link pass for labeled links in plain paragraphs (outside table cells).
|
|
|
|
|
|
// Runs AFTER the table pass so table cells are processed by inlineMd() only.
|
|
|
|
|
|
// Stash existing <a> tags first to avoid re-linking already-linked URLs.
|
|
|
|
|
|
const _a_stash=[];
|
|
|
|
|
|
s=s.replace(/(<a\b[^>]*>[\s\S]*?<\/a>)/g,m=>{_a_stash.push(m);return `\x00A${_a_stash.length-1}\x00`;});
|
|
|
|
|
|
s=s.replace(/\[([^\]]+)\]\((https?:\/\/[^\)]+)\)/g,(_,label,url)=>`<a href="${url.replace(/"/g,'%22')}" target="_blank" rel="noopener">${esc(label)}</a>`);
|
|
|
|
|
|
s=s.replace(/\x00A(\d+)\x00/g,(_,i)=>_a_stash[+i]);
|
feat: safe HTML rendering in AI responses + active session gold style + Sprint 16 tests
renderMd() now correctly renders safe inline HTML tags that AI models
emit in their responses:
Pre-pass (ui.js):
Converts <strong>, <b>, <em>, <i>, <code>, <br> to their markdown
equivalents (**text**, *text*, `text`, newline) before the pipeline
runs. Code blocks and backtick spans are stashed first so their content
is never modified.
inlineMd() helper (ui.js):
New helper for processing inline formatting inside list items,
blockquotes, and headings. Previously these used esc() directly, which
escaped <strong>/<code> tags that had already been converted from HTML
by the pre-pass — causing them to appear as literal <strong> text
instead of rendering as bold. inlineMd() applies bold/italic/code
processing and then escapes only unknown tags.
Safety net (ui.js):
After the full pipeline, any HTML tags NOT emitted by our own renderer
(i.e. <img>, <script>, <iframe>, <svg>, <object>, etc.) are escaped
via esc(). The SAFE_TAGS allowlist covers every tag the pipeline itself
produces. XSS is fully blocked.
Active session gold style (sessions.js, style.css):
Active session item now uses gold/amber (#e8a030) instead of blue,
matching the logo gradient color for better visual hierarchy.
Project color border-left is skipped when the session is active
(gold always wins). Session items get border-radius: 0 8px 8px 0
to complement the left border indicator.
Tests (tests/test_sprint16.py — 74 tests):
- Static analysis: pre-pass, SAFE_TAGS, SAFE_INLINE, inlineMd present
- Behavioural: all safe tags render in paragraphs, list items (ul+ol),
blockquotes, headings (h1/h2/h3)
- Exact screenshot regression: the 4-item list with <strong> labels
and <code> values that was showing as literal text
- XSS: 7 attack vectors blocked (<img>, <script>, <iframe>, <svg>,
<object>, XSS inside bold, XSS nested inside <strong>)
- Edge cases: code block protection, double-escaping guards, br tag,
mixed markdown+HTML, inlineMd called in list/blockquote handlers
Tests: 312 passed, 0 failed.
2026-04-03 00:27:43 +00:00
|
|
|
|
// Escape any remaining HTML tags that are NOT from our own markdown output.
|
|
|
|
|
|
// Our pipeline only emits: <strong>,<em>,<code>,<pre>,<h1-6>,<ul>,<ol>,<li>,
|
|
|
|
|
|
// <table>,<thead>,<tbody>,<tr>,<th>,<td>,<hr>,<blockquote>,<p>,<br>,<a>,
|
|
|
|
|
|
// <div class="..."> (mermaid/pre-header). Everything else is untrusted input.
|
fix: code-in-table CSS sizing + markdown image rendering (#486, #487)
- static/style.css: add td code / th code rules (font-size 0.85em,
padding 1px 4px, vertical-align baseline) for both .msg-body and
.preview-md to fix cramped inline code in table cells (#486)
- static/ui.js inlineMd(): add image pass ( → <img
class=msg-media-img>) running while _code_stash is active (protects
image syntax inside backticks), add _img_stash (\x00G) to shield
rendered <img> src= from autolink, add img to SAFE_INLINE (#487)
- static/ui.js renderMd() outer: add image pass before outer link pass
for images in plain paragraphs, add img to SAFE_TAGS allowlist (#487)
- tests/test_issue486_487.py: 45 new tests covering CSS source checks,
JS source structure, rendering behaviour, and combination edge cases
(code + image + link in same table cell, image inside code span, etc.)
Closes #486, closes #487
2026-04-14 21:52:34 +00:00
|
|
|
|
const SAFE_TAGS=/^<\/?(strong|em|code|pre|h[1-6]|ul|ol|li|table|thead|tbody|tr|th|td|hr|blockquote|p|br|a|img|div|span)([\s>]|$)/i;
|
feat: safe HTML rendering in AI responses + active session gold style + Sprint 16 tests
renderMd() now correctly renders safe inline HTML tags that AI models
emit in their responses:
Pre-pass (ui.js):
Converts <strong>, <b>, <em>, <i>, <code>, <br> to their markdown
equivalents (**text**, *text*, `text`, newline) before the pipeline
runs. Code blocks and backtick spans are stashed first so their content
is never modified.
inlineMd() helper (ui.js):
New helper for processing inline formatting inside list items,
blockquotes, and headings. Previously these used esc() directly, which
escaped <strong>/<code> tags that had already been converted from HTML
by the pre-pass — causing them to appear as literal <strong> text
instead of rendering as bold. inlineMd() applies bold/italic/code
processing and then escapes only unknown tags.
Safety net (ui.js):
After the full pipeline, any HTML tags NOT emitted by our own renderer
(i.e. <img>, <script>, <iframe>, <svg>, <object>, etc.) are escaped
via esc(). The SAFE_TAGS allowlist covers every tag the pipeline itself
produces. XSS is fully blocked.
Active session gold style (sessions.js, style.css):
Active session item now uses gold/amber (#e8a030) instead of blue,
matching the logo gradient color for better visual hierarchy.
Project color border-left is skipped when the session is active
(gold always wins). Session items get border-radius: 0 8px 8px 0
to complement the left border indicator.
Tests (tests/test_sprint16.py — 74 tests):
- Static analysis: pre-pass, SAFE_TAGS, SAFE_INLINE, inlineMd present
- Behavioural: all safe tags render in paragraphs, list items (ul+ol),
blockquotes, headings (h1/h2/h3)
- Exact screenshot regression: the 4-item list with <strong> labels
and <code> values that was showing as literal text
- XSS: 7 attack vectors blocked (<img>, <script>, <iframe>, <svg>,
<object>, XSS inside bold, XSS nested inside <strong>)
- Edge cases: code block protection, double-escaping guards, br tag,
mixed markdown+HTML, inlineMd called in list/blockquote handlers
Tests: 312 passed, 0 failed.
2026-04-03 00:27:43 +00:00
|
|
|
|
s=s.replace(/<\/?[a-z][^>]*>/gi,tag=>SAFE_TAGS.test(tag)?tag:esc(tag));
|
2026-04-14 21:13:33 +00:00
|
|
|
|
// Autolink: convert plain URLs to clickable links.
|
2026-04-21 16:26:51 +00:00
|
|
|
|
// Stash <a>, <img> and <pre> blocks so autolink never runs inside them.
|
2026-04-14 21:13:33 +00:00
|
|
|
|
const _al_stash=[];
|
2026-04-21 16:26:51 +00:00
|
|
|
|
s=s.replace(/(<a\b[^>]*>[\s\S]*?<\/a>|<img\b[^>]*>|<pre\b[^>]*>[\s\S]*?<\/pre>)/g,m=>{_al_stash.push(m);return `\x00B${_al_stash.length-1}\x00`;});
|
2026-04-14 21:13:33 +00:00
|
|
|
|
s=s.replace(/(https?:\/\/[^\s<>"'\)\]]+)/g,(url)=>{
|
2026-04-13 07:05:04 +00:00
|
|
|
|
// Strip trailing punctuation that was likely not part of the URL
|
|
|
|
|
|
const trail=url.match(/[.,;:!?)]$/)?url.slice(-1):'';
|
|
|
|
|
|
const clean=trail?url.slice(0,-1):url;
|
2026-04-14 21:13:33 +00:00
|
|
|
|
return `<a href="${clean}" target="_blank" rel="noopener">${esc(clean)}</a>${trail}`;
|
2026-04-13 07:05:04 +00:00
|
|
|
|
});
|
2026-04-14 21:13:33 +00:00
|
|
|
|
s=s.replace(/\x00B(\d+)\x00/g,(_,i)=>_al_stash[+i]);
|
feat: KaTeX math rendering for LaTeX in chat + workspace previews (#352)
* feat: KaTeX math rendering for $..$ and $$..$$ in chat and previews (fixes #347)
- Stash math delimiters before markdown pipeline, restore as .katex-block/.katex-inline elements
- KaTeX JS lazy-loaded from CDN on first math block (mirrors mermaid pattern)
- KaTeX CSS loaded eagerly in <head> to prevent layout shift
- SRI hashes on both CDN tags
- throwOnError:false — bad LaTeX degrades to code span
- Supports $$, $, \\(...\\), \\[...\\] delimiters
- 18 new tests, 831/831 passing
* fix: remove invalid \' escape sequences in math stash lines
Lines 311, 314, 316, 317 had \' (backslash-quote) instead of plain '
in the arrow function bodies. This is a JS syntax error — node --check
fails with 'Invalid or unexpected token'. Likely caused by a
serialization artifact during code generation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: swap stash order (fence before math) to protect code spans; add renderKatexBlocks to workspace preview
- static/ui.js: fence_stash now runs BEFORE math_stash so dollar signs
inside backtick code spans are not extracted as math. Previously
`$x$` would render as KaTeX inside a <code> tag instead of
showing the literal string $x$.
- static/workspace.js: add requestAnimationFrame(renderKatexBlocks)
after markdown preview renders so math works in workspace file
previews, not only in chat messages.
* feat: KaTeX math rendering + stash order fix + workspace wiring (#352)
- tests/test_issue347.py: 11 new tests (29 total) covering fence-before-math
ordering, workspace.js renderKatexBlocks call, stash token distinctness,
false-positive prevention, safe-tags boundary check
- CHANGELOG.md: v0.50.15 entry; 870 tests total (up from 841)
* fix: use literal null byte (\x00M) in math stash token — matches restore regex
The original PR's second commit (fix: remove invalid \' escapes) accidentally
doubled the backslash in the math stash tokens: '\\x00M' is a 5-char string
(backslash + x + 0 + 0 + M) but the restore regex /\x00M/ expects a null byte.
Result: $...$ in messages produced visible \x00M0\x00 tokens instead of
KaTeX spans.
Changed all 4 math stash return statements to use '\x00M' (single backslash =
null byte, same convention as fence_stash's '\x00F').
Also updates test_stash_tokens_distinct to check for the correct pattern.
* fix: add null-byte token test; update CHANGELOG to v0.50.15 with fixes
- tests/test_issue347.py: add test_math_stash_token_uses_single_backslash_null_byte
to catch the \\x00M double-backslash regression; 30 tests total (up from 29)
- CHANGELOG.md: v0.50.15 entry documents all fixes including the token bug
and workspace preview wiring; 871 tests total
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:40:15 +00:00
|
|
|
|
// Restore math stash → katex placeholder spans/divs
|
|
|
|
|
|
// These will be rendered by renderKatexBlocks() after DOM insertion
|
|
|
|
|
|
s=s.replace(/\x00M(\d+)\x00/g,(_,i)=>{
|
|
|
|
|
|
const item=math_stash[+i];
|
|
|
|
|
|
if(item.type==='display'){
|
|
|
|
|
|
return `<div class="katex-block" data-katex="display">${esc(item.src)}</div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
return `<span class="katex-inline" data-katex="inline">${esc(item.src)}</span>`;
|
|
|
|
|
|
});
|
2026-04-20 07:04:27 +00:00
|
|
|
|
// Stash rendered <pre> blocks (with optional pre-header div) and mermaid/katex
|
|
|
|
|
|
// divs before paragraph splitting so \n inside code blocks is never replaced
|
|
|
|
|
|
// with <br>. Token \x00E (next free after B D F G L M C O A).
|
|
|
|
|
|
// Fixes #745: code blocks collapse to single line when not preceded by blank line.
|
|
|
|
|
|
const _pre_stash=[];
|
|
|
|
|
|
s=s.replace(/(<div class="pre-header">[\s\S]*?<\/div>)?<pre>[\s\S]*?<\/pre>|<div class="(mermaid-block|katex-block)"[\s\S]*?<\/div>/g,m=>{
|
|
|
|
|
|
_pre_stash.push(m);
|
|
|
|
|
|
return '\x00E'+(_pre_stash.length-1)+'\x00';
|
|
|
|
|
|
});
|
2026-03-31 03:40:19 +00:00
|
|
|
|
const parts=s.split(/\n{2,}/);
|
2026-04-20 07:04:27 +00:00
|
|
|
|
s=parts.map(p=>{p=p.trim();if(!p)return '';if(/^<(h[1-6]|ul|ol|pre|hr|blockquote)|^\x00E/.test(p))return p;return `<p>${p.replace(/\n/g,'<br>')}</p>`;}).join('\n');
|
|
|
|
|
|
s=s.replace(/\x00E(\d+)\x00/g,(_,i)=>_pre_stash[+i]);
|
2026-04-14 19:35:52 +00:00
|
|
|
|
// ── Restore MEDIA stash → inline images or download links ─────────────────
|
|
|
|
|
|
s=s.replace(/\x00D(\d+)\x00/g,(_,i)=>{
|
|
|
|
|
|
const ref=media_stash[+i];
|
|
|
|
|
|
// HTTP(S) URL
|
|
|
|
|
|
if(/^https?:\/\//i.test(ref)){
|
release: v0.50.102–v0.50.108 batch (code blocks, utf-8, image URLs, deletion warning, PermissionError, Docker docs, kimi-k2.5) (#755)
## Batch release: v0.50.102 – v0.50.108
Seven self-built PRs reviewed and approved by @nesquena, now consolidated into a single release branch.
### Included fixes
| Version | PR | What it fixes |
|---|---|---|
| v0.50.102 | #746 | Code blocks lose newlines when not preceded by blank line (fixes #745) |
| v0.50.103 | #743 | `encoding='utf-8'` on `write_text()` in `api/profiles.py` — Windows `.env` detection (fixes #741) |
| v0.50.104 | #735 | Agent `MEDIA:localhost:*` image URLs rewritten to `document.baseURI` — remote users get working images (fixes #642) |
| v0.50.105 | #736 | Profile deletion warning strengthened: "permanently deleted, cannot be undone" across all 6 locales (fixes #637) |
| v0.50.106 | #738 | Catch `PermissionError` in `_signing_key()` — three-container Docker UID mismatch no longer crashes all HTTP requests |
| v0.50.107 | #737 | Docs: three-container UID/GID alignment guide in README + `HERMES_UID`/`HERMES_GID` forwarded in compose (fixes #645) |
| v0.50.108 | #742 | Add `kimi-k2.5` to Kimi/Moonshot provider model list (fixes #740) |
### Testing
- **pytest**: 1510 passed, 1 warning (1 pre-existing unrelated failure excluded)
- **QA harness**: 20/20 passed (`~/WebUI/scripts/run-browser-tests.sh`)
- **Browser**: layout, slash autocomplete width, edit button, image URL rewrite, profile deletion dialog all verified
All PRs reviewed and approved by @nesquena. Ready to merge and tag **v0.50.108**.
2026-04-20 07:26:55 +00:00
|
|
|
|
// Rewrite localhost/127.0.0.1 to the actual server base URL so remote
|
|
|
|
|
|
// users (VPN, Docker, deployed) can load agent-generated images (#642).
|
|
|
|
|
|
// Strip the trailing slash from document.baseURI so the URL's own path
|
|
|
|
|
|
// joins cleanly — this preserves any subpath mount (e.g. /hermes/).
|
|
|
|
|
|
let src=ref;
|
|
|
|
|
|
if(/^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?/i.test(src)){
|
|
|
|
|
|
const base=document.baseURI.replace(/\/$/,'');
|
|
|
|
|
|
src=src.replace(/^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?/i,base);
|
2026-04-14 19:35:52 +00:00
|
|
|
|
}
|
2026-04-22 20:20:01 +00:00
|
|
|
|
// MEDIA: tokens are only emitted for tool-generated images (image_generate etc.).
|
|
|
|
|
|
// Render all https:// URLs as <img> — extension check would miss extensionless
|
|
|
|
|
|
// CDN paths like fal.media content-addressed URLs (closes #853).
|
|
|
|
|
|
if(_IMAGE_EXTS.test(src.split('?')[0]) || /^https?:\/\//i.test(src)){
|
release: v0.50.102–v0.50.108 batch (code blocks, utf-8, image URLs, deletion warning, PermissionError, Docker docs, kimi-k2.5) (#755)
## Batch release: v0.50.102 – v0.50.108
Seven self-built PRs reviewed and approved by @nesquena, now consolidated into a single release branch.
### Included fixes
| Version | PR | What it fixes |
|---|---|---|
| v0.50.102 | #746 | Code blocks lose newlines when not preceded by blank line (fixes #745) |
| v0.50.103 | #743 | `encoding='utf-8'` on `write_text()` in `api/profiles.py` — Windows `.env` detection (fixes #741) |
| v0.50.104 | #735 | Agent `MEDIA:localhost:*` image URLs rewritten to `document.baseURI` — remote users get working images (fixes #642) |
| v0.50.105 | #736 | Profile deletion warning strengthened: "permanently deleted, cannot be undone" across all 6 locales (fixes #637) |
| v0.50.106 | #738 | Catch `PermissionError` in `_signing_key()` — three-container Docker UID mismatch no longer crashes all HTTP requests |
| v0.50.107 | #737 | Docs: three-container UID/GID alignment guide in README + `HERMES_UID`/`HERMES_GID` forwarded in compose (fixes #645) |
| v0.50.108 | #742 | Add `kimi-k2.5` to Kimi/Moonshot provider model list (fixes #740) |
### Testing
- **pytest**: 1510 passed, 1 warning (1 pre-existing unrelated failure excluded)
- **QA harness**: 20/20 passed (`~/WebUI/scripts/run-browser-tests.sh`)
- **Browser**: layout, slash autocomplete width, edit button, image URL rewrite, profile deletion dialog all verified
All PRs reviewed and approved by @nesquena. Ready to merge and tag **v0.50.108**.
2026-04-20 07:26:55 +00:00
|
|
|
|
return `<img class="msg-media-img" src="${esc(src)}" alt="image" loading="lazy" onclick="this.classList.toggle('msg-media-img--full')">`;
|
|
|
|
|
|
}
|
|
|
|
|
|
return `<a href="${esc(src)}" target="_blank" rel="noopener">${esc(src)}</a>`;
|
2026-04-14 19:35:52 +00:00
|
|
|
|
}
|
|
|
|
|
|
// Local file path
|
2026-04-16 18:20:08 +00:00
|
|
|
|
const apiUrl='api/media?path='+encodeURIComponent(ref);
|
2026-04-14 19:35:52 +00:00
|
|
|
|
if(_IMAGE_EXTS.test(ref)){
|
|
|
|
|
|
return `<img class="msg-media-img" src="${esc(apiUrl)}" alt="${esc(ref.split('/').pop())}" loading="lazy" onclick="this.classList.toggle('msg-media-img--full')">`;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Non-image local file — show download link with filename
|
|
|
|
|
|
const fname=esc(ref.split('/').pop()||ref);
|
|
|
|
|
|
return `<a class="msg-media-link" href="${esc(apiUrl+'&download=1')}" download="${fname}">📎 ${fname}</a>`;
|
|
|
|
|
|
});
|
|
|
|
|
|
// ── End MEDIA restore ──────────────────────────────────────────────────────
|
2026-03-31 03:40:19 +00:00
|
|
|
|
return s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function setStatus(t){
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
if(!t)return;
|
|
|
|
|
|
showToast(t, 4000);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function setComposerStatus(t){
|
|
|
|
|
|
const el=$('composerStatus');
|
|
|
|
|
|
if(!el)return;
|
2026-03-31 03:40:19 +00:00
|
|
|
|
if(!t){
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
el.style.display='none';
|
|
|
|
|
|
el.textContent='';
|
|
|
|
|
|
return;
|
2026-03-31 03:40:19 +00:00
|
|
|
|
}
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
el.textContent=t;
|
|
|
|
|
|
el.style.display='';
|
2026-03-31 03:40:19 +00:00
|
|
|
|
}
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
|
2026-04-15 05:10:50 +00:00
|
|
|
|
let _composerLockState=null;
|
|
|
|
|
|
|
|
|
|
|
|
function lockComposerForClarify(placeholderText){
|
|
|
|
|
|
const input=$('msg');
|
|
|
|
|
|
if(!input) return;
|
|
|
|
|
|
if(!_composerLockState){
|
|
|
|
|
|
_composerLockState={
|
|
|
|
|
|
disabled: input.disabled,
|
|
|
|
|
|
placeholder: input.placeholder,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
input.disabled=true;
|
|
|
|
|
|
if(placeholderText) input.placeholder=placeholderText;
|
|
|
|
|
|
updateSendBtn();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function unlockComposerForClarify(){
|
|
|
|
|
|
const input=$('msg');
|
|
|
|
|
|
if(!input) return;
|
|
|
|
|
|
if(_composerLockState){
|
|
|
|
|
|
input.disabled=!!_composerLockState.disabled;
|
|
|
|
|
|
if(typeof _composerLockState.placeholder==='string'){
|
|
|
|
|
|
input.placeholder=_composerLockState.placeholder;
|
|
|
|
|
|
}
|
|
|
|
|
|
_composerLockState=null;
|
|
|
|
|
|
}else{
|
|
|
|
|
|
input.disabled=false;
|
|
|
|
|
|
}
|
|
|
|
|
|
updateSendBtn();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: polish send button — hidden until content, icon-circle, pop-in animation
- index.html: btnSend hidden by default (display:none), icon-only (upward
arrow SVG, no text label), title attribute for accessibility
- style.css: new send-btn design — 34px circle, blue fill (#7cb9ff),
subtle glow box-shadow, scale() hover/active for tactile feel,
.send-btn.visible with @keyframes send-pop-in (scale+opacity spring
using cubic-bezier(.34,1.56,.64,1) for a satisfying pop). Mobile
override updated to preserve circle dimensions.
- ui.js: updateSendBtn() — shows button with pop-in animation when
textarea has content OR files are attached and agent is not busy;
hides instantly when content is cleared. Hooked into setBusy() and
renderTray() so button state tracks all content sources correctly.
- boot.js: input event listener calls updateSendBtn() on every keystroke.
- messages.js: autoResize() calls updateSendBtn() so button disappears
immediately after send clears the textarea.
- tests/test_sprint21.py: 33 tests covering HTML structure, CSS design
(circle shape, colors, animations, keyframes), JS logic (updateSendBtn,
setBusy, renderTray, autoResize integration), and regressions
(363 total, all pass).
2026-04-03 14:10:59 +00:00
|
|
|
|
function updateSendBtn(){
|
|
|
|
|
|
const btn=$('btnSend');
|
|
|
|
|
|
if(!btn) return;
|
2026-04-15 05:10:50 +00:00
|
|
|
|
const msg=$('msg');
|
|
|
|
|
|
const hasContent=msg&&msg.value.trim().length>0||S.pendingFiles.length>0;
|
|
|
|
|
|
const canSend=hasContent&&!S.busy&&!(msg&&msg.disabled);
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
// Hide while busy (cancel button takes its place); show otherwise
|
|
|
|
|
|
btn.style.display=S.busy?'none':'';
|
|
|
|
|
|
btn.disabled=!canSend;
|
|
|
|
|
|
if(canSend&&!btn.classList.contains('visible')){
|
feat: polish send button — hidden until content, icon-circle, pop-in animation
- index.html: btnSend hidden by default (display:none), icon-only (upward
arrow SVG, no text label), title attribute for accessibility
- style.css: new send-btn design — 34px circle, blue fill (#7cb9ff),
subtle glow box-shadow, scale() hover/active for tactile feel,
.send-btn.visible with @keyframes send-pop-in (scale+opacity spring
using cubic-bezier(.34,1.56,.64,1) for a satisfying pop). Mobile
override updated to preserve circle dimensions.
- ui.js: updateSendBtn() — shows button with pop-in animation when
textarea has content OR files are attached and agent is not busy;
hides instantly when content is cleared. Hooked into setBusy() and
renderTray() so button state tracks all content sources correctly.
- boot.js: input event listener calls updateSendBtn() on every keystroke.
- messages.js: autoResize() calls updateSendBtn() so button disappears
immediately after send clears the textarea.
- tests/test_sprint21.py: 33 tests covering HTML structure, CSS design
(circle shape, colors, animations, keyframes), JS logic (updateSendBtn,
setBusy, renderTray, autoResize integration), and regressions
(363 total, all pass).
2026-04-03 14:10:59 +00:00
|
|
|
|
btn.classList.remove('visible');
|
|
|
|
|
|
requestAnimationFrame(()=>btn.classList.add('visible'));
|
2026-04-18 06:37:09 +00:00
|
|
|
|
} else if(!canSend){
|
|
|
|
|
|
btn.classList.remove('visible');
|
feat: polish send button — hidden until content, icon-circle, pop-in animation
- index.html: btnSend hidden by default (display:none), icon-only (upward
arrow SVG, no text label), title attribute for accessibility
- style.css: new send-btn design — 34px circle, blue fill (#7cb9ff),
subtle glow box-shadow, scale() hover/active for tactile feel,
.send-btn.visible with @keyframes send-pop-in (scale+opacity spring
using cubic-bezier(.34,1.56,.64,1) for a satisfying pop). Mobile
override updated to preserve circle dimensions.
- ui.js: updateSendBtn() — shows button with pop-in animation when
textarea has content OR files are attached and agent is not busy;
hides instantly when content is cleared. Hooked into setBusy() and
renderTray() so button state tracks all content sources correctly.
- boot.js: input event listener calls updateSendBtn() on every keystroke.
- messages.js: autoResize() calls updateSendBtn() so button disappears
immediately after send clears the textarea.
- tests/test_sprint21.py: 33 tests covering HTML structure, CSS design
(circle shape, colors, animations, keyframes), JS logic (updateSendBtn,
setBusy, renderTray, autoResize integration), and regressions
(363 total, all pass).
2026-04-03 14:10:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-31 03:40:19 +00:00
|
|
|
|
function setBusy(v){
|
|
|
|
|
|
S.busy=v;
|
feat: polish send button — hidden until content, icon-circle, pop-in animation
- index.html: btnSend hidden by default (display:none), icon-only (upward
arrow SVG, no text label), title attribute for accessibility
- style.css: new send-btn design — 34px circle, blue fill (#7cb9ff),
subtle glow box-shadow, scale() hover/active for tactile feel,
.send-btn.visible with @keyframes send-pop-in (scale+opacity spring
using cubic-bezier(.34,1.56,.64,1) for a satisfying pop). Mobile
override updated to preserve circle dimensions.
- ui.js: updateSendBtn() — shows button with pop-in animation when
textarea has content OR files are attached and agent is not busy;
hides instantly when content is cleared. Hooked into setBusy() and
renderTray() so button state tracks all content sources correctly.
- boot.js: input event listener calls updateSendBtn() on every keystroke.
- messages.js: autoResize() calls updateSendBtn() so button disappears
immediately after send clears the textarea.
- tests/test_sprint21.py: 33 tests covering HTML structure, CSS design
(circle shape, colors, animations, keyframes), JS logic (updateSendBtn,
setBusy, renderTray, autoResize integration), and regressions
(363 total, all pass).
2026-04-03 14:10:59 +00:00
|
|
|
|
updateSendBtn();
|
2026-03-31 03:40:19 +00:00
|
|
|
|
if(!v){
|
|
|
|
|
|
setStatus('');
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
setComposerStatus('');
|
2026-03-31 03:40:19 +00:00
|
|
|
|
// Always hide Cancel button when not busy
|
|
|
|
|
|
const _cb=$('btnCancel');if(_cb)_cb.style.display='none';
|
2026-04-24 19:33:56 +00:00
|
|
|
|
const sid=_queueDrainSid||(S.session&&S.session.session_id);
|
|
|
|
|
|
_queueDrainSid=null;
|
2026-04-13 23:18:15 +00:00
|
|
|
|
updateQueueBadge(sid);
|
2026-04-24 19:33:56 +00:00
|
|
|
|
// Drain one queued message for the finished session after UI settles
|
|
|
|
|
|
const _isViewedSid=!S.session||sid===S.session.session_id;
|
|
|
|
|
|
const next=sid&&_isViewedSid?shiftQueuedSessionMessage(sid):null;
|
2026-04-13 23:18:15 +00:00
|
|
|
|
if(next){
|
|
|
|
|
|
updateQueueBadge(sid);
|
|
|
|
|
|
setTimeout(()=>{
|
|
|
|
|
|
$('msg').value=next.text||'';
|
|
|
|
|
|
S.pendingFiles=Array.isArray(next.files)?[...next.files]:[];
|
feat(queue): Codex-style message queue flyout above composer (#1040)
* chore: apply pending #965 queue flyout patches on local master
Queue flyout implementation (PR #965 — pending merge) applied on top of
upstream v0.50.205. Features:
- Queue card slides up from behind composer (approval-card pattern)
- Lucide icons via li(), CSS class system, no inline SVG dumps
- Drag-to-reorder by _queued_at timestamp (survives re-renders)
- Inline contenteditable edit with focus guard and blur-commit
- Combine preserves first item files, merge immediate (no 200ms race)
- Files/model compact badges per item
- Hide/expand via header chevron + composer pill + titlebar chip
- All 3 expand paths sync correctly
- border-bottom CSS order fixed, fingerprint improved, _dragTs guards
CF CSP domains also applied (deployment-specific, not in upstream PR).
* fix(queue): harden merge closure, toggleQueue sid, and drain flash
- mergeBtn _doMerge now reads live queue (_getSessionQueue) instead of stale closure q
- toggleQueue reads activeSid from S.session at call time, not captured param
- updateQueueBadge defers chips.innerHTML='' by 360ms so slide-out transition completes before content clears
* style(queue): contain:paint on inner, pill fade-in animation
* feat(queue): pill outside composer, compact collapsed state matching card width
- Move #queuePill out of .composer-box to between .composer-flyout and .composer-box
- Pill styled as compact queue-card-inner (same border, radius:14px 14px 0 0, no border-bottom)
- Pill width matches card inner: max-width:calc(var(--msg-max)-40px), centered
- Pill stays visible until user re-expands or queue drains (updateQueueBadge no longer
hides pill when card is manually collapsed)
- Remove all queue-active/queue-pill-active composer modifications — composer untouched
- Fix: mergeBtn reads live queue not stale closure
- Fix: toggleQueue uses S.session.session_id at call time not captured param
- Fix: chips.innerHTML deferred 360ms on drain to avoid empty-card flash
* fix(queue): collapsed state persists + cross-session DOM isolation
- Add _queueCollapsed[sid] flag: set by hideBtn, cleared by pill expand / queue drain
- _renderQueueChips respects flag — no longer reopens card when new message queued while collapsed
- updateQueueBadge else-branch: DOM mutations now gated on sid===active session
- _syncQueueTitlebar only fires for active session in else-branch
- Fixes Opus/Codex-identified bugs: pill auto-reopen and cross-session DOM corruption
* fix(queue): proper pill wrapper matching queue-card structure
- Add .queue-pill-outer div wrapper (max-width:var(--msg-max); padding:0 20px)
identical to .queue-card outer — positions pill button at exact card-inner width
- .queue-pill button fills slot with width:100%
- Removes hardcoded 740px — width is derived correctly from the same CSS variables
the card uses, scales with --msg-max across all viewports
- JS toggles .show on pillOuter (parentElement), not on pill button directly
---------
Co-authored-by: Basit Mustafa <basit.mustafa@gmail.com>
2026-04-25 21:21:50 +00:00
|
|
|
|
// Restore model from queued item (sent in /api/chat/start payload)
|
|
|
|
|
|
// Note: profile is NOT restored — full profile switch requires server interaction
|
|
|
|
|
|
if(next.model&&S.session&&next.model!==S.session.model){
|
|
|
|
|
|
S.session.model=next.model;
|
|
|
|
|
|
if(typeof _applyModelToDropdown==='function'&&$('modelSelect')) _applyModelToDropdown(next.model,$('modelSelect'));
|
|
|
|
|
|
if(typeof syncModelChip==='function') syncModelChip();
|
|
|
|
|
|
}
|
2026-04-13 23:18:15 +00:00
|
|
|
|
autoResize();
|
|
|
|
|
|
renderTray();
|
|
|
|
|
|
send();
|
|
|
|
|
|
},120);
|
2026-03-31 03:40:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(queue): Codex-style message queue flyout above composer (#1040)
* chore: apply pending #965 queue flyout patches on local master
Queue flyout implementation (PR #965 — pending merge) applied on top of
upstream v0.50.205. Features:
- Queue card slides up from behind composer (approval-card pattern)
- Lucide icons via li(), CSS class system, no inline SVG dumps
- Drag-to-reorder by _queued_at timestamp (survives re-renders)
- Inline contenteditable edit with focus guard and blur-commit
- Combine preserves first item files, merge immediate (no 200ms race)
- Files/model compact badges per item
- Hide/expand via header chevron + composer pill + titlebar chip
- All 3 expand paths sync correctly
- border-bottom CSS order fixed, fingerprint improved, _dragTs guards
CF CSP domains also applied (deployment-specific, not in upstream PR).
* fix(queue): harden merge closure, toggleQueue sid, and drain flash
- mergeBtn _doMerge now reads live queue (_getSessionQueue) instead of stale closure q
- toggleQueue reads activeSid from S.session at call time, not captured param
- updateQueueBadge defers chips.innerHTML='' by 360ms so slide-out transition completes before content clears
* style(queue): contain:paint on inner, pill fade-in animation
* feat(queue): pill outside composer, compact collapsed state matching card width
- Move #queuePill out of .composer-box to between .composer-flyout and .composer-box
- Pill styled as compact queue-card-inner (same border, radius:14px 14px 0 0, no border-bottom)
- Pill width matches card inner: max-width:calc(var(--msg-max)-40px), centered
- Pill stays visible until user re-expands or queue drains (updateQueueBadge no longer
hides pill when card is manually collapsed)
- Remove all queue-active/queue-pill-active composer modifications — composer untouched
- Fix: mergeBtn reads live queue not stale closure
- Fix: toggleQueue uses S.session.session_id at call time not captured param
- Fix: chips.innerHTML deferred 360ms on drain to avoid empty-card flash
* fix(queue): collapsed state persists + cross-session DOM isolation
- Add _queueCollapsed[sid] flag: set by hideBtn, cleared by pill expand / queue drain
- _renderQueueChips respects flag — no longer reopens card when new message queued while collapsed
- updateQueueBadge else-branch: DOM mutations now gated on sid===active session
- _syncQueueTitlebar only fires for active session in else-branch
- Fixes Opus/Codex-identified bugs: pill auto-reopen and cross-session DOM corruption
* fix(queue): proper pill wrapper matching queue-card structure
- Add .queue-pill-outer div wrapper (max-width:var(--msg-max); padding:0 20px)
identical to .queue-card outer — positions pill button at exact card-inner width
- .queue-pill button fills slot with width:100%
- Removes hardcoded 740px — width is derived correctly from the same CSS variables
the card uses, scales with --msg-max across all viewports
- JS toggles .show on pillOuter (parentElement), not on pill button directly
---------
Co-authored-by: Basit Mustafa <basit.mustafa@gmail.com>
2026-04-25 21:21:50 +00:00
|
|
|
|
// ── Queue chip display (Codex Desktop pattern) ─────────────────────────────
|
|
|
|
|
|
// Queued messages appear as chips inside #queueChips (above the textarea)
|
|
|
|
|
|
// while pending. When the session fires the queued message it becomes a
|
|
|
|
|
|
// normal user bubble in the chat — the chip is removed at drain time.
|
|
|
|
|
|
const _queueRenderKeys={}; // per-session fingerprint to avoid redundant rebuilds
|
|
|
|
|
|
const _queueCollapsed={}; // per-session: true when user explicitly collapsed the card
|
|
|
|
|
|
|
|
|
|
|
|
function _renderQueueChips(sid){
|
|
|
|
|
|
const card=document.getElementById('queueCard');
|
|
|
|
|
|
const inner=document.getElementById('queueChips');
|
|
|
|
|
|
if(!card||!inner) return;
|
|
|
|
|
|
const q=_getSessionQueue(sid,false);
|
|
|
|
|
|
const key=q.map(e=>{const t=e&&(e.text||e.message||e.content||'');return(e&&e._queued_at||0)+':'+t.length+':'+t.slice(0,20);}).join('|');
|
|
|
|
|
|
if(key===(_queueRenderKeys[sid]||'')&&key!='') return;
|
|
|
|
|
|
// Skip re-render if user is actively editing inside the queue panel
|
|
|
|
|
|
if(inner.contains(document.activeElement)&&document.activeElement!==inner) return;
|
|
|
|
|
|
_queueRenderKeys[sid]=key;
|
|
|
|
|
|
inner.innerHTML='';
|
|
|
|
|
|
if(!q.length){
|
|
|
|
|
|
card.classList.remove('visible');
|
|
|
|
|
|
const _msgs=document.getElementById('messages');
|
|
|
|
|
|
if(_msgs) _msgs.classList.remove('queue-open');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Respect user-collapsed state — don't reopen if user explicitly hid the card
|
|
|
|
|
|
if(_queueCollapsed[sid]){
|
|
|
|
|
|
// Update chips content without showing card (so data is fresh if user re-expands)
|
|
|
|
|
|
inner.innerHTML='';
|
|
|
|
|
|
// fall through to render rows into inner but skip making card visible
|
|
|
|
|
|
} else {
|
|
|
|
|
|
card.classList.add('visible');
|
|
|
|
|
|
}
|
|
|
|
|
|
// Push messages area up so content isn't hidden behind the flyout
|
|
|
|
|
|
const _msgs=document.getElementById('messages');
|
|
|
|
|
|
if(_msgs&&!_queueCollapsed[sid]){
|
|
|
|
|
|
_msgs.classList.add('queue-open');
|
|
|
|
|
|
// Measure after 350ms transition completes (not mid-animation — height would be wrong)
|
|
|
|
|
|
setTimeout(()=>{
|
|
|
|
|
|
if(!card.classList.contains('visible')) return;
|
|
|
|
|
|
const h=card.getBoundingClientRect().height;
|
|
|
|
|
|
if(h>0) _msgs.style.setProperty('--queue-card-height', h+'px');
|
|
|
|
|
|
if(typeof scrollToBottom==='function') scrollToBottom();
|
|
|
|
|
|
}, 360);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function _saveAndRefresh(){
|
|
|
|
|
|
const liveQ=_getSessionQueue(sid,false);
|
|
|
|
|
|
if(!liveQ.length){delete SESSION_QUEUES[sid];try{sessionStorage.removeItem('hermes-queue-'+sid);}catch(_){}}
|
|
|
|
|
|
else{SESSION_QUEUES[sid]=[...liveQ];try{sessionStorage.setItem('hermes-queue-'+sid,JSON.stringify(liveQ));}catch(_){}}
|
|
|
|
|
|
delete _queueRenderKeys[sid];
|
|
|
|
|
|
updateQueueBadge(sid);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Header (2+ items)
|
|
|
|
|
|
if(q.length>1){
|
|
|
|
|
|
const header=document.createElement('div');
|
|
|
|
|
|
header.className='queue-card-header';
|
|
|
|
|
|
const lbl=document.createElement('span');
|
|
|
|
|
|
lbl.textContent=typeof t==='function'?t('queued_count',q.length):(q.length===1?'1 queued':`${q.length} queued`);
|
|
|
|
|
|
lbl.title='Sends automatically after the current response completes';
|
|
|
|
|
|
const actions=document.createElement('span');
|
|
|
|
|
|
actions.className='queue-card-header-actions';
|
|
|
|
|
|
const hasFiles=q.some(e=>e&&Array.isArray(e.files)&&e.files.length>0);
|
|
|
|
|
|
const mergeBtn=document.createElement('button');
|
|
|
|
|
|
mergeBtn.className='queue-card-btn';
|
|
|
|
|
|
mergeBtn.title='Combine all into one message'+(hasFiles?' — attachments will be removed':'');
|
|
|
|
|
|
mergeBtn.innerHTML=li('layers',12)+'Combine';
|
|
|
|
|
|
mergeBtn.onclick=()=>{
|
|
|
|
|
|
const _doMerge=(snapshot)=>{
|
|
|
|
|
|
const combined=snapshot.map(e=>e&&(e.text||e.message||e.content||'')).filter(Boolean).join('\n\n');
|
|
|
|
|
|
const liveQ=_getSessionQueue(sid,false);
|
|
|
|
|
|
const firstFiles=(snapshot.find(e=>e&&Array.isArray(e.files)&&e.files.length)||{files:[]}).files;
|
|
|
|
|
|
liveQ.length=0;liveQ.push({text:combined,files:firstFiles,_queued_at:Date.now()});
|
|
|
|
|
|
SESSION_QUEUES[sid]=liveQ;
|
|
|
|
|
|
try{sessionStorage.setItem('hermes-queue-'+sid,JSON.stringify(liveQ));}catch(_){}
|
|
|
|
|
|
delete _queueRenderKeys[sid];
|
|
|
|
|
|
updateQueueBadge(sid);
|
|
|
|
|
|
};
|
|
|
|
|
|
if(hasFiles){
|
|
|
|
|
|
if(typeof showToast==='function') showToast('Attachments on queued items will be removed',2600,'warning');
|
|
|
|
|
|
}
|
|
|
|
|
|
// Merge from current live queue (no delay — snapshot + defer caused data-loss races)
|
|
|
|
|
|
_doMerge([..._getSessionQueue(sid,false)]);
|
|
|
|
|
|
};
|
|
|
|
|
|
const clearBtn=document.createElement('button');
|
|
|
|
|
|
clearBtn.className='queue-card-icon-btn';
|
|
|
|
|
|
clearBtn.title='Clear all queued messages';
|
|
|
|
|
|
clearBtn.setAttribute('aria-label','Clear all queued messages');
|
|
|
|
|
|
clearBtn.innerHTML=li('x',13);
|
|
|
|
|
|
clearBtn.onclick=()=>{q.length=0;_saveAndRefresh();};
|
|
|
|
|
|
actions.appendChild(mergeBtn);
|
|
|
|
|
|
actions.appendChild(clearBtn);
|
|
|
|
|
|
// Hide button — collapses flyout entirely; titlebar "N queued" re-shows it
|
|
|
|
|
|
const hideBtn=document.createElement('button');
|
|
|
|
|
|
hideBtn.className='queue-card-icon-btn';
|
|
|
|
|
|
hideBtn.title='Hide queue (click the titlebar badge to show again)';
|
|
|
|
|
|
hideBtn.setAttribute('aria-label','Hide queue panel');
|
|
|
|
|
|
hideBtn.innerHTML=li('chevron-down',14);
|
|
|
|
|
|
hideBtn.onclick=()=>{
|
|
|
|
|
|
_queueCollapsed[sid]=true;
|
|
|
|
|
|
card.classList.remove('visible');
|
|
|
|
|
|
// Read live count at click time (not stale closure q)
|
|
|
|
|
|
_updateQueuePill(sid,_getSessionQueue(sid,false).length);
|
|
|
|
|
|
};
|
|
|
|
|
|
actions.appendChild(hideBtn);
|
|
|
|
|
|
header.appendChild(lbl);
|
|
|
|
|
|
header.appendChild(actions);
|
|
|
|
|
|
inner.appendChild(header);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let _dragTs=null; // use _queued_at timestamp — survives re-renders, not an index
|
|
|
|
|
|
q.forEach((entry,i)=>{
|
|
|
|
|
|
const _entryTs=entry&&entry._queued_at;
|
|
|
|
|
|
const entryText=entry&&(entry.text||entry.message||entry.content||'');
|
|
|
|
|
|
const _files=entry&&Array.isArray(entry.files)?entry.files.filter(Boolean):[];
|
|
|
|
|
|
const row=document.createElement('div');
|
|
|
|
|
|
row.className='queue-card-row';
|
|
|
|
|
|
row.setAttribute('role','listitem');
|
|
|
|
|
|
row.setAttribute('draggable','true');
|
|
|
|
|
|
row.ondragstart=(e)=>{if(_entryTs==null) return;_dragTs=_entryTs;row.style.opacity='.4';e.dataTransfer.effectAllowed='move';};
|
|
|
|
|
|
row.ondragend=()=>{row.style.opacity='';};
|
|
|
|
|
|
row.ondragover=(e)=>{e.preventDefault();row.style.background='var(--hover-bg)';};
|
|
|
|
|
|
row.ondragleave=()=>{row.style.background='';};
|
|
|
|
|
|
row.ondrop=(e)=>{
|
|
|
|
|
|
e.preventDefault();row.style.background='';
|
|
|
|
|
|
if(_dragTs!=null&&_dragTs!==_entryTs){
|
|
|
|
|
|
const fromIdx=q.findIndex(e=>e&&e._queued_at===_dragTs);
|
|
|
|
|
|
if(fromIdx!==-1&&fromIdx!==i){const moved=q.splice(fromIdx,1)[0];q.splice(i,0,moved);}
|
|
|
|
|
|
_dragTs=null;_saveAndRefresh();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
// Drag handle
|
|
|
|
|
|
const drag=document.createElement('span');
|
|
|
|
|
|
drag.className='queue-card-drag';
|
|
|
|
|
|
drag.setAttribute('aria-hidden','true');
|
|
|
|
|
|
drag.innerHTML=typeof li==='function'?li('list-todo',13):'≡';
|
|
|
|
|
|
// Inline-editable text
|
|
|
|
|
|
const msgSpan=document.createElement('span');
|
|
|
|
|
|
msgSpan.className='queue-card-text';
|
|
|
|
|
|
msgSpan.setAttribute('contenteditable','true');
|
|
|
|
|
|
msgSpan.setAttribute('role','textbox');
|
|
|
|
|
|
msgSpan.setAttribute('aria-label','Queued message — edit in place');
|
|
|
|
|
|
msgSpan.textContent=entryText||(_files.length?'':'—');
|
|
|
|
|
|
msgSpan.setAttribute('draggable','false');
|
|
|
|
|
|
msgSpan.onfocus=()=>{msgSpan.style.overflow='auto';msgSpan.style.whiteSpace='pre-wrap';msgSpan.style.textOverflow='clip';};
|
|
|
|
|
|
msgSpan.onblur=()=>{
|
|
|
|
|
|
msgSpan.style.overflow='';msgSpan.style.whiteSpace='';msgSpan.style.textOverflow='';
|
|
|
|
|
|
const newText=msgSpan.textContent.trim();
|
|
|
|
|
|
if(newText===''&&!_files.length){ msgSpan.textContent=entryText||'—'; return; }
|
|
|
|
|
|
if(newText!==entryText){
|
|
|
|
|
|
const liveQ=_getSessionQueue(sid,false);
|
|
|
|
|
|
const idx=_entryTs!=null?liveQ.findIndex(e=>e&&e._queued_at===_entryTs):i;
|
|
|
|
|
|
if(idx!==-1){
|
|
|
|
|
|
liveQ[idx]={...liveQ[idx],text:newText};
|
|
|
|
|
|
try{sessionStorage.setItem('hermes-queue-'+sid,JSON.stringify(liveQ));}catch(_){}
|
|
|
|
|
|
delete _queueRenderKeys[sid];
|
|
|
|
|
|
updateQueueBadge(sid);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
msgSpan.onkeydown=(e)=>{if(e.key==='Enter'){e.preventDefault();msgSpan.blur();}if(e.key==='Escape'){msgSpan.textContent=entryText||'—';msgSpan.blur();}};
|
|
|
|
|
|
// Compact badges (files, model, profile)
|
|
|
|
|
|
const badges=document.createElement('span');
|
|
|
|
|
|
badges.className='queue-card-badges';
|
|
|
|
|
|
if(_files.length>0){
|
|
|
|
|
|
const fb=document.createElement('span');
|
|
|
|
|
|
fb.className='queue-card-file-badge';
|
|
|
|
|
|
fb.title=_files.map(f=>f&&f.name||'file').join(', ');
|
|
|
|
|
|
fb.innerHTML=li('paperclip',11)+_files.length;
|
|
|
|
|
|
badges.appendChild(fb);
|
|
|
|
|
|
}
|
|
|
|
|
|
const _model=entry&&entry.model;
|
|
|
|
|
|
if(_model){
|
|
|
|
|
|
const mb=document.createElement('span');
|
|
|
|
|
|
mb.title='Model: '+_model;
|
|
|
|
|
|
// Use the app's friendly label system if available
|
|
|
|
|
|
const _modelLabel=(typeof _dynamicModelLabels!=='undefined'&&_dynamicModelLabels[_model])
|
|
|
|
|
|
||_model.split('/').pop().replace(/^(gpt-|claude-3\.?5?-|claude-|gemini-)/,'').replace(/-\d{4}-\d{2}-\d{2}$/,'').slice(0,12);
|
|
|
|
|
|
mb.textContent=_modelLabel;
|
|
|
|
|
|
badges.appendChild(mb);
|
|
|
|
|
|
}
|
|
|
|
|
|
// Profile badge removed — drain cannot server-switch profiles so badge was misleading
|
|
|
|
|
|
// Delete button
|
|
|
|
|
|
const delBtn=document.createElement('button');
|
|
|
|
|
|
delBtn.className='queue-card-icon-btn';
|
|
|
|
|
|
delBtn.setAttribute('aria-label',typeof t==='function'?t('queued_cancel'):'Remove queued message');
|
|
|
|
|
|
delBtn.setAttribute('draggable','false');
|
|
|
|
|
|
delBtn.title='Remove from queue';
|
|
|
|
|
|
delBtn.innerHTML=li('x',13);
|
|
|
|
|
|
delBtn.onclick=()=>{
|
|
|
|
|
|
const liveQ=_getSessionQueue(sid,false);
|
|
|
|
|
|
const idx=_entryTs!=null?liveQ.findIndex(e=>e&&e._queued_at===_entryTs):i;
|
|
|
|
|
|
if(idx!==-1) liveQ.splice(idx,1);
|
|
|
|
|
|
if(!liveQ.length){delete SESSION_QUEUES[sid];try{sessionStorage.removeItem('hermes-queue-'+sid);}catch(_){}}
|
|
|
|
|
|
else{SESSION_QUEUES[sid]=[...liveQ];try{sessionStorage.setItem('hermes-queue-'+sid,JSON.stringify(liveQ));}catch(_){}}
|
|
|
|
|
|
delete _queueRenderKeys[sid];
|
|
|
|
|
|
updateQueueBadge(sid);
|
|
|
|
|
|
};
|
|
|
|
|
|
row.appendChild(drag);
|
|
|
|
|
|
row.appendChild(msgSpan);
|
|
|
|
|
|
if(badges.childNodes.length) row.appendChild(badges);
|
|
|
|
|
|
row.appendChild(delBtn);
|
|
|
|
|
|
inner.appendChild(row);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function _updateQueuePill(sid,count){
|
|
|
|
|
|
const pill=document.getElementById('queuePill');
|
|
|
|
|
|
if(!pill) return;
|
|
|
|
|
|
const pillOuter=pill.parentElement; // .queue-pill-outer — same wrapper as .queue-card
|
|
|
|
|
|
const card=document.getElementById('queueCard');
|
|
|
|
|
|
const flyoutVisible=card&&card.classList.contains('visible');
|
|
|
|
|
|
if(count>0&&!flyoutVisible){
|
|
|
|
|
|
const label=typeof t==='function'?t('queued_count',count):(count===1?'1 queued':`${count} queued`);
|
|
|
|
|
|
pill.innerHTML=(typeof li==='function'?li('list-todo',12):'')+
|
|
|
|
|
|
`<span class="queue-pill-count">${label}</span>`+
|
|
|
|
|
|
`<span class="queue-pill-chevron">`+(typeof li==='function'?li('chevron-up',12):'▲')+`</span>`;
|
|
|
|
|
|
pill.title='Show queued messages';
|
|
|
|
|
|
if(pillOuter) pillOuter.classList.add('show');
|
|
|
|
|
|
pill.onclick=()=>{
|
|
|
|
|
|
delete _queueCollapsed[sid];
|
|
|
|
|
|
const c=document.getElementById('queueCard');
|
|
|
|
|
|
if(c){
|
|
|
|
|
|
c.classList.add('visible');
|
|
|
|
|
|
setTimeout(()=>{
|
|
|
|
|
|
const firstFocusable=c.querySelector('.queue-card-text, .queue-card-icon-btn');
|
|
|
|
|
|
if(firstFocusable) firstFocusable.focus();
|
|
|
|
|
|
}, 360);
|
|
|
|
|
|
}
|
|
|
|
|
|
if(pillOuter) pillOuter.classList.remove('show');
|
|
|
|
|
|
if(typeof scrollToBottom==='function') scrollToBottom();
|
|
|
|
|
|
};
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if(pillOuter) pillOuter.classList.remove('show');
|
|
|
|
|
|
pill.onclick=null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function _syncQueueTitlebar(sid,count){
|
|
|
|
|
|
const sub=document.getElementById('appTitlebarSub');
|
|
|
|
|
|
if(!sub) return;
|
|
|
|
|
|
if(count>0){
|
|
|
|
|
|
const label=typeof t==='function'?t('queued_count',count):(count===1?'1 queued':`${count} queued`);
|
|
|
|
|
|
sub.textContent=label;
|
|
|
|
|
|
sub.hidden=false;
|
|
|
|
|
|
sub.setAttribute('role','button');
|
|
|
|
|
|
sub.setAttribute('tabindex','0');
|
|
|
|
|
|
sub.setAttribute('aria-label',label);
|
|
|
|
|
|
sub.style.cursor='pointer';sub.style.color='var(--muted)';sub.style.fontWeight='600';sub.style.fontSize='11px';sub.style.background='var(--hover-bg)';sub.style.padding='2px 6px';sub.style.borderRadius='6px';sub.style.border='1px solid var(--border)';
|
|
|
|
|
|
const toggleQueue=()=>{
|
|
|
|
|
|
const activeSid=S.session&&S.session.session_id;
|
|
|
|
|
|
if(!activeSid) return;
|
|
|
|
|
|
const qCard=document.getElementById('queueCard');
|
|
|
|
|
|
if(!qCard) return;
|
|
|
|
|
|
if(qCard.classList.contains('visible')){
|
|
|
|
|
|
qCard.classList.remove('visible');
|
|
|
|
|
|
// Show pill since flyout is now hidden
|
|
|
|
|
|
const liveCount=_getSessionQueue(activeSid,false).length;
|
|
|
|
|
|
_updateQueuePill(activeSid,liveCount);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
qCard.classList.add('visible');
|
|
|
|
|
|
// Hide pill since flyout is now showing
|
|
|
|
|
|
_updateQueuePill(activeSid,0);
|
|
|
|
|
|
if(typeof scrollToBottom==='function') scrollToBottom();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
sub.onclick=toggleQueue;
|
|
|
|
|
|
sub.onkeydown=(e)=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();toggleQueue();}};
|
|
|
|
|
|
sub.title='Click to show/hide queue panel';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
sub.textContent='';sub.hidden=true;
|
|
|
|
|
|
sub.removeAttribute('role');sub.removeAttribute('tabindex');sub.removeAttribute('aria-label');
|
|
|
|
|
|
sub.style.cssText='';
|
|
|
|
|
|
sub.onclick=null;sub.onkeydown=null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 23:18:15 +00:00
|
|
|
|
function updateQueueBadge(sessionId){
|
|
|
|
|
|
const sid=sessionId||(S.session&&S.session.session_id);
|
|
|
|
|
|
const count=sid?getQueuedSessionCount(sid):0;
|
feat(queue): Codex-style message queue flyout above composer (#1040)
* chore: apply pending #965 queue flyout patches on local master
Queue flyout implementation (PR #965 — pending merge) applied on top of
upstream v0.50.205. Features:
- Queue card slides up from behind composer (approval-card pattern)
- Lucide icons via li(), CSS class system, no inline SVG dumps
- Drag-to-reorder by _queued_at timestamp (survives re-renders)
- Inline contenteditable edit with focus guard and blur-commit
- Combine preserves first item files, merge immediate (no 200ms race)
- Files/model compact badges per item
- Hide/expand via header chevron + composer pill + titlebar chip
- All 3 expand paths sync correctly
- border-bottom CSS order fixed, fingerprint improved, _dragTs guards
CF CSP domains also applied (deployment-specific, not in upstream PR).
* fix(queue): harden merge closure, toggleQueue sid, and drain flash
- mergeBtn _doMerge now reads live queue (_getSessionQueue) instead of stale closure q
- toggleQueue reads activeSid from S.session at call time, not captured param
- updateQueueBadge defers chips.innerHTML='' by 360ms so slide-out transition completes before content clears
* style(queue): contain:paint on inner, pill fade-in animation
* feat(queue): pill outside composer, compact collapsed state matching card width
- Move #queuePill out of .composer-box to between .composer-flyout and .composer-box
- Pill styled as compact queue-card-inner (same border, radius:14px 14px 0 0, no border-bottom)
- Pill width matches card inner: max-width:calc(var(--msg-max)-40px), centered
- Pill stays visible until user re-expands or queue drains (updateQueueBadge no longer
hides pill when card is manually collapsed)
- Remove all queue-active/queue-pill-active composer modifications — composer untouched
- Fix: mergeBtn reads live queue not stale closure
- Fix: toggleQueue uses S.session.session_id at call time not captured param
- Fix: chips.innerHTML deferred 360ms on drain to avoid empty-card flash
* fix(queue): collapsed state persists + cross-session DOM isolation
- Add _queueCollapsed[sid] flag: set by hideBtn, cleared by pill expand / queue drain
- _renderQueueChips respects flag — no longer reopens card when new message queued while collapsed
- updateQueueBadge else-branch: DOM mutations now gated on sid===active session
- _syncQueueTitlebar only fires for active session in else-branch
- Fixes Opus/Codex-identified bugs: pill auto-reopen and cross-session DOM corruption
* fix(queue): proper pill wrapper matching queue-card structure
- Add .queue-pill-outer div wrapper (max-width:var(--msg-max); padding:0 20px)
identical to .queue-card outer — positions pill button at exact card-inner width
- .queue-pill button fills slot with width:100%
- Removes hardcoded 740px — width is derived correctly from the same CSS variables
the card uses, scales with --msg-max across all viewports
- JS toggles .show on pillOuter (parentElement), not on pill button directly
---------
Co-authored-by: Basit Mustafa <basit.mustafa@gmail.com>
2026-04-25 21:21:50 +00:00
|
|
|
|
if(count>0&&S.session&&sid===S.session.session_id){
|
|
|
|
|
|
_renderQueueChips(sid);
|
|
|
|
|
|
// If card is visible, hide pill. If card is collapsed, update pill count.
|
|
|
|
|
|
const _cardEl=document.getElementById('queueCard');
|
|
|
|
|
|
_updateQueuePill(sid,(_cardEl&&_cardEl.classList.contains('visible'))?0:count);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Always clean up per-session data
|
|
|
|
|
|
if(sid){delete _queueRenderKeys[sid];delete _queueCollapsed[sid];}
|
|
|
|
|
|
// Only wipe global DOM if this is the currently active session
|
|
|
|
|
|
const isActive=S.session&&sid===S.session.session_id;
|
|
|
|
|
|
if(isActive){
|
|
|
|
|
|
const card=document.getElementById('queueCard');
|
|
|
|
|
|
const chips=document.getElementById('queueChips');
|
|
|
|
|
|
if(card) card.classList.remove('visible');
|
|
|
|
|
|
// Defer clear until after slide-out transition so content doesn't vanish mid-animation
|
|
|
|
|
|
if(chips){const _chips=chips;const _card=card;setTimeout(()=>{if(!_card||!_card.classList.contains('visible'))_chips.innerHTML='';},360);}
|
|
|
|
|
|
const _msgsEl=document.getElementById('messages');
|
|
|
|
|
|
if(_msgsEl) _msgsEl.classList.remove('queue-open');
|
|
|
|
|
|
_updateQueuePill(sid,0);
|
2026-03-31 03:40:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
feat(queue): Codex-style message queue flyout above composer (#1040)
* chore: apply pending #965 queue flyout patches on local master
Queue flyout implementation (PR #965 — pending merge) applied on top of
upstream v0.50.205. Features:
- Queue card slides up from behind composer (approval-card pattern)
- Lucide icons via li(), CSS class system, no inline SVG dumps
- Drag-to-reorder by _queued_at timestamp (survives re-renders)
- Inline contenteditable edit with focus guard and blur-commit
- Combine preserves first item files, merge immediate (no 200ms race)
- Files/model compact badges per item
- Hide/expand via header chevron + composer pill + titlebar chip
- All 3 expand paths sync correctly
- border-bottom CSS order fixed, fingerprint improved, _dragTs guards
CF CSP domains also applied (deployment-specific, not in upstream PR).
* fix(queue): harden merge closure, toggleQueue sid, and drain flash
- mergeBtn _doMerge now reads live queue (_getSessionQueue) instead of stale closure q
- toggleQueue reads activeSid from S.session at call time, not captured param
- updateQueueBadge defers chips.innerHTML='' by 360ms so slide-out transition completes before content clears
* style(queue): contain:paint on inner, pill fade-in animation
* feat(queue): pill outside composer, compact collapsed state matching card width
- Move #queuePill out of .composer-box to between .composer-flyout and .composer-box
- Pill styled as compact queue-card-inner (same border, radius:14px 14px 0 0, no border-bottom)
- Pill width matches card inner: max-width:calc(var(--msg-max)-40px), centered
- Pill stays visible until user re-expands or queue drains (updateQueueBadge no longer
hides pill when card is manually collapsed)
- Remove all queue-active/queue-pill-active composer modifications — composer untouched
- Fix: mergeBtn reads live queue not stale closure
- Fix: toggleQueue uses S.session.session_id at call time not captured param
- Fix: chips.innerHTML deferred 360ms on drain to avoid empty-card flash
* fix(queue): collapsed state persists + cross-session DOM isolation
- Add _queueCollapsed[sid] flag: set by hideBtn, cleared by pill expand / queue drain
- _renderQueueChips respects flag — no longer reopens card when new message queued while collapsed
- updateQueueBadge else-branch: DOM mutations now gated on sid===active session
- _syncQueueTitlebar only fires for active session in else-branch
- Fixes Opus/Codex-identified bugs: pill auto-reopen and cross-session DOM corruption
* fix(queue): proper pill wrapper matching queue-card structure
- Add .queue-pill-outer div wrapper (max-width:var(--msg-max); padding:0 20px)
identical to .queue-card outer — positions pill button at exact card-inner width
- .queue-pill button fills slot with width:100%
- Removes hardcoded 740px — width is derived correctly from the same CSS variables
the card uses, scales with --msg-max across all viewports
- JS toggles .show on pillOuter (parentElement), not on pill button directly
---------
Co-authored-by: Basit Mustafa <basit.mustafa@gmail.com>
2026-04-25 21:21:50 +00:00
|
|
|
|
// Only update titlebar if this is the active session
|
|
|
|
|
|
if(!S.session||sid===S.session.session_id) _syncQueueTitlebar(sid,count);
|
2026-03-31 03:40:19 +00:00
|
|
|
|
}
|
refactor(ui): three-column layout with left rail + main-view migration (#899)
refactor(ui): three-column layout with left rail + main-view migration (#899)
Unifies the shell into a three-column layout (rail + sidebar + main) matching the
hermes-desktop reference, and migrates every per-item detail/edit surface into a
shared main-view canvas with consistent headers, empty states, and action buttons.
Changes:
- New desktop-only left rail (48px) with 8 nav tabs (chat/tasks/skills/memory/workspaces/profiles/todos/settings)
- Persistent app titlebar (replaces per-chat topbar), active conversation title shown
- All panel detail/create/edit views migrated to #mainSkills, #mainTasks, #mainSettings, #mainWorkspaces, #mainProfiles, #mainMemory
- Settings moved out of modal into main-view page; ESC closes it
- YAML frontmatter rendered in collapsible <details> block in skill detail
- Toasts repositioned from bottom-center to top-right with theme-aware success/error/warning/info variants
- Composer workspace chip split into two-button group: files-icon toggles file panel, label opens workspace picker
- .settings-menu → .side-menu / .side-menu-item (generalised, shared by memory and settings panels)
- i18n: ~25 new keys across en/ru/es/de/zh/zh-Hant for all new form labels, placeholders, and empty states
- Mobile: hamburger in titlebar, slide-in sidebar; box-shadow removed from sidebar
- New regression test: tests/test_settings_navigation_and_detail_refresh.py (9 tests)
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
2026-04-24 16:05:25 +00:00
|
|
|
|
function showToast(msg,ms,type){const el=$('toast');if(!el)return;const s=String(msg==null?'':msg);let t=type;if(!t){const low=s.toLowerCase();if(/fail|error|denied|invalid|unavailable|no active|no workspace match|no model match|no personalities/.test(low))t='error';else if(/warn|queued|takes effect|skipped|fallback/.test(low))t='warning';else if(/saved|created|imported|restored|switched|set to|updated|duplicated|moved to|renamed|deleted|complete|pinned|archived|cleared|stopped/.test(low))t='success';else t='info';}el.textContent=s;el.className='toast show '+t;clearTimeout(el._t);el._t=setTimeout(()=>{el.classList.remove('show');},ms||2800);}
|
2026-03-31 03:40:19 +00:00
|
|
|
|
|
v0.47.0: dialogs, session menu, /skills, mobile fixes, mobile QA suite
* fix: custom provider with slash model name no longer rerouted to OpenRouter (#255)
When base_url is configured in config.yaml, resolve_model_provider() now
trusts the configured provider/base_url entirely and skips the slash-based
OpenRouter heuristic. Fixes google/gemma-4-26b-a4b with provider:custom
being silently routed to OpenRouter, resulting in 401 errors.
Fixes #230
* test: mobile layout regression suite — 14 tests for every QA run (#254)
Adds tests/test_mobile_layout.py with 14 static regression tests that run
on every QA pass to catch mobile layout breakage before it reaches prod.
Covers: breakpoints at 900px/640px, right panel slide-over CSS, mobile
overlay, bottom nav, files button, profile dropdown z-index, chip overflow,
workspace close, 100dvh, 44px touch targets, 16px font-size on textarea.
* feat: /skills slash command lists and filters available Hermes skills (#257)
Adds /skills [query] command to commands.js. Fetches from /api/skills,
groups by category (alphabetically sorted), displays as a formatted
assistant message. Optional query filters by name, description, or category.
i18n keys added for en, de, zh, zh-Hant. 1 regression test added.
Fixes #248
* feat: shared app dialogs replace native confirm()/prompt() calls (#251)
Adds showConfirmDialog() and showPromptDialog() helpers to ui.js, backed
by a themed #appDialogOverlay. Replaces all 11 native browser confirm/prompt
call sites across panels.js, sessions.js, ui.js, workspace.js.
Supports: danger mode, keyboard focus trap (Tab/Escape/Enter), focus restore,
ARIA roles, mobile-responsive stacked buttons at 640px. i18n for en/de/zh/zh-Hant.
5 new tests in test_sprint33.py verify markup, CSS, helpers, and absence of
native dialog calls.
Extracted from PR #242.
* fix: Android Chrome mobile — workspace panel close + profile dropdown (#256)
Fix #247: toggleMobileFiles() now shows/hides the mobile overlay when
toggling the right workspace panel. New closeMobileFiles() helper closes
the panel with correct overlay state tracking. Overlay onclick calls both
closeMobileSidebar() and closeMobileFiles(). Mobile-only close button (x)
added to workspace panel header.
Fix #246: profile dropdown uses position:fixed;top:56px;right:8px at
max-width:900px, escaping the overflow-x:auto stacking context that was
clipping it on Android Chrome.
Fix applied during review: closeMobileSidebar() now checks if the right
panel is still open before hiding the overlay, preventing the overlay from
disappearing when only the sidebar is closed.
Fixes #247 Fixes #246
* feat: session ⋯ action dropdown replaces per-row buttons (#252)
Replaces the 5 per-row hover action buttons (pin/move/archive/duplicate/trash)
with a single ⋯ trigger that opens a positioned dropdown menu. Menu has full
keyboard (Escape), click-outside, scroll, and resize-reposition handling.
Position:fixed prevents sidebar clipping.
5 actions: Pin/Unpin, Move to project, Archive/Unarchive, Duplicate, Delete
(danger style). Each with icon and descriptive subtitle.
Updated test_sprint16.py: test_sessions_js_uses_action_menu_not_per_row_buttons
asserts the new trigger and menu functions exist, old per-row classes are gone.
Extracted from PR #242.
* docs: v0.47.0 release notes, bump version, update test counts (645)
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 19:19:12 +00:00
|
|
|
|
// ── Shared app dialogs ───────────────────────────────────────────────────────
|
|
|
|
|
|
// showConfirmDialog(opts) and showPromptDialog(opts) replace browser-native dialog calls
|
|
|
|
|
|
// throughout the UI. Both return Promises and support: title, message, confirmLabel,
|
|
|
|
|
|
// cancelLabel, danger (confirm only), placeholder/value/inputType (prompt only).
|
|
|
|
|
|
|
|
|
|
|
|
const APP_DIALOG={resolve:null,kind:null,lastFocus:null};
|
|
|
|
|
|
let _appDialogBound=false;
|
|
|
|
|
|
|
|
|
|
|
|
function _isAppDialogOpen(){
|
|
|
|
|
|
const overlay=$('appDialogOverlay');
|
|
|
|
|
|
return !!(overlay&&overlay.style.display!=='none');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function _getAppDialogFocusable(){
|
|
|
|
|
|
return [$('appDialogInput'), $('appDialogCancel'), $('appDialogConfirm'), $('appDialogClose')]
|
|
|
|
|
|
.filter(el=>el&&el.style.display!=='none'&&!el.disabled);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function _finishAppDialog(result, restoreFocus=true){
|
|
|
|
|
|
const overlay=$('appDialogOverlay');
|
|
|
|
|
|
const dialog=$('appDialog');
|
|
|
|
|
|
const input=$('appDialogInput');
|
|
|
|
|
|
const confirmBtn=$('appDialogConfirm');
|
|
|
|
|
|
const resolve=APP_DIALOG.resolve;
|
|
|
|
|
|
const lastFocus=APP_DIALOG.lastFocus;
|
|
|
|
|
|
APP_DIALOG.resolve=null;
|
|
|
|
|
|
APP_DIALOG.kind=null;
|
|
|
|
|
|
APP_DIALOG.lastFocus=null;
|
|
|
|
|
|
if(overlay){overlay.style.display='none';overlay.setAttribute('aria-hidden','true');}
|
|
|
|
|
|
if(dialog) dialog.setAttribute('role','dialog');
|
|
|
|
|
|
if(input){input.value='';input.style.display='none';input.placeholder='';}
|
|
|
|
|
|
if(confirmBtn){confirmBtn.classList.remove('danger');confirmBtn.textContent=t('dialog_confirm_btn');}
|
|
|
|
|
|
if(restoreFocus&&lastFocus&&typeof lastFocus.focus==='function'){setTimeout(()=>lastFocus.focus(),0);}
|
|
|
|
|
|
if(resolve) resolve(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function _ensureAppDialogBindings(){
|
|
|
|
|
|
if(_appDialogBound) return;
|
|
|
|
|
|
_appDialogBound=true;
|
|
|
|
|
|
const overlay=$('appDialogOverlay');
|
|
|
|
|
|
const cancelBtn=$('appDialogCancel');
|
|
|
|
|
|
const confirmBtn=$('appDialogConfirm');
|
|
|
|
|
|
const closeBtn=$('appDialogClose');
|
|
|
|
|
|
if(overlay){
|
|
|
|
|
|
overlay.addEventListener('click',e=>{
|
|
|
|
|
|
if(e.target===overlay) _finishAppDialog(APP_DIALOG.kind==='prompt'?null:false);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
if(cancelBtn) cancelBtn.addEventListener('click',()=>_finishAppDialog(APP_DIALOG.kind==='prompt'?null:false));
|
|
|
|
|
|
if(closeBtn) closeBtn.addEventListener('click',()=>_finishAppDialog(APP_DIALOG.kind==='prompt'?null:false));
|
|
|
|
|
|
if(confirmBtn){
|
|
|
|
|
|
confirmBtn.addEventListener('click',()=>{
|
|
|
|
|
|
if(APP_DIALOG.kind==='prompt'){
|
|
|
|
|
|
const input=$('appDialogInput');
|
|
|
|
|
|
_finishAppDialog(input?input.value:null);
|
|
|
|
|
|
}else{
|
|
|
|
|
|
_finishAppDialog(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
document.addEventListener('keydown',e=>{
|
|
|
|
|
|
if(!_isAppDialogOpen()) return;
|
|
|
|
|
|
if(e.key==='Escape'){
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
_finishAppDialog(APP_DIALOG.kind==='prompt'?null:false);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if(e.key==='Enter'){
|
2026-04-15 15:12:47 +00:00
|
|
|
|
if(e.isComposing) return;
|
v0.47.0: dialogs, session menu, /skills, mobile fixes, mobile QA suite
* fix: custom provider with slash model name no longer rerouted to OpenRouter (#255)
When base_url is configured in config.yaml, resolve_model_provider() now
trusts the configured provider/base_url entirely and skips the slash-based
OpenRouter heuristic. Fixes google/gemma-4-26b-a4b with provider:custom
being silently routed to OpenRouter, resulting in 401 errors.
Fixes #230
* test: mobile layout regression suite — 14 tests for every QA run (#254)
Adds tests/test_mobile_layout.py with 14 static regression tests that run
on every QA pass to catch mobile layout breakage before it reaches prod.
Covers: breakpoints at 900px/640px, right panel slide-over CSS, mobile
overlay, bottom nav, files button, profile dropdown z-index, chip overflow,
workspace close, 100dvh, 44px touch targets, 16px font-size on textarea.
* feat: /skills slash command lists and filters available Hermes skills (#257)
Adds /skills [query] command to commands.js. Fetches from /api/skills,
groups by category (alphabetically sorted), displays as a formatted
assistant message. Optional query filters by name, description, or category.
i18n keys added for en, de, zh, zh-Hant. 1 regression test added.
Fixes #248
* feat: shared app dialogs replace native confirm()/prompt() calls (#251)
Adds showConfirmDialog() and showPromptDialog() helpers to ui.js, backed
by a themed #appDialogOverlay. Replaces all 11 native browser confirm/prompt
call sites across panels.js, sessions.js, ui.js, workspace.js.
Supports: danger mode, keyboard focus trap (Tab/Escape/Enter), focus restore,
ARIA roles, mobile-responsive stacked buttons at 640px. i18n for en/de/zh/zh-Hant.
5 new tests in test_sprint33.py verify markup, CSS, helpers, and absence of
native dialog calls.
Extracted from PR #242.
* fix: Android Chrome mobile — workspace panel close + profile dropdown (#256)
Fix #247: toggleMobileFiles() now shows/hides the mobile overlay when
toggling the right workspace panel. New closeMobileFiles() helper closes
the panel with correct overlay state tracking. Overlay onclick calls both
closeMobileSidebar() and closeMobileFiles(). Mobile-only close button (x)
added to workspace panel header.
Fix #246: profile dropdown uses position:fixed;top:56px;right:8px at
max-width:900px, escaping the overflow-x:auto stacking context that was
clipping it on Android Chrome.
Fix applied during review: closeMobileSidebar() now checks if the right
panel is still open before hiding the overlay, preventing the overlay from
disappearing when only the sidebar is closed.
Fixes #247 Fixes #246
* feat: session ⋯ action dropdown replaces per-row buttons (#252)
Replaces the 5 per-row hover action buttons (pin/move/archive/duplicate/trash)
with a single ⋯ trigger that opens a positioned dropdown menu. Menu has full
keyboard (Escape), click-outside, scroll, and resize-reposition handling.
Position:fixed prevents sidebar clipping.
5 actions: Pin/Unpin, Move to project, Archive/Unarchive, Duplicate, Delete
(danger style). Each with icon and descriptive subtitle.
Updated test_sprint16.py: test_sessions_js_uses_action_menu_not_per_row_buttons
asserts the new trigger and menu functions exist, old per-row classes are gone.
Extracted from PR #242.
* docs: v0.47.0 release notes, bump version, update test counts (645)
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 19:19:12 +00:00
|
|
|
|
const target=e.target;
|
|
|
|
|
|
const isTextarea=target&&target.tagName==='TEXTAREA';
|
|
|
|
|
|
if(!isTextarea){
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
if(target===cancelBtn||target===closeBtn){
|
|
|
|
|
|
_finishAppDialog(APP_DIALOG.kind==='prompt'?null:false);
|
|
|
|
|
|
}else if(APP_DIALOG.kind==='prompt'){
|
|
|
|
|
|
const input=$('appDialogInput');
|
|
|
|
|
|
_finishAppDialog(input?input.value:null);
|
|
|
|
|
|
}else{
|
|
|
|
|
|
_finishAppDialog(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if(e.key==='Tab'){
|
|
|
|
|
|
const nodes=_getAppDialogFocusable();
|
|
|
|
|
|
if(!nodes.length) return;
|
|
|
|
|
|
const idx=nodes.indexOf(document.activeElement);
|
|
|
|
|
|
let nextIdx=idx;
|
|
|
|
|
|
if(e.shiftKey){nextIdx=idx<=0?nodes.length-1:idx-1;}
|
|
|
|
|
|
else{nextIdx=idx===-1||idx===nodes.length-1?0:idx+1;}
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
nodes[nextIdx].focus();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function showConfirmDialog(opts={}){
|
|
|
|
|
|
_ensureAppDialogBindings();
|
|
|
|
|
|
if(APP_DIALOG.resolve) _finishAppDialog(false,false);
|
|
|
|
|
|
const overlay=$('appDialogOverlay'),dialog=$('appDialog'),title=$('appDialogTitle'),
|
|
|
|
|
|
desc=$('appDialogDesc'),input=$('appDialogInput'),cancelBtn=$('appDialogCancel'),confirmBtn=$('appDialogConfirm');
|
|
|
|
|
|
APP_DIALOG.resolve=null;APP_DIALOG.kind='confirm';APP_DIALOG.lastFocus=document.activeElement;
|
|
|
|
|
|
if(title) title.textContent=opts.title||t('dialog_confirm_title');
|
|
|
|
|
|
if(desc) desc.textContent=opts.message||'';
|
|
|
|
|
|
if(input){input.style.display='none';input.value='';}
|
|
|
|
|
|
if(cancelBtn) cancelBtn.textContent=opts.cancelLabel||t('cancel');
|
|
|
|
|
|
if(confirmBtn){
|
|
|
|
|
|
confirmBtn.textContent=opts.confirmLabel||t('dialog_confirm_btn');
|
|
|
|
|
|
confirmBtn.classList.toggle('danger',!!opts.danger);
|
|
|
|
|
|
}
|
|
|
|
|
|
if(dialog) dialog.setAttribute('role',opts.danger?'alertdialog':'dialog');
|
|
|
|
|
|
if(overlay){overlay.style.display='flex';overlay.setAttribute('aria-hidden','false');}
|
|
|
|
|
|
return new Promise(resolve=>{
|
|
|
|
|
|
APP_DIALOG.resolve=resolve;
|
|
|
|
|
|
setTimeout(()=>((opts.focusCancel?cancelBtn:confirmBtn)||confirmBtn||cancelBtn).focus(),0);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function showPromptDialog(opts={}){
|
|
|
|
|
|
_ensureAppDialogBindings();
|
|
|
|
|
|
if(APP_DIALOG.resolve) _finishAppDialog(null,false);
|
|
|
|
|
|
const overlay=$('appDialogOverlay'),dialog=$('appDialog'),title=$('appDialogTitle'),
|
|
|
|
|
|
desc=$('appDialogDesc'),input=$('appDialogInput'),cancelBtn=$('appDialogCancel'),confirmBtn=$('appDialogConfirm');
|
|
|
|
|
|
APP_DIALOG.resolve=null;APP_DIALOG.kind='prompt';APP_DIALOG.lastFocus=document.activeElement;
|
|
|
|
|
|
if(title) title.textContent=opts.title||t('dialog_prompt_title');
|
|
|
|
|
|
if(desc) desc.textContent=opts.message||'';
|
|
|
|
|
|
if(input){
|
|
|
|
|
|
input.type=opts.inputType||'text';input.style.display='';
|
|
|
|
|
|
input.value=opts.value||'';input.placeholder=opts.placeholder||'';
|
|
|
|
|
|
input.autocomplete='off';input.spellcheck=false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if(cancelBtn) cancelBtn.textContent=opts.cancelLabel||t('cancel');
|
|
|
|
|
|
if(confirmBtn){confirmBtn.textContent=opts.confirmLabel||t('create');confirmBtn.classList.remove('danger');}
|
|
|
|
|
|
if(dialog) dialog.setAttribute('role','dialog');
|
|
|
|
|
|
if(overlay){overlay.style.display='flex';overlay.setAttribute('aria-hidden','false');}
|
|
|
|
|
|
return new Promise(resolve=>{
|
|
|
|
|
|
APP_DIALOG.resolve=resolve;
|
|
|
|
|
|
setTimeout(()=>{if(input&&input.style.display!=='none')input.focus();else if(confirmBtn)confirmBtn.focus();},0);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-31 03:40:19 +00:00
|
|
|
|
function copyMsg(btn){
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
const row=btn.closest('[data-raw-text]');
|
2026-03-31 03:40:19 +00:00
|
|
|
|
const text=row?row.dataset.rawText:'';
|
|
|
|
|
|
if(!text)return;
|
|
|
|
|
|
navigator.clipboard.writeText(text).then(()=>{
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
const orig=btn.innerHTML;btn.innerHTML=li('check',13);btn.style.color='var(--blue)';
|
2026-03-31 03:40:19 +00:00
|
|
|
|
setTimeout(()=>{btn.innerHTML=orig;btn.style.color='';},1500);
|
|
|
|
|
|
}).catch(()=>showToast('Copy failed'));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Reconnect banner (B4/B5: reload resilience) ──
|
|
|
|
|
|
const INFLIGHT_KEY = 'hermes-webui-inflight'; // localStorage key for in-flight session tracking
|
2026-04-13 23:25:31 +00:00
|
|
|
|
const INFLIGHT_STATE_KEY = 'hermes-webui-inflight-state'; // localStorage snapshots for mid-stream reload recovery
|
|
|
|
|
|
|
|
|
|
|
|
function _readInflightStateMap(){
|
|
|
|
|
|
try{
|
|
|
|
|
|
const raw=localStorage.getItem(INFLIGHT_STATE_KEY);
|
|
|
|
|
|
const parsed=raw?JSON.parse(raw):{};
|
|
|
|
|
|
return parsed&&typeof parsed==='object'?parsed:{};
|
|
|
|
|
|
}catch(_){
|
|
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
function saveInflightState(sid, state){
|
|
|
|
|
|
if(!sid||!state) return;
|
|
|
|
|
|
try{
|
|
|
|
|
|
const all=_readInflightStateMap();
|
|
|
|
|
|
all[sid]={...state,updated_at:Date.now()};
|
|
|
|
|
|
localStorage.setItem(INFLIGHT_STATE_KEY, JSON.stringify(all));
|
|
|
|
|
|
}catch(_){ }
|
|
|
|
|
|
}
|
|
|
|
|
|
function loadInflightState(sid, streamId){
|
|
|
|
|
|
if(!sid) return null;
|
|
|
|
|
|
const all=_readInflightStateMap();
|
|
|
|
|
|
const entry=all[sid];
|
|
|
|
|
|
if(!entry) return null;
|
|
|
|
|
|
if(streamId&&entry.streamId&&entry.streamId!==streamId) return null;
|
|
|
|
|
|
if(entry.updated_at&&Date.now()-entry.updated_at>10*60*1000){
|
|
|
|
|
|
clearInflightState(sid);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
return entry;
|
|
|
|
|
|
}
|
|
|
|
|
|
function clearInflightState(sid){
|
|
|
|
|
|
if(!sid) return;
|
|
|
|
|
|
try{
|
|
|
|
|
|
const all=_readInflightStateMap();
|
|
|
|
|
|
if(!(sid in all)) return;
|
|
|
|
|
|
delete all[sid];
|
|
|
|
|
|
if(Object.keys(all).length) localStorage.setItem(INFLIGHT_STATE_KEY, JSON.stringify(all));
|
|
|
|
|
|
else localStorage.removeItem(INFLIGHT_STATE_KEY);
|
|
|
|
|
|
}catch(_){ }
|
|
|
|
|
|
}
|
2026-03-31 03:40:19 +00:00
|
|
|
|
|
|
|
|
|
|
function markInflight(sid, streamId) {
|
|
|
|
|
|
localStorage.setItem(INFLIGHT_KEY, JSON.stringify({sid, streamId, ts: Date.now()}));
|
|
|
|
|
|
}
|
|
|
|
|
|
function clearInflight() {
|
|
|
|
|
|
localStorage.removeItem(INFLIGHT_KEY);
|
|
|
|
|
|
}
|
|
|
|
|
|
function showReconnectBanner(msg) {
|
|
|
|
|
|
$('reconnectMsg').textContent = msg || 'A response may have been in progress when you last left.';
|
|
|
|
|
|
$('reconnectBanner').classList.add('visible');
|
|
|
|
|
|
}
|
|
|
|
|
|
function dismissReconnect() {
|
|
|
|
|
|
$('reconnectBanner').classList.remove('visible');
|
|
|
|
|
|
clearInflight();
|
|
|
|
|
|
}
|
|
|
|
|
|
async function refreshSession() {
|
2026-04-23 00:51:12 +00:00
|
|
|
|
// When the banner is in post-update restart mode, the "Reload" button
|
|
|
|
|
|
// should do a full page reload — a session refresh would just 502 while
|
|
|
|
|
|
// the server is still restarting.
|
|
|
|
|
|
if (window._restartingForUpdate) { location.reload(); return; }
|
2026-03-31 03:40:19 +00:00
|
|
|
|
dismissReconnect();
|
|
|
|
|
|
if (!S.session) return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await api(`/api/session?session_id=${encodeURIComponent(S.session.session_id)}`);
|
|
|
|
|
|
S.session = data.session;
|
2026-04-13 23:18:15 +00:00
|
|
|
|
S.messages = data.session.messages || [];
|
|
|
|
|
|
const pendingMsg=getPendingSessionMessage(data.session);
|
|
|
|
|
|
if(pendingMsg) S.messages.push(pendingMsg);
|
|
|
|
|
|
S.activeStreamId=data.session.active_stream_id||null;
|
|
|
|
|
|
|
2026-03-31 03:40:19 +00:00
|
|
|
|
syncTopbar(); renderMessages();
|
|
|
|
|
|
showToast('Conversation refreshed');
|
|
|
|
|
|
} catch(e) { setStatus('Refresh failed: ' + e.message); }
|
|
|
|
|
|
}
|
feat: self-update checker with one-click update for WebUI + Agent
Shows a blue banner when the webui or hermes-agent git repos are behind
their upstream branches. One-click 'Update Now' button does stash, pull
--ff-only, stash pop, then reloads the page.
Backend (api/updates.py):
- _check_repo(): git fetch + rev-list count with 15s timeout
- check_for_updates(): 30-min server-side cache, thread-safe, skips
Docker (no .git dir)
- apply_update(): stash (if dirty), pull --ff-only, pop, invalidate cache
Routes:
- GET /api/updates/check -- returns cached {webui, agent} with behind count
- POST /api/updates/apply -- {target: 'webui'|'agent'}
Frontend:
- Blue banner (matches reconnect-banner pattern) with 'Later' / 'Update Now'
- Non-blocking boot check via fire-and-forget .then(), once per tab session
- sessionStorage guards prevent re-checking and re-showing after dismiss
Settings:
- 'Check for updates' checkbox (default: on) -- when off, no git operations
- Removed 'Default Workspace' dropdown to keep settings panel compact
Performance:
- Server cache: git fetch at most 2x/hour regardless of client count
- sessionStorage: one check per browser tab session
- _check_in_progress flag prevents concurrent fetch storms
- Fire-and-forget: does NOT block the boot sequence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:11:44 +00:00
|
|
|
|
// ── Update banner ──
|
|
|
|
|
|
function _showUpdateBanner(data){
|
|
|
|
|
|
const parts=[];
|
|
|
|
|
|
if(data.webui&&data.webui.behind>0) parts.push(`WebUI: ${data.webui.behind} update${data.webui.behind>1?'s':''}`);
|
|
|
|
|
|
if(data.agent&&data.agent.behind>0) parts.push(`Agent: ${data.agent.behind} update${data.agent.behind>1?'s':''}`);
|
|
|
|
|
|
if(!parts.length)return;
|
|
|
|
|
|
const msg=$('updateMsg');
|
|
|
|
|
|
if(msg) msg.textContent='\u2B06 '+parts.join(', ')+' available';
|
|
|
|
|
|
const banner=$('updateBanner');
|
|
|
|
|
|
if(banner) banner.classList.add('visible');
|
|
|
|
|
|
window._updateData=data;
|
|
|
|
|
|
}
|
|
|
|
|
|
function dismissUpdate(){
|
|
|
|
|
|
const b=$('updateBanner');if(b)b.classList.remove('visible');
|
|
|
|
|
|
sessionStorage.setItem('hermes-update-dismissed','1');
|
|
|
|
|
|
}
|
|
|
|
|
|
async function applyUpdates(){
|
|
|
|
|
|
const btn=$('btnApplyUpdate');
|
|
|
|
|
|
if(btn){btn.disabled=true;btn.textContent='Updating\u2026';}
|
fix: update banner — conflict recovery path + server self-restart after update (#816)
* fix: update banner conflict recovery + server self-restart after update (#813 #814)
* fix(update): restart must wait for in-flight update + reset force button on retry
Two defects in the update banner flow found during review of PR #816:
1. Two-target race (webui + agent sequential)
The client posts targets sequentially: webui succeeds and schedules
a restart timer (2 s delay); client then posts agent; server begins
agent fetch+pull; at T=2 s the restart timer fires os.execv mid-pull,
killing the agent update and closing the client connection. User
sees "Update failed (agent): Failed to fetch" even though webui did
update, and the agent repo is in an unknown partial state.
Fix: _schedule_restart() now blocks on _apply_lock before calling
os.execv. If a second update is in flight when the timer fires, the
restart thread waits until it completes. If nothing is in flight the
lock acquire is instant, so no-op updates still restart immediately.
2. Stale force-update button across retries
_showUpdateError sets btnForceUpdate to display:inline-block when
res.conflict / res.diverged. Nothing resets it on the next retry,
so a subsequent non-conflict error (e.g. network) leaves the stale
force button visible pointing at the previous target.
Fix: applyUpdates() now hides the force button and clears its
data-target at the start of each attempt.
Tests:
- test_schedule_restart_waits_for_apply_lock: holds _apply_lock from a
helper thread, verifies execv is delayed until the lock is released.
- test_schedule_restart_still_fires_when_no_update_in_flight: sanity
check that the common path still works with no contention.
- test_apply_updates_resets_force_button_at_start: regression guard
that the reset appears before the update loop begins.
Full suite: 1683 passed, 0 failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(update): hold _apply_lock through execv + fix banner error layout
Two fixes from Opus review:
1. TOCTOU gap in _schedule_restart (api/updates.py): the original pattern
acquired _apply_lock, released it, then called os.execv — leaving a brief
window where a new update could start between release and execv. Fixed by
moving os.execv inside the 'with _apply_lock:' block so the process is
replaced while still holding the lock; no new update can acquire it.
2. Banner CSS layout (static/index.html): #updateError was a direct flex child
of .update-banner (display:flex row), so long error messages sat inline
between #updateMsg and the buttons instead of below the message.
Wrapped #updateMsg + #updateError in a flex-column container so errors
stack vertically under the status line.
* docs: add v0.50.134 CHANGELOG entry
---------
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 00:10:41 +00:00
|
|
|
|
const errEl=$('updateError');
|
|
|
|
|
|
if(errEl){errEl.style.display='none';errEl.textContent='';}
|
|
|
|
|
|
// Hide any leftover force-update button from a prior conflict so a fresh
|
|
|
|
|
|
// retry starts clean (otherwise stale state points at the wrong target).
|
|
|
|
|
|
const forceBtnReset=$('btnForceUpdate');
|
|
|
|
|
|
if(forceBtnReset){forceBtnReset.style.display='none';forceBtnReset.dataset.target='';}
|
feat: self-update checker with one-click update for WebUI + Agent
Shows a blue banner when the webui or hermes-agent git repos are behind
their upstream branches. One-click 'Update Now' button does stash, pull
--ff-only, stash pop, then reloads the page.
Backend (api/updates.py):
- _check_repo(): git fetch + rev-list count with 15s timeout
- check_for_updates(): 30-min server-side cache, thread-safe, skips
Docker (no .git dir)
- apply_update(): stash (if dirty), pull --ff-only, pop, invalidate cache
Routes:
- GET /api/updates/check -- returns cached {webui, agent} with behind count
- POST /api/updates/apply -- {target: 'webui'|'agent'}
Frontend:
- Blue banner (matches reconnect-banner pattern) with 'Later' / 'Update Now'
- Non-blocking boot check via fire-and-forget .then(), once per tab session
- sessionStorage guards prevent re-checking and re-showing after dismiss
Settings:
- 'Check for updates' checkbox (default: on) -- when off, no git operations
- Removed 'Default Workspace' dropdown to keep settings panel compact
Performance:
- Server cache: git fetch at most 2x/hour regardless of client count
- sessionStorage: one check per browser tab session
- _check_in_progress flag prevents concurrent fetch storms
- Fire-and-forget: does NOT block the boot sequence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:11:44 +00:00
|
|
|
|
const targets=[];
|
|
|
|
|
|
if(window._updateData?.webui?.behind>0) targets.push('webui');
|
|
|
|
|
|
if(window._updateData?.agent?.behind>0) targets.push('agent');
|
|
|
|
|
|
try{
|
|
|
|
|
|
for(const target of targets){
|
|
|
|
|
|
const res=await api('/api/updates/apply',{method:'POST',body:JSON.stringify({target})});
|
|
|
|
|
|
if(!res.ok){
|
fix: update banner — conflict recovery path + server self-restart after update (#816)
* fix: update banner conflict recovery + server self-restart after update (#813 #814)
* fix(update): restart must wait for in-flight update + reset force button on retry
Two defects in the update banner flow found during review of PR #816:
1. Two-target race (webui + agent sequential)
The client posts targets sequentially: webui succeeds and schedules
a restart timer (2 s delay); client then posts agent; server begins
agent fetch+pull; at T=2 s the restart timer fires os.execv mid-pull,
killing the agent update and closing the client connection. User
sees "Update failed (agent): Failed to fetch" even though webui did
update, and the agent repo is in an unknown partial state.
Fix: _schedule_restart() now blocks on _apply_lock before calling
os.execv. If a second update is in flight when the timer fires, the
restart thread waits until it completes. If nothing is in flight the
lock acquire is instant, so no-op updates still restart immediately.
2. Stale force-update button across retries
_showUpdateError sets btnForceUpdate to display:inline-block when
res.conflict / res.diverged. Nothing resets it on the next retry,
so a subsequent non-conflict error (e.g. network) leaves the stale
force button visible pointing at the previous target.
Fix: applyUpdates() now hides the force button and clears its
data-target at the start of each attempt.
Tests:
- test_schedule_restart_waits_for_apply_lock: holds _apply_lock from a
helper thread, verifies execv is delayed until the lock is released.
- test_schedule_restart_still_fires_when_no_update_in_flight: sanity
check that the common path still works with no contention.
- test_apply_updates_resets_force_button_at_start: regression guard
that the reset appears before the update loop begins.
Full suite: 1683 passed, 0 failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(update): hold _apply_lock through execv + fix banner error layout
Two fixes from Opus review:
1. TOCTOU gap in _schedule_restart (api/updates.py): the original pattern
acquired _apply_lock, released it, then called os.execv — leaving a brief
window where a new update could start between release and execv. Fixed by
moving os.execv inside the 'with _apply_lock:' block so the process is
replaced while still holding the lock; no new update can acquire it.
2. Banner CSS layout (static/index.html): #updateError was a direct flex child
of .update-banner (display:flex row), so long error messages sat inline
between #updateMsg and the buttons instead of below the message.
Wrapped #updateMsg + #updateError in a flex-column container so errors
stack vertically under the status line.
* docs: add v0.50.134 CHANGELOG entry
---------
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 00:10:41 +00:00
|
|
|
|
_showUpdateError(target,res);
|
feat: self-update checker with one-click update for WebUI + Agent
Shows a blue banner when the webui or hermes-agent git repos are behind
their upstream branches. One-click 'Update Now' button does stash, pull
--ff-only, stash pop, then reloads the page.
Backend (api/updates.py):
- _check_repo(): git fetch + rev-list count with 15s timeout
- check_for_updates(): 30-min server-side cache, thread-safe, skips
Docker (no .git dir)
- apply_update(): stash (if dirty), pull --ff-only, pop, invalidate cache
Routes:
- GET /api/updates/check -- returns cached {webui, agent} with behind count
- POST /api/updates/apply -- {target: 'webui'|'agent'}
Frontend:
- Blue banner (matches reconnect-banner pattern) with 'Later' / 'Update Now'
- Non-blocking boot check via fire-and-forget .then(), once per tab session
- sessionStorage guards prevent re-checking and re-showing after dismiss
Settings:
- 'Check for updates' checkbox (default: on) -- when off, no git operations
- Removed 'Default Workspace' dropdown to keep settings panel compact
Performance:
- Server cache: git fetch at most 2x/hour regardless of client count
- sessionStorage: one check per browser tab session
- _check_in_progress flag prevents concurrent fetch storms
- Fire-and-forget: does NOT block the boot sequence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:11:44 +00:00
|
|
|
|
if(btn){btn.disabled=false;btn.textContent='Update Now';}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-23 00:51:12 +00:00
|
|
|
|
showToast('Update applied — restarting…');
|
feat: self-update checker with one-click update for WebUI + Agent
Shows a blue banner when the webui or hermes-agent git repos are behind
their upstream branches. One-click 'Update Now' button does stash, pull
--ff-only, stash pop, then reloads the page.
Backend (api/updates.py):
- _check_repo(): git fetch + rev-list count with 15s timeout
- check_for_updates(): 30-min server-side cache, thread-safe, skips
Docker (no .git dir)
- apply_update(): stash (if dirty), pull --ff-only, pop, invalidate cache
Routes:
- GET /api/updates/check -- returns cached {webui, agent} with behind count
- POST /api/updates/apply -- {target: 'webui'|'agent'}
Frontend:
- Blue banner (matches reconnect-banner pattern) with 'Later' / 'Update Now'
- Non-blocking boot check via fire-and-forget .then(), once per tab session
- sessionStorage guards prevent re-checking and re-showing after dismiss
Settings:
- 'Check for updates' checkbox (default: on) -- when off, no git operations
- Removed 'Default Workspace' dropdown to keep settings panel compact
Performance:
- Server cache: git fetch at most 2x/hour regardless of client count
- sessionStorage: one check per browser tab session
- _check_in_progress flag prevents concurrent fetch storms
- Fire-and-forget: does NOT block the boot sequence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:11:44 +00:00
|
|
|
|
sessionStorage.removeItem('hermes-update-checked');
|
|
|
|
|
|
sessionStorage.removeItem('hermes-update-dismissed');
|
2026-04-23 00:51:12 +00:00
|
|
|
|
_waitForServerThenReload();
|
feat: self-update checker with one-click update for WebUI + Agent
Shows a blue banner when the webui or hermes-agent git repos are behind
their upstream branches. One-click 'Update Now' button does stash, pull
--ff-only, stash pop, then reloads the page.
Backend (api/updates.py):
- _check_repo(): git fetch + rev-list count with 15s timeout
- check_for_updates(): 30-min server-side cache, thread-safe, skips
Docker (no .git dir)
- apply_update(): stash (if dirty), pull --ff-only, pop, invalidate cache
Routes:
- GET /api/updates/check -- returns cached {webui, agent} with behind count
- POST /api/updates/apply -- {target: 'webui'|'agent'}
Frontend:
- Blue banner (matches reconnect-banner pattern) with 'Later' / 'Update Now'
- Non-blocking boot check via fire-and-forget .then(), once per tab session
- sessionStorage guards prevent re-checking and re-showing after dismiss
Settings:
- 'Check for updates' checkbox (default: on) -- when off, no git operations
- Removed 'Default Workspace' dropdown to keep settings panel compact
Performance:
- Server cache: git fetch at most 2x/hour regardless of client count
- sessionStorage: one check per browser tab session
- _check_in_progress flag prevents concurrent fetch storms
- Fire-and-forget: does NOT block the boot sequence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:11:44 +00:00
|
|
|
|
}catch(e){
|
fix: update banner — conflict recovery path + server self-restart after update (#816)
* fix: update banner conflict recovery + server self-restart after update (#813 #814)
* fix(update): restart must wait for in-flight update + reset force button on retry
Two defects in the update banner flow found during review of PR #816:
1. Two-target race (webui + agent sequential)
The client posts targets sequentially: webui succeeds and schedules
a restart timer (2 s delay); client then posts agent; server begins
agent fetch+pull; at T=2 s the restart timer fires os.execv mid-pull,
killing the agent update and closing the client connection. User
sees "Update failed (agent): Failed to fetch" even though webui did
update, and the agent repo is in an unknown partial state.
Fix: _schedule_restart() now blocks on _apply_lock before calling
os.execv. If a second update is in flight when the timer fires, the
restart thread waits until it completes. If nothing is in flight the
lock acquire is instant, so no-op updates still restart immediately.
2. Stale force-update button across retries
_showUpdateError sets btnForceUpdate to display:inline-block when
res.conflict / res.diverged. Nothing resets it on the next retry,
so a subsequent non-conflict error (e.g. network) leaves the stale
force button visible pointing at the previous target.
Fix: applyUpdates() now hides the force button and clears its
data-target at the start of each attempt.
Tests:
- test_schedule_restart_waits_for_apply_lock: holds _apply_lock from a
helper thread, verifies execv is delayed until the lock is released.
- test_schedule_restart_still_fires_when_no_update_in_flight: sanity
check that the common path still works with no contention.
- test_apply_updates_resets_force_button_at_start: regression guard
that the reset appears before the update loop begins.
Full suite: 1683 passed, 0 failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(update): hold _apply_lock through execv + fix banner error layout
Two fixes from Opus review:
1. TOCTOU gap in _schedule_restart (api/updates.py): the original pattern
acquired _apply_lock, released it, then called os.execv — leaving a brief
window where a new update could start between release and execv. Fixed by
moving os.execv inside the 'with _apply_lock:' block so the process is
replaced while still holding the lock; no new update can acquire it.
2. Banner CSS layout (static/index.html): #updateError was a direct flex child
of .update-banner (display:flex row), so long error messages sat inline
between #updateMsg and the buttons instead of below the message.
Wrapped #updateMsg + #updateError in a flex-column container so errors
stack vertically under the status line.
* docs: add v0.50.134 CHANGELOG entry
---------
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 00:10:41 +00:00
|
|
|
|
if(errEl){errEl.textContent='Update failed: '+e.message;errEl.style.display='block';}
|
|
|
|
|
|
else showToast('Update failed: '+e.message);
|
feat: self-update checker with one-click update for WebUI + Agent
Shows a blue banner when the webui or hermes-agent git repos are behind
their upstream branches. One-click 'Update Now' button does stash, pull
--ff-only, stash pop, then reloads the page.
Backend (api/updates.py):
- _check_repo(): git fetch + rev-list count with 15s timeout
- check_for_updates(): 30-min server-side cache, thread-safe, skips
Docker (no .git dir)
- apply_update(): stash (if dirty), pull --ff-only, pop, invalidate cache
Routes:
- GET /api/updates/check -- returns cached {webui, agent} with behind count
- POST /api/updates/apply -- {target: 'webui'|'agent'}
Frontend:
- Blue banner (matches reconnect-banner pattern) with 'Later' / 'Update Now'
- Non-blocking boot check via fire-and-forget .then(), once per tab session
- sessionStorage guards prevent re-checking and re-showing after dismiss
Settings:
- 'Check for updates' checkbox (default: on) -- when off, no git operations
- Removed 'Default Workspace' dropdown to keep settings panel compact
Performance:
- Server cache: git fetch at most 2x/hour regardless of client count
- sessionStorage: one check per browser tab session
- _check_in_progress flag prevents concurrent fetch storms
- Fire-and-forget: does NOT block the boot sequence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:11:44 +00:00
|
|
|
|
if(btn){btn.disabled=false;btn.textContent='Update Now';}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
fix: update banner — conflict recovery path + server self-restart after update (#816)
* fix: update banner conflict recovery + server self-restart after update (#813 #814)
* fix(update): restart must wait for in-flight update + reset force button on retry
Two defects in the update banner flow found during review of PR #816:
1. Two-target race (webui + agent sequential)
The client posts targets sequentially: webui succeeds and schedules
a restart timer (2 s delay); client then posts agent; server begins
agent fetch+pull; at T=2 s the restart timer fires os.execv mid-pull,
killing the agent update and closing the client connection. User
sees "Update failed (agent): Failed to fetch" even though webui did
update, and the agent repo is in an unknown partial state.
Fix: _schedule_restart() now blocks on _apply_lock before calling
os.execv. If a second update is in flight when the timer fires, the
restart thread waits until it completes. If nothing is in flight the
lock acquire is instant, so no-op updates still restart immediately.
2. Stale force-update button across retries
_showUpdateError sets btnForceUpdate to display:inline-block when
res.conflict / res.diverged. Nothing resets it on the next retry,
so a subsequent non-conflict error (e.g. network) leaves the stale
force button visible pointing at the previous target.
Fix: applyUpdates() now hides the force button and clears its
data-target at the start of each attempt.
Tests:
- test_schedule_restart_waits_for_apply_lock: holds _apply_lock from a
helper thread, verifies execv is delayed until the lock is released.
- test_schedule_restart_still_fires_when_no_update_in_flight: sanity
check that the common path still works with no contention.
- test_apply_updates_resets_force_button_at_start: regression guard
that the reset appears before the update loop begins.
Full suite: 1683 passed, 0 failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(update): hold _apply_lock through execv + fix banner error layout
Two fixes from Opus review:
1. TOCTOU gap in _schedule_restart (api/updates.py): the original pattern
acquired _apply_lock, released it, then called os.execv — leaving a brief
window where a new update could start between release and execv. Fixed by
moving os.execv inside the 'with _apply_lock:' block so the process is
replaced while still holding the lock; no new update can acquire it.
2. Banner CSS layout (static/index.html): #updateError was a direct flex child
of .update-banner (display:flex row), so long error messages sat inline
between #updateMsg and the buttons instead of below the message.
Wrapped #updateMsg + #updateError in a flex-column container so errors
stack vertically under the status line.
* docs: add v0.50.134 CHANGELOG entry
---------
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 00:10:41 +00:00
|
|
|
|
function _showUpdateError(target,res){
|
|
|
|
|
|
const errEl=$('updateError');
|
|
|
|
|
|
const forceBtn=$('btnForceUpdate');
|
|
|
|
|
|
const msg='Update failed ('+target+'): '+(res.message||'unknown error');
|
|
|
|
|
|
if(errEl){
|
|
|
|
|
|
errEl.textContent=msg;
|
|
|
|
|
|
errEl.style.display='block';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showToast(msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
// Show "Force update" button when the error is recoverable by a hard reset
|
|
|
|
|
|
if(forceBtn&&(res.conflict||res.diverged)){
|
|
|
|
|
|
forceBtn.dataset.target=target;
|
|
|
|
|
|
forceBtn.style.display='inline-block';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
async function forceUpdate(btn){
|
|
|
|
|
|
const target=btn&&btn.dataset.target;
|
|
|
|
|
|
if(!target) return;
|
|
|
|
|
|
const confirmed=await showConfirmDialog({
|
|
|
|
|
|
title:'Force update '+target+'?',
|
|
|
|
|
|
message:'This will discard all local changes in the '+target+' repo and reset to the latest remote version. This cannot be undone.',
|
|
|
|
|
|
confirmLabel:'Force update',
|
|
|
|
|
|
danger:true,
|
|
|
|
|
|
focusCancel:true,
|
|
|
|
|
|
});
|
|
|
|
|
|
if(!confirmed) return;
|
|
|
|
|
|
btn.disabled=true;btn.textContent='Force updating\u2026';
|
|
|
|
|
|
const errEl=$('updateError');
|
|
|
|
|
|
if(errEl){errEl.style.display='none';}
|
|
|
|
|
|
try{
|
|
|
|
|
|
const res=await api('/api/updates/force',{method:'POST',body:JSON.stringify({target})});
|
|
|
|
|
|
if(!res.ok){
|
|
|
|
|
|
if(errEl){errEl.textContent='Force update failed: '+(res.message||'unknown error');errEl.style.display='block';}
|
|
|
|
|
|
btn.disabled=false;btn.textContent='Force update';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-04-23 00:51:12 +00:00
|
|
|
|
showToast('Force update applied — restarting…');
|
fix: update banner — conflict recovery path + server self-restart after update (#816)
* fix: update banner conflict recovery + server self-restart after update (#813 #814)
* fix(update): restart must wait for in-flight update + reset force button on retry
Two defects in the update banner flow found during review of PR #816:
1. Two-target race (webui + agent sequential)
The client posts targets sequentially: webui succeeds and schedules
a restart timer (2 s delay); client then posts agent; server begins
agent fetch+pull; at T=2 s the restart timer fires os.execv mid-pull,
killing the agent update and closing the client connection. User
sees "Update failed (agent): Failed to fetch" even though webui did
update, and the agent repo is in an unknown partial state.
Fix: _schedule_restart() now blocks on _apply_lock before calling
os.execv. If a second update is in flight when the timer fires, the
restart thread waits until it completes. If nothing is in flight the
lock acquire is instant, so no-op updates still restart immediately.
2. Stale force-update button across retries
_showUpdateError sets btnForceUpdate to display:inline-block when
res.conflict / res.diverged. Nothing resets it on the next retry,
so a subsequent non-conflict error (e.g. network) leaves the stale
force button visible pointing at the previous target.
Fix: applyUpdates() now hides the force button and clears its
data-target at the start of each attempt.
Tests:
- test_schedule_restart_waits_for_apply_lock: holds _apply_lock from a
helper thread, verifies execv is delayed until the lock is released.
- test_schedule_restart_still_fires_when_no_update_in_flight: sanity
check that the common path still works with no contention.
- test_apply_updates_resets_force_button_at_start: regression guard
that the reset appears before the update loop begins.
Full suite: 1683 passed, 0 failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(update): hold _apply_lock through execv + fix banner error layout
Two fixes from Opus review:
1. TOCTOU gap in _schedule_restart (api/updates.py): the original pattern
acquired _apply_lock, released it, then called os.execv — leaving a brief
window where a new update could start between release and execv. Fixed by
moving os.execv inside the 'with _apply_lock:' block so the process is
replaced while still holding the lock; no new update can acquire it.
2. Banner CSS layout (static/index.html): #updateError was a direct flex child
of .update-banner (display:flex row), so long error messages sat inline
between #updateMsg and the buttons instead of below the message.
Wrapped #updateMsg + #updateError in a flex-column container so errors
stack vertically under the status line.
* docs: add v0.50.134 CHANGELOG entry
---------
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 00:10:41 +00:00
|
|
|
|
sessionStorage.removeItem('hermes-update-checked');
|
|
|
|
|
|
sessionStorage.removeItem('hermes-update-dismissed');
|
2026-04-23 00:51:12 +00:00
|
|
|
|
_waitForServerThenReload();
|
fix: update banner — conflict recovery path + server self-restart after update (#816)
* fix: update banner conflict recovery + server self-restart after update (#813 #814)
* fix(update): restart must wait for in-flight update + reset force button on retry
Two defects in the update banner flow found during review of PR #816:
1. Two-target race (webui + agent sequential)
The client posts targets sequentially: webui succeeds and schedules
a restart timer (2 s delay); client then posts agent; server begins
agent fetch+pull; at T=2 s the restart timer fires os.execv mid-pull,
killing the agent update and closing the client connection. User
sees "Update failed (agent): Failed to fetch" even though webui did
update, and the agent repo is in an unknown partial state.
Fix: _schedule_restart() now blocks on _apply_lock before calling
os.execv. If a second update is in flight when the timer fires, the
restart thread waits until it completes. If nothing is in flight the
lock acquire is instant, so no-op updates still restart immediately.
2. Stale force-update button across retries
_showUpdateError sets btnForceUpdate to display:inline-block when
res.conflict / res.diverged. Nothing resets it on the next retry,
so a subsequent non-conflict error (e.g. network) leaves the stale
force button visible pointing at the previous target.
Fix: applyUpdates() now hides the force button and clears its
data-target at the start of each attempt.
Tests:
- test_schedule_restart_waits_for_apply_lock: holds _apply_lock from a
helper thread, verifies execv is delayed until the lock is released.
- test_schedule_restart_still_fires_when_no_update_in_flight: sanity
check that the common path still works with no contention.
- test_apply_updates_resets_force_button_at_start: regression guard
that the reset appears before the update loop begins.
Full suite: 1683 passed, 0 failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(update): hold _apply_lock through execv + fix banner error layout
Two fixes from Opus review:
1. TOCTOU gap in _schedule_restart (api/updates.py): the original pattern
acquired _apply_lock, released it, then called os.execv — leaving a brief
window where a new update could start between release and execv. Fixed by
moving os.execv inside the 'with _apply_lock:' block so the process is
replaced while still holding the lock; no new update can acquire it.
2. Banner CSS layout (static/index.html): #updateError was a direct flex child
of .update-banner (display:flex row), so long error messages sat inline
between #updateMsg and the buttons instead of below the message.
Wrapped #updateMsg + #updateError in a flex-column container so errors
stack vertically under the status line.
* docs: add v0.50.134 CHANGELOG entry
---------
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 00:10:41 +00:00
|
|
|
|
}catch(e){
|
|
|
|
|
|
if(errEl){errEl.textContent='Force update failed: '+e.message;errEl.style.display='block';}
|
|
|
|
|
|
btn.disabled=false;btn.textContent='Force update';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
feat: self-update checker with one-click update for WebUI + Agent
Shows a blue banner when the webui or hermes-agent git repos are behind
their upstream branches. One-click 'Update Now' button does stash, pull
--ff-only, stash pop, then reloads the page.
Backend (api/updates.py):
- _check_repo(): git fetch + rev-list count with 15s timeout
- check_for_updates(): 30-min server-side cache, thread-safe, skips
Docker (no .git dir)
- apply_update(): stash (if dirty), pull --ff-only, pop, invalidate cache
Routes:
- GET /api/updates/check -- returns cached {webui, agent} with behind count
- POST /api/updates/apply -- {target: 'webui'|'agent'}
Frontend:
- Blue banner (matches reconnect-banner pattern) with 'Later' / 'Update Now'
- Non-blocking boot check via fire-and-forget .then(), once per tab session
- sessionStorage guards prevent re-checking and re-showing after dismiss
Settings:
- 'Check for updates' checkbox (default: on) -- when off, no git operations
- Removed 'Default Workspace' dropdown to keep settings panel compact
Performance:
- Server cache: git fetch at most 2x/hour regardless of client count
- sessionStorage: one check per browser tab session
- _check_in_progress flag prevents concurrent fetch storms
- Fire-and-forget: does NOT block the boot sequence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:11:44 +00:00
|
|
|
|
|
2026-04-23 00:51:12 +00:00
|
|
|
|
// Poll /health after an update-triggered restart, then reload. Replaces the
|
|
|
|
|
|
// blind setTimeout(reload, 2500) that race-lost against slow hardware or
|
|
|
|
|
|
// reverse proxies that 502 immediately when the upstream socket closes (#874).
|
|
|
|
|
|
async function _waitForServerThenReload(opts){
|
|
|
|
|
|
opts=opts||{};
|
|
|
|
|
|
const interval=opts.interval||500;
|
|
|
|
|
|
const maxMs=opts.maxMs||15000;
|
|
|
|
|
|
window._restartingForUpdate=true;
|
|
|
|
|
|
const msgEl=$('reconnectMsg');
|
|
|
|
|
|
const banner=$('reconnectBanner');
|
|
|
|
|
|
if(msgEl) msgEl.textContent='⏳ Restarting… please wait';
|
|
|
|
|
|
if(banner) banner.classList.add('visible');
|
|
|
|
|
|
const deadline=Date.now()+maxMs;
|
|
|
|
|
|
// Give the server a moment to actually begin its restart before the first
|
|
|
|
|
|
// probe — otherwise the old process may still respond ok on the first poll.
|
|
|
|
|
|
await new Promise(r=>setTimeout(r, interval));
|
|
|
|
|
|
while(Date.now()<deadline){
|
|
|
|
|
|
try{
|
|
|
|
|
|
const r=await fetch('/health',{cache:'no-store'});
|
|
|
|
|
|
if(r.ok){
|
|
|
|
|
|
let data={};
|
|
|
|
|
|
try{ data=await r.json(); }catch(_){}
|
|
|
|
|
|
if(data && data.status==='ok'){
|
|
|
|
|
|
location.reload();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}catch(_){ /* socket closed during restart — retry */ }
|
|
|
|
|
|
await new Promise(r=>setTimeout(r, interval));
|
|
|
|
|
|
}
|
|
|
|
|
|
if(msgEl) msgEl.textContent='⚠️ Server is taking longer than expected — click Reload when ready';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 23:18:15 +00:00
|
|
|
|
function getPendingSessionMessage(session){
|
|
|
|
|
|
const text=String(session?.pending_user_message||'').trim();
|
|
|
|
|
|
if(!text) return null;
|
|
|
|
|
|
const attachments=Array.isArray(session?.pending_attachments)?session.pending_attachments.filter(Boolean):[];
|
|
|
|
|
|
const messages=Array.isArray(session?.messages)?session.messages:[];
|
|
|
|
|
|
const lastUser=[...messages].reverse().find(m=>m&&m.role==='user');
|
|
|
|
|
|
if(lastUser){
|
|
|
|
|
|
const lastText=String(msgContent(lastUser)||'').trim();
|
|
|
|
|
|
if(lastText===text){
|
|
|
|
|
|
if(attachments.length&&!lastUser.attachments?.length) lastUser.attachments=attachments;
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
role:'user',
|
|
|
|
|
|
content:text,
|
|
|
|
|
|
attachments:attachments.length?attachments:undefined,
|
|
|
|
|
|
_ts:session?.pending_started_at||Date.now()/1000,
|
|
|
|
|
|
_pending:true,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2026-03-31 03:40:19 +00:00
|
|
|
|
async function checkInflightOnBoot(sid) {
|
|
|
|
|
|
const raw = localStorage.getItem(INFLIGHT_KEY);
|
|
|
|
|
|
if (!raw) return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const {sid: inflightSid, streamId, ts} = JSON.parse(raw);
|
|
|
|
|
|
if (inflightSid !== sid) { clearInflight(); return; }
|
2026-04-13 23:18:15 +00:00
|
|
|
|
if (S.activeStreamId && S.activeStreamId === streamId) return;
|
2026-03-31 03:40:19 +00:00
|
|
|
|
// Only show banner if the in-flight entry is less than 10 minutes old
|
|
|
|
|
|
if (Date.now() - ts > 10 * 60 * 1000) { clearInflight(); return; }
|
|
|
|
|
|
// Check if stream is still active
|
|
|
|
|
|
const status = await api(`/api/chat/stream/status?stream_id=${encodeURIComponent(streamId || '')}`);
|
|
|
|
|
|
if (status.active) {
|
|
|
|
|
|
// Stream is genuinely still running -- show the banner
|
2026-04-08 14:55:03 +00:00
|
|
|
|
showReconnectBanner(t('reconnect_active'));
|
2026-03-31 03:40:19 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
// Stream finished. Only show banner if reload happened within 90 seconds
|
|
|
|
|
|
// (longer gap = normal completed session, not a mid-stream reload)
|
|
|
|
|
|
if (Date.now() - ts < 90 * 1000) {
|
2026-04-08 14:55:03 +00:00
|
|
|
|
showReconnectBanner(t('reconnect_finished'));
|
2026-03-31 03:40:19 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
clearInflight(); // completed normally, no banner needed
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch(e) { clearInflight(); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function syncTopbar(){
|
|
|
|
|
|
if(!S.session){
|
2026-04-06 05:10:59 +00:00
|
|
|
|
document.title=window._botName||'Hermes';
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
if(typeof syncWorkspaceDisplays==='function') syncWorkspaceDisplays();
|
|
|
|
|
|
if(typeof syncModelChip==='function') syncModelChip();
|
|
|
|
|
|
if(typeof _syncHermesPanelSessionActions==='function') _syncHermesPanelSessionActions();
|
|
|
|
|
|
else {
|
|
|
|
|
|
const sidebarName=$('sidebarWsName');
|
|
|
|
|
|
if(sidebarName && sidebarName.textContent==='Workspace'){
|
|
|
|
|
|
sidebarName.textContent=t('no_workspace');
|
|
|
|
|
|
}
|
2026-03-31 03:40:19 +00:00
|
|
|
|
}
|
refactor(ui): three-column layout with left rail + main-view migration (#899)
refactor(ui): three-column layout with left rail + main-view migration (#899)
Unifies the shell into a three-column layout (rail + sidebar + main) matching the
hermes-desktop reference, and migrates every per-item detail/edit surface into a
shared main-view canvas with consistent headers, empty states, and action buttons.
Changes:
- New desktop-only left rail (48px) with 8 nav tabs (chat/tasks/skills/memory/workspaces/profiles/todos/settings)
- Persistent app titlebar (replaces per-chat topbar), active conversation title shown
- All panel detail/create/edit views migrated to #mainSkills, #mainTasks, #mainSettings, #mainWorkspaces, #mainProfiles, #mainMemory
- Settings moved out of modal into main-view page; ESC closes it
- YAML frontmatter rendered in collapsible <details> block in skill detail
- Toasts repositioned from bottom-center to top-right with theme-aware success/error/warning/info variants
- Composer workspace chip split into two-button group: files-icon toggles file panel, label opens workspace picker
- .settings-menu → .side-menu / .side-menu-item (generalised, shared by memory and settings panels)
- i18n: ~25 new keys across en/ru/es/de/zh/zh-Hant for all new form labels, placeholders, and empty states
- Mobile: hamburger in titlebar, slide-in sidebar; box-shadow removed from sidebar
- New regression test: tests/test_settings_navigation_and_detail_refresh.py (9 tests)
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
2026-04-24 16:05:25 +00:00
|
|
|
|
if(typeof syncAppTitlebar==='function') syncAppTitlebar();
|
2026-03-31 03:40:19 +00:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-04-08 14:55:03 +00:00
|
|
|
|
const sessionTitle=S.session.title||t('untitled');
|
refactor(ui): three-column layout with left rail + main-view migration (#899)
refactor(ui): three-column layout with left rail + main-view migration (#899)
Unifies the shell into a three-column layout (rail + sidebar + main) matching the
hermes-desktop reference, and migrates every per-item detail/edit surface into a
shared main-view canvas with consistent headers, empty states, and action buttons.
Changes:
- New desktop-only left rail (48px) with 8 nav tabs (chat/tasks/skills/memory/workspaces/profiles/todos/settings)
- Persistent app titlebar (replaces per-chat topbar), active conversation title shown
- All panel detail/create/edit views migrated to #mainSkills, #mainTasks, #mainSettings, #mainWorkspaces, #mainProfiles, #mainMemory
- Settings moved out of modal into main-view page; ESC closes it
- YAML frontmatter rendered in collapsible <details> block in skill detail
- Toasts repositioned from bottom-center to top-right with theme-aware success/error/warning/info variants
- Composer workspace chip split into two-button group: files-icon toggles file panel, label opens workspace picker
- .settings-menu → .side-menu / .side-menu-item (generalised, shared by memory and settings panels)
- i18n: ~25 new keys across en/ru/es/de/zh/zh-Hant for all new form labels, placeholders, and empty states
- Mobile: hamburger in titlebar, slide-in sidebar; box-shadow removed from sidebar
- New regression test: tests/test_settings_navigation_and_detail_refresh.py (9 tests)
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
2026-04-24 16:05:25 +00:00
|
|
|
|
const _topbarTitle=$('topbarTitle');if(_topbarTitle)_topbarTitle.textContent=sessionTitle;
|
2026-04-06 05:10:59 +00:00
|
|
|
|
document.title=sessionTitle+' \u2014 '+(window._botName||'Hermes');
|
2026-03-31 03:40:19 +00:00
|
|
|
|
const vis=S.messages.filter(m=>m&&m.role&&m.role!=='tool');
|
refactor(ui): three-column layout with left rail + main-view migration (#899)
refactor(ui): three-column layout with left rail + main-view migration (#899)
Unifies the shell into a three-column layout (rail + sidebar + main) matching the
hermes-desktop reference, and migrates every per-item detail/edit surface into a
shared main-view canvas with consistent headers, empty states, and action buttons.
Changes:
- New desktop-only left rail (48px) with 8 nav tabs (chat/tasks/skills/memory/workspaces/profiles/todos/settings)
- Persistent app titlebar (replaces per-chat topbar), active conversation title shown
- All panel detail/create/edit views migrated to #mainSkills, #mainTasks, #mainSettings, #mainWorkspaces, #mainProfiles, #mainMemory
- Settings moved out of modal into main-view page; ESC closes it
- YAML frontmatter rendered in collapsible <details> block in skill detail
- Toasts repositioned from bottom-center to top-right with theme-aware success/error/warning/info variants
- Composer workspace chip split into two-button group: files-icon toggles file panel, label opens workspace picker
- .settings-menu → .side-menu / .side-menu-item (generalised, shared by memory and settings panels)
- i18n: ~25 new keys across en/ru/es/de/zh/zh-Hant for all new form labels, placeholders, and empty states
- Mobile: hamburger in titlebar, slide-in sidebar; box-shadow removed from sidebar
- New regression test: tests/test_settings_navigation_and_detail_refresh.py (9 tests)
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
2026-04-24 16:05:25 +00:00
|
|
|
|
const _topbarMeta=$('topbarMeta');if(_topbarMeta)_topbarMeta.textContent=t('n_messages',vis.length);
|
|
|
|
|
|
if(typeof syncAppTitlebar==='function') syncAppTitlebar();
|
feat(queue): Codex-style message queue flyout above composer (#1040)
* chore: apply pending #965 queue flyout patches on local master
Queue flyout implementation (PR #965 — pending merge) applied on top of
upstream v0.50.205. Features:
- Queue card slides up from behind composer (approval-card pattern)
- Lucide icons via li(), CSS class system, no inline SVG dumps
- Drag-to-reorder by _queued_at timestamp (survives re-renders)
- Inline contenteditable edit with focus guard and blur-commit
- Combine preserves first item files, merge immediate (no 200ms race)
- Files/model compact badges per item
- Hide/expand via header chevron + composer pill + titlebar chip
- All 3 expand paths sync correctly
- border-bottom CSS order fixed, fingerprint improved, _dragTs guards
CF CSP domains also applied (deployment-specific, not in upstream PR).
* fix(queue): harden merge closure, toggleQueue sid, and drain flash
- mergeBtn _doMerge now reads live queue (_getSessionQueue) instead of stale closure q
- toggleQueue reads activeSid from S.session at call time, not captured param
- updateQueueBadge defers chips.innerHTML='' by 360ms so slide-out transition completes before content clears
* style(queue): contain:paint on inner, pill fade-in animation
* feat(queue): pill outside composer, compact collapsed state matching card width
- Move #queuePill out of .composer-box to between .composer-flyout and .composer-box
- Pill styled as compact queue-card-inner (same border, radius:14px 14px 0 0, no border-bottom)
- Pill width matches card inner: max-width:calc(var(--msg-max)-40px), centered
- Pill stays visible until user re-expands or queue drains (updateQueueBadge no longer
hides pill when card is manually collapsed)
- Remove all queue-active/queue-pill-active composer modifications — composer untouched
- Fix: mergeBtn reads live queue not stale closure
- Fix: toggleQueue uses S.session.session_id at call time not captured param
- Fix: chips.innerHTML deferred 360ms on drain to avoid empty-card flash
* fix(queue): collapsed state persists + cross-session DOM isolation
- Add _queueCollapsed[sid] flag: set by hideBtn, cleared by pill expand / queue drain
- _renderQueueChips respects flag — no longer reopens card when new message queued while collapsed
- updateQueueBadge else-branch: DOM mutations now gated on sid===active session
- _syncQueueTitlebar only fires for active session in else-branch
- Fixes Opus/Codex-identified bugs: pill auto-reopen and cross-session DOM corruption
* fix(queue): proper pill wrapper matching queue-card structure
- Add .queue-pill-outer div wrapper (max-width:var(--msg-max); padding:0 20px)
identical to .queue-card outer — positions pill button at exact card-inner width
- .queue-pill button fills slot with width:100%
- Removes hardcoded 740px — width is derived correctly from the same CSS variables
the card uses, scales with --msg-max across all viewports
- JS toggles .show on pillOuter (parentElement), not on pill button directly
---------
Co-authored-by: Basit Mustafa <basit.mustafa@gmail.com>
2026-04-25 21:21:50 +00:00
|
|
|
|
if(typeof _syncQueueTitlebar==='function'){const _qs=S.session&&S.session.session_id;_syncQueueTitlebar(_qs,_qs?getQueuedSessionCount(_qs):0);}
|
fix: model picker correctly updates on profile switch without flicker or raw injection
Root cause: three interacting bugs caused the model picker to show the wrong
model or flicker after a profile switch.
Bug 1 — syncTopbar() fought switchToProfile().
After switchToProfile() set the picker to the profile's model, syncTopbar()
was called (via renderSessionList -> loadSession, then explicitly at the end)
and overwrote it with S.session.model -- the old session's model.
Fix: added S._pendingProfileModel flag. switchToProfile() sets it;
syncTopbar() checks it first, applies the override, then clears it.
S.session.model is also updated to the resolved value so subsequent
syncTopbar() calls are consistent.
Bug 2 — Raw option injected at top of list for mismatched model IDs.
Profile configs store model IDs like 'claude-sonnet-4-6' (hermes-agent
format: hyphens, no namespace prefix) but the dropdown has
'anthropic/claude-sonnet-4.6' (OpenRouter format: dots, with prefix).
The old code did sel.value = id, found no match, then injected a new
<option> at the top of the list -- creating a lowercase duplicate that
didn't match any real provider group entry.
Fix: _findModelInDropdown() normalises both sides (strip prefix, hyphens->dots,
lowercase) and finds the best matching existing option. No new options are ever
injected for profile switching.
Bug 3 — populateModelDropdown() injected raw option on cold load.
Same issue: if default_model from /api/models didn't exactly match a dropdown
value, an extra option was added. Fixed by using _applyModelToDropdown()
which only selects existing options.
New helpers in ui.js:
_findModelInDropdown(modelId, sel) -- smart fuzzy match, returns matched value
_applyModelToDropdown(modelId, sel) -- sets picker, returns resolved value
Tests: 426 passed, 0 failed.
2026-04-03 20:10:47 +00:00
|
|
|
|
// If a profile switch just happened, apply its model rather than the session's stale value.
|
|
|
|
|
|
// S._pendingProfileModel is set by switchToProfile() and cleared here after one application.
|
|
|
|
|
|
const modelOverride=S._pendingProfileModel;
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
let currentModel=S.session.model||'';
|
fix: model picker correctly updates on profile switch without flicker or raw injection
Root cause: three interacting bugs caused the model picker to show the wrong
model or flicker after a profile switch.
Bug 1 — syncTopbar() fought switchToProfile().
After switchToProfile() set the picker to the profile's model, syncTopbar()
was called (via renderSessionList -> loadSession, then explicitly at the end)
and overwrote it with S.session.model -- the old session's model.
Fix: added S._pendingProfileModel flag. switchToProfile() sets it;
syncTopbar() checks it first, applies the override, then clears it.
S.session.model is also updated to the resolved value so subsequent
syncTopbar() calls are consistent.
Bug 2 — Raw option injected at top of list for mismatched model IDs.
Profile configs store model IDs like 'claude-sonnet-4-6' (hermes-agent
format: hyphens, no namespace prefix) but the dropdown has
'anthropic/claude-sonnet-4.6' (OpenRouter format: dots, with prefix).
The old code did sel.value = id, found no match, then injected a new
<option> at the top of the list -- creating a lowercase duplicate that
didn't match any real provider group entry.
Fix: _findModelInDropdown() normalises both sides (strip prefix, hyphens->dots,
lowercase) and finds the best matching existing option. No new options are ever
injected for profile switching.
Bug 3 — populateModelDropdown() injected raw option on cold load.
Same issue: if default_model from /api/models didn't exactly match a dropdown
value, an extra option was added. Fixed by using _applyModelToDropdown()
which only selects existing options.
New helpers in ui.js:
_findModelInDropdown(modelId, sel) -- smart fuzzy match, returns matched value
_applyModelToDropdown(modelId, sel) -- sets picker, returns resolved value
Tests: 426 passed, 0 failed.
2026-04-03 20:10:47 +00:00
|
|
|
|
if(modelOverride){
|
|
|
|
|
|
S._pendingProfileModel=null;
|
|
|
|
|
|
_applyModelToDropdown(modelOverride,$('modelSelect'));
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
currentModel=modelOverride;
|
fix: model picker correctly updates on profile switch without flicker or raw injection
Root cause: three interacting bugs caused the model picker to show the wrong
model or flicker after a profile switch.
Bug 1 — syncTopbar() fought switchToProfile().
After switchToProfile() set the picker to the profile's model, syncTopbar()
was called (via renderSessionList -> loadSession, then explicitly at the end)
and overwrote it with S.session.model -- the old session's model.
Fix: added S._pendingProfileModel flag. switchToProfile() sets it;
syncTopbar() checks it first, applies the override, then clears it.
S.session.model is also updated to the resolved value so subsequent
syncTopbar() calls are consistent.
Bug 2 — Raw option injected at top of list for mismatched model IDs.
Profile configs store model IDs like 'claude-sonnet-4-6' (hermes-agent
format: hyphens, no namespace prefix) but the dropdown has
'anthropic/claude-sonnet-4.6' (OpenRouter format: dots, with prefix).
The old code did sel.value = id, found no match, then injected a new
<option> at the top of the list -- creating a lowercase duplicate that
didn't match any real provider group entry.
Fix: _findModelInDropdown() normalises both sides (strip prefix, hyphens->dots,
lowercase) and finds the best matching existing option. No new options are ever
injected for profile switching.
Bug 3 — populateModelDropdown() injected raw option on cold load.
Same issue: if default_model from /api/models didn't exactly match a dropdown
value, an extra option was added. Fixed by using _applyModelToDropdown()
which only selects existing options.
New helpers in ui.js:
_findModelInDropdown(modelId, sel) -- smart fuzzy match, returns matched value
_applyModelToDropdown(modelId, sel) -- sets picker, returns resolved value
Tests: 426 passed, 0 failed.
2026-04-03 20:10:47 +00:00
|
|
|
|
} else {
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
const applied=_applyModelToDropdown(currentModel,$('modelSelect'));
|
2026-04-22 05:20:08 +00:00
|
|
|
|
// If the model isn't in the current provider list, silently reset to the
|
|
|
|
|
|
// first available model so stale values don't pollute the picker (#829).
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
if(!applied && currentModel){
|
v0.50.207: batch of 10 PRs — TPS stat, SSE guard, session polish, cron UX, folder create, model errors, session speed, title gen (#1031)
* fix: remove orphaned i18n keys from top-level LOCALES object
Three Traditional Chinese translation keys (cmd_status, memory_saved,
profile_delete_title) were placed outside any locale block between the
en and ru blocks in static/i18n.js. They became top-level properties
of the LOCALES object, causing them to appear as invalid language
options in the Settings > Preferences dropdown.
The correct translations already exist in the zh-Hant locale block.
Fixes #1008
* fix: block stale SSE events from polluting new session's DOM
- appendThinking(): guard with !S.session||!S.activeStreamId to drop
events from a previous session's SSE stream during a session switch
- appendLiveToolCard(): same guard for consistency
- finalizeThinkingCard(): scroll thinking-card-body to top when
scroll is pinned, so completed response is immediately visible
- appendThinking(): auto-scroll thinking card body to bottom while
streaming if user is watching (scroll pinned)
* Fix empty agent sessions in sidebar
* fix: resolve cron UI UX issues — icon ambiguity, toast overlap, running status
Fixes #995 — three sub-issues in the Cron Jobs UI:
1. Dual play icons ambiguous: Resume button now shows a distinct
play+bar icon (play triangle + vertical line) instead of the
identical triangle used by Run now.
2. Toast notification overlapping header buttons: Added
position:relative; z-index:10 to .main-view-header so it
stacks above the fixed toast (z-index:100 within its layer).
3. No running status after trigger: After triggering a job, the
status badge immediately shows 'running…' with a CSS spinner
animation, and polls the cron list every 3s (up to 30s) to
refresh when the job completes.
- Added cron_status_running i18n key in all 5 locales (en, es, de, ru, zh, zh-Hant)
- Added .detail-badge.running CSS class with spinner animation
- New functions: _setCronDetailStatus(), _startCronRunningPoll()
* fix(#1011): address review feedback — poll cleanup, badge persistence, 30s fallback
- _clearCronDetail() now clears _cronRunningPoll interval on navigation
- Poll re-applies 'running' badge after loadCrons() re-render (prevents flicker)
- When poll ends (30s max), detail re-renders with actual status as fallback
* feat: create folder and add space directly from UI (#782)
- After creating a folder via the file tree New folder button, offer to add it as a space via confirm dialog
- Add Create folder if it doesnt exist checkbox in the New Space form
- Backend: support create flag in /api/workspaces/add to mkdir before validation
- i18n: 4 new keys (folder_add_as_space_title/msg/btn, workspace_auto_create_folder) in all 6 locales
* fix: validate workspace path before mkdir to prevent orphan directories
Review feedback (critical): the previous code called mkdir() before
validate_workspace_to_add(), which meant a rejected path (e.g. system dir)
would leave an orphan directory on disk.
New flow:
1. Resolve path and check against blocked system roots BEFORE any mutation
2. mkdir() only if path passes the blocklist check
3. Full validation (exists, is_dir) after mkdir
Also imports _workspace_blocked_roots for the pre-mutation blocklist check.
* fix(#1014): classify model-not-found errors with helpful message
- Add model_not_found error type to streaming.py exception classifier
- Detect 404, 'not found', 'does not exist', 'invalid model' patterns
- Strip HTML tags from provider error messages (nginx 404 pages, etc.)
- Add model_not_found branch to apperror handler in messages.js
- Add i18n key model_not_found_label in all 6 locales
- 15 tests covering detection, sanitization, frontend, and i18n
* feat(ui): add live TPS stat to header
Adds a TPS (Tokens Per Second) chip to the right of the header title bar
that updates live while AI output is streaming.
Metering (api/metering.py)
- Tracks per-session output + reasoning tokens via GlobalMeter singleton
- Per-session TPS = total_tokens / elapsed_time
- Global TPS = average of active sessions' TPS values
- HIGH/LOW are max/min of global_tps snapshots over a 60-minute rolling
window (only recorded when > 0, so idle periods are excluded)
- Thread-safe with a single lock
Metering events emitted from streaming.py
- Throttled at 100ms from token/reasoning/tool callbacks so the display
updates rapidly during fast token streams
- 1Hz ticker as fallback for slow streams (exits when no active sessions)
- Final stats emitted on stream end
Routes (api/routes.py)
- Removed POST /api/metering/interval endpoint (dynamic interval via
focus/blur was replaced with simple always-1s-when-active approach)
UI (static/messages.js, index.html, style.css)
- TPS chip in titlebar: shows 'N.N t/s . N.N high . N.N low'
- Default: '0.0 t/s . 0.0 high' when idle
- Display updates on every metering SSE event (throttled to 100ms)
* feat: session restore speed + title gen reasoning hardening (#1025, #1026)
PR #1025 (@franksong2702): Speed up large session restore paths
- GET /api/session?messages=0 now parses only metadata before the messages array
- Metadata-only loads no longer populate the full-session LRU cache
- Frontend lazy fetch uses resolve_model=0 to avoid cold model-catalog lookup
- Hard reload no longer waits for populateModelDropdown() before restoring session
PR #1026 (@franksong2702): Harden auto title generation for reasoning models
- Raises title-gen completion budget to 512 tokens (reasoning-safe)
- Retries once with 1024 tokens on empty content / finish_reason:length
- Applies retry to both auxiliary and active-agent fallback routes
- Preserves underlying failure reason in title_status on local fallback
Co-authored-by: Frank Song <franksong2702@gmail.com>
* feat: session attention indicators in right slot + last_message_at timestamps (#1024)
PR #1024 (@franksong2702): Polish session attention indicators
- Streaming spinners and unread dots now reuse the right-side actions slot
- Running/unread rows hide timestamps; idle/read rows keep right-aligned timestamps
- Date group carets point down when expanded, right when collapsed
- Pinned group no longer repeats pinned-star icon per row
- Running indicators appear immediately after send (local busy state while /api/sessions catches up)
- Sidebar sorting/grouping/timestamps now prefer last_message_at (derived from last real message)
so metadata-only saves don't make old sessions appear under Today
Co-authored-by: Frank Song <franksong2702@gmail.com>
* docs: v0.50.207 release notes — 10 PRs, 2169 tests (+36)
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: Josh <josh@fyul.link>
Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-25 20:07:35 +00:00
|
|
|
|
const deferModelCorrection=Boolean(S.session._modelResolutionDeferred);
|
2026-04-22 05:20:08 +00:00
|
|
|
|
// Stale session model not in the current provider catalog — reset to the
|
|
|
|
|
|
// first available model rather than injecting an "(unavailable)" option
|
|
|
|
|
|
// that visually appears under the wrong provider group (#829).
|
|
|
|
|
|
const modelSel=$('modelSelect');
|
|
|
|
|
|
const first=modelSel&&modelSel.querySelector('optgroup > option, option');
|
|
|
|
|
|
if(first){
|
|
|
|
|
|
modelSel.value=first.value;
|
v0.50.207: batch of 10 PRs — TPS stat, SSE guard, session polish, cron UX, folder create, model errors, session speed, title gen (#1031)
* fix: remove orphaned i18n keys from top-level LOCALES object
Three Traditional Chinese translation keys (cmd_status, memory_saved,
profile_delete_title) were placed outside any locale block between the
en and ru blocks in static/i18n.js. They became top-level properties
of the LOCALES object, causing them to appear as invalid language
options in the Settings > Preferences dropdown.
The correct translations already exist in the zh-Hant locale block.
Fixes #1008
* fix: block stale SSE events from polluting new session's DOM
- appendThinking(): guard with !S.session||!S.activeStreamId to drop
events from a previous session's SSE stream during a session switch
- appendLiveToolCard(): same guard for consistency
- finalizeThinkingCard(): scroll thinking-card-body to top when
scroll is pinned, so completed response is immediately visible
- appendThinking(): auto-scroll thinking card body to bottom while
streaming if user is watching (scroll pinned)
* Fix empty agent sessions in sidebar
* fix: resolve cron UI UX issues — icon ambiguity, toast overlap, running status
Fixes #995 — three sub-issues in the Cron Jobs UI:
1. Dual play icons ambiguous: Resume button now shows a distinct
play+bar icon (play triangle + vertical line) instead of the
identical triangle used by Run now.
2. Toast notification overlapping header buttons: Added
position:relative; z-index:10 to .main-view-header so it
stacks above the fixed toast (z-index:100 within its layer).
3. No running status after trigger: After triggering a job, the
status badge immediately shows 'running…' with a CSS spinner
animation, and polls the cron list every 3s (up to 30s) to
refresh when the job completes.
- Added cron_status_running i18n key in all 5 locales (en, es, de, ru, zh, zh-Hant)
- Added .detail-badge.running CSS class with spinner animation
- New functions: _setCronDetailStatus(), _startCronRunningPoll()
* fix(#1011): address review feedback — poll cleanup, badge persistence, 30s fallback
- _clearCronDetail() now clears _cronRunningPoll interval on navigation
- Poll re-applies 'running' badge after loadCrons() re-render (prevents flicker)
- When poll ends (30s max), detail re-renders with actual status as fallback
* feat: create folder and add space directly from UI (#782)
- After creating a folder via the file tree New folder button, offer to add it as a space via confirm dialog
- Add Create folder if it doesnt exist checkbox in the New Space form
- Backend: support create flag in /api/workspaces/add to mkdir before validation
- i18n: 4 new keys (folder_add_as_space_title/msg/btn, workspace_auto_create_folder) in all 6 locales
* fix: validate workspace path before mkdir to prevent orphan directories
Review feedback (critical): the previous code called mkdir() before
validate_workspace_to_add(), which meant a rejected path (e.g. system dir)
would leave an orphan directory on disk.
New flow:
1. Resolve path and check against blocked system roots BEFORE any mutation
2. mkdir() only if path passes the blocklist check
3. Full validation (exists, is_dir) after mkdir
Also imports _workspace_blocked_roots for the pre-mutation blocklist check.
* fix(#1014): classify model-not-found errors with helpful message
- Add model_not_found error type to streaming.py exception classifier
- Detect 404, 'not found', 'does not exist', 'invalid model' patterns
- Strip HTML tags from provider error messages (nginx 404 pages, etc.)
- Add model_not_found branch to apperror handler in messages.js
- Add i18n key model_not_found_label in all 6 locales
- 15 tests covering detection, sanitization, frontend, and i18n
* feat(ui): add live TPS stat to header
Adds a TPS (Tokens Per Second) chip to the right of the header title bar
that updates live while AI output is streaming.
Metering (api/metering.py)
- Tracks per-session output + reasoning tokens via GlobalMeter singleton
- Per-session TPS = total_tokens / elapsed_time
- Global TPS = average of active sessions' TPS values
- HIGH/LOW are max/min of global_tps snapshots over a 60-minute rolling
window (only recorded when > 0, so idle periods are excluded)
- Thread-safe with a single lock
Metering events emitted from streaming.py
- Throttled at 100ms from token/reasoning/tool callbacks so the display
updates rapidly during fast token streams
- 1Hz ticker as fallback for slow streams (exits when no active sessions)
- Final stats emitted on stream end
Routes (api/routes.py)
- Removed POST /api/metering/interval endpoint (dynamic interval via
focus/blur was replaced with simple always-1s-when-active approach)
UI (static/messages.js, index.html, style.css)
- TPS chip in titlebar: shows 'N.N t/s . N.N high . N.N low'
- Default: '0.0 t/s . 0.0 high' when idle
- Display updates on every metering SSE event (throttled to 100ms)
* feat: session restore speed + title gen reasoning hardening (#1025, #1026)
PR #1025 (@franksong2702): Speed up large session restore paths
- GET /api/session?messages=0 now parses only metadata before the messages array
- Metadata-only loads no longer populate the full-session LRU cache
- Frontend lazy fetch uses resolve_model=0 to avoid cold model-catalog lookup
- Hard reload no longer waits for populateModelDropdown() before restoring session
PR #1026 (@franksong2702): Harden auto title generation for reasoning models
- Raises title-gen completion budget to 512 tokens (reasoning-safe)
- Retries once with 1024 tokens on empty content / finish_reason:length
- Applies retry to both auxiliary and active-agent fallback routes
- Preserves underlying failure reason in title_status on local fallback
Co-authored-by: Frank Song <franksong2702@gmail.com>
* feat: session attention indicators in right slot + last_message_at timestamps (#1024)
PR #1024 (@franksong2702): Polish session attention indicators
- Streaming spinners and unread dots now reuse the right-side actions slot
- Running/unread rows hide timestamps; idle/read rows keep right-aligned timestamps
- Date group carets point down when expanded, right when collapsed
- Pinned group no longer repeats pinned-star icon per row
- Running indicators appear immediately after send (local busy state while /api/sessions catches up)
- Sidebar sorting/grouping/timestamps now prefer last_message_at (derived from last real message)
so metadata-only saves don't make old sessions appear under Today
Co-authored-by: Frank Song <franksong2702@gmail.com>
* docs: v0.50.207 release notes — 10 PRs, 2169 tests (+36)
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: Josh <josh@fyul.link>
Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-25 20:07:35 +00:00
|
|
|
|
if(!deferModelCorrection){
|
|
|
|
|
|
S.session.model=first.value;
|
|
|
|
|
|
// Persist the correction so the session doesn't re-inject on next load.
|
|
|
|
|
|
fetch(new URL('api/session/update',location.href).href,{
|
|
|
|
|
|
method:'POST',credentials:'include',
|
|
|
|
|
|
headers:{'Content-Type':'application/json'},
|
|
|
|
|
|
body:JSON.stringify({session_id:S.session.id||S.session.session_id,model:first.value})
|
|
|
|
|
|
}).catch(()=>{});
|
|
|
|
|
|
}
|
2026-04-22 05:20:08 +00:00
|
|
|
|
}
|
fix: model picker correctly updates on profile switch without flicker or raw injection
Root cause: three interacting bugs caused the model picker to show the wrong
model or flicker after a profile switch.
Bug 1 — syncTopbar() fought switchToProfile().
After switchToProfile() set the picker to the profile's model, syncTopbar()
was called (via renderSessionList -> loadSession, then explicitly at the end)
and overwrote it with S.session.model -- the old session's model.
Fix: added S._pendingProfileModel flag. switchToProfile() sets it;
syncTopbar() checks it first, applies the override, then clears it.
S.session.model is also updated to the resolved value so subsequent
syncTopbar() calls are consistent.
Bug 2 — Raw option injected at top of list for mismatched model IDs.
Profile configs store model IDs like 'claude-sonnet-4-6' (hermes-agent
format: hyphens, no namespace prefix) but the dropdown has
'anthropic/claude-sonnet-4.6' (OpenRouter format: dots, with prefix).
The old code did sel.value = id, found no match, then injected a new
<option> at the top of the list -- creating a lowercase duplicate that
didn't match any real provider group entry.
Fix: _findModelInDropdown() normalises both sides (strip prefix, hyphens->dots,
lowercase) and finds the best matching existing option. No new options are ever
injected for profile switching.
Bug 3 — populateModelDropdown() injected raw option on cold load.
Same issue: if default_model from /api/models didn't exactly match a dropdown
value, an extra option was added. Fixed by using _applyModelToDropdown()
which only selects existing options.
New helpers in ui.js:
_findModelInDropdown(modelId, sel) -- smart fuzzy match, returns matched value
_applyModelToDropdown(modelId, sel) -- sets picker, returns resolved value
Tests: 426 passed, 0 failed.
2026-04-03 20:10:47 +00:00
|
|
|
|
}
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
}
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
if(typeof syncModelChip==='function') syncModelChip();
|
2026-04-24 01:24:51 +00:00
|
|
|
|
if(typeof syncReasoningChip==='function') syncReasoningChip();
|
2026-03-31 03:40:19 +00:00
|
|
|
|
// Show Clear button only when session has messages
|
|
|
|
|
|
const clearBtn=$('btnClearConv');
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
if(clearBtn) clearBtn.style.display=(S.messages&&S.messages.filter(msg=>msg.role!=='tool').length>0)?'':'none';
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
if(typeof _syncHermesPanelSessionActions==='function') _syncHermesPanelSessionActions();
|
|
|
|
|
|
if(typeof syncWorkspaceDisplays==='function') syncWorkspaceDisplays();
|
2026-03-31 03:40:19 +00:00
|
|
|
|
// modelSelect already set above
|
feat: multi-profile support -- create, switch, delete profiles from web UI (Issue #28)
Add full profile management to the web UI, matching the hermes-agent CLI
profile system. Profiles are isolated HERMES_HOME instances with their own
config, skills, memory, cron, and API keys.
Backend: new api/profiles.py wrapping hermes_cli.profiles, dynamic config
reloading, 5 new API endpoints, profile-aware path resolution, HERMES_HOME
env save/restore in streaming, module-level cache patching for skills_tool
and cron/jobs.
Frontend: profile chip in topbar with dropdown, Profiles sidebar panel with
CRUD UI, boot-time profile fetch, cascade refresh on switch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 17:50:21 +00:00
|
|
|
|
// Update profile chip label
|
|
|
|
|
|
const profileLabel=$('profileChipLabel');
|
|
|
|
|
|
if(profileLabel) profileLabel.textContent=S.activeProfile||'default';
|
2026-03-31 03:40:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function msgContent(m){
|
|
|
|
|
|
// Extract plain text content from a message for filtering
|
|
|
|
|
|
let c=m.content||'';
|
|
|
|
|
|
if(Array.isArray(c))c=c.filter(p=>p&&p.type==='text').map(p=>p.text||'').join('').trim();
|
|
|
|
|
|
return String(c).trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
function _fmtDateSep(d){
|
|
|
|
|
|
const todayStart=new Date();todayStart.setHours(0,0,0,0);
|
|
|
|
|
|
const dStart=new Date(d);dStart.setHours(0,0,0,0);
|
|
|
|
|
|
const diffDays=Math.round((todayStart-dStart)/86400000);
|
|
|
|
|
|
if(diffDays===0) return 'Today';
|
|
|
|
|
|
if(diffDays===1) return 'Yesterday';
|
|
|
|
|
|
if(diffDays>0 && diffDays<7) return dStart.toLocaleDateString([], {weekday:'long'});
|
|
|
|
|
|
const opts={month:'short', day:'numeric'};
|
|
|
|
|
|
if(todayStart.getFullYear()!==dStart.getFullYear()) opts.year='numeric';
|
|
|
|
|
|
return dStart.toLocaleDateString([], opts);
|
|
|
|
|
|
}
|
|
|
|
|
|
const _ERR_MSG_RE=/^(?:\*\*error\b|error:|connection lost|no response received)/i;
|
|
|
|
|
|
function _messageHasReasoningPayload(m){
|
|
|
|
|
|
if(!m||m.role!=='assistant') return false;
|
|
|
|
|
|
if(m.reasoning) return true;
|
|
|
|
|
|
if(Array.isArray(m.content)) return m.content.some(p=>p&&(p.type==='thinking'||p.type==='reasoning'));
|
2026-04-18 06:45:39 +00:00
|
|
|
|
return /<think>[\s\S]*?<\/think>|<\|channel>thought\n[\s\S]*?<channel\|>|<\|turn\|>thinking\n[\s\S]*?<turn\|>/.test(String(m.content||''));
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
function _assistantRoleHtml(tsTitle=''){
|
|
|
|
|
|
const _bn=window._botName||'Hermes';
|
|
|
|
|
|
return `<div class="msg-role assistant" ${tsTitle?`title="${esc(tsTitle)}"`:''}><div class="role-icon assistant">${esc(_bn.charAt(0).toUpperCase())}</div><span style="font-size:12px">${esc(_bn)}</span></div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
function _createAssistantTurn(tsTitle=''){
|
|
|
|
|
|
const row=document.createElement('div');
|
|
|
|
|
|
row.className='msg-row assistant-turn';
|
|
|
|
|
|
row.dataset.role='assistant';
|
|
|
|
|
|
row.innerHTML=`${_assistantRoleHtml(tsTitle)}<div class="assistant-turn-blocks"></div>`;
|
|
|
|
|
|
return row;
|
|
|
|
|
|
}
|
|
|
|
|
|
function _assistantTurnBlocks(turn){
|
|
|
|
|
|
return turn?turn.querySelector('.assistant-turn-blocks'):null;
|
|
|
|
|
|
}
|
|
|
|
|
|
function _thinkingCardHtml(text){
|
2026-04-24 18:04:16 +00:00
|
|
|
|
const clean=_sanitizeThinkingDisplayText(text);
|
|
|
|
|
|
return `<div class="thinking-card"><div class="thinking-card-header" onclick="this.parentElement.classList.toggle('open')"><span class="thinking-card-icon">${li('lightbulb',14)}</span><span class="thinking-card-label">${t('thinking')}</span><span class="thinking-card-toggle">${li('chevron-right',12)}</span></div><div class="thinking-card-body"><pre>${esc(clean)}</pre></div></div>`;
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
}
|
2026-04-18 06:55:04 +00:00
|
|
|
|
function _compressionStateForCurrentSession(){
|
|
|
|
|
|
const state=window._compressionUi;
|
|
|
|
|
|
if(!state||!S.session||state.sessionId!==S.session.session_id) return null;
|
|
|
|
|
|
return state;
|
|
|
|
|
|
}
|
|
|
|
|
|
function isCompressionUiRunning(){
|
|
|
|
|
|
const state=_compressionStateForCurrentSession();
|
|
|
|
|
|
const lock=_compressionSessionLock();
|
|
|
|
|
|
return !!((state&&state.phase==='running') || (lock && S.session && lock===S.session.session_id));
|
|
|
|
|
|
}
|
|
|
|
|
|
function clearCompressionUi(){
|
|
|
|
|
|
window._compressionUi=null;
|
|
|
|
|
|
_setCompressionSessionLock(null);
|
|
|
|
|
|
renderCompressionUi();
|
|
|
|
|
|
}
|
|
|
|
|
|
function setCompressionUi(state){
|
|
|
|
|
|
if(!state){
|
|
|
|
|
|
clearCompressionUi();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
window._compressionUi={...state};
|
|
|
|
|
|
if(state.sessionId) _setCompressionSessionLock(state.sessionId);
|
|
|
|
|
|
renderCompressionUi();
|
|
|
|
|
|
}
|
|
|
|
|
|
function _compressionCardsHtml(state){
|
|
|
|
|
|
if(!state) return '';
|
|
|
|
|
|
const cmdText=state.commandText||'/compress';
|
|
|
|
|
|
const focusText=state.focusTopic?`${t('focus_label')}: ${state.focusTopic}`:'';
|
|
|
|
|
|
const headerText=state.phase==='done'
|
|
|
|
|
|
? (state.summary?.headline||t('compress_complete_label'))
|
|
|
|
|
|
: state.phase==='error'
|
|
|
|
|
|
? (state.errorText||t('compress_failed_label'))
|
|
|
|
|
|
: (typeof state.beforeCount==='number' ? t('n_messages', state.beforeCount) : '');
|
|
|
|
|
|
const statusBody=state.phase==='error'
|
|
|
|
|
|
? [state.errorText||t('compress_failed_label'), focusText].filter(Boolean).join('\n')
|
|
|
|
|
|
: [t('compressing'), focusText].filter(Boolean).join('\n');
|
|
|
|
|
|
const statusLabel=state.phase==='done'
|
|
|
|
|
|
? t('compress_complete_label')
|
|
|
|
|
|
: state.phase==='error'
|
|
|
|
|
|
? t('compress_failed_label')
|
|
|
|
|
|
: t('compress_running_label');
|
|
|
|
|
|
const statusIcon=state.phase==='done'
|
|
|
|
|
|
? li('check',13)
|
|
|
|
|
|
: state.phase==='error'
|
|
|
|
|
|
? li('x',13)
|
|
|
|
|
|
: `<span class="tool-card-running-dot"></span>`;
|
|
|
|
|
|
const doneCardHtml=state.phase==='done'
|
|
|
|
|
|
? _compressionStatusCardHtml({
|
|
|
|
|
|
statusLabel,
|
|
|
|
|
|
previewText: headerText,
|
|
|
|
|
|
detail: [state.summary?.token_line, state.summary?.note, focusText].filter(Boolean).join('\n'),
|
|
|
|
|
|
icon: statusIcon,
|
|
|
|
|
|
open: true,
|
|
|
|
|
|
variantClass: 'tool-card-compress-complete',
|
|
|
|
|
|
})
|
|
|
|
|
|
: '';
|
|
|
|
|
|
const referenceHtml=(state.phase==='done'&&state.referenceText)
|
|
|
|
|
|
? _compressionReferenceCardHtml(state.referenceText, false)
|
|
|
|
|
|
: '';
|
|
|
|
|
|
return `
|
|
|
|
|
|
<div class="tool-card-row compression-card-row" data-compression-card="1">
|
|
|
|
|
|
<div class="tool-card tool-card-compress-command">
|
|
|
|
|
|
<div class="tool-card-header" onclick="this.closest('.tool-card').classList.toggle('open')">
|
|
|
|
|
|
<span class="tool-card-icon">${li('settings',13)}</span>
|
|
|
|
|
|
<span class="tool-card-name">${esc(t('command_label'))}</span>
|
|
|
|
|
|
<span class="tool-card-preview">${esc(cmdText)}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="tool-card-row compression-card-row" data-compression-card="1">
|
|
|
|
|
|
${state.phase==='done'
|
|
|
|
|
|
? doneCardHtml
|
|
|
|
|
|
: _compressionStatusCardHtml({
|
|
|
|
|
|
statusLabel,
|
|
|
|
|
|
previewText: headerText,
|
|
|
|
|
|
detail: statusBody,
|
|
|
|
|
|
icon: statusIcon,
|
|
|
|
|
|
open: false,
|
|
|
|
|
|
variantClass: state.phase==='error'
|
|
|
|
|
|
? 'tool-card-compress-error'
|
|
|
|
|
|
: 'tool-card-compress-running',
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
${referenceHtml}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
function _compressionCardsNode(state){
|
|
|
|
|
|
const wrap=document.createElement('div');
|
|
|
|
|
|
wrap.className='compression-turn';
|
|
|
|
|
|
wrap.innerHTML=`<div class="compression-turn-blocks">${_compressionCardsHtml(state)}</div>`;
|
|
|
|
|
|
return wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
function _isContextCompactionMessage(m){
|
|
|
|
|
|
if(!m||m.role!=='assistant') return false;
|
|
|
|
|
|
const text=msgContent(m)||String(m.content||'');
|
|
|
|
|
|
return /^\s*\[context compaction/i.test(text) || /^\s*context compaction/i.test(text);
|
|
|
|
|
|
}
|
|
|
|
|
|
function _compressionMessageAnchorKey(m){
|
|
|
|
|
|
if(!m||!m.role||m.role==='tool') return null;
|
|
|
|
|
|
let content='';
|
|
|
|
|
|
try{
|
|
|
|
|
|
content=String(msgContent(m)||'');
|
|
|
|
|
|
}catch(_){
|
|
|
|
|
|
content=String(m.content||'');
|
|
|
|
|
|
}
|
|
|
|
|
|
const norm=content.replace(/\s+/g,' ').trim().slice(0,160);
|
|
|
|
|
|
const ts=m._ts||m.timestamp||null;
|
|
|
|
|
|
const attachments=Array.isArray(m.attachments)?m.attachments.length:0;
|
|
|
|
|
|
if(!norm && !attachments && !ts) return null;
|
|
|
|
|
|
return {role:String(m.role||''), ts, text:norm, attachments};
|
|
|
|
|
|
}
|
|
|
|
|
|
function _compressionAnchorIndex(visWithIdx, anchorKey, fallbackIdx=null){
|
|
|
|
|
|
if(anchorKey&&Array.isArray(visWithIdx)){
|
|
|
|
|
|
for(let i=visWithIdx.length-1;i>=0;i--){
|
|
|
|
|
|
const candidate=_compressionMessageAnchorKey(visWithIdx[i].m);
|
|
|
|
|
|
if(!candidate) continue;
|
|
|
|
|
|
if(
|
|
|
|
|
|
candidate.role===String(anchorKey.role||'') &&
|
|
|
|
|
|
String(candidate.ts??'')===String(anchorKey.ts??'') &&
|
|
|
|
|
|
String(candidate.text||'')===String(anchorKey.text||'') &&
|
|
|
|
|
|
Number(candidate.attachments||0)===Number(anchorKey.attachments||0)
|
|
|
|
|
|
){
|
|
|
|
|
|
return i;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return typeof fallbackIdx==='number' ? fallbackIdx : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
function _compressionReferenceCardHtml(text, open=false){
|
|
|
|
|
|
const preview=text.split(/\n+/).filter(Boolean).slice(0,2).join(' ');
|
|
|
|
|
|
return `
|
|
|
|
|
|
<div class="tool-card-row compression-card-row" data-compression-card="1" data-raw-text="${esc(text)}">
|
|
|
|
|
|
<div class="tool-card tool-card-compress-reference${open?' open':''}">
|
|
|
|
|
|
<div class="tool-card-header" onclick="this.closest('.tool-card').classList.toggle('open')">
|
|
|
|
|
|
<span class="tool-card-icon">${li('star',13)}</span>
|
|
|
|
|
|
<span class="tool-card-name">${esc(t('context_compaction_label'))}</span>
|
|
|
|
|
|
<span class="tool-card-preview">${esc(t('reference_only_label'))} · ${esc(preview)}</span>
|
|
|
|
|
|
<span class="tool-card-toggle">${li('chevron-right',12)}</span>
|
|
|
|
|
|
<button class="msg-copy-btn msg-action-btn tool-card-copy compression-reference-copy" title="${t('copy')}" onclick="copyMsg(this);event.stopPropagation()">${li('copy',13)}</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="tool-card-detail">
|
|
|
|
|
|
<div class="tool-card-result">
|
|
|
|
|
|
<pre>${esc(text)}</pre>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
</div>`;
|
|
|
|
|
|
}
|
2026-04-20 07:53:19 +00:00
|
|
|
|
function _isSameLocalDay(dateA, dateB){
|
|
|
|
|
|
return dateA.getFullYear()===dateB.getFullYear()
|
|
|
|
|
|
&& dateA.getMonth()===dateB.getMonth()
|
|
|
|
|
|
&& dateA.getDate()===dateB.getDate();
|
|
|
|
|
|
}
|
|
|
|
|
|
function _formatMessageFooterTimestamp(tsVal){
|
|
|
|
|
|
if(!tsVal) return '';
|
|
|
|
|
|
const date=new Date(tsVal*1000);
|
|
|
|
|
|
const now=new Date();
|
|
|
|
|
|
if(_isSameLocalDay(date, now)){
|
|
|
|
|
|
return date.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'});
|
|
|
|
|
|
}
|
|
|
|
|
|
return date.toLocaleString([], {
|
|
|
|
|
|
month:'short',
|
|
|
|
|
|
day:'numeric',
|
|
|
|
|
|
hour:'numeric',
|
|
|
|
|
|
minute:'2-digit',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-04-18 06:55:04 +00:00
|
|
|
|
function _compressionStatusCardHtml({
|
|
|
|
|
|
statusLabel,
|
|
|
|
|
|
previewText,
|
|
|
|
|
|
detail,
|
|
|
|
|
|
icon,
|
|
|
|
|
|
open=false,
|
|
|
|
|
|
variantClass='',
|
|
|
|
|
|
}){
|
|
|
|
|
|
const statusDetail = String(detail || '').trim();
|
|
|
|
|
|
const hasBody = !!statusDetail;
|
|
|
|
|
|
const openClass = open ? ' open' : '';
|
|
|
|
|
|
const statusIcon = icon;
|
|
|
|
|
|
const bodyHtml = hasBody ? `<div class="tool-card-detail"><div class="tool-card-result"><pre>${esc(statusDetail)}</pre></div></div>` : '';
|
|
|
|
|
|
const toggleHtml = hasBody ? `<span class="tool-card-toggle">${li('chevron-right',12)}</span>` : '';
|
|
|
|
|
|
return `
|
|
|
|
|
|
<div class="tool-card ${variantClass}${openClass}">
|
|
|
|
|
|
<div class="tool-card-header" onclick="this.closest('.tool-card').classList.toggle('open')">
|
|
|
|
|
|
${statusIcon}
|
|
|
|
|
|
<span class="tool-card-name">${esc(statusLabel)}</span>
|
|
|
|
|
|
<span class="tool-card-preview">${esc(previewText)}</span>
|
|
|
|
|
|
${toggleHtml}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
${bodyHtml}
|
|
|
|
|
|
</div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
function _contextCompactionMessageHtml(m, tsTitle=''){
|
|
|
|
|
|
const text=msgContent(m)||String(m.content||'');
|
|
|
|
|
|
return `<div class="compression-turn"><div class="compression-turn-blocks">${_compressionReferenceCardHtml(text, false, tsTitle)}</div></div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
function renderCompressionUi(){
|
|
|
|
|
|
const el=$('liveCompressionCards');
|
|
|
|
|
|
if(!el) return;
|
|
|
|
|
|
el.innerHTML='';
|
|
|
|
|
|
el.style.display='none';
|
|
|
|
|
|
}
|
perf(ui): cache renderMessages per session, skip O(n) rebuild on back-navigation (#963)
renderMessages() tears down and rebuilds every message's DOM from scratch on
every call — renderMd() (markdown parse), Prism highlight, and KaTeX per
message, O(n) total. With large sessions the main thread blocks for 1-5
seconds on each call.
A Chrome perf trace (78s, many open sessions) showed:
- 9,373ms of GC across 34,049 GC events (sustained, not burst)
- Peak 273 messages.js FunctionCalls/second
- 4.7s, 3.5s, 3.2s main-thread blocks from repeated renderMessages invocations
The render bottleneck is unaddressed by PR #959 (which improves the network/
parse leg of session switching, not the render leg).
Fix: a session-keyed innerHTML cache. After a full rebuild, the rendered HTML
is stored against the session_id + message count. When switching back to a
session that was already rendered with the same count, the DOM is restored from
cache (fast innerHTML set + re-highlight) instead of rebuilt from scratch.
Guard: the cache is only used on cross-session navigation (sid !== current).
In-session updates (new messages, edits, tool_complete, stream events) always
get a full rebuild — no stale content is ever shown.
Cache is capped at 30 sessions and evicts oldest-first to bound memory.
Co-authored with Claude Sonnet 4.6 / Anthropic.
2026-04-24 18:49:14 +00:00
|
|
|
|
// Session render cache: avoids full markdown+DOM rebuild when switching back
|
|
|
|
|
|
// to a session that was already rendered with the same message count.
|
|
|
|
|
|
// Keyed by session_id. Only used on cross-session navigation, never for
|
|
|
|
|
|
// in-session updates (new messages, edits, stream events).
|
|
|
|
|
|
//
|
|
|
|
|
|
// Known limitation: cache key is session_id + message count. Edits and retries
|
|
|
|
|
|
// that mutate message content without changing the count will serve stale HTML
|
|
|
|
|
|
// on back-navigation until the user triggers an in-session update. Acceptable
|
|
|
|
|
|
// for the common read-only back-navigation case; not suitable as a general cache.
|
|
|
|
|
|
const _sessionHtmlCache=new Map();
|
|
|
|
|
|
let _sessionHtmlCacheSid=null; // session_id currently rendered in the DOM
|
|
|
|
|
|
|
2026-03-31 03:40:19 +00:00
|
|
|
|
function renderMessages(){
|
|
|
|
|
|
const inner=$('msgInner');
|
perf(ui): cache renderMessages per session, skip O(n) rebuild on back-navigation (#963)
renderMessages() tears down and rebuilds every message's DOM from scratch on
every call — renderMd() (markdown parse), Prism highlight, and KaTeX per
message, O(n) total. With large sessions the main thread blocks for 1-5
seconds on each call.
A Chrome perf trace (78s, many open sessions) showed:
- 9,373ms of GC across 34,049 GC events (sustained, not burst)
- Peak 273 messages.js FunctionCalls/second
- 4.7s, 3.5s, 3.2s main-thread blocks from repeated renderMessages invocations
The render bottleneck is unaddressed by PR #959 (which improves the network/
parse leg of session switching, not the render leg).
Fix: a session-keyed innerHTML cache. After a full rebuild, the rendered HTML
is stored against the session_id + message count. When switching back to a
session that was already rendered with the same count, the DOM is restored from
cache (fast innerHTML set + re-highlight) instead of rebuilt from scratch.
Guard: the cache is only used on cross-session navigation (sid !== current).
In-session updates (new messages, edits, tool_complete, stream events) always
get a full rebuild — no stale content is ever shown.
Cache is capped at 30 sessions and evicts oldest-first to bound memory.
Co-authored with Claude Sonnet 4.6 / Anthropic.
2026-04-24 18:49:14 +00:00
|
|
|
|
const sid=S.session?S.session.session_id:null;
|
|
|
|
|
|
const msgCount=S.messages.length;
|
|
|
|
|
|
|
|
|
|
|
|
// Fast path: switching back to a previously rendered session with same count.
|
|
|
|
|
|
// Guard: sid !== _sessionHtmlCacheSid ensures in-session updates (edits,
|
|
|
|
|
|
// new messages, tool_complete) always get a fresh rebuild.
|
|
|
|
|
|
// Skip cache if this session is still streaming — the live smd parser writes
|
|
|
|
|
|
// into a DOM node inside the cached subtree; serving cached HTML detaches it.
|
|
|
|
|
|
if(sid&&sid!==_sessionHtmlCacheSid&&!INFLIGHT[sid]){
|
|
|
|
|
|
const cached=_sessionHtmlCache.get(sid);
|
|
|
|
|
|
if(cached&&cached.msgCount===msgCount){
|
|
|
|
|
|
inner.innerHTML=cached.html;
|
|
|
|
|
|
_sessionHtmlCacheSid=sid;
|
|
|
|
|
|
if(S.activeStreamId){scrollIfPinned();}else{scrollToBottom();}
|
|
|
|
|
|
requestAnimationFrame(()=>{highlightCode();addCopyButtons();renderMermaidBlocks();renderKatexBlocks();});
|
|
|
|
|
|
if(typeof loadTodos==='function'&&document.getElementById('panelTodos')&&document.getElementById('panelTodos').classList.contains('active')){loadTodos();}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-18 06:55:04 +00:00
|
|
|
|
const compressionState=_compressionStateForCurrentSession();
|
|
|
|
|
|
if(window._compressionUi && !compressionState) clearCompressionUi();
|
|
|
|
|
|
const sessionCompressionAnchor=(
|
|
|
|
|
|
S.session && typeof S.session.compression_anchor_visible_idx==='number'
|
|
|
|
|
|
) ? S.session.compression_anchor_visible_idx : null;
|
|
|
|
|
|
const sessionCompressionAnchorKey=(
|
|
|
|
|
|
S.session && S.session.compression_anchor_message_key && typeof S.session.compression_anchor_message_key==='object'
|
|
|
|
|
|
) ? S.session.compression_anchor_message_key : null;
|
2026-03-31 03:40:19 +00:00
|
|
|
|
const vis=S.messages.filter(m=>{
|
|
|
|
|
|
if(!m||!m.role||m.role==='tool')return false;
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
if(m.role==='assistant'){
|
|
|
|
|
|
const hasTc=Array.isArray(m.tool_calls)&&m.tool_calls.length>0;
|
|
|
|
|
|
const hasTu=Array.isArray(m.content)&&m.content.some(p=>p&&p.type==='tool_use');
|
|
|
|
|
|
if(hasTc||hasTu||_messageHasReasoningPayload(m)) return true;
|
|
|
|
|
|
}
|
2026-03-31 03:40:19 +00:00
|
|
|
|
return msgContent(m)||m.attachments?.length;
|
|
|
|
|
|
});
|
|
|
|
|
|
$('emptyState').style.display=vis.length?'none':'';
|
|
|
|
|
|
inner.innerHTML='';
|
2026-04-18 06:55:04 +00:00
|
|
|
|
const compressionNode=compressionState?_compressionCardsNode(compressionState):null;
|
|
|
|
|
|
const referenceMessage=S.messages.find(m=>_isContextCompactionMessage(m));
|
|
|
|
|
|
const referenceText=referenceMessage?msgContent(referenceMessage)||String(referenceMessage.content||''):'';
|
|
|
|
|
|
const referenceNode=(!compressionState && referenceMessage && (sessionCompressionAnchor!==null || sessionCompressionAnchorKey))
|
|
|
|
|
|
? (()=>{const row=document.createElement('div');row.innerHTML=_compressionReferenceCardHtml(referenceText,false);return row.firstElementChild;})()
|
|
|
|
|
|
: null;
|
2026-03-31 03:40:19 +00:00
|
|
|
|
const visWithIdx=[];
|
|
|
|
|
|
let rawIdx=0;
|
|
|
|
|
|
for(const m of S.messages){
|
|
|
|
|
|
if(!m||!m.role||m.role==='tool'){rawIdx++;continue;}
|
2026-04-06 21:23:26 +00:00
|
|
|
|
const hasTc=Array.isArray(m.tool_calls)&&m.tool_calls.length>0;
|
|
|
|
|
|
const hasTu=Array.isArray(m.content)&&m.content.some(p=>p&&p.type==='tool_use');
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
if(msgContent(m)||m.attachments?.length||(m.role==='assistant'&&(hasTc||hasTu||_messageHasReasoningPayload(m)))) visWithIdx.push({m,rawIdx});
|
2026-03-31 03:40:19 +00:00
|
|
|
|
rawIdx++;
|
|
|
|
|
|
}
|
2026-04-20 06:11:49 +00:00
|
|
|
|
let lastUserRawIdx=-1;
|
|
|
|
|
|
for(let i=visWithIdx.length-1;i>=0;i--){
|
|
|
|
|
|
if(visWithIdx[i].m&&visWithIdx[i].m.role==='user'){
|
|
|
|
|
|
lastUserRawIdx=visWithIdx[i].rawIdx;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-18 06:55:04 +00:00
|
|
|
|
const insertionAnchor=_compressionAnchorIndex(
|
|
|
|
|
|
visWithIdx,
|
|
|
|
|
|
compressionState ? compressionState.anchorMessageKey : sessionCompressionAnchorKey,
|
|
|
|
|
|
compressionState
|
|
|
|
|
|
? (typeof compressionState.anchorVisibleIdx==='number' ? compressionState.anchorVisibleIdx : compressionState.anchorRawIdx)
|
|
|
|
|
|
: sessionCompressionAnchor
|
|
|
|
|
|
);
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
let _prevSepKey=null;
|
|
|
|
|
|
let currentAssistantTurn=null;
|
|
|
|
|
|
const assistantSegments=new Map();
|
2026-04-18 06:55:04 +00:00
|
|
|
|
const userRows=new Map();
|
2026-03-31 03:40:19 +00:00
|
|
|
|
for(let vi=0;vi<visWithIdx.length;vi++){
|
|
|
|
|
|
const {m,rawIdx}=visWithIdx[vi];
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
const _tsSep=m._ts||m.timestamp;
|
|
|
|
|
|
if(_tsSep){
|
|
|
|
|
|
const _d=new Date(_tsSep*1000);
|
|
|
|
|
|
const _key=_d.toDateString();
|
|
|
|
|
|
if(_prevSepKey && _prevSepKey!==_key){
|
|
|
|
|
|
const sep=document.createElement('div');
|
|
|
|
|
|
sep.className='msg-date-sep';
|
|
|
|
|
|
sep.textContent=_fmtDateSep(_d);
|
|
|
|
|
|
inner.appendChild(sep);
|
|
|
|
|
|
}
|
|
|
|
|
|
_prevSepKey=_key;
|
|
|
|
|
|
}
|
2026-03-31 03:40:19 +00:00
|
|
|
|
let content=m.content||'';
|
2026-04-03 11:33:24 +00:00
|
|
|
|
let thinkingText='';
|
|
|
|
|
|
if(Array.isArray(content)){
|
|
|
|
|
|
thinkingText=content.filter(p=>p&&(p.type==='thinking'||p.type==='reasoning')).map(p=>p.thinking||p.reasoning||p.text||'').join('\n');
|
|
|
|
|
|
content=content.filter(p=>p&&p.type==='text').map(p=>p.text||p.content||'').join('\n');
|
|
|
|
|
|
}
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
if(!thinkingText && m.reasoning) thinkingText=m.reasoning;
|
2026-04-08 16:51:57 +00:00
|
|
|
|
if(!thinkingText && typeof content==='string'){
|
2026-04-12 21:07:00 +00:00
|
|
|
|
const thinkMatch=content.match(/<think>([\s\S]*?)<\/think>/);
|
2026-04-08 16:51:57 +00:00
|
|
|
|
if(thinkMatch){
|
|
|
|
|
|
thinkingText=thinkMatch[1].trim();
|
2026-04-12 21:07:00 +00:00
|
|
|
|
content=content.replace(/<think>[\s\S]*?<\/think>\s*/,'').trimStart();
|
2026-04-08 16:51:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
if(!thinkingText){
|
2026-04-18 06:45:39 +00:00
|
|
|
|
// Historical name "gemmaMatch" refers to MiniMax <|channel>thought format.
|
2026-04-12 21:07:00 +00:00
|
|
|
|
const gemmaMatch=content.match(/<\|channel>thought\n([\s\S]*?)<channel\|>/);
|
2026-04-08 16:51:57 +00:00
|
|
|
|
if(gemmaMatch){
|
|
|
|
|
|
thinkingText=gemmaMatch[1].trim();
|
2026-04-12 21:07:00 +00:00
|
|
|
|
content=content.replace(/<\|channel>thought\n[\s\S]*?<channel\|>\s*/,'').trimStart();
|
2026-04-08 16:51:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-18 06:45:39 +00:00
|
|
|
|
if(!thinkingText){
|
|
|
|
|
|
// Gemma 4 uses asymmetric <|turn|>thinking\n...<turn|> delimiters.
|
|
|
|
|
|
const gemmaTurnMatch=content.match(/<\|turn\|>thinking\n([\s\S]*?)<turn\|>/);
|
|
|
|
|
|
if(gemmaTurnMatch){
|
|
|
|
|
|
thinkingText=gemmaTurnMatch[1].trim();
|
|
|
|
|
|
content=content.replace(/<\|turn\|>thinking\n[\s\S]*?<turn\|>\s*/,'').trimStart();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-08 16:51:57 +00:00
|
|
|
|
}
|
2026-03-31 03:40:19 +00:00
|
|
|
|
const isUser=m.role==='user';
|
|
|
|
|
|
const isLastAssistant=!isUser&&vi===visWithIdx.length-1;
|
|
|
|
|
|
let filesHtml='';
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
if(m.attachments&&m.attachments.length){
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
filesHtml=`<div class="msg-files">${m.attachments.map(f=>`<div class="msg-file-badge">${li('paperclip',12)} ${esc(f)}</div>`).join('')}</div>`;
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
}
|
2026-04-19 05:40:37 +00:00
|
|
|
|
const bodyHtml = isUser ? esc(String(content)).replace(/\n/g,'<br>') : renderMd(_stripXmlToolCallsDisplay(String(content)));
|
2026-04-20 06:11:49 +00:00
|
|
|
|
const isEditableUser=isUser&&rawIdx===lastUserRawIdx;
|
|
|
|
|
|
const editBtn = isEditableUser ? `<button class="msg-action-btn" title="${t('edit_message')}" onclick="editMessage(this)">${li('pencil',13)}</button>` : '';
|
refactor(ui): three-column layout with left rail + main-view migration (#899)
refactor(ui): three-column layout with left rail + main-view migration (#899)
Unifies the shell into a three-column layout (rail + sidebar + main) matching the
hermes-desktop reference, and migrates every per-item detail/edit surface into a
shared main-view canvas with consistent headers, empty states, and action buttons.
Changes:
- New desktop-only left rail (48px) with 8 nav tabs (chat/tasks/skills/memory/workspaces/profiles/todos/settings)
- Persistent app titlebar (replaces per-chat topbar), active conversation title shown
- All panel detail/create/edit views migrated to #mainSkills, #mainTasks, #mainSettings, #mainWorkspaces, #mainProfiles, #mainMemory
- Settings moved out of modal into main-view page; ESC closes it
- YAML frontmatter rendered in collapsible <details> block in skill detail
- Toasts repositioned from bottom-center to top-right with theme-aware success/error/warning/info variants
- Composer workspace chip split into two-button group: files-icon toggles file panel, label opens workspace picker
- .settings-menu → .side-menu / .side-menu-item (generalised, shared by memory and settings panels)
- i18n: ~25 new keys across en/ru/es/de/zh/zh-Hant for all new form labels, placeholders, and empty states
- Mobile: hamburger in titlebar, slide-in sidebar; box-shadow removed from sidebar
- New regression test: tests/test_settings_navigation_and_detail_refresh.py (9 tests)
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
2026-04-24 16:05:25 +00:00
|
|
|
|
const undoBtn = isLastAssistant ? `<button class="msg-action-btn" title="${t('undo_exchange')}" onclick="undoLastExchange()">${li('undo',13)}</button>` : '';
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
const retryBtn = isLastAssistant ? `<button class="msg-action-btn" title="${t('regenerate')}" onclick="regenerateResponse(this)">${li('rotate-ccw',13)}</button>` : '';
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
const copyBtn = `<button class="msg-copy-btn msg-action-btn" title="${t('copy')}" onclick="copyMsg(this)">${li('copy',13)}</button>`;
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
const tsVal=m._ts||m.timestamp;
|
|
|
|
|
|
const tsTitle=tsVal?new Date(tsVal*1000).toLocaleString():'';
|
2026-04-20 07:53:19 +00:00
|
|
|
|
const tsTime=_formatMessageFooterTimestamp(tsVal);
|
|
|
|
|
|
const timeHtml = tsTime ? `<span class="msg-time" title="${esc(tsTitle)}">${tsTime}</span>` : '';
|
|
|
|
|
|
const footHtml = `<div class="msg-foot">${timeHtml}<span class="msg-actions">${editBtn}${copyBtn}${retryBtn}</span></div>`;
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
|
|
|
|
|
|
if(isUser){
|
|
|
|
|
|
currentAssistantTurn=null;
|
|
|
|
|
|
const row=document.createElement('div');
|
|
|
|
|
|
row.className='msg-row';
|
|
|
|
|
|
row.dataset.msgIdx=rawIdx;
|
|
|
|
|
|
row.dataset.role='user';
|
|
|
|
|
|
row.dataset.rawText=String(content).trim();
|
|
|
|
|
|
row.innerHTML=`${filesHtml}<div class="msg-body">${bodyHtml}</div>${footHtml}`;
|
|
|
|
|
|
inner.appendChild(row);
|
2026-04-18 06:55:04 +00:00
|
|
|
|
userRows.set(rawIdx, row);
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-18 06:55:04 +00:00
|
|
|
|
if(_isContextCompactionMessage(m)){
|
|
|
|
|
|
if(compressionState || referenceNode){
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}else{
|
|
|
|
|
|
currentAssistantTurn=null;
|
|
|
|
|
|
const row=document.createElement('div');
|
|
|
|
|
|
row.innerHTML=_contextCompactionMessageHtml(m, tsTitle);
|
|
|
|
|
|
inner.appendChild(row.firstElementChild);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
if(!currentAssistantTurn){
|
|
|
|
|
|
currentAssistantTurn=_createAssistantTurn(tsTitle);
|
|
|
|
|
|
inner.appendChild(currentAssistantTurn);
|
|
|
|
|
|
}
|
|
|
|
|
|
const seg=document.createElement('div');
|
|
|
|
|
|
seg.className='assistant-segment';
|
|
|
|
|
|
seg.dataset.msgIdx=rawIdx;
|
|
|
|
|
|
seg.dataset.rawText=String(content).trim();
|
|
|
|
|
|
if(m._live){
|
|
|
|
|
|
currentAssistantTurn.id='liveAssistantTurn';
|
|
|
|
|
|
seg.setAttribute('data-live-assistant','1');
|
|
|
|
|
|
}
|
|
|
|
|
|
if(_ERR_MSG_RE.test(String(content||'').trim())) seg.dataset.error='1';
|
2026-04-21 22:26:52 +00:00
|
|
|
|
if(thinkingText&&window._showThinking!==false) seg.insertAdjacentHTML('beforeend', _thinkingCardHtml(thinkingText));
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
const hasVisibleBody=!!(String(content||'').trim()||filesHtml);
|
|
|
|
|
|
if(hasVisibleBody){
|
|
|
|
|
|
seg.insertAdjacentHTML('beforeend', `${filesHtml}<div class="msg-body">${bodyHtml}</div>${footHtml}`);
|
|
|
|
|
|
}else if(!thinkingText){
|
|
|
|
|
|
seg.classList.add('assistant-segment-anchor');
|
|
|
|
|
|
}
|
|
|
|
|
|
_assistantTurnBlocks(currentAssistantTurn).appendChild(seg);
|
|
|
|
|
|
assistantSegments.set(rawIdx, seg);
|
2026-03-31 03:40:19 +00:00
|
|
|
|
}
|
2026-04-18 06:55:04 +00:00
|
|
|
|
|
|
|
|
|
|
function _insertCompressionLikeNode(node){
|
|
|
|
|
|
if(!node) return;
|
|
|
|
|
|
if(insertionAnchor!==null && visWithIdx[insertionAnchor]){
|
|
|
|
|
|
const anchorRawIdx=visWithIdx[insertionAnchor].rawIdx;
|
|
|
|
|
|
const anchorSeg=assistantSegments.get(anchorRawIdx);
|
|
|
|
|
|
if(anchorSeg){
|
|
|
|
|
|
const turn=anchorSeg.closest('.assistant-turn');
|
|
|
|
|
|
const blocks=_assistantTurnBlocks(turn);
|
|
|
|
|
|
if(blocks){
|
|
|
|
|
|
blocks.appendChild(node);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
const userRow=userRows.get(anchorRawIdx);
|
|
|
|
|
|
if(userRow && userRow.parentElement){
|
|
|
|
|
|
userRow.parentElement.insertBefore(node, userRow.nextSibling);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
inner.appendChild(node);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_insertCompressionLikeNode(compressionNode);
|
|
|
|
|
|
_insertCompressionLikeNode(referenceNode);
|
|
|
|
|
|
renderCompressionUi();
|
2026-03-31 03:40:19 +00:00
|
|
|
|
// Insert settled tool call cards (history view only).
|
|
|
|
|
|
// During live streaming, tool cards are rendered in #liveToolCards by the
|
|
|
|
|
|
// tool SSE handler and never mixed into the message list until done fires.
|
2026-04-06 21:23:26 +00:00
|
|
|
|
//
|
|
|
|
|
|
// Fallback: if S.toolCalls is empty (sessions that predate session-level tool
|
|
|
|
|
|
// tracking, or runs that didn't go through the normal streaming path), build
|
|
|
|
|
|
// a display list from per-message tool_calls (OpenAI format) stored in each
|
|
|
|
|
|
// assistant message. This covers the reload case described in issue #140.
|
|
|
|
|
|
if(!S.busy && (!S.toolCalls||!S.toolCalls.length)){
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
// Pass 1: index tool outputs by tool_call_id / tool_use_id so the
|
|
|
|
|
|
// fallback-built cards carry their result snippet (not just the command).
|
|
|
|
|
|
// Without this step CLI-origin sessions reload with empty tool cards.
|
|
|
|
|
|
const resultsByTid={};
|
|
|
|
|
|
const _snipFromRaw=(raw)=>{
|
|
|
|
|
|
const s=String(raw||'');
|
|
|
|
|
|
try{
|
|
|
|
|
|
const rd=JSON.parse(s);
|
|
|
|
|
|
if(rd && typeof rd==='object') return String(rd.output||rd.result||rd.error||s).slice(0,200);
|
|
|
|
|
|
}catch(e){}
|
|
|
|
|
|
return s.slice(0,200);
|
|
|
|
|
|
};
|
|
|
|
|
|
S.messages.forEach(m=>{
|
|
|
|
|
|
if(!m) return;
|
|
|
|
|
|
// OpenAI / Hermes CLI format: role=tool with tool_call_id
|
|
|
|
|
|
if(m.role==='tool'){
|
|
|
|
|
|
const tid=m.tool_call_id||m.tool_use_id||'';
|
|
|
|
|
|
if(tid) resultsByTid[tid]=_snipFromRaw(m.content);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Anthropic format: tool_result blocks inside a user message content array
|
|
|
|
|
|
if(Array.isArray(m.content)){
|
|
|
|
|
|
m.content.forEach(p=>{
|
|
|
|
|
|
if(!p||typeof p!=='object'||p.type!=='tool_result') return;
|
|
|
|
|
|
const tid=p.tool_use_id||'';
|
|
|
|
|
|
if(!tid) return;
|
|
|
|
|
|
const raw=typeof p.content==='string'?p.content
|
|
|
|
|
|
:Array.isArray(p.content)?p.content.map(c=>c&&c.text?c.text:'').join('')
|
|
|
|
|
|
:'';
|
|
|
|
|
|
resultsByTid[tid]=_snipFromRaw(raw);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-04-06 21:23:26 +00:00
|
|
|
|
const derived=[];
|
|
|
|
|
|
S.messages.forEach((m,rawIdx)=>{
|
|
|
|
|
|
if(m.role!=='assistant') return;
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
// OpenAI format: top-level tool_calls field on the assistant message
|
2026-04-06 21:23:26 +00:00
|
|
|
|
(m.tool_calls||[]).forEach(tc=>{
|
|
|
|
|
|
if(!tc||typeof tc!=='object') return;
|
|
|
|
|
|
const fn=tc.function||{};
|
|
|
|
|
|
const name=fn.name||tc.name||'tool';
|
|
|
|
|
|
let args={};
|
|
|
|
|
|
try{ args=JSON.parse(fn.arguments||'{}'); }catch(e){}
|
|
|
|
|
|
let argsSnap={};
|
|
|
|
|
|
Object.keys(args).slice(0,4).forEach(k=>{ const v=String(args[k]); argsSnap[k]=v.slice(0,120)+(v.length>120?'...':''); });
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
const tid=tc.id||tc.call_id||'';
|
|
|
|
|
|
derived.push({name,snippet:resultsByTid[tid]||'',tid,assistant_msg_idx:rawIdx,args:argsSnap,done:true});
|
2026-04-06 21:23:26 +00:00
|
|
|
|
});
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
// Anthropic format: tool_use blocks inside assistant content array
|
|
|
|
|
|
if(Array.isArray(m.content)){
|
|
|
|
|
|
m.content.forEach(p=>{
|
|
|
|
|
|
if(!p||typeof p!=='object'||p.type!=='tool_use') return;
|
|
|
|
|
|
const name=p.name||'tool';
|
|
|
|
|
|
const args=p.input||{};
|
|
|
|
|
|
const argsSnap={};
|
|
|
|
|
|
if(args && typeof args==='object'){
|
|
|
|
|
|
Object.keys(args).slice(0,4).forEach(k=>{ const v=String(args[k]); argsSnap[k]=v.slice(0,120)+(v.length>120?'...':''); });
|
|
|
|
|
|
}
|
|
|
|
|
|
const tid=p.id||'';
|
|
|
|
|
|
derived.push({name,snippet:resultsByTid[tid]||'',tid,assistant_msg_idx:rawIdx,args:argsSnap,done:true});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-04-06 21:23:26 +00:00
|
|
|
|
});
|
|
|
|
|
|
if(derived.length) S.toolCalls=derived;
|
|
|
|
|
|
}
|
2026-03-31 03:40:19 +00:00
|
|
|
|
if(!S.busy && S.toolCalls && S.toolCalls.length){
|
2026-04-18 06:55:04 +00:00
|
|
|
|
inner.querySelectorAll('.tool-card-row:not([data-compression-card])').forEach(el=>el.remove());
|
2026-03-31 03:40:19 +00:00
|
|
|
|
const byAssistant = {};
|
|
|
|
|
|
for(const tc of S.toolCalls){
|
|
|
|
|
|
const key = tc.assistant_msg_idx !== undefined ? tc.assistant_msg_idx : -1;
|
|
|
|
|
|
if(!byAssistant[key]) byAssistant[key] = [];
|
|
|
|
|
|
byAssistant[key].push(tc);
|
|
|
|
|
|
}
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
const assistantIdxs=[...assistantSegments.keys()].sort((a,b)=>a-b);
|
2026-04-06 18:02:25 +00:00
|
|
|
|
const anchorInsertAfter = new Map();
|
2026-03-31 03:40:19 +00:00
|
|
|
|
for(const [key, cards] of Object.entries(byAssistant)){
|
|
|
|
|
|
const aIdx = parseInt(key);
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
let anchorRow=assistantSegments.get(aIdx)||null;
|
|
|
|
|
|
if(!anchorRow&&assistantIdxs.length){
|
|
|
|
|
|
const fallbackIdx=[...assistantIdxs].reverse().find(idx=>idx<=aIdx);
|
|
|
|
|
|
anchorRow=fallbackIdx!==undefined?assistantSegments.get(fallbackIdx):assistantSegments.get(assistantIdxs[assistantIdxs.length-1]);
|
2026-03-31 03:40:19 +00:00
|
|
|
|
}
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
if(!anchorRow) continue;
|
|
|
|
|
|
const anchorParent=anchorRow.parentElement;
|
2026-03-31 03:40:19 +00:00
|
|
|
|
const frag=document.createDocumentFragment();
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
let lastInsertedNode=null;
|
|
|
|
|
|
for(const tc of cards){
|
|
|
|
|
|
const card=buildToolCard(tc);
|
|
|
|
|
|
frag.appendChild(card);
|
|
|
|
|
|
lastInsertedNode=card;
|
|
|
|
|
|
}
|
2026-04-02 06:55:21 +00:00
|
|
|
|
// Add expand/collapse toggle for groups with 2+ cards
|
|
|
|
|
|
if(cards.length>=2){
|
|
|
|
|
|
const toggle=document.createElement('div');
|
|
|
|
|
|
toggle.className='tool-cards-toggle';
|
|
|
|
|
|
// Collect card elements before they get moved to DOM
|
|
|
|
|
|
const cardEls=Array.from(frag.querySelectorAll('.tool-card'));
|
|
|
|
|
|
const expandBtn=document.createElement('button');
|
2026-04-08 14:55:03 +00:00
|
|
|
|
expandBtn.textContent=t('expand_all');
|
2026-04-02 06:55:21 +00:00
|
|
|
|
expandBtn.onclick=()=>cardEls.forEach(c=>c.classList.add('open'));
|
|
|
|
|
|
const collapseBtn=document.createElement('button');
|
2026-04-08 14:55:03 +00:00
|
|
|
|
collapseBtn.textContent=t('collapse_all');
|
2026-04-02 06:55:21 +00:00
|
|
|
|
collapseBtn.onclick=()=>cardEls.forEach(c=>c.classList.remove('open'));
|
|
|
|
|
|
toggle.appendChild(expandBtn);
|
|
|
|
|
|
toggle.appendChild(collapseBtn);
|
|
|
|
|
|
frag.insertBefore(toggle,frag.firstChild);
|
|
|
|
|
|
}
|
2026-04-06 18:02:25 +00:00
|
|
|
|
const insertAfterNode = anchorInsertAfter.get(anchorRow) || anchorRow;
|
|
|
|
|
|
const refNode = insertAfterNode ? insertAfterNode.nextSibling : null;
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
if(refNode) anchorParent.insertBefore(frag,refNode);
|
|
|
|
|
|
else anchorParent.appendChild(frag);
|
|
|
|
|
|
if(anchorRow&&lastInsertedNode) anchorInsertAfter.set(anchorRow, lastInsertedNode);
|
2026-03-31 03:40:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-20 07:53:19 +00:00
|
|
|
|
// Render cumulative usage on the last assistant footer row (if enabled).
|
2026-04-04 02:04:41 +00:00
|
|
|
|
if(window._showTokenUsage&&S.session&&(S.session.input_tokens||S.session.output_tokens)){
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
const rows=inner.querySelectorAll('.assistant-turn');
|
2026-04-04 01:54:53 +00:00
|
|
|
|
let lastAssist=null;
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
for(let i=rows.length-1;i>=0;i--){lastAssist=rows[i];break;}
|
2026-04-20 07:53:19 +00:00
|
|
|
|
if(lastAssist){
|
|
|
|
|
|
const footerRows=lastAssist.querySelectorAll('.msg-foot');
|
|
|
|
|
|
const targetFoot=footerRows.length?footerRows[footerRows.length-1]:null;
|
|
|
|
|
|
if(targetFoot&&!targetFoot.querySelector('.msg-usage-inline')){
|
|
|
|
|
|
const usage=document.createElement('span');
|
|
|
|
|
|
usage.className='msg-usage-inline';
|
|
|
|
|
|
const inTok=S.session.input_tokens||0;
|
|
|
|
|
|
const outTok=S.session.output_tokens||0;
|
|
|
|
|
|
const cost=S.session.estimated_cost;
|
|
|
|
|
|
let text=`${_fmtTokens(inTok)} in · ${_fmtTokens(outTok)} out`;
|
|
|
|
|
|
if(cost) text+=` · ~$${cost<0.01?cost.toFixed(4):cost.toFixed(2)}`;
|
|
|
|
|
|
usage.textContent=text;
|
|
|
|
|
|
targetFoot.classList.add('msg-foot-with-usage');
|
|
|
|
|
|
targetFoot.insertBefore(usage, targetFoot.firstChild);
|
|
|
|
|
|
}
|
feat: Sprint 23 — agentic transparency + polish
Track A: Token/cost display
- Read agent usage attrs (session_prompt_tokens, session_completion_tokens,
session_estimated_cost_usd) after run_conversation in streaming.py
- Add input_tokens, output_tokens, estimated_cost fields to Session model
- Include usage in done SSE event payload
- Store usage on S.lastUsage in messages.js done handler
- Render usage badge below last assistant message (input/output/cost)
Track B: Subagent delegation cards
- Add subagent_progress to toolIcon map with shuffle emoji
- Special-case subagent_progress in buildToolCard: "Subagent" label,
strip double emoji from preview, add tool-card-subagent CSS class
- Indented border-left styling for subagent cards
- Clean delegate_task display name
Track C: Skill picker in cron create form
- Add skill search input + tag chips to cron create form HTML
- Skill picker JS in panels.js: search/filter, click-to-add tags,
remove tag chips, pre-fetch skill list on form open
- submitCronCreate sends skills array in POST body
- Skill picker dropdown + tag CSS
Track D: Skill linked files viewer
- Add file query param to /api/skills/content endpoint
- Serve linked files from skill directory with path traversal protection
- Ensure linked_files key always present in skill content response
- Render linked files section below SKILL.md content in preview panel
- openSkillFile function for viewing individual linked files
Track E: Bug fixes and code quality
- Expand Session.__init__ and compact() to readable multi-line format
- Remove inline import json as _j2 inside loop in streaming.py
- Fix tool_calls: capture args from assistant messages, skip unresolved names
- Store args snapshot in persisted tool_calls for reload display
6 new tests. Total: 421 (409 passing).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:33:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-18 17:09:59 +00:00
|
|
|
|
// Only force-scroll when not actively streaming — mid-stream re-renders
|
|
|
|
|
|
// (tool completion, session switch) must not override the user's scroll position.
|
|
|
|
|
|
// scrollIfPinned() respects _scrollPinned, so it's a no-op if user scrolled up.
|
|
|
|
|
|
if(S.activeStreamId){
|
|
|
|
|
|
scrollIfPinned();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
scrollToBottom();
|
|
|
|
|
|
}
|
2026-03-31 03:40:19 +00:00
|
|
|
|
// Apply syntax highlighting after DOM is built
|
feat: KaTeX math rendering for LaTeX in chat + workspace previews (#352)
* feat: KaTeX math rendering for $..$ and $$..$$ in chat and previews (fixes #347)
- Stash math delimiters before markdown pipeline, restore as .katex-block/.katex-inline elements
- KaTeX JS lazy-loaded from CDN on first math block (mirrors mermaid pattern)
- KaTeX CSS loaded eagerly in <head> to prevent layout shift
- SRI hashes on both CDN tags
- throwOnError:false — bad LaTeX degrades to code span
- Supports $$, $, \\(...\\), \\[...\\] delimiters
- 18 new tests, 831/831 passing
* fix: remove invalid \' escape sequences in math stash lines
Lines 311, 314, 316, 317 had \' (backslash-quote) instead of plain '
in the arrow function bodies. This is a JS syntax error — node --check
fails with 'Invalid or unexpected token'. Likely caused by a
serialization artifact during code generation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: swap stash order (fence before math) to protect code spans; add renderKatexBlocks to workspace preview
- static/ui.js: fence_stash now runs BEFORE math_stash so dollar signs
inside backtick code spans are not extracted as math. Previously
`$x$` would render as KaTeX inside a <code> tag instead of
showing the literal string $x$.
- static/workspace.js: add requestAnimationFrame(renderKatexBlocks)
after markdown preview renders so math works in workspace file
previews, not only in chat messages.
* feat: KaTeX math rendering + stash order fix + workspace wiring (#352)
- tests/test_issue347.py: 11 new tests (29 total) covering fence-before-math
ordering, workspace.js renderKatexBlocks call, stash token distinctness,
false-positive prevention, safe-tags boundary check
- CHANGELOG.md: v0.50.15 entry; 870 tests total (up from 841)
* fix: use literal null byte (\x00M) in math stash token — matches restore regex
The original PR's second commit (fix: remove invalid \' escapes) accidentally
doubled the backslash in the math stash tokens: '\\x00M' is a 5-char string
(backslash + x + 0 + 0 + M) but the restore regex /\x00M/ expects a null byte.
Result: $...$ in messages produced visible \x00M0\x00 tokens instead of
KaTeX spans.
Changed all 4 math stash return statements to use '\x00M' (single backslash =
null byte, same convention as fence_stash's '\x00F').
Also updates test_stash_tokens_distinct to check for the correct pattern.
* fix: add null-byte token test; update CHANGELOG to v0.50.15 with fixes
- tests/test_issue347.py: add test_math_stash_token_uses_single_backslash_null_byte
to catch the \\x00M double-backslash regression; 30 tests total (up from 29)
- CHANGELOG.md: v0.50.15 entry documents all fixes including the token bug
and workspace preview wiring; 871 tests total
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:40:15 +00:00
|
|
|
|
requestAnimationFrame(()=>{highlightCode();addCopyButtons();renderMermaidBlocks();renderKatexBlocks();});
|
2026-03-31 03:40:19 +00:00
|
|
|
|
// Refresh todo panel if it's currently open
|
|
|
|
|
|
if(typeof loadTodos==='function' && document.getElementById('panelTodos') && document.getElementById('panelTodos').classList.contains('active')){
|
|
|
|
|
|
loadTodos();
|
|
|
|
|
|
}
|
perf(ui): cache renderMessages per session, skip O(n) rebuild on back-navigation (#963)
renderMessages() tears down and rebuilds every message's DOM from scratch on
every call — renderMd() (markdown parse), Prism highlight, and KaTeX per
message, O(n) total. With large sessions the main thread blocks for 1-5
seconds on each call.
A Chrome perf trace (78s, many open sessions) showed:
- 9,373ms of GC across 34,049 GC events (sustained, not burst)
- Peak 273 messages.js FunctionCalls/second
- 4.7s, 3.5s, 3.2s main-thread blocks from repeated renderMessages invocations
The render bottleneck is unaddressed by PR #959 (which improves the network/
parse leg of session switching, not the render leg).
Fix: a session-keyed innerHTML cache. After a full rebuild, the rendered HTML
is stored against the session_id + message count. When switching back to a
session that was already rendered with the same count, the DOM is restored from
cache (fast innerHTML set + re-highlight) instead of rebuilt from scratch.
Guard: the cache is only used on cross-session navigation (sid !== current).
In-session updates (new messages, edits, tool_complete, stream events) always
get a full rebuild — no stale content is ever shown.
Cache is capped at 30 sessions and evicts oldest-first to bound memory.
Co-authored with Claude Sonnet 4.6 / Anthropic.
2026-04-24 18:49:14 +00:00
|
|
|
|
// Populate session cache so switching back here skips a full rebuild.
|
|
|
|
|
|
_sessionHtmlCacheSid=sid;
|
|
|
|
|
|
if(sid){
|
|
|
|
|
|
const _html=inner.innerHTML;
|
|
|
|
|
|
// Only cache sessions with <300KB rendered HTML; evict oldest beyond 8 sessions.
|
|
|
|
|
|
if(_html.length<300_000){
|
|
|
|
|
|
_sessionHtmlCache.set(sid,{html:_html,msgCount});
|
|
|
|
|
|
if(_sessionHtmlCache.size>8){_sessionHtmlCache.delete(_sessionHtmlCache.keys().next().value);}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-31 03:40:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toolIcon(name){
|
v0.44.0: approval fix, login CSP, update diagnostics, Lucide icons
* fix: approval pending check broken by stale has_pending import (#228)
api/routes.py imported has_pending/pop_pending from tools.approval, but the
agent module renamed has_pending to has_blocking_approval (checks gateway
queue, not _pending dict) and removed pop_pending. The import fell through
to fallback lambdas that always returned False, making GET /api/approval/pending
always return {pending:null} even after a successful inject_test.
Fix: check _pending directly under _lock — same dict submit_pending writes to.
Stale imports removed.
Before: 554 pass, 1 fail | After: 555 pass, 0 fail
* fix: move login JS into external file, remove inline handlers (#226)
Login page used inline onsubmit/onkeydown handlers and an inline <script>
block — all blocked by strict script-src CSP, causing silent login failure.
Fix: extract doLogin() and Enter key listener into static/login.js (served
from /static/, already a public path). Form uses id='login-form' and
data-* attributes for i18n strings instead of injected JS literals.
Also guards res.json() parse with try/catch so non-JSON error bodies
(e.g. HTTP 500) show the password-error fallback instead of 'Connection failed'.
Fixes #222.
* fix: improve update error messages when pull fails (#227)
_apply_update_inner() ran git pull --ff-only and returned only raw stderr
on failure, making all failure modes indistinguishable.
Fix: explicit git fetch before pull; if fetch fails, returns human-readable
network error. Diverged history and missing upstream tracking branch each
get distinct messages with exact recovery commands. Generic fallback
truncates to 300 chars and shows sentinel when git produces no output.
Also adds tests/test_update_checker.py with 13 tests covering all 4 new
diagnostic code paths (0 tests existed before).
Fixes #223.
* fix: stabilize 30s terminal approval prompt visibility (#225)
Adds minimum 30-second visibility guard for the approval card using
_approvalVisibleSince, _approvalHideTimer, and a signature fingerprint
to deduplicate repeated poll ticks.
Fix: respondApproval() and all stream-end paths (done/cancel/apperror/
error/start-error) now call hideApprovalCard(true) so the card hides
immediately when the user responds or the session ends. The 30s guard
only applies to mid-session poll ticks where the approval is still live
but briefly absent.
Adds 11 structural tests covering the new timer variables, force
parameter, force-on-respond, force-on-stream-end, and poll-loop
no-force behavior.
* feat: replace emoji icons with self-hosted Lucide SVG icons (#221)
Replaces all sidebar/button emoji icons with SVG paths from Lucide bundled
in static/icons.js (no CDN dependency). Adds li(name) function returning
inline SVG geometry from a hardcoded whitelist — unknown keys return '' so
dynamic server-supplied names never inject arbitrary SVG.
Changes:
- static/icons.js: new file with 21 icon paths + li() renderer
- static/index.html: all nav/action buttons now use li() icons
- static/ui.js: toolIcon(), fileIcon() use li() for tool/file icons
- static/messages.js: cancelStream button uses SVG square stop icon
- .gitignore: adds node_modules/ entry
Verified: all 35 onclick= functions exist in JS, all 21 li() calls
reference defined icons, applyBotName() selectors intact, version
label present, no removed IDs referenced by JS.
* docs: v0.44.0 release notes, bump version, update test counts
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-10 17:02:28 +00:00
|
|
|
|
const icons={
|
|
|
|
|
|
terminal: li('terminal'),
|
|
|
|
|
|
read_file: li('file-text'),
|
|
|
|
|
|
write_file: li('file-pen'),
|
|
|
|
|
|
search_files: li('search'),
|
|
|
|
|
|
web_search: li('globe'),
|
|
|
|
|
|
web_extract: li('globe'),
|
|
|
|
|
|
execute_code: li('play'),
|
|
|
|
|
|
patch: li('wrench'),
|
|
|
|
|
|
memory: li('brain'),
|
|
|
|
|
|
skill_manage: li('book-open'),
|
|
|
|
|
|
todo: li('list-todo'),
|
|
|
|
|
|
cronjob: li('clock'),
|
|
|
|
|
|
delegate_task: li('bot'),
|
|
|
|
|
|
send_message: li('message-square'),
|
|
|
|
|
|
browser_navigate:li('globe'),
|
|
|
|
|
|
vision_analyze: li('eye'),
|
|
|
|
|
|
subagent_progress:li('shuffle'),
|
|
|
|
|
|
};
|
|
|
|
|
|
return icons[name]||li('wrench');
|
2026-03-31 03:40:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildToolCard(tc){
|
|
|
|
|
|
const row=document.createElement('div');
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
row.className='tool-card-row';
|
2026-03-31 03:40:19 +00:00
|
|
|
|
const icon=toolIcon(tc.name);
|
|
|
|
|
|
const hasDetail=tc.snippet||(tc.args&&Object.keys(tc.args).length>0);
|
|
|
|
|
|
let displaySnippet='';
|
|
|
|
|
|
if(tc.snippet){
|
|
|
|
|
|
const s=tc.snippet;
|
|
|
|
|
|
if(s.length<=220){displaySnippet=s;}
|
|
|
|
|
|
else{
|
|
|
|
|
|
const cutoff=s.slice(0,220);
|
|
|
|
|
|
const lastBreak=Math.max(cutoff.lastIndexOf('. '),cutoff.lastIndexOf('\n'),cutoff.lastIndexOf('; '));
|
|
|
|
|
|
displaySnippet=lastBreak>80?s.slice(0,lastBreak+1):cutoff;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
const hasMore=tc.snippet&&tc.snippet.length>displaySnippet.length;
|
|
|
|
|
|
const runIndicator=tc.done===false?'<span class="tool-card-running-dot"></span>':'';
|
feat: Sprint 23 — agentic transparency + polish
Track A: Token/cost display
- Read agent usage attrs (session_prompt_tokens, session_completion_tokens,
session_estimated_cost_usd) after run_conversation in streaming.py
- Add input_tokens, output_tokens, estimated_cost fields to Session model
- Include usage in done SSE event payload
- Store usage on S.lastUsage in messages.js done handler
- Render usage badge below last assistant message (input/output/cost)
Track B: Subagent delegation cards
- Add subagent_progress to toolIcon map with shuffle emoji
- Special-case subagent_progress in buildToolCard: "Subagent" label,
strip double emoji from preview, add tool-card-subagent CSS class
- Indented border-left styling for subagent cards
- Clean delegate_task display name
Track C: Skill picker in cron create form
- Add skill search input + tag chips to cron create form HTML
- Skill picker JS in panels.js: search/filter, click-to-add tags,
remove tag chips, pre-fetch skill list on form open
- submitCronCreate sends skills array in POST body
- Skill picker dropdown + tag CSS
Track D: Skill linked files viewer
- Add file query param to /api/skills/content endpoint
- Serve linked files from skill directory with path traversal protection
- Ensure linked_files key always present in skill content response
- Render linked files section below SKILL.md content in preview panel
- openSkillFile function for viewing individual linked files
Track E: Bug fixes and code quality
- Expand Session.__init__ and compact() to readable multi-line format
- Remove inline import json as _j2 inside loop in streaming.py
- Fix tool_calls: capture args from assistant messages, skip unresolved names
- Store args snapshot in persisted tool_calls for reload display
6 new tests. Total: 421 (409 passing).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:33:49 +00:00
|
|
|
|
const isSubagent=tc.name==='subagent_progress';
|
|
|
|
|
|
const isDelegation=tc.name==='delegate_task';
|
|
|
|
|
|
const cardClass='tool-card'+(tc.done===false?' tool-card-running':'')+(isSubagent?' tool-card-subagent':'');
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
// Clean up legacy subagent prefixes since the Lucide icon already shows it
|
feat: Sprint 23 — agentic transparency + polish
Track A: Token/cost display
- Read agent usage attrs (session_prompt_tokens, session_completion_tokens,
session_estimated_cost_usd) after run_conversation in streaming.py
- Add input_tokens, output_tokens, estimated_cost fields to Session model
- Include usage in done SSE event payload
- Store usage on S.lastUsage in messages.js done handler
- Render usage badge below last assistant message (input/output/cost)
Track B: Subagent delegation cards
- Add subagent_progress to toolIcon map with shuffle emoji
- Special-case subagent_progress in buildToolCard: "Subagent" label,
strip double emoji from preview, add tool-card-subagent CSS class
- Indented border-left styling for subagent cards
- Clean delegate_task display name
Track C: Skill picker in cron create form
- Add skill search input + tag chips to cron create form HTML
- Skill picker JS in panels.js: search/filter, click-to-add tags,
remove tag chips, pre-fetch skill list on form open
- submitCronCreate sends skills array in POST body
- Skill picker dropdown + tag CSS
Track D: Skill linked files viewer
- Add file query param to /api/skills/content endpoint
- Serve linked files from skill directory with path traversal protection
- Ensure linked_files key always present in skill content response
- Render linked files section below SKILL.md content in preview panel
- openSkillFile function for viewing individual linked files
Track E: Bug fixes and code quality
- Expand Session.__init__ and compact() to readable multi-line format
- Remove inline import json as _j2 inside loop in streaming.py
- Fix tool_calls: capture args from assistant messages, skip unresolved names
- Store args snapshot in persisted tool_calls for reload display
6 new tests. Total: 421 (409 passing).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:33:49 +00:00
|
|
|
|
let displayName=tc.name;
|
|
|
|
|
|
if(isSubagent) displayName='Subagent';
|
|
|
|
|
|
if(isDelegation) displayName='Delegate task';
|
|
|
|
|
|
let previewText=tc.preview||displaySnippet||'';
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
if(isSubagent) previewText=previewText.replace(/^(?:\u{1F500}|↳)\s*/u,'');
|
2026-03-31 03:40:19 +00:00
|
|
|
|
row.innerHTML=`
|
feat: Sprint 23 — agentic transparency + polish
Track A: Token/cost display
- Read agent usage attrs (session_prompt_tokens, session_completion_tokens,
session_estimated_cost_usd) after run_conversation in streaming.py
- Add input_tokens, output_tokens, estimated_cost fields to Session model
- Include usage in done SSE event payload
- Store usage on S.lastUsage in messages.js done handler
- Render usage badge below last assistant message (input/output/cost)
Track B: Subagent delegation cards
- Add subagent_progress to toolIcon map with shuffle emoji
- Special-case subagent_progress in buildToolCard: "Subagent" label,
strip double emoji from preview, add tool-card-subagent CSS class
- Indented border-left styling for subagent cards
- Clean delegate_task display name
Track C: Skill picker in cron create form
- Add skill search input + tag chips to cron create form HTML
- Skill picker JS in panels.js: search/filter, click-to-add tags,
remove tag chips, pre-fetch skill list on form open
- submitCronCreate sends skills array in POST body
- Skill picker dropdown + tag CSS
Track D: Skill linked files viewer
- Add file query param to /api/skills/content endpoint
- Serve linked files from skill directory with path traversal protection
- Ensure linked_files key always present in skill content response
- Render linked files section below SKILL.md content in preview panel
- openSkillFile function for viewing individual linked files
Track E: Bug fixes and code quality
- Expand Session.__init__ and compact() to readable multi-line format
- Remove inline import json as _j2 inside loop in streaming.py
- Fix tool_calls: capture args from assistant messages, skip unresolved names
- Store args snapshot in persisted tool_calls for reload display
6 new tests. Total: 421 (409 passing).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:33:49 +00:00
|
|
|
|
<div class="${cardClass}">
|
2026-03-31 03:40:19 +00:00
|
|
|
|
<div class="tool-card-header" onclick="this.closest('.tool-card').classList.toggle('open')">
|
|
|
|
|
|
${runIndicator}
|
|
|
|
|
|
<span class="tool-card-icon">${icon}</span>
|
feat: Sprint 23 — agentic transparency + polish
Track A: Token/cost display
- Read agent usage attrs (session_prompt_tokens, session_completion_tokens,
session_estimated_cost_usd) after run_conversation in streaming.py
- Add input_tokens, output_tokens, estimated_cost fields to Session model
- Include usage in done SSE event payload
- Store usage on S.lastUsage in messages.js done handler
- Render usage badge below last assistant message (input/output/cost)
Track B: Subagent delegation cards
- Add subagent_progress to toolIcon map with shuffle emoji
- Special-case subagent_progress in buildToolCard: "Subagent" label,
strip double emoji from preview, add tool-card-subagent CSS class
- Indented border-left styling for subagent cards
- Clean delegate_task display name
Track C: Skill picker in cron create form
- Add skill search input + tag chips to cron create form HTML
- Skill picker JS in panels.js: search/filter, click-to-add tags,
remove tag chips, pre-fetch skill list on form open
- submitCronCreate sends skills array in POST body
- Skill picker dropdown + tag CSS
Track D: Skill linked files viewer
- Add file query param to /api/skills/content endpoint
- Serve linked files from skill directory with path traversal protection
- Ensure linked_files key always present in skill content response
- Render linked files section below SKILL.md content in preview panel
- openSkillFile function for viewing individual linked files
Track E: Bug fixes and code quality
- Expand Session.__init__ and compact() to readable multi-line format
- Remove inline import json as _j2 inside loop in streaming.py
- Fix tool_calls: capture args from assistant messages, skip unresolved names
- Store args snapshot in persisted tool_calls for reload display
6 new tests. Total: 421 (409 passing).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:33:49 +00:00
|
|
|
|
<span class="tool-card-name">${esc(displayName)}</span>
|
|
|
|
|
|
<span class="tool-card-preview">${esc(previewText)}</span>
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
${hasDetail?`<span class="tool-card-toggle">${li('chevron-right',12)}</span>`:''}
|
2026-03-31 03:40:19 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
${hasDetail?`<div class="tool-card-detail">
|
|
|
|
|
|
${tc.args&&Object.keys(tc.args).length?`<div class="tool-card-args">${
|
|
|
|
|
|
Object.entries(tc.args).map(([k,v])=>`<div><span class="tool-arg-key">${esc(k)}</span> <span class="tool-arg-val">${esc(String(v))}</span></div>`).join('')
|
|
|
|
|
|
}</div>`:''}
|
|
|
|
|
|
${displaySnippet?`<div class="tool-card-result">
|
|
|
|
|
|
<pre>${esc(displaySnippet)}</pre>
|
|
|
|
|
|
${hasMore?`<button class="tool-card-more" data-full="${esc(tc.snippet||'').replace(/"/g,'"')}" data-short="${esc(displaySnippet||'').replace(/"/g,'"')}" onclick="event.stopPropagation();const p=this.previousElementSibling;const full=this.dataset.full;const short=this.dataset.short;p.textContent=p.textContent===short?full:short;this.textContent=p.textContent===short?'Show more':'Show less'">Show more</button>`:''}
|
|
|
|
|
|
</div>`:''}
|
|
|
|
|
|
</div>`:''}
|
|
|
|
|
|
</div>`;
|
|
|
|
|
|
return row;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Live tool card helpers (called during SSE streaming) ──
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
// Live cards are inserted INLINE inside #msgInner (tagged with data-live-tid)
|
|
|
|
|
|
// so the streaming layout matches the settled layout produced by renderMessages
|
|
|
|
|
|
// (user → thinking → tool cards → response). The legacy #liveToolCards
|
|
|
|
|
|
// sibling container is no longer used for placement — keeping the cards in the
|
|
|
|
|
|
// message column eliminates the visible "jump" users saw when renderMessages
|
|
|
|
|
|
// fired on the done event.
|
2026-03-31 03:40:19 +00:00
|
|
|
|
function appendLiveToolCard(tc){
|
v0.50.207: batch of 10 PRs — TPS stat, SSE guard, session polish, cron UX, folder create, model errors, session speed, title gen (#1031)
* fix: remove orphaned i18n keys from top-level LOCALES object
Three Traditional Chinese translation keys (cmd_status, memory_saved,
profile_delete_title) were placed outside any locale block between the
en and ru blocks in static/i18n.js. They became top-level properties
of the LOCALES object, causing them to appear as invalid language
options in the Settings > Preferences dropdown.
The correct translations already exist in the zh-Hant locale block.
Fixes #1008
* fix: block stale SSE events from polluting new session's DOM
- appendThinking(): guard with !S.session||!S.activeStreamId to drop
events from a previous session's SSE stream during a session switch
- appendLiveToolCard(): same guard for consistency
- finalizeThinkingCard(): scroll thinking-card-body to top when
scroll is pinned, so completed response is immediately visible
- appendThinking(): auto-scroll thinking card body to bottom while
streaming if user is watching (scroll pinned)
* Fix empty agent sessions in sidebar
* fix: resolve cron UI UX issues — icon ambiguity, toast overlap, running status
Fixes #995 — three sub-issues in the Cron Jobs UI:
1. Dual play icons ambiguous: Resume button now shows a distinct
play+bar icon (play triangle + vertical line) instead of the
identical triangle used by Run now.
2. Toast notification overlapping header buttons: Added
position:relative; z-index:10 to .main-view-header so it
stacks above the fixed toast (z-index:100 within its layer).
3. No running status after trigger: After triggering a job, the
status badge immediately shows 'running…' with a CSS spinner
animation, and polls the cron list every 3s (up to 30s) to
refresh when the job completes.
- Added cron_status_running i18n key in all 5 locales (en, es, de, ru, zh, zh-Hant)
- Added .detail-badge.running CSS class with spinner animation
- New functions: _setCronDetailStatus(), _startCronRunningPoll()
* fix(#1011): address review feedback — poll cleanup, badge persistence, 30s fallback
- _clearCronDetail() now clears _cronRunningPoll interval on navigation
- Poll re-applies 'running' badge after loadCrons() re-render (prevents flicker)
- When poll ends (30s max), detail re-renders with actual status as fallback
* feat: create folder and add space directly from UI (#782)
- After creating a folder via the file tree New folder button, offer to add it as a space via confirm dialog
- Add Create folder if it doesnt exist checkbox in the New Space form
- Backend: support create flag in /api/workspaces/add to mkdir before validation
- i18n: 4 new keys (folder_add_as_space_title/msg/btn, workspace_auto_create_folder) in all 6 locales
* fix: validate workspace path before mkdir to prevent orphan directories
Review feedback (critical): the previous code called mkdir() before
validate_workspace_to_add(), which meant a rejected path (e.g. system dir)
would leave an orphan directory on disk.
New flow:
1. Resolve path and check against blocked system roots BEFORE any mutation
2. mkdir() only if path passes the blocklist check
3. Full validation (exists, is_dir) after mkdir
Also imports _workspace_blocked_roots for the pre-mutation blocklist check.
* fix(#1014): classify model-not-found errors with helpful message
- Add model_not_found error type to streaming.py exception classifier
- Detect 404, 'not found', 'does not exist', 'invalid model' patterns
- Strip HTML tags from provider error messages (nginx 404 pages, etc.)
- Add model_not_found branch to apperror handler in messages.js
- Add i18n key model_not_found_label in all 6 locales
- 15 tests covering detection, sanitization, frontend, and i18n
* feat(ui): add live TPS stat to header
Adds a TPS (Tokens Per Second) chip to the right of the header title bar
that updates live while AI output is streaming.
Metering (api/metering.py)
- Tracks per-session output + reasoning tokens via GlobalMeter singleton
- Per-session TPS = total_tokens / elapsed_time
- Global TPS = average of active sessions' TPS values
- HIGH/LOW are max/min of global_tps snapshots over a 60-minute rolling
window (only recorded when > 0, so idle periods are excluded)
- Thread-safe with a single lock
Metering events emitted from streaming.py
- Throttled at 100ms from token/reasoning/tool callbacks so the display
updates rapidly during fast token streams
- 1Hz ticker as fallback for slow streams (exits when no active sessions)
- Final stats emitted on stream end
Routes (api/routes.py)
- Removed POST /api/metering/interval endpoint (dynamic interval via
focus/blur was replaced with simple always-1s-when-active approach)
UI (static/messages.js, index.html, style.css)
- TPS chip in titlebar: shows 'N.N t/s . N.N high . N.N low'
- Default: '0.0 t/s . 0.0 high' when idle
- Display updates on every metering SSE event (throttled to 100ms)
* feat: session restore speed + title gen reasoning hardening (#1025, #1026)
PR #1025 (@franksong2702): Speed up large session restore paths
- GET /api/session?messages=0 now parses only metadata before the messages array
- Metadata-only loads no longer populate the full-session LRU cache
- Frontend lazy fetch uses resolve_model=0 to avoid cold model-catalog lookup
- Hard reload no longer waits for populateModelDropdown() before restoring session
PR #1026 (@franksong2702): Harden auto title generation for reasoning models
- Raises title-gen completion budget to 512 tokens (reasoning-safe)
- Retries once with 1024 tokens on empty content / finish_reason:length
- Applies retry to both auxiliary and active-agent fallback routes
- Preserves underlying failure reason in title_status on local fallback
Co-authored-by: Frank Song <franksong2702@gmail.com>
* feat: session attention indicators in right slot + last_message_at timestamps (#1024)
PR #1024 (@franksong2702): Polish session attention indicators
- Streaming spinners and unread dots now reuse the right-side actions slot
- Running/unread rows hide timestamps; idle/read rows keep right-aligned timestamps
- Date group carets point down when expanded, right when collapsed
- Pinned group no longer repeats pinned-star icon per row
- Running indicators appear immediately after send (local busy state while /api/sessions catches up)
- Sidebar sorting/grouping/timestamps now prefer last_message_at (derived from last real message)
so metadata-only saves don't make old sessions appear under Today
Co-authored-by: Frank Song <franksong2702@gmail.com>
* docs: v0.50.207 release notes — 10 PRs, 2169 tests (+36)
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: Josh <josh@fyul.link>
Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-25 20:07:35 +00:00
|
|
|
|
// Guard: ignore if session was switched. Prevents stale tool events from
|
|
|
|
|
|
// a previous session's SSE stream from manipulating the new session's DOM.
|
|
|
|
|
|
if(!S.session||!S.activeStreamId) return;
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
let turn=$('liveAssistantTurn');
|
|
|
|
|
|
if(!turn){
|
|
|
|
|
|
appendThinking();
|
|
|
|
|
|
turn=$('liveAssistantTurn');
|
|
|
|
|
|
}
|
|
|
|
|
|
const inner=_assistantTurnBlocks(turn);
|
|
|
|
|
|
if(!inner) return;
|
|
|
|
|
|
const tid=tc.tid||'';
|
|
|
|
|
|
// Update existing card in place (tool_complete after tool_start)
|
|
|
|
|
|
if(tid){
|
|
|
|
|
|
const existing=inner.querySelector(`.tool-card-row[data-live-tid="${CSS.escape(tid)}"]`);
|
|
|
|
|
|
if(existing){
|
|
|
|
|
|
const replacement=buildToolCard(tc);
|
|
|
|
|
|
replacement.dataset.liveTid=tid;
|
|
|
|
|
|
existing.replaceWith(replacement);
|
2026-04-23 20:23:43 +00:00
|
|
|
|
// Keep #toolRunningRow alive — dots stay until text starts streaming
|
|
|
|
|
|
// or the next tool fires (which replaces them). Removing here caused
|
|
|
|
|
|
// a gap between tool completion and the first text token arriving.
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
const row=buildToolCard(tc);
|
|
|
|
|
|
if(tid) row.dataset.liveTid=tid;
|
2026-04-23 20:23:43 +00:00
|
|
|
|
// Insert after whichever comes last: the current live assistant segment or
|
|
|
|
|
|
// the last tool card. This handles both cases:
|
|
|
|
|
|
// text → tool1 → tool2 (no text between tools: anchor is card1)
|
|
|
|
|
|
// text1 → tool1 → text2 → tool2 (text between tools: anchor is text2)
|
|
|
|
|
|
const children=Array.from(inner.children);
|
|
|
|
|
|
// Include .thinking-card-row so tool cards land AFTER a finalized thinking
|
|
|
|
|
|
// card, not between the text segment and thinking.
|
|
|
|
|
|
const anchor=children.filter(el=>el.matches('[data-live-assistant="1"],.tool-card-row,.thinking-card-row')).pop();
|
|
|
|
|
|
if(anchor) anchor.insertAdjacentElement('afterend', row);
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
else inner.appendChild(row);
|
2026-04-23 20:23:43 +00:00
|
|
|
|
// Add a 3-dot waiting indicator below the tool card so there's visual
|
|
|
|
|
|
// feedback while the tool is running. Removed when text starts streaming
|
|
|
|
|
|
// (ensureAssistantRow) or when tool_complete fires.
|
|
|
|
|
|
const oldWait=$('toolRunningRow');if(oldWait)oldWait.remove();
|
|
|
|
|
|
const waitRow=document.createElement('div');
|
|
|
|
|
|
waitRow.id='toolRunningRow';
|
|
|
|
|
|
waitRow.className='assistant-segment';
|
|
|
|
|
|
waitRow.innerHTML='<div class="thinking"><div class="dot"></div><div class="dot"></div><div class="dot"></div></div>';
|
|
|
|
|
|
row.insertAdjacentElement('afterend', waitRow);
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
if(typeof scrollIfPinned==='function') scrollIfPinned();
|
2026-03-31 03:40:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function clearLiveToolCards(){
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
const inner=_assistantTurnBlocks($('liveAssistantTurn'));
|
|
|
|
|
|
if(inner) inner.querySelectorAll('.tool-card-row[data-live-tid]').forEach(el=>el.remove());
|
|
|
|
|
|
// Legacy #liveToolCards container cleanup — kept for safety in case any
|
|
|
|
|
|
// leftover cards were inserted there before this refactor took effect.
|
2026-03-31 03:40:19 +00:00
|
|
|
|
const container=$('liveToolCards');
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
if(container){container.innerHTML='';container.style.display='none';}
|
2026-03-31 03:40:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Edit + Regenerate ──
|
|
|
|
|
|
|
|
|
|
|
|
function editMessage(btn) {
|
|
|
|
|
|
if(S.busy) return;
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
const row = btn.closest('[data-msg-idx]');
|
2026-03-31 03:40:19 +00:00
|
|
|
|
if(!row) return;
|
|
|
|
|
|
const msgIdx = parseInt(row.dataset.msgIdx, 10);
|
|
|
|
|
|
const originalText = row.dataset.rawText || '';
|
|
|
|
|
|
const body = row.querySelector('.msg-body');
|
|
|
|
|
|
if(!body || row.dataset.editing) return;
|
|
|
|
|
|
row.dataset.editing = '1';
|
|
|
|
|
|
|
|
|
|
|
|
// Replace msg-body with an editable textarea
|
|
|
|
|
|
const ta = document.createElement('textarea');
|
|
|
|
|
|
ta.className = 'msg-edit-area';
|
|
|
|
|
|
ta.value = originalText;
|
|
|
|
|
|
body.replaceWith(ta);
|
|
|
|
|
|
// Resize after DOM insertion so scrollHeight is correct
|
|
|
|
|
|
requestAnimationFrame(() => { autoResizeTextarea(ta); ta.focus(); ta.setSelectionRange(ta.value.length, ta.value.length); });
|
|
|
|
|
|
ta.addEventListener('input', () => autoResizeTextarea(ta));
|
|
|
|
|
|
|
|
|
|
|
|
// Action bar below the textarea
|
|
|
|
|
|
const bar = document.createElement('div');
|
|
|
|
|
|
bar.className = 'msg-edit-bar';
|
|
|
|
|
|
bar.innerHTML = `<button class="msg-edit-send">Send edit</button><button class="msg-edit-cancel">Cancel</button>`;
|
|
|
|
|
|
ta.after(bar);
|
|
|
|
|
|
|
|
|
|
|
|
bar.querySelector('.msg-edit-send').onclick = async () => {
|
|
|
|
|
|
const newText = ta.value.trim();
|
|
|
|
|
|
if(!newText) return;
|
|
|
|
|
|
await submitEdit(msgIdx, newText);
|
|
|
|
|
|
};
|
|
|
|
|
|
bar.querySelector('.msg-edit-cancel').onclick = () => cancelEdit(row, originalText, body);
|
|
|
|
|
|
|
|
|
|
|
|
ta.addEventListener('keydown', e => {
|
2026-04-15 15:12:47 +00:00
|
|
|
|
if(e.key==='Enter' && !e.shiftKey) { if(e.isComposing) return; e.preventDefault(); bar.querySelector('.msg-edit-send').click(); }
|
2026-03-31 03:40:19 +00:00
|
|
|
|
if(e.key==='Escape') { e.preventDefault(); cancelEdit(row, originalText, body); }
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function cancelEdit(row, originalText, originalBody) {
|
|
|
|
|
|
delete row.dataset.editing;
|
|
|
|
|
|
const ta = row.querySelector('.msg-edit-area');
|
|
|
|
|
|
const bar = row.querySelector('.msg-edit-bar');
|
|
|
|
|
|
if(ta) ta.replaceWith(originalBody);
|
|
|
|
|
|
if(bar) bar.remove();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function autoResizeTextarea(ta) {
|
|
|
|
|
|
ta.style.height = 'auto';
|
|
|
|
|
|
ta.style.height = Math.min(ta.scrollHeight, 300) + 'px';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function submitEdit(msgIdx, newText) {
|
|
|
|
|
|
if(!S.session || S.busy) return;
|
|
|
|
|
|
// Truncate session at msgIdx (keep messages before the edited one)
|
|
|
|
|
|
// then re-send the edited text
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api('/api/session/truncate', {method:'POST', body:JSON.stringify({
|
|
|
|
|
|
session_id: S.session.session_id,
|
|
|
|
|
|
keep_count: msgIdx // keep messages[0..msgIdx-1], discard from msgIdx onward
|
|
|
|
|
|
})});
|
|
|
|
|
|
S.messages = S.messages.slice(0, msgIdx);
|
|
|
|
|
|
renderMessages();
|
|
|
|
|
|
// Now send the edited message as a new chat
|
|
|
|
|
|
$('msg').value = newText;
|
|
|
|
|
|
await send();
|
2026-04-08 14:55:03 +00:00
|
|
|
|
} catch(e) { setStatus(t('edit_failed') + e.message); }
|
2026-03-31 03:40:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function regenerateResponse(btn) {
|
|
|
|
|
|
if(!S.session || S.busy) return;
|
|
|
|
|
|
// Find the last user message and re-run it
|
|
|
|
|
|
// Remove the last assistant message first (truncate to before it)
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
const row = btn.closest('[data-msg-idx]');
|
2026-03-31 03:40:19 +00:00
|
|
|
|
if(!row) return;
|
|
|
|
|
|
const assistantIdx = parseInt(row.dataset.msgIdx, 10);
|
|
|
|
|
|
// Find the last user message text (one before this assistant message)
|
|
|
|
|
|
let lastUserText = '';
|
|
|
|
|
|
for(let i = assistantIdx - 1; i >= 0; i--) {
|
|
|
|
|
|
const m = S.messages[i];
|
|
|
|
|
|
if(m && m.role === 'user') { lastUserText = msgContent(m); break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
if(!lastUserText) return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api('/api/session/truncate', {method:'POST', body:JSON.stringify({
|
|
|
|
|
|
session_id: S.session.session_id,
|
|
|
|
|
|
keep_count: assistantIdx // remove the assistant message
|
|
|
|
|
|
})});
|
|
|
|
|
|
S.messages = S.messages.slice(0, assistantIdx);
|
|
|
|
|
|
renderMessages();
|
|
|
|
|
|
$('msg').value = lastUserText;
|
|
|
|
|
|
await send();
|
2026-04-08 14:55:03 +00:00
|
|
|
|
} catch(e) { setStatus(t('regen_failed') + e.message); }
|
2026-03-31 03:40:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function highlightCode(container) {
|
|
|
|
|
|
// Apply Prism.js syntax highlighting to all code blocks in container (or whole messages area)
|
|
|
|
|
|
if(typeof Prism === 'undefined' || !Prism.highlightAllUnder) return;
|
|
|
|
|
|
const el = container || $('msgInner');
|
|
|
|
|
|
if(!el) return;
|
|
|
|
|
|
Prism.highlightAllUnder(el);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 06:55:21 +00:00
|
|
|
|
function addCopyButtons(container){
|
|
|
|
|
|
const el=container||$('msgInner');
|
|
|
|
|
|
if(!el) return;
|
|
|
|
|
|
el.querySelectorAll('pre > code').forEach(codeEl=>{
|
|
|
|
|
|
const pre=codeEl.parentElement;
|
|
|
|
|
|
if(pre.querySelector('.code-copy-btn')) return;
|
|
|
|
|
|
const btn=document.createElement('button');
|
|
|
|
|
|
btn.className='code-copy-btn';
|
2026-04-08 14:55:03 +00:00
|
|
|
|
btn.textContent=t('copy');
|
2026-04-02 06:55:21 +00:00
|
|
|
|
btn.onclick=(e)=>{
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
navigator.clipboard.writeText(codeEl.textContent).then(()=>{
|
2026-04-08 14:55:03 +00:00
|
|
|
|
btn.textContent=t('copied');
|
|
|
|
|
|
setTimeout(()=>{btn.textContent=t('copy');},1500);
|
2026-04-02 06:55:21 +00:00
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
const header=pre.previousElementSibling;
|
|
|
|
|
|
if(header&&header.classList.contains('pre-header')){
|
|
|
|
|
|
header.style.display='flex';
|
|
|
|
|
|
header.style.justifyContent='space-between';
|
|
|
|
|
|
header.style.alignItems='center';
|
|
|
|
|
|
header.appendChild(btn);
|
|
|
|
|
|
}else{
|
|
|
|
|
|
pre.style.position='relative';
|
|
|
|
|
|
btn.style.cssText='position:absolute;top:6px;right:6px;';
|
|
|
|
|
|
pre.appendChild(btn);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
let _mermaidLoading=false;
|
|
|
|
|
|
let _mermaidReady=false;
|
|
|
|
|
|
|
|
|
|
|
|
function renderMermaidBlocks(){
|
|
|
|
|
|
const blocks=document.querySelectorAll('.mermaid-block:not([data-rendered])');
|
|
|
|
|
|
if(!blocks.length) return;
|
|
|
|
|
|
if(!_mermaidReady){
|
|
|
|
|
|
if(!_mermaidLoading){
|
|
|
|
|
|
_mermaidLoading=true;
|
|
|
|
|
|
const script=document.createElement('script');
|
2026-04-02 06:46:40 +00:00
|
|
|
|
script.src='https://cdn.jsdelivr.net/npm/mermaid@10.9.3/dist/mermaid.min.js';
|
|
|
|
|
|
script.integrity='sha384-R63zfMfSwJF4xCR11wXii+QUsbiBIdiDzDbtxia72oGWfkT7WHJfmD/I/eeHPJyT';
|
|
|
|
|
|
script.crossOrigin='anonymous';
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
script.onload=()=>{
|
|
|
|
|
|
if(typeof mermaid!=='undefined'){
|
2026-04-18 06:37:09 +00:00
|
|
|
|
mermaid.initialize({startOnLoad:false,theme:document.documentElement.classList.contains('dark')?'dark':'default',themeVariables:{
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
primaryColor:'#4a6fa5',primaryTextColor:'#e2e8f0',lineColor:'#718096',
|
|
|
|
|
|
secondaryColor:'#2d3748',tertiaryColor:'#1a202c',primaryBorderColor:'#4a5568',
|
|
|
|
|
|
}});
|
|
|
|
|
|
_mermaidReady=true;
|
|
|
|
|
|
renderMermaidBlocks();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
document.head.appendChild(script);
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
blocks.forEach(async(block)=>{
|
|
|
|
|
|
block.dataset.rendered='true';
|
|
|
|
|
|
const code=block.textContent;
|
|
|
|
|
|
const id=block.dataset.mermaidId||('m-'+Math.random().toString(36).slice(2));
|
|
|
|
|
|
try{
|
|
|
|
|
|
const {svg}=await mermaid.render(id,code);
|
|
|
|
|
|
block.innerHTML=svg;
|
|
|
|
|
|
block.classList.add('mermaid-rendered');
|
|
|
|
|
|
}catch(e){
|
|
|
|
|
|
// Fall back to showing as a code block
|
|
|
|
|
|
block.innerHTML=`<div class="pre-header">mermaid</div><pre><code>${esc(code)}</code></pre>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: KaTeX math rendering for LaTeX in chat + workspace previews (#352)
* feat: KaTeX math rendering for $..$ and $$..$$ in chat and previews (fixes #347)
- Stash math delimiters before markdown pipeline, restore as .katex-block/.katex-inline elements
- KaTeX JS lazy-loaded from CDN on first math block (mirrors mermaid pattern)
- KaTeX CSS loaded eagerly in <head> to prevent layout shift
- SRI hashes on both CDN tags
- throwOnError:false — bad LaTeX degrades to code span
- Supports $$, $, \\(...\\), \\[...\\] delimiters
- 18 new tests, 831/831 passing
* fix: remove invalid \' escape sequences in math stash lines
Lines 311, 314, 316, 317 had \' (backslash-quote) instead of plain '
in the arrow function bodies. This is a JS syntax error — node --check
fails with 'Invalid or unexpected token'. Likely caused by a
serialization artifact during code generation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: swap stash order (fence before math) to protect code spans; add renderKatexBlocks to workspace preview
- static/ui.js: fence_stash now runs BEFORE math_stash so dollar signs
inside backtick code spans are not extracted as math. Previously
`$x$` would render as KaTeX inside a <code> tag instead of
showing the literal string $x$.
- static/workspace.js: add requestAnimationFrame(renderKatexBlocks)
after markdown preview renders so math works in workspace file
previews, not only in chat messages.
* feat: KaTeX math rendering + stash order fix + workspace wiring (#352)
- tests/test_issue347.py: 11 new tests (29 total) covering fence-before-math
ordering, workspace.js renderKatexBlocks call, stash token distinctness,
false-positive prevention, safe-tags boundary check
- CHANGELOG.md: v0.50.15 entry; 870 tests total (up from 841)
* fix: use literal null byte (\x00M) in math stash token — matches restore regex
The original PR's second commit (fix: remove invalid \' escapes) accidentally
doubled the backslash in the math stash tokens: '\\x00M' is a 5-char string
(backslash + x + 0 + 0 + M) but the restore regex /\x00M/ expects a null byte.
Result: $...$ in messages produced visible \x00M0\x00 tokens instead of
KaTeX spans.
Changed all 4 math stash return statements to use '\x00M' (single backslash =
null byte, same convention as fence_stash's '\x00F').
Also updates test_stash_tokens_distinct to check for the correct pattern.
* fix: add null-byte token test; update CHANGELOG to v0.50.15 with fixes
- tests/test_issue347.py: add test_math_stash_token_uses_single_backslash_null_byte
to catch the \\x00M double-backslash regression; 30 tests total (up from 29)
- CHANGELOG.md: v0.50.15 entry documents all fixes including the token bug
and workspace preview wiring; 871 tests total
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:40:15 +00:00
|
|
|
|
let _katexLoading=false;
|
|
|
|
|
|
let _katexReady=false;
|
|
|
|
|
|
|
|
|
|
|
|
function renderKatexBlocks(){
|
|
|
|
|
|
const blocks=document.querySelectorAll('.katex-block:not([data-rendered]),.katex-inline:not([data-rendered])');
|
|
|
|
|
|
if(!blocks.length) return;
|
|
|
|
|
|
if(!_katexReady){
|
|
|
|
|
|
if(!_katexLoading){
|
|
|
|
|
|
_katexLoading=true;
|
|
|
|
|
|
const script=document.createElement('script');
|
|
|
|
|
|
script.src='https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.js';
|
|
|
|
|
|
script.integrity='sha384-cMkvdD8LoxVzGF/RPUKAcvmm49FQ0oxwDF3BGKtDXcEc+T1b2N+teh/OJfpU0jr6';
|
|
|
|
|
|
script.crossOrigin='anonymous';
|
|
|
|
|
|
script.onload=()=>{
|
|
|
|
|
|
if(typeof katex!=='undefined'){
|
|
|
|
|
|
_katexReady=true;
|
|
|
|
|
|
renderKatexBlocks();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
document.head.appendChild(script);
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
blocks.forEach(el=>{
|
|
|
|
|
|
el.dataset.rendered='true';
|
|
|
|
|
|
const src=el.textContent||'';
|
|
|
|
|
|
const displayMode=el.dataset.katex==='display';
|
|
|
|
|
|
try{
|
|
|
|
|
|
katex.render(src,el,{
|
|
|
|
|
|
displayMode,
|
|
|
|
|
|
throwOnError:false,
|
|
|
|
|
|
trust:false,
|
|
|
|
|
|
strict:'ignore',
|
|
|
|
|
|
});
|
|
|
|
|
|
}catch(e){
|
|
|
|
|
|
// Leave as raw text in a code span on failure
|
|
|
|
|
|
el.outerHTML=`<code>${esc(src)}</code>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 23:18:15 +00:00
|
|
|
|
function _thinkingMarkup(text=''){
|
2026-04-24 18:04:16 +00:00
|
|
|
|
const clean=_sanitizeThinkingDisplayText(text);
|
|
|
|
|
|
return (clean&&String(clean).trim())
|
|
|
|
|
|
? `<div class="thinking-card open"><div class="thinking-card-header" onclick="this.parentElement.classList.toggle('open')"><span class="thinking-card-icon">${li('lightbulb',14)}</span><span class="thinking-card-label">${t('thinking')}</span><span class="thinking-card-toggle">${li('chevron-right',12)}</span></div><div class="thinking-card-body"><pre>${esc(String(clean).trim())}</pre></div></div>`
|
2026-04-13 23:18:15 +00:00
|
|
|
|
: `<div class="thinking"><div class="dot"></div><div class="dot"></div><div class="dot"></div></div>`;
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
function finalizeThinkingCard(){
|
|
|
|
|
|
const row=$('thinkingRow');
|
|
|
|
|
|
if(!row) return;
|
2026-04-23 20:23:43 +00:00
|
|
|
|
// If the row is still just a spinner (no thinking content rendered),
|
|
|
|
|
|
// remove it entirely — it's the initial waiting dots.
|
|
|
|
|
|
const hasContent=row.querySelector('.thinking-card') || row.classList.contains('thinking-card-row');
|
|
|
|
|
|
if(!hasContent && row.getAttribute('data-thinking-active')==='1'){
|
|
|
|
|
|
row.remove();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
v0.50.207: batch of 10 PRs — TPS stat, SSE guard, session polish, cron UX, folder create, model errors, session speed, title gen (#1031)
* fix: remove orphaned i18n keys from top-level LOCALES object
Three Traditional Chinese translation keys (cmd_status, memory_saved,
profile_delete_title) were placed outside any locale block between the
en and ru blocks in static/i18n.js. They became top-level properties
of the LOCALES object, causing them to appear as invalid language
options in the Settings > Preferences dropdown.
The correct translations already exist in the zh-Hant locale block.
Fixes #1008
* fix: block stale SSE events from polluting new session's DOM
- appendThinking(): guard with !S.session||!S.activeStreamId to drop
events from a previous session's SSE stream during a session switch
- appendLiveToolCard(): same guard for consistency
- finalizeThinkingCard(): scroll thinking-card-body to top when
scroll is pinned, so completed response is immediately visible
- appendThinking(): auto-scroll thinking card body to bottom while
streaming if user is watching (scroll pinned)
* Fix empty agent sessions in sidebar
* fix: resolve cron UI UX issues — icon ambiguity, toast overlap, running status
Fixes #995 — three sub-issues in the Cron Jobs UI:
1. Dual play icons ambiguous: Resume button now shows a distinct
play+bar icon (play triangle + vertical line) instead of the
identical triangle used by Run now.
2. Toast notification overlapping header buttons: Added
position:relative; z-index:10 to .main-view-header so it
stacks above the fixed toast (z-index:100 within its layer).
3. No running status after trigger: After triggering a job, the
status badge immediately shows 'running…' with a CSS spinner
animation, and polls the cron list every 3s (up to 30s) to
refresh when the job completes.
- Added cron_status_running i18n key in all 5 locales (en, es, de, ru, zh, zh-Hant)
- Added .detail-badge.running CSS class with spinner animation
- New functions: _setCronDetailStatus(), _startCronRunningPoll()
* fix(#1011): address review feedback — poll cleanup, badge persistence, 30s fallback
- _clearCronDetail() now clears _cronRunningPoll interval on navigation
- Poll re-applies 'running' badge after loadCrons() re-render (prevents flicker)
- When poll ends (30s max), detail re-renders with actual status as fallback
* feat: create folder and add space directly from UI (#782)
- After creating a folder via the file tree New folder button, offer to add it as a space via confirm dialog
- Add Create folder if it doesnt exist checkbox in the New Space form
- Backend: support create flag in /api/workspaces/add to mkdir before validation
- i18n: 4 new keys (folder_add_as_space_title/msg/btn, workspace_auto_create_folder) in all 6 locales
* fix: validate workspace path before mkdir to prevent orphan directories
Review feedback (critical): the previous code called mkdir() before
validate_workspace_to_add(), which meant a rejected path (e.g. system dir)
would leave an orphan directory on disk.
New flow:
1. Resolve path and check against blocked system roots BEFORE any mutation
2. mkdir() only if path passes the blocklist check
3. Full validation (exists, is_dir) after mkdir
Also imports _workspace_blocked_roots for the pre-mutation blocklist check.
* fix(#1014): classify model-not-found errors with helpful message
- Add model_not_found error type to streaming.py exception classifier
- Detect 404, 'not found', 'does not exist', 'invalid model' patterns
- Strip HTML tags from provider error messages (nginx 404 pages, etc.)
- Add model_not_found branch to apperror handler in messages.js
- Add i18n key model_not_found_label in all 6 locales
- 15 tests covering detection, sanitization, frontend, and i18n
* feat(ui): add live TPS stat to header
Adds a TPS (Tokens Per Second) chip to the right of the header title bar
that updates live while AI output is streaming.
Metering (api/metering.py)
- Tracks per-session output + reasoning tokens via GlobalMeter singleton
- Per-session TPS = total_tokens / elapsed_time
- Global TPS = average of active sessions' TPS values
- HIGH/LOW are max/min of global_tps snapshots over a 60-minute rolling
window (only recorded when > 0, so idle periods are excluded)
- Thread-safe with a single lock
Metering events emitted from streaming.py
- Throttled at 100ms from token/reasoning/tool callbacks so the display
updates rapidly during fast token streams
- 1Hz ticker as fallback for slow streams (exits when no active sessions)
- Final stats emitted on stream end
Routes (api/routes.py)
- Removed POST /api/metering/interval endpoint (dynamic interval via
focus/blur was replaced with simple always-1s-when-active approach)
UI (static/messages.js, index.html, style.css)
- TPS chip in titlebar: shows 'N.N t/s . N.N high . N.N low'
- Default: '0.0 t/s . 0.0 high' when idle
- Display updates on every metering SSE event (throttled to 100ms)
* feat: session restore speed + title gen reasoning hardening (#1025, #1026)
PR #1025 (@franksong2702): Speed up large session restore paths
- GET /api/session?messages=0 now parses only metadata before the messages array
- Metadata-only loads no longer populate the full-session LRU cache
- Frontend lazy fetch uses resolve_model=0 to avoid cold model-catalog lookup
- Hard reload no longer waits for populateModelDropdown() before restoring session
PR #1026 (@franksong2702): Harden auto title generation for reasoning models
- Raises title-gen completion budget to 512 tokens (reasoning-safe)
- Retries once with 1024 tokens on empty content / finish_reason:length
- Applies retry to both auxiliary and active-agent fallback routes
- Preserves underlying failure reason in title_status on local fallback
Co-authored-by: Frank Song <franksong2702@gmail.com>
* feat: session attention indicators in right slot + last_message_at timestamps (#1024)
PR #1024 (@franksong2702): Polish session attention indicators
- Streaming spinners and unread dots now reuse the right-side actions slot
- Running/unread rows hide timestamps; idle/read rows keep right-aligned timestamps
- Date group carets point down when expanded, right when collapsed
- Pinned group no longer repeats pinned-star icon per row
- Running indicators appear immediately after send (local busy state while /api/sessions catches up)
- Sidebar sorting/grouping/timestamps now prefer last_message_at (derived from last real message)
so metadata-only saves don't make old sessions appear under Today
Co-authored-by: Frank Song <franksong2702@gmail.com>
* docs: v0.50.207 release notes — 10 PRs, 2169 tests (+36)
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: Josh <josh@fyul.link>
Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-25 20:07:35 +00:00
|
|
|
|
// If the user was watching (scroll pinned = at bottom), scroll the thinking
|
|
|
|
|
|
// card back to the top so the completed response is visible underneath without
|
|
|
|
|
|
// the thinking content blocking it. If they scrolled up to read history,
|
|
|
|
|
|
// leave their scroll position intact.
|
|
|
|
|
|
if(_scrollPinned){
|
|
|
|
|
|
const body=row&&row.querySelector('.thinking-card-body');
|
|
|
|
|
|
if(body) body.scrollTop=0;
|
|
|
|
|
|
}
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
row.removeAttribute('id');
|
|
|
|
|
|
row.removeAttribute('data-thinking-active');
|
2026-04-13 23:18:15 +00:00
|
|
|
|
}
|
|
|
|
|
|
function appendThinking(text=''){
|
v0.50.207: batch of 10 PRs — TPS stat, SSE guard, session polish, cron UX, folder create, model errors, session speed, title gen (#1031)
* fix: remove orphaned i18n keys from top-level LOCALES object
Three Traditional Chinese translation keys (cmd_status, memory_saved,
profile_delete_title) were placed outside any locale block between the
en and ru blocks in static/i18n.js. They became top-level properties
of the LOCALES object, causing them to appear as invalid language
options in the Settings > Preferences dropdown.
The correct translations already exist in the zh-Hant locale block.
Fixes #1008
* fix: block stale SSE events from polluting new session's DOM
- appendThinking(): guard with !S.session||!S.activeStreamId to drop
events from a previous session's SSE stream during a session switch
- appendLiveToolCard(): same guard for consistency
- finalizeThinkingCard(): scroll thinking-card-body to top when
scroll is pinned, so completed response is immediately visible
- appendThinking(): auto-scroll thinking card body to bottom while
streaming if user is watching (scroll pinned)
* Fix empty agent sessions in sidebar
* fix: resolve cron UI UX issues — icon ambiguity, toast overlap, running status
Fixes #995 — three sub-issues in the Cron Jobs UI:
1. Dual play icons ambiguous: Resume button now shows a distinct
play+bar icon (play triangle + vertical line) instead of the
identical triangle used by Run now.
2. Toast notification overlapping header buttons: Added
position:relative; z-index:10 to .main-view-header so it
stacks above the fixed toast (z-index:100 within its layer).
3. No running status after trigger: After triggering a job, the
status badge immediately shows 'running…' with a CSS spinner
animation, and polls the cron list every 3s (up to 30s) to
refresh when the job completes.
- Added cron_status_running i18n key in all 5 locales (en, es, de, ru, zh, zh-Hant)
- Added .detail-badge.running CSS class with spinner animation
- New functions: _setCronDetailStatus(), _startCronRunningPoll()
* fix(#1011): address review feedback — poll cleanup, badge persistence, 30s fallback
- _clearCronDetail() now clears _cronRunningPoll interval on navigation
- Poll re-applies 'running' badge after loadCrons() re-render (prevents flicker)
- When poll ends (30s max), detail re-renders with actual status as fallback
* feat: create folder and add space directly from UI (#782)
- After creating a folder via the file tree New folder button, offer to add it as a space via confirm dialog
- Add Create folder if it doesnt exist checkbox in the New Space form
- Backend: support create flag in /api/workspaces/add to mkdir before validation
- i18n: 4 new keys (folder_add_as_space_title/msg/btn, workspace_auto_create_folder) in all 6 locales
* fix: validate workspace path before mkdir to prevent orphan directories
Review feedback (critical): the previous code called mkdir() before
validate_workspace_to_add(), which meant a rejected path (e.g. system dir)
would leave an orphan directory on disk.
New flow:
1. Resolve path and check against blocked system roots BEFORE any mutation
2. mkdir() only if path passes the blocklist check
3. Full validation (exists, is_dir) after mkdir
Also imports _workspace_blocked_roots for the pre-mutation blocklist check.
* fix(#1014): classify model-not-found errors with helpful message
- Add model_not_found error type to streaming.py exception classifier
- Detect 404, 'not found', 'does not exist', 'invalid model' patterns
- Strip HTML tags from provider error messages (nginx 404 pages, etc.)
- Add model_not_found branch to apperror handler in messages.js
- Add i18n key model_not_found_label in all 6 locales
- 15 tests covering detection, sanitization, frontend, and i18n
* feat(ui): add live TPS stat to header
Adds a TPS (Tokens Per Second) chip to the right of the header title bar
that updates live while AI output is streaming.
Metering (api/metering.py)
- Tracks per-session output + reasoning tokens via GlobalMeter singleton
- Per-session TPS = total_tokens / elapsed_time
- Global TPS = average of active sessions' TPS values
- HIGH/LOW are max/min of global_tps snapshots over a 60-minute rolling
window (only recorded when > 0, so idle periods are excluded)
- Thread-safe with a single lock
Metering events emitted from streaming.py
- Throttled at 100ms from token/reasoning/tool callbacks so the display
updates rapidly during fast token streams
- 1Hz ticker as fallback for slow streams (exits when no active sessions)
- Final stats emitted on stream end
Routes (api/routes.py)
- Removed POST /api/metering/interval endpoint (dynamic interval via
focus/blur was replaced with simple always-1s-when-active approach)
UI (static/messages.js, index.html, style.css)
- TPS chip in titlebar: shows 'N.N t/s . N.N high . N.N low'
- Default: '0.0 t/s . 0.0 high' when idle
- Display updates on every metering SSE event (throttled to 100ms)
* feat: session restore speed + title gen reasoning hardening (#1025, #1026)
PR #1025 (@franksong2702): Speed up large session restore paths
- GET /api/session?messages=0 now parses only metadata before the messages array
- Metadata-only loads no longer populate the full-session LRU cache
- Frontend lazy fetch uses resolve_model=0 to avoid cold model-catalog lookup
- Hard reload no longer waits for populateModelDropdown() before restoring session
PR #1026 (@franksong2702): Harden auto title generation for reasoning models
- Raises title-gen completion budget to 512 tokens (reasoning-safe)
- Retries once with 1024 tokens on empty content / finish_reason:length
- Applies retry to both auxiliary and active-agent fallback routes
- Preserves underlying failure reason in title_status on local fallback
Co-authored-by: Frank Song <franksong2702@gmail.com>
* feat: session attention indicators in right slot + last_message_at timestamps (#1024)
PR #1024 (@franksong2702): Polish session attention indicators
- Streaming spinners and unread dots now reuse the right-side actions slot
- Running/unread rows hide timestamps; idle/read rows keep right-aligned timestamps
- Date group carets point down when expanded, right when collapsed
- Pinned group no longer repeats pinned-star icon per row
- Running indicators appear immediately after send (local busy state while /api/sessions catches up)
- Sidebar sorting/grouping/timestamps now prefer last_message_at (derived from last real message)
so metadata-only saves don't make old sessions appear under Today
Co-authored-by: Frank Song <franksong2702@gmail.com>
* docs: v0.50.207 release notes — 10 PRs, 2169 tests (+36)
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: Josh <josh@fyul.link>
Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-25 20:07:35 +00:00
|
|
|
|
// Guard: ignore if session was switched during an async SSE stream.
|
|
|
|
|
|
// The old stream's reasoning events can still fire after switch;
|
|
|
|
|
|
// without this check they would pollute the new session's DOM.
|
|
|
|
|
|
if(!S.session||!S.activeStreamId) return;
|
2026-03-31 03:40:19 +00:00
|
|
|
|
$('emptyState').style.display='none';
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
let turn=$('liveAssistantTurn');
|
|
|
|
|
|
if(!turn){
|
|
|
|
|
|
turn=_createAssistantTurn();
|
|
|
|
|
|
turn.id='liveAssistantTurn';
|
|
|
|
|
|
$('msgInner').appendChild(turn);
|
|
|
|
|
|
}
|
|
|
|
|
|
const blocks=_assistantTurnBlocks(turn);
|
2026-04-13 23:18:15 +00:00
|
|
|
|
let row=$('thinkingRow');
|
|
|
|
|
|
if(!row){
|
|
|
|
|
|
row=document.createElement('div');
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
row.className='assistant-segment';
|
2026-04-13 23:18:15 +00:00
|
|
|
|
row.id='thinkingRow';
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
row.setAttribute('data-thinking-active','1');
|
2026-04-23 20:23:43 +00:00
|
|
|
|
// Insert after whichever comes last: a live assistant segment or a tool card.
|
|
|
|
|
|
// This mirrors appendLiveToolCard's anchor logic so thinking always appears
|
|
|
|
|
|
// in the right position in the interleaved sequence.
|
|
|
|
|
|
// Also skip #toolRunningRow (dots) — thinking should go before dots, not after.
|
|
|
|
|
|
const allChildren=Array.from(blocks.children);
|
|
|
|
|
|
const anchor=allChildren.filter(el=>
|
|
|
|
|
|
el.id!=='toolRunningRow' &&
|
|
|
|
|
|
el.matches('[data-live-assistant="1"],.tool-card-row')
|
|
|
|
|
|
).pop();
|
|
|
|
|
|
if(anchor) anchor.insertAdjacentElement('afterend', row);
|
|
|
|
|
|
else blocks.appendChild(row);
|
2026-04-13 23:18:15 +00:00
|
|
|
|
}
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
row.className=(text&&String(text).trim())?'assistant-segment thinking-card-row':'assistant-segment';
|
2026-04-13 23:18:15 +00:00
|
|
|
|
row.innerHTML=_thinkingMarkup(text);
|
2026-04-18 17:09:59 +00:00
|
|
|
|
scrollIfPinned();
|
v0.50.207: batch of 10 PRs — TPS stat, SSE guard, session polish, cron UX, folder create, model errors, session speed, title gen (#1031)
* fix: remove orphaned i18n keys from top-level LOCALES object
Three Traditional Chinese translation keys (cmd_status, memory_saved,
profile_delete_title) were placed outside any locale block between the
en and ru blocks in static/i18n.js. They became top-level properties
of the LOCALES object, causing them to appear as invalid language
options in the Settings > Preferences dropdown.
The correct translations already exist in the zh-Hant locale block.
Fixes #1008
* fix: block stale SSE events from polluting new session's DOM
- appendThinking(): guard with !S.session||!S.activeStreamId to drop
events from a previous session's SSE stream during a session switch
- appendLiveToolCard(): same guard for consistency
- finalizeThinkingCard(): scroll thinking-card-body to top when
scroll is pinned, so completed response is immediately visible
- appendThinking(): auto-scroll thinking card body to bottom while
streaming if user is watching (scroll pinned)
* Fix empty agent sessions in sidebar
* fix: resolve cron UI UX issues — icon ambiguity, toast overlap, running status
Fixes #995 — three sub-issues in the Cron Jobs UI:
1. Dual play icons ambiguous: Resume button now shows a distinct
play+bar icon (play triangle + vertical line) instead of the
identical triangle used by Run now.
2. Toast notification overlapping header buttons: Added
position:relative; z-index:10 to .main-view-header so it
stacks above the fixed toast (z-index:100 within its layer).
3. No running status after trigger: After triggering a job, the
status badge immediately shows 'running…' with a CSS spinner
animation, and polls the cron list every 3s (up to 30s) to
refresh when the job completes.
- Added cron_status_running i18n key in all 5 locales (en, es, de, ru, zh, zh-Hant)
- Added .detail-badge.running CSS class with spinner animation
- New functions: _setCronDetailStatus(), _startCronRunningPoll()
* fix(#1011): address review feedback — poll cleanup, badge persistence, 30s fallback
- _clearCronDetail() now clears _cronRunningPoll interval on navigation
- Poll re-applies 'running' badge after loadCrons() re-render (prevents flicker)
- When poll ends (30s max), detail re-renders with actual status as fallback
* feat: create folder and add space directly from UI (#782)
- After creating a folder via the file tree New folder button, offer to add it as a space via confirm dialog
- Add Create folder if it doesnt exist checkbox in the New Space form
- Backend: support create flag in /api/workspaces/add to mkdir before validation
- i18n: 4 new keys (folder_add_as_space_title/msg/btn, workspace_auto_create_folder) in all 6 locales
* fix: validate workspace path before mkdir to prevent orphan directories
Review feedback (critical): the previous code called mkdir() before
validate_workspace_to_add(), which meant a rejected path (e.g. system dir)
would leave an orphan directory on disk.
New flow:
1. Resolve path and check against blocked system roots BEFORE any mutation
2. mkdir() only if path passes the blocklist check
3. Full validation (exists, is_dir) after mkdir
Also imports _workspace_blocked_roots for the pre-mutation blocklist check.
* fix(#1014): classify model-not-found errors with helpful message
- Add model_not_found error type to streaming.py exception classifier
- Detect 404, 'not found', 'does not exist', 'invalid model' patterns
- Strip HTML tags from provider error messages (nginx 404 pages, etc.)
- Add model_not_found branch to apperror handler in messages.js
- Add i18n key model_not_found_label in all 6 locales
- 15 tests covering detection, sanitization, frontend, and i18n
* feat(ui): add live TPS stat to header
Adds a TPS (Tokens Per Second) chip to the right of the header title bar
that updates live while AI output is streaming.
Metering (api/metering.py)
- Tracks per-session output + reasoning tokens via GlobalMeter singleton
- Per-session TPS = total_tokens / elapsed_time
- Global TPS = average of active sessions' TPS values
- HIGH/LOW are max/min of global_tps snapshots over a 60-minute rolling
window (only recorded when > 0, so idle periods are excluded)
- Thread-safe with a single lock
Metering events emitted from streaming.py
- Throttled at 100ms from token/reasoning/tool callbacks so the display
updates rapidly during fast token streams
- 1Hz ticker as fallback for slow streams (exits when no active sessions)
- Final stats emitted on stream end
Routes (api/routes.py)
- Removed POST /api/metering/interval endpoint (dynamic interval via
focus/blur was replaced with simple always-1s-when-active approach)
UI (static/messages.js, index.html, style.css)
- TPS chip in titlebar: shows 'N.N t/s . N.N high . N.N low'
- Default: '0.0 t/s . 0.0 high' when idle
- Display updates on every metering SSE event (throttled to 100ms)
* feat: session restore speed + title gen reasoning hardening (#1025, #1026)
PR #1025 (@franksong2702): Speed up large session restore paths
- GET /api/session?messages=0 now parses only metadata before the messages array
- Metadata-only loads no longer populate the full-session LRU cache
- Frontend lazy fetch uses resolve_model=0 to avoid cold model-catalog lookup
- Hard reload no longer waits for populateModelDropdown() before restoring session
PR #1026 (@franksong2702): Harden auto title generation for reasoning models
- Raises title-gen completion budget to 512 tokens (reasoning-safe)
- Retries once with 1024 tokens on empty content / finish_reason:length
- Applies retry to both auxiliary and active-agent fallback routes
- Preserves underlying failure reason in title_status on local fallback
Co-authored-by: Frank Song <franksong2702@gmail.com>
* feat: session attention indicators in right slot + last_message_at timestamps (#1024)
PR #1024 (@franksong2702): Polish session attention indicators
- Streaming spinners and unread dots now reuse the right-side actions slot
- Running/unread rows hide timestamps; idle/read rows keep right-aligned timestamps
- Date group carets point down when expanded, right when collapsed
- Pinned group no longer repeats pinned-star icon per row
- Running indicators appear immediately after send (local busy state while /api/sessions catches up)
- Sidebar sorting/grouping/timestamps now prefer last_message_at (derived from last real message)
so metadata-only saves don't make old sessions appear under Today
Co-authored-by: Frank Song <franksong2702@gmail.com>
* docs: v0.50.207 release notes — 10 PRs, 2169 tests (+36)
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: Josh <josh@fyul.link>
Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-25 20:07:35 +00:00
|
|
|
|
// Auto-scroll the thinking card body to bottom if the user is watching
|
|
|
|
|
|
// (scroll pinned). If the user scrolled up to read history, leave it alone.
|
|
|
|
|
|
if(_scrollPinned){
|
|
|
|
|
|
const body=row&&row.querySelector('.thinking-card-body');
|
|
|
|
|
|
if(body) body.scrollTop=body.scrollHeight;
|
|
|
|
|
|
}
|
2026-03-31 03:40:19 +00:00
|
|
|
|
}
|
2026-04-13 23:18:15 +00:00
|
|
|
|
function updateThinking(text=''){appendThinking(text);}
|
feat: redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70 (PR #587 by @aronprins)
Redesign chat transcript + fix streaming/persistence lifecycle — v0.50.70
Squash-merges PR #587 by @aronprins (Aron Prins). Full credit to @aronprins for all feature and fix work.
Transcript redesign: unified --msg-rail/--msg-max CSS variables, user turns as tinted cards, thinking cards as bordered panels, error card treatment, day-change separators, composer fade.
Approval/clarify as composer flyouts: cards slide up from behind composer top, overflow:hidden + translateY clip prevents travel visibility, focus({preventScroll:true}).
Streaming lifecycle: DOM order user→thinking→tool cards→response, no mid-stream jump. Live tool cards inserted before [data-live-assistant].
Persistence: reasoning attached before s.save(), _restore_reasoning_metadata on reload, role=tool rows preserved in S.messages, CLI-session tool-result fallback.
Workspace panel FOUC fix: [data-workspace-panel] set at parse time.
Docs: docs/ui-ux/index.html + two-stage-proposal.html.
Maintainer additions (433b867): CHANGELOG v0.50.70, version badge, usage badge loop simplification.
Reviewed and approved by @nesquena (independent review). 1361 tests passing.
2026-04-16 21:04:42 +00:00
|
|
|
|
function removeThinking(){
|
|
|
|
|
|
const el=$('thinkingRow');
|
|
|
|
|
|
if(el) el.remove();
|
|
|
|
|
|
const turn=$('liveAssistantTurn');
|
|
|
|
|
|
const blocks=_assistantTurnBlocks(turn);
|
|
|
|
|
|
if(turn&&blocks&&!blocks.children.length) turn.remove();
|
|
|
|
|
|
}
|
2026-03-31 03:40:19 +00:00
|
|
|
|
|
|
|
|
|
|
function fileIcon(name, type){
|
v0.44.0: approval fix, login CSP, update diagnostics, Lucide icons
* fix: approval pending check broken by stale has_pending import (#228)
api/routes.py imported has_pending/pop_pending from tools.approval, but the
agent module renamed has_pending to has_blocking_approval (checks gateway
queue, not _pending dict) and removed pop_pending. The import fell through
to fallback lambdas that always returned False, making GET /api/approval/pending
always return {pending:null} even after a successful inject_test.
Fix: check _pending directly under _lock — same dict submit_pending writes to.
Stale imports removed.
Before: 554 pass, 1 fail | After: 555 pass, 0 fail
* fix: move login JS into external file, remove inline handlers (#226)
Login page used inline onsubmit/onkeydown handlers and an inline <script>
block — all blocked by strict script-src CSP, causing silent login failure.
Fix: extract doLogin() and Enter key listener into static/login.js (served
from /static/, already a public path). Form uses id='login-form' and
data-* attributes for i18n strings instead of injected JS literals.
Also guards res.json() parse with try/catch so non-JSON error bodies
(e.g. HTTP 500) show the password-error fallback instead of 'Connection failed'.
Fixes #222.
* fix: improve update error messages when pull fails (#227)
_apply_update_inner() ran git pull --ff-only and returned only raw stderr
on failure, making all failure modes indistinguishable.
Fix: explicit git fetch before pull; if fetch fails, returns human-readable
network error. Diverged history and missing upstream tracking branch each
get distinct messages with exact recovery commands. Generic fallback
truncates to 300 chars and shows sentinel when git produces no output.
Also adds tests/test_update_checker.py with 13 tests covering all 4 new
diagnostic code paths (0 tests existed before).
Fixes #223.
* fix: stabilize 30s terminal approval prompt visibility (#225)
Adds minimum 30-second visibility guard for the approval card using
_approvalVisibleSince, _approvalHideTimer, and a signature fingerprint
to deduplicate repeated poll ticks.
Fix: respondApproval() and all stream-end paths (done/cancel/apperror/
error/start-error) now call hideApprovalCard(true) so the card hides
immediately when the user responds or the session ends. The 30s guard
only applies to mid-session poll ticks where the approval is still live
but briefly absent.
Adds 11 structural tests covering the new timer variables, force
parameter, force-on-respond, force-on-stream-end, and poll-loop
no-force behavior.
* feat: replace emoji icons with self-hosted Lucide SVG icons (#221)
Replaces all sidebar/button emoji icons with SVG paths from Lucide bundled
in static/icons.js (no CDN dependency). Adds li(name) function returning
inline SVG geometry from a hardcoded whitelist — unknown keys return '' so
dynamic server-supplied names never inject arbitrary SVG.
Changes:
- static/icons.js: new file with 21 icon paths + li() renderer
- static/index.html: all nav/action buttons now use li() icons
- static/ui.js: toolIcon(), fileIcon() use li() for tool/file icons
- static/messages.js: cancelStream button uses SVG square stop icon
- .gitignore: adds node_modules/ entry
Verified: all 35 onclick= functions exist in JS, all 21 li() calls
reference defined icons, applyBotName() selectors intact, version
label present, no removed IDs referenced by JS.
* docs: v0.44.0 release notes, bump version, update test counts
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-10 17:02:28 +00:00
|
|
|
|
if(type==='dir') return li('folder',14);
|
2026-03-31 03:40:19 +00:00
|
|
|
|
const e=fileExt(name);
|
v0.44.0: approval fix, login CSP, update diagnostics, Lucide icons
* fix: approval pending check broken by stale has_pending import (#228)
api/routes.py imported has_pending/pop_pending from tools.approval, but the
agent module renamed has_pending to has_blocking_approval (checks gateway
queue, not _pending dict) and removed pop_pending. The import fell through
to fallback lambdas that always returned False, making GET /api/approval/pending
always return {pending:null} even after a successful inject_test.
Fix: check _pending directly under _lock — same dict submit_pending writes to.
Stale imports removed.
Before: 554 pass, 1 fail | After: 555 pass, 0 fail
* fix: move login JS into external file, remove inline handlers (#226)
Login page used inline onsubmit/onkeydown handlers and an inline <script>
block — all blocked by strict script-src CSP, causing silent login failure.
Fix: extract doLogin() and Enter key listener into static/login.js (served
from /static/, already a public path). Form uses id='login-form' and
data-* attributes for i18n strings instead of injected JS literals.
Also guards res.json() parse with try/catch so non-JSON error bodies
(e.g. HTTP 500) show the password-error fallback instead of 'Connection failed'.
Fixes #222.
* fix: improve update error messages when pull fails (#227)
_apply_update_inner() ran git pull --ff-only and returned only raw stderr
on failure, making all failure modes indistinguishable.
Fix: explicit git fetch before pull; if fetch fails, returns human-readable
network error. Diverged history and missing upstream tracking branch each
get distinct messages with exact recovery commands. Generic fallback
truncates to 300 chars and shows sentinel when git produces no output.
Also adds tests/test_update_checker.py with 13 tests covering all 4 new
diagnostic code paths (0 tests existed before).
Fixes #223.
* fix: stabilize 30s terminal approval prompt visibility (#225)
Adds minimum 30-second visibility guard for the approval card using
_approvalVisibleSince, _approvalHideTimer, and a signature fingerprint
to deduplicate repeated poll ticks.
Fix: respondApproval() and all stream-end paths (done/cancel/apperror/
error/start-error) now call hideApprovalCard(true) so the card hides
immediately when the user responds or the session ends. The 30s guard
only applies to mid-session poll ticks where the approval is still live
but briefly absent.
Adds 11 structural tests covering the new timer variables, force
parameter, force-on-respond, force-on-stream-end, and poll-loop
no-force behavior.
* feat: replace emoji icons with self-hosted Lucide SVG icons (#221)
Replaces all sidebar/button emoji icons with SVG paths from Lucide bundled
in static/icons.js (no CDN dependency). Adds li(name) function returning
inline SVG geometry from a hardcoded whitelist — unknown keys return '' so
dynamic server-supplied names never inject arbitrary SVG.
Changes:
- static/icons.js: new file with 21 icon paths + li() renderer
- static/index.html: all nav/action buttons now use li() icons
- static/ui.js: toolIcon(), fileIcon() use li() for tool/file icons
- static/messages.js: cancelStream button uses SVG square stop icon
- .gitignore: adds node_modules/ entry
Verified: all 35 onclick= functions exist in JS, all 21 li() calls
reference defined icons, applyBotName() selectors intact, version
label present, no removed IDs referenced by JS.
* docs: v0.44.0 release notes, bump version, update test counts
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-10 17:02:28 +00:00
|
|
|
|
if(IMAGE_EXTS.has(e)) return li('image',14);
|
|
|
|
|
|
if(MD_EXTS.has(e)) return li('file-text',14);
|
|
|
|
|
|
if(typeof DOWNLOAD_EXTS!=='undefined'&&DOWNLOAD_EXTS.has(e)) return li('download',14);
|
|
|
|
|
|
if(e==='.py') return li('file-code',14);
|
|
|
|
|
|
if(e==='.js'||e==='.ts'||e==='.jsx'||e==='.tsx') return li('zap',14);
|
|
|
|
|
|
if(e==='.json'||e==='.yaml'||e==='.yml'||e==='.toml') return li('settings',14);
|
|
|
|
|
|
if(e==='.sh'||e==='.bash') return li('terminal',14);
|
|
|
|
|
|
if(e==='.pdf') return li('download',14);
|
|
|
|
|
|
return li('file-text',14);
|
2026-03-31 03:40:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
|
function renderBreadcrumb(){
|
|
|
|
|
|
const bar=$('breadcrumbBar');
|
|
|
|
|
|
const upBtn=$('btnUpDir');
|
|
|
|
|
|
if(!bar)return;
|
|
|
|
|
|
if(S.currentDir==='.'){
|
|
|
|
|
|
bar.style.display='none';
|
|
|
|
|
|
if(upBtn)upBtn.style.display='none';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
bar.style.display='flex';
|
|
|
|
|
|
if(upBtn)upBtn.style.display='';
|
|
|
|
|
|
bar.innerHTML='';
|
|
|
|
|
|
// Root segment
|
|
|
|
|
|
const root=document.createElement('span');
|
|
|
|
|
|
root.className='breadcrumb-seg breadcrumb-link';
|
|
|
|
|
|
root.textContent='~';
|
|
|
|
|
|
root.onclick=()=>loadDir('.');
|
|
|
|
|
|
bar.appendChild(root);
|
|
|
|
|
|
// Path segments
|
|
|
|
|
|
const parts=S.currentDir.split('/');
|
|
|
|
|
|
let accumulated='';
|
|
|
|
|
|
for(let i=0;i<parts.length;i++){
|
|
|
|
|
|
const sep=document.createElement('span');
|
|
|
|
|
|
sep.className='breadcrumb-sep';sep.textContent='/';
|
|
|
|
|
|
bar.appendChild(sep);
|
|
|
|
|
|
accumulated+=(accumulated?'/':'')+parts[i];
|
|
|
|
|
|
const seg=document.createElement('span');
|
|
|
|
|
|
seg.textContent=parts[i];
|
|
|
|
|
|
if(i<parts.length-1){
|
|
|
|
|
|
seg.className='breadcrumb-seg breadcrumb-link';
|
|
|
|
|
|
const target=accumulated;
|
|
|
|
|
|
seg.onclick=()=>loadDir(target);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
seg.className='breadcrumb-seg breadcrumb-current';
|
|
|
|
|
|
}
|
|
|
|
|
|
bar.appendChild(seg);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 11:33:24 +00:00
|
|
|
|
// Track expanded directories for tree view
|
|
|
|
|
|
if(!S._expandedDirs) S._expandedDirs=new Set();
|
|
|
|
|
|
// Cache of fetched directory contents: path -> entries[]
|
|
|
|
|
|
if(!S._dirCache) S._dirCache={};
|
|
|
|
|
|
|
2026-03-31 03:40:19 +00:00
|
|
|
|
function renderFileTree(){
|
|
|
|
|
|
const box=$('fileTree');box.innerHTML='';
|
2026-04-03 11:33:24 +00:00
|
|
|
|
// Cache current dir entries
|
|
|
|
|
|
S._dirCache[S.currentDir||'.']=S.entries;
|
2026-04-19 05:40:37 +00:00
|
|
|
|
// Show empty-state when no workspace is set or the directory is empty (#703)
|
|
|
|
|
|
const emptyEl=$('wsEmptyState');
|
|
|
|
|
|
const hasWorkspace=!!(S.session&&S.session.workspace);
|
|
|
|
|
|
if(!hasWorkspace){
|
|
|
|
|
|
if(emptyEl){emptyEl.textContent=t('workspace_empty_no_path');emptyEl.style.display='flex';}
|
|
|
|
|
|
box.style.display='none';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if(emptyEl) emptyEl.style.display='none';
|
|
|
|
|
|
box.style.display='';
|
|
|
|
|
|
if(!S.entries||!S.entries.length){
|
|
|
|
|
|
if(emptyEl){emptyEl.textContent=t('workspace_empty_dir');emptyEl.style.display='flex';}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-04-03 11:33:24 +00:00
|
|
|
|
_renderTreeItems(box, S.entries, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function _renderTreeItems(container, entries, depth){
|
|
|
|
|
|
for(const item of entries){
|
2026-03-31 03:40:19 +00:00
|
|
|
|
const el=document.createElement('div');el.className='file-item';
|
2026-04-03 11:33:24 +00:00
|
|
|
|
el.style.paddingLeft=(8+depth*16)+'px';
|
|
|
|
|
|
|
|
|
|
|
|
if(item.type==='dir'){
|
|
|
|
|
|
// Toggle arrow for directories
|
|
|
|
|
|
const arrow=document.createElement('span');
|
|
|
|
|
|
arrow.className='file-tree-toggle';
|
|
|
|
|
|
const isExpanded=S._expandedDirs.has(item.path);
|
|
|
|
|
|
arrow.textContent=isExpanded?'\u25BE':'\u25B8';
|
|
|
|
|
|
el.appendChild(arrow);
|
|
|
|
|
|
}
|
2026-03-31 03:40:19 +00:00
|
|
|
|
|
|
|
|
|
|
// Icon
|
|
|
|
|
|
const iconEl=document.createElement('span');
|
v0.44.0: approval fix, login CSP, update diagnostics, Lucide icons
* fix: approval pending check broken by stale has_pending import (#228)
api/routes.py imported has_pending/pop_pending from tools.approval, but the
agent module renamed has_pending to has_blocking_approval (checks gateway
queue, not _pending dict) and removed pop_pending. The import fell through
to fallback lambdas that always returned False, making GET /api/approval/pending
always return {pending:null} even after a successful inject_test.
Fix: check _pending directly under _lock — same dict submit_pending writes to.
Stale imports removed.
Before: 554 pass, 1 fail | After: 555 pass, 0 fail
* fix: move login JS into external file, remove inline handlers (#226)
Login page used inline onsubmit/onkeydown handlers and an inline <script>
block — all blocked by strict script-src CSP, causing silent login failure.
Fix: extract doLogin() and Enter key listener into static/login.js (served
from /static/, already a public path). Form uses id='login-form' and
data-* attributes for i18n strings instead of injected JS literals.
Also guards res.json() parse with try/catch so non-JSON error bodies
(e.g. HTTP 500) show the password-error fallback instead of 'Connection failed'.
Fixes #222.
* fix: improve update error messages when pull fails (#227)
_apply_update_inner() ran git pull --ff-only and returned only raw stderr
on failure, making all failure modes indistinguishable.
Fix: explicit git fetch before pull; if fetch fails, returns human-readable
network error. Diverged history and missing upstream tracking branch each
get distinct messages with exact recovery commands. Generic fallback
truncates to 300 chars and shows sentinel when git produces no output.
Also adds tests/test_update_checker.py with 13 tests covering all 4 new
diagnostic code paths (0 tests existed before).
Fixes #223.
* fix: stabilize 30s terminal approval prompt visibility (#225)
Adds minimum 30-second visibility guard for the approval card using
_approvalVisibleSince, _approvalHideTimer, and a signature fingerprint
to deduplicate repeated poll ticks.
Fix: respondApproval() and all stream-end paths (done/cancel/apperror/
error/start-error) now call hideApprovalCard(true) so the card hides
immediately when the user responds or the session ends. The 30s guard
only applies to mid-session poll ticks where the approval is still live
but briefly absent.
Adds 11 structural tests covering the new timer variables, force
parameter, force-on-respond, force-on-stream-end, and poll-loop
no-force behavior.
* feat: replace emoji icons with self-hosted Lucide SVG icons (#221)
Replaces all sidebar/button emoji icons with SVG paths from Lucide bundled
in static/icons.js (no CDN dependency). Adds li(name) function returning
inline SVG geometry from a hardcoded whitelist — unknown keys return '' so
dynamic server-supplied names never inject arbitrary SVG.
Changes:
- static/icons.js: new file with 21 icon paths + li() renderer
- static/index.html: all nav/action buttons now use li() icons
- static/ui.js: toolIcon(), fileIcon() use li() for tool/file icons
- static/messages.js: cancelStream button uses SVG square stop icon
- .gitignore: adds node_modules/ entry
Verified: all 35 onclick= functions exist in JS, all 21 li() calls
reference defined icons, applyBotName() selectors intact, version
label present, no removed IDs referenced by JS.
* docs: v0.44.0 release notes, bump version, update test counts
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-10 17:02:28 +00:00
|
|
|
|
iconEl.className='file-icon';iconEl.innerHTML=fileIcon(item.name,item.type);
|
2026-03-31 03:40:19 +00:00
|
|
|
|
el.appendChild(iconEl);
|
|
|
|
|
|
|
2026-04-03 11:33:24 +00:00
|
|
|
|
// Name
|
2026-03-31 03:40:19 +00:00
|
|
|
|
const nameEl=document.createElement('span');
|
2026-04-08 14:55:03 +00:00
|
|
|
|
nameEl.className='file-name';nameEl.textContent=item.name;nameEl.title=t('double_click_rename');
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
nameEl.ondblclick=(e)=>{
|
|
|
|
|
|
e.stopPropagation();
|
2026-04-03 11:33:24 +00:00
|
|
|
|
// For directories, double-click navigates (breadcrumb view)
|
|
|
|
|
|
if(item.type==='dir'){loadDir(item.path);return;}
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
const inp=document.createElement('input');
|
|
|
|
|
|
inp.className='file-rename-input';inp.value=item.name;
|
|
|
|
|
|
inp.onclick=(e2)=>e2.stopPropagation();
|
|
|
|
|
|
const finish=async(save)=>{
|
2026-04-03 11:33:24 +00:00
|
|
|
|
inp.onblur=null;
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
if(save){
|
|
|
|
|
|
const newName=inp.value.trim();
|
|
|
|
|
|
if(newName&&newName!==item.name){
|
|
|
|
|
|
try{
|
|
|
|
|
|
await api('/api/file/rename',{method:'POST',body:JSON.stringify({
|
|
|
|
|
|
session_id:S.session.session_id,path:item.path,new_name:newName
|
|
|
|
|
|
})});
|
2026-04-08 14:55:03 +00:00
|
|
|
|
showToast(t('renamed_to')+newName);
|
2026-04-03 11:33:24 +00:00
|
|
|
|
// Invalidate cache and re-render
|
|
|
|
|
|
delete S._dirCache[S.currentDir];
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
|
await loadDir(S.currentDir);
|
2026-04-08 14:55:03 +00:00
|
|
|
|
}catch(err){showToast(t('rename_failed')+err.message);}
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
inp.replaceWith(nameEl);
|
|
|
|
|
|
};
|
|
|
|
|
|
inp.onkeydown=(e2)=>{
|
2026-04-15 15:12:47 +00:00
|
|
|
|
if(e2.key==='Enter'){
|
|
|
|
|
|
if(e2.isComposing){return;}
|
|
|
|
|
|
e2.preventDefault();
|
|
|
|
|
|
finish(true);
|
|
|
|
|
|
}
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
if(e2.key==='Escape'){e2.preventDefault();finish(false);}
|
|
|
|
|
|
};
|
|
|
|
|
|
inp.onblur=()=>finish(false);
|
|
|
|
|
|
nameEl.replaceWith(inp);
|
|
|
|
|
|
setTimeout(()=>{inp.focus();inp.select();},10);
|
|
|
|
|
|
};
|
2026-03-31 03:40:19 +00:00
|
|
|
|
el.appendChild(nameEl);
|
|
|
|
|
|
|
2026-04-03 11:33:24 +00:00
|
|
|
|
// Size -- only for files
|
2026-03-31 03:40:19 +00:00
|
|
|
|
if(item.type==='file'&&item.size){
|
|
|
|
|
|
const sizeEl=document.createElement('span');
|
|
|
|
|
|
sizeEl.className='file-size';
|
|
|
|
|
|
sizeEl.textContent=`${(item.size/1024).toFixed(1)}k`;
|
|
|
|
|
|
el.appendChild(sizeEl);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 11:33:24 +00:00
|
|
|
|
// Delete button -- for files
|
2026-03-31 03:40:19 +00:00
|
|
|
|
if(item.type==='file'){
|
|
|
|
|
|
const del=document.createElement('button');
|
2026-04-08 14:55:03 +00:00
|
|
|
|
del.className='file-del-btn';del.title=t('delete_title');del.textContent='\u00d7';
|
2026-03-31 03:40:19 +00:00
|
|
|
|
del.onclick=async(e)=>{e.stopPropagation();await deleteWorkspaceFile(item.path,item.name);};
|
|
|
|
|
|
el.appendChild(del);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 11:33:24 +00:00
|
|
|
|
if(item.type==='dir'){
|
|
|
|
|
|
// Single-click toggles expand/collapse
|
|
|
|
|
|
el.onclick=async(e)=>{
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
if(S._expandedDirs.has(item.path)){
|
|
|
|
|
|
S._expandedDirs.delete(item.path);
|
2026-04-04 01:59:49 +00:00
|
|
|
|
if(typeof _saveExpandedDirs==='function')_saveExpandedDirs();
|
2026-04-03 11:33:24 +00:00
|
|
|
|
renderFileTree();
|
|
|
|
|
|
}else{
|
|
|
|
|
|
S._expandedDirs.add(item.path);
|
2026-04-04 01:59:49 +00:00
|
|
|
|
if(typeof _saveExpandedDirs==='function')_saveExpandedDirs();
|
2026-04-03 11:33:24 +00:00
|
|
|
|
// Fetch children if not cached
|
|
|
|
|
|
if(!S._dirCache[item.path]){
|
|
|
|
|
|
try{
|
|
|
|
|
|
const data=await api(`/api/list?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(item.path)}`);
|
|
|
|
|
|
S._dirCache[item.path]=data.entries||[];
|
|
|
|
|
|
}catch(e2){S._dirCache[item.path]=[];}
|
|
|
|
|
|
}
|
|
|
|
|
|
renderFileTree();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}else{
|
|
|
|
|
|
el.onclick=async()=>openFile(item.path);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
container.appendChild(el);
|
|
|
|
|
|
|
|
|
|
|
|
// Render children if directory is expanded
|
|
|
|
|
|
if(item.type==='dir'&&S._expandedDirs.has(item.path)){
|
|
|
|
|
|
const children=S._dirCache[item.path]||[];
|
|
|
|
|
|
if(children.length){
|
|
|
|
|
|
_renderTreeItems(container, children, depth+1);
|
|
|
|
|
|
}else{
|
|
|
|
|
|
const empty=document.createElement('div');
|
|
|
|
|
|
empty.className='file-item file-empty';
|
|
|
|
|
|
empty.style.paddingLeft=(8+(depth+1)*16)+'px';
|
2026-04-08 14:55:03 +00:00
|
|
|
|
empty.textContent=t('empty_dir');
|
2026-04-03 11:33:24 +00:00
|
|
|
|
container.appendChild(empty);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-31 03:40:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function deleteWorkspaceFile(relPath, name){
|
|
|
|
|
|
if(!S.session)return;
|
v0.47.0: dialogs, session menu, /skills, mobile fixes, mobile QA suite
* fix: custom provider with slash model name no longer rerouted to OpenRouter (#255)
When base_url is configured in config.yaml, resolve_model_provider() now
trusts the configured provider/base_url entirely and skips the slash-based
OpenRouter heuristic. Fixes google/gemma-4-26b-a4b with provider:custom
being silently routed to OpenRouter, resulting in 401 errors.
Fixes #230
* test: mobile layout regression suite — 14 tests for every QA run (#254)
Adds tests/test_mobile_layout.py with 14 static regression tests that run
on every QA pass to catch mobile layout breakage before it reaches prod.
Covers: breakpoints at 900px/640px, right panel slide-over CSS, mobile
overlay, bottom nav, files button, profile dropdown z-index, chip overflow,
workspace close, 100dvh, 44px touch targets, 16px font-size on textarea.
* feat: /skills slash command lists and filters available Hermes skills (#257)
Adds /skills [query] command to commands.js. Fetches from /api/skills,
groups by category (alphabetically sorted), displays as a formatted
assistant message. Optional query filters by name, description, or category.
i18n keys added for en, de, zh, zh-Hant. 1 regression test added.
Fixes #248
* feat: shared app dialogs replace native confirm()/prompt() calls (#251)
Adds showConfirmDialog() and showPromptDialog() helpers to ui.js, backed
by a themed #appDialogOverlay. Replaces all 11 native browser confirm/prompt
call sites across panels.js, sessions.js, ui.js, workspace.js.
Supports: danger mode, keyboard focus trap (Tab/Escape/Enter), focus restore,
ARIA roles, mobile-responsive stacked buttons at 640px. i18n for en/de/zh/zh-Hant.
5 new tests in test_sprint33.py verify markup, CSS, helpers, and absence of
native dialog calls.
Extracted from PR #242.
* fix: Android Chrome mobile — workspace panel close + profile dropdown (#256)
Fix #247: toggleMobileFiles() now shows/hides the mobile overlay when
toggling the right workspace panel. New closeMobileFiles() helper closes
the panel with correct overlay state tracking. Overlay onclick calls both
closeMobileSidebar() and closeMobileFiles(). Mobile-only close button (x)
added to workspace panel header.
Fix #246: profile dropdown uses position:fixed;top:56px;right:8px at
max-width:900px, escaping the overflow-x:auto stacking context that was
clipping it on Android Chrome.
Fix applied during review: closeMobileSidebar() now checks if the right
panel is still open before hiding the overlay, preventing the overlay from
disappearing when only the sidebar is closed.
Fixes #247 Fixes #246
* feat: session ⋯ action dropdown replaces per-row buttons (#252)
Replaces the 5 per-row hover action buttons (pin/move/archive/duplicate/trash)
with a single ⋯ trigger that opens a positioned dropdown menu. Menu has full
keyboard (Escape), click-outside, scroll, and resize-reposition handling.
Position:fixed prevents sidebar clipping.
5 actions: Pin/Unpin, Move to project, Archive/Unarchive, Duplicate, Delete
(danger style). Each with icon and descriptive subtitle.
Updated test_sprint16.py: test_sessions_js_uses_action_menu_not_per_row_buttons
asserts the new trigger and menu functions exist, old per-row classes are gone.
Extracted from PR #242.
* docs: v0.47.0 release notes, bump version, update test counts (645)
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 19:19:12 +00:00
|
|
|
|
const _delFile=await showConfirmDialog({title:t('delete_confirm',name),message:'',confirmLabel:'Delete',danger:true,focusCancel:true});
|
|
|
|
|
|
if(!_delFile) return;
|
2026-03-31 03:40:19 +00:00
|
|
|
|
try{
|
|
|
|
|
|
await api('/api/file/delete',{method:'POST',body:JSON.stringify({session_id:S.session.session_id,path:relPath})});
|
2026-04-08 14:55:03 +00:00
|
|
|
|
showToast(t('deleted')+name);
|
2026-03-31 03:40:19 +00:00
|
|
|
|
// Close preview if we just deleted the viewed file
|
|
|
|
|
|
if($('previewPathText').textContent===relPath)$('btnClearPreview').onclick();
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
|
await loadDir(S.currentDir);
|
2026-04-08 14:55:03 +00:00
|
|
|
|
}catch(e){setStatus(t('delete_failed')+e.message);}
|
2026-03-31 03:40:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function promptNewFile(){
|
2026-04-22 01:47:40 +00:00
|
|
|
|
// If no active session but a default workspace is configured, auto-create
|
|
|
|
|
|
// a session bound to it so workspace actions work on the blank new-chat page.
|
|
|
|
|
|
if(!S.session){
|
|
|
|
|
|
const ws=(typeof S._profileDefaultWorkspace==='string'&&S._profileDefaultWorkspace)||'';
|
|
|
|
|
|
if(!ws) return;
|
|
|
|
|
|
try{
|
|
|
|
|
|
const r=await api('/api/session/new',{method:'POST',body:JSON.stringify({workspace:ws})});
|
|
|
|
|
|
if(r&&r.session){S.session=r.session;S.messages=[];syncTopbar();renderMessages();await renderSessionList();}
|
|
|
|
|
|
}catch(e){setStatus(t('create_failed')+e.message);return;}
|
|
|
|
|
|
}
|
2026-03-31 03:40:19 +00:00
|
|
|
|
if(!S.session)return;
|
v0.47.0: dialogs, session menu, /skills, mobile fixes, mobile QA suite
* fix: custom provider with slash model name no longer rerouted to OpenRouter (#255)
When base_url is configured in config.yaml, resolve_model_provider() now
trusts the configured provider/base_url entirely and skips the slash-based
OpenRouter heuristic. Fixes google/gemma-4-26b-a4b with provider:custom
being silently routed to OpenRouter, resulting in 401 errors.
Fixes #230
* test: mobile layout regression suite — 14 tests for every QA run (#254)
Adds tests/test_mobile_layout.py with 14 static regression tests that run
on every QA pass to catch mobile layout breakage before it reaches prod.
Covers: breakpoints at 900px/640px, right panel slide-over CSS, mobile
overlay, bottom nav, files button, profile dropdown z-index, chip overflow,
workspace close, 100dvh, 44px touch targets, 16px font-size on textarea.
* feat: /skills slash command lists and filters available Hermes skills (#257)
Adds /skills [query] command to commands.js. Fetches from /api/skills,
groups by category (alphabetically sorted), displays as a formatted
assistant message. Optional query filters by name, description, or category.
i18n keys added for en, de, zh, zh-Hant. 1 regression test added.
Fixes #248
* feat: shared app dialogs replace native confirm()/prompt() calls (#251)
Adds showConfirmDialog() and showPromptDialog() helpers to ui.js, backed
by a themed #appDialogOverlay. Replaces all 11 native browser confirm/prompt
call sites across panels.js, sessions.js, ui.js, workspace.js.
Supports: danger mode, keyboard focus trap (Tab/Escape/Enter), focus restore,
ARIA roles, mobile-responsive stacked buttons at 640px. i18n for en/de/zh/zh-Hant.
5 new tests in test_sprint33.py verify markup, CSS, helpers, and absence of
native dialog calls.
Extracted from PR #242.
* fix: Android Chrome mobile — workspace panel close + profile dropdown (#256)
Fix #247: toggleMobileFiles() now shows/hides the mobile overlay when
toggling the right workspace panel. New closeMobileFiles() helper closes
the panel with correct overlay state tracking. Overlay onclick calls both
closeMobileSidebar() and closeMobileFiles(). Mobile-only close button (x)
added to workspace panel header.
Fix #246: profile dropdown uses position:fixed;top:56px;right:8px at
max-width:900px, escaping the overflow-x:auto stacking context that was
clipping it on Android Chrome.
Fix applied during review: closeMobileSidebar() now checks if the right
panel is still open before hiding the overlay, preventing the overlay from
disappearing when only the sidebar is closed.
Fixes #247 Fixes #246
* feat: session ⋯ action dropdown replaces per-row buttons (#252)
Replaces the 5 per-row hover action buttons (pin/move/archive/duplicate/trash)
with a single ⋯ trigger that opens a positioned dropdown menu. Menu has full
keyboard (Escape), click-outside, scroll, and resize-reposition handling.
Position:fixed prevents sidebar clipping.
5 actions: Pin/Unpin, Move to project, Archive/Unarchive, Duplicate, Delete
(danger style). Each with icon and descriptive subtitle.
Updated test_sprint16.py: test_sessions_js_uses_action_menu_not_per_row_buttons
asserts the new trigger and menu functions exist, old per-row classes are gone.
Extracted from PR #242.
* docs: v0.47.0 release notes, bump version, update test counts (645)
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 19:19:12 +00:00
|
|
|
|
const name=await showPromptDialog({title:t('new_file_prompt'),placeholder:'filename.txt',confirmLabel:t('create')});
|
2026-03-31 03:40:19 +00:00
|
|
|
|
if(!name||!name.trim())return;
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
|
const relPath=S.currentDir==='.'?name.trim():(S.currentDir+'/'+name.trim());
|
2026-03-31 03:40:19 +00:00
|
|
|
|
try{
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
|
await api('/api/file/create',{method:'POST',body:JSON.stringify({session_id:S.session.session_id,path:relPath,content:''})});
|
2026-04-08 14:55:03 +00:00
|
|
|
|
showToast(t('created')+name.trim());
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
|
await loadDir(S.currentDir);
|
|
|
|
|
|
openFile(relPath);
|
2026-04-08 14:55:03 +00:00
|
|
|
|
}catch(e){setStatus(t('create_failed')+e.message);}
|
2026-03-31 03:40:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
async function promptNewFolder(){
|
2026-04-22 01:47:40 +00:00
|
|
|
|
// Same auto-create-session logic as promptNewFile for the blank page.
|
|
|
|
|
|
if(!S.session){
|
|
|
|
|
|
const ws=(typeof S._profileDefaultWorkspace==='string'&&S._profileDefaultWorkspace)||'';
|
|
|
|
|
|
if(!ws) return;
|
|
|
|
|
|
try{
|
|
|
|
|
|
const r=await api('/api/session/new',{method:'POST',body:JSON.stringify({workspace:ws})});
|
|
|
|
|
|
if(r&&r.session){S.session=r.session;S.messages=[];syncTopbar();renderMessages();await renderSessionList();}
|
|
|
|
|
|
}catch(e){setStatus(t('folder_create_failed')+e.message);return;}
|
|
|
|
|
|
}
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
if(!S.session)return;
|
v0.47.0: dialogs, session menu, /skills, mobile fixes, mobile QA suite
* fix: custom provider with slash model name no longer rerouted to OpenRouter (#255)
When base_url is configured in config.yaml, resolve_model_provider() now
trusts the configured provider/base_url entirely and skips the slash-based
OpenRouter heuristic. Fixes google/gemma-4-26b-a4b with provider:custom
being silently routed to OpenRouter, resulting in 401 errors.
Fixes #230
* test: mobile layout regression suite — 14 tests for every QA run (#254)
Adds tests/test_mobile_layout.py with 14 static regression tests that run
on every QA pass to catch mobile layout breakage before it reaches prod.
Covers: breakpoints at 900px/640px, right panel slide-over CSS, mobile
overlay, bottom nav, files button, profile dropdown z-index, chip overflow,
workspace close, 100dvh, 44px touch targets, 16px font-size on textarea.
* feat: /skills slash command lists and filters available Hermes skills (#257)
Adds /skills [query] command to commands.js. Fetches from /api/skills,
groups by category (alphabetically sorted), displays as a formatted
assistant message. Optional query filters by name, description, or category.
i18n keys added for en, de, zh, zh-Hant. 1 regression test added.
Fixes #248
* feat: shared app dialogs replace native confirm()/prompt() calls (#251)
Adds showConfirmDialog() and showPromptDialog() helpers to ui.js, backed
by a themed #appDialogOverlay. Replaces all 11 native browser confirm/prompt
call sites across panels.js, sessions.js, ui.js, workspace.js.
Supports: danger mode, keyboard focus trap (Tab/Escape/Enter), focus restore,
ARIA roles, mobile-responsive stacked buttons at 640px. i18n for en/de/zh/zh-Hant.
5 new tests in test_sprint33.py verify markup, CSS, helpers, and absence of
native dialog calls.
Extracted from PR #242.
* fix: Android Chrome mobile — workspace panel close + profile dropdown (#256)
Fix #247: toggleMobileFiles() now shows/hides the mobile overlay when
toggling the right workspace panel. New closeMobileFiles() helper closes
the panel with correct overlay state tracking. Overlay onclick calls both
closeMobileSidebar() and closeMobileFiles(). Mobile-only close button (x)
added to workspace panel header.
Fix #246: profile dropdown uses position:fixed;top:56px;right:8px at
max-width:900px, escaping the overflow-x:auto stacking context that was
clipping it on Android Chrome.
Fix applied during review: closeMobileSidebar() now checks if the right
panel is still open before hiding the overlay, preventing the overlay from
disappearing when only the sidebar is closed.
Fixes #247 Fixes #246
* feat: session ⋯ action dropdown replaces per-row buttons (#252)
Replaces the 5 per-row hover action buttons (pin/move/archive/duplicate/trash)
with a single ⋯ trigger that opens a positioned dropdown menu. Menu has full
keyboard (Escape), click-outside, scroll, and resize-reposition handling.
Position:fixed prevents sidebar clipping.
5 actions: Pin/Unpin, Move to project, Archive/Unarchive, Duplicate, Delete
(danger style). Each with icon and descriptive subtitle.
Updated test_sprint16.py: test_sessions_js_uses_action_menu_not_per_row_buttons
asserts the new trigger and menu functions exist, old per-row classes are gone.
Extracted from PR #242.
* docs: v0.47.0 release notes, bump version, update test counts (645)
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 19:19:12 +00:00
|
|
|
|
const name=await showPromptDialog({title:t('new_folder_prompt'),placeholder:'folder-name',confirmLabel:t('create')});
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
if(!name||!name.trim())return;
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
|
const relPath=S.currentDir==='.'?name.trim():(S.currentDir+'/'+name.trim());
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
try{
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
|
await api('/api/file/create-dir',{method:'POST',body:JSON.stringify({session_id:S.session.session_id,path:relPath})});
|
2026-04-08 14:55:03 +00:00
|
|
|
|
showToast(t('folder_created')+name.trim());
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
|
await loadDir(S.currentDir);
|
v0.50.207: batch of 10 PRs — TPS stat, SSE guard, session polish, cron UX, folder create, model errors, session speed, title gen (#1031)
* fix: remove orphaned i18n keys from top-level LOCALES object
Three Traditional Chinese translation keys (cmd_status, memory_saved,
profile_delete_title) were placed outside any locale block between the
en and ru blocks in static/i18n.js. They became top-level properties
of the LOCALES object, causing them to appear as invalid language
options in the Settings > Preferences dropdown.
The correct translations already exist in the zh-Hant locale block.
Fixes #1008
* fix: block stale SSE events from polluting new session's DOM
- appendThinking(): guard with !S.session||!S.activeStreamId to drop
events from a previous session's SSE stream during a session switch
- appendLiveToolCard(): same guard for consistency
- finalizeThinkingCard(): scroll thinking-card-body to top when
scroll is pinned, so completed response is immediately visible
- appendThinking(): auto-scroll thinking card body to bottom while
streaming if user is watching (scroll pinned)
* Fix empty agent sessions in sidebar
* fix: resolve cron UI UX issues — icon ambiguity, toast overlap, running status
Fixes #995 — three sub-issues in the Cron Jobs UI:
1. Dual play icons ambiguous: Resume button now shows a distinct
play+bar icon (play triangle + vertical line) instead of the
identical triangle used by Run now.
2. Toast notification overlapping header buttons: Added
position:relative; z-index:10 to .main-view-header so it
stacks above the fixed toast (z-index:100 within its layer).
3. No running status after trigger: After triggering a job, the
status badge immediately shows 'running…' with a CSS spinner
animation, and polls the cron list every 3s (up to 30s) to
refresh when the job completes.
- Added cron_status_running i18n key in all 5 locales (en, es, de, ru, zh, zh-Hant)
- Added .detail-badge.running CSS class with spinner animation
- New functions: _setCronDetailStatus(), _startCronRunningPoll()
* fix(#1011): address review feedback — poll cleanup, badge persistence, 30s fallback
- _clearCronDetail() now clears _cronRunningPoll interval on navigation
- Poll re-applies 'running' badge after loadCrons() re-render (prevents flicker)
- When poll ends (30s max), detail re-renders with actual status as fallback
* feat: create folder and add space directly from UI (#782)
- After creating a folder via the file tree New folder button, offer to add it as a space via confirm dialog
- Add Create folder if it doesnt exist checkbox in the New Space form
- Backend: support create flag in /api/workspaces/add to mkdir before validation
- i18n: 4 new keys (folder_add_as_space_title/msg/btn, workspace_auto_create_folder) in all 6 locales
* fix: validate workspace path before mkdir to prevent orphan directories
Review feedback (critical): the previous code called mkdir() before
validate_workspace_to_add(), which meant a rejected path (e.g. system dir)
would leave an orphan directory on disk.
New flow:
1. Resolve path and check against blocked system roots BEFORE any mutation
2. mkdir() only if path passes the blocklist check
3. Full validation (exists, is_dir) after mkdir
Also imports _workspace_blocked_roots for the pre-mutation blocklist check.
* fix(#1014): classify model-not-found errors with helpful message
- Add model_not_found error type to streaming.py exception classifier
- Detect 404, 'not found', 'does not exist', 'invalid model' patterns
- Strip HTML tags from provider error messages (nginx 404 pages, etc.)
- Add model_not_found branch to apperror handler in messages.js
- Add i18n key model_not_found_label in all 6 locales
- 15 tests covering detection, sanitization, frontend, and i18n
* feat(ui): add live TPS stat to header
Adds a TPS (Tokens Per Second) chip to the right of the header title bar
that updates live while AI output is streaming.
Metering (api/metering.py)
- Tracks per-session output + reasoning tokens via GlobalMeter singleton
- Per-session TPS = total_tokens / elapsed_time
- Global TPS = average of active sessions' TPS values
- HIGH/LOW are max/min of global_tps snapshots over a 60-minute rolling
window (only recorded when > 0, so idle periods are excluded)
- Thread-safe with a single lock
Metering events emitted from streaming.py
- Throttled at 100ms from token/reasoning/tool callbacks so the display
updates rapidly during fast token streams
- 1Hz ticker as fallback for slow streams (exits when no active sessions)
- Final stats emitted on stream end
Routes (api/routes.py)
- Removed POST /api/metering/interval endpoint (dynamic interval via
focus/blur was replaced with simple always-1s-when-active approach)
UI (static/messages.js, index.html, style.css)
- TPS chip in titlebar: shows 'N.N t/s . N.N high . N.N low'
- Default: '0.0 t/s . 0.0 high' when idle
- Display updates on every metering SSE event (throttled to 100ms)
* feat: session restore speed + title gen reasoning hardening (#1025, #1026)
PR #1025 (@franksong2702): Speed up large session restore paths
- GET /api/session?messages=0 now parses only metadata before the messages array
- Metadata-only loads no longer populate the full-session LRU cache
- Frontend lazy fetch uses resolve_model=0 to avoid cold model-catalog lookup
- Hard reload no longer waits for populateModelDropdown() before restoring session
PR #1026 (@franksong2702): Harden auto title generation for reasoning models
- Raises title-gen completion budget to 512 tokens (reasoning-safe)
- Retries once with 1024 tokens on empty content / finish_reason:length
- Applies retry to both auxiliary and active-agent fallback routes
- Preserves underlying failure reason in title_status on local fallback
Co-authored-by: Frank Song <franksong2702@gmail.com>
* feat: session attention indicators in right slot + last_message_at timestamps (#1024)
PR #1024 (@franksong2702): Polish session attention indicators
- Streaming spinners and unread dots now reuse the right-side actions slot
- Running/unread rows hide timestamps; idle/read rows keep right-aligned timestamps
- Date group carets point down when expanded, right when collapsed
- Pinned group no longer repeats pinned-star icon per row
- Running indicators appear immediately after send (local busy state while /api/sessions catches up)
- Sidebar sorting/grouping/timestamps now prefer last_message_at (derived from last real message)
so metadata-only saves don't make old sessions appear under Today
Co-authored-by: Frank Song <franksong2702@gmail.com>
* docs: v0.50.207 release notes — 10 PRs, 2169 tests (+36)
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: Josh <josh@fyul.link>
Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-25 20:07:35 +00:00
|
|
|
|
// Offer to add the new folder as a space (#782)
|
|
|
|
|
|
const absPath=S.session.workspace?((S.currentDir==='.'?S.session.workspace:S.session.workspace+'/'+S.currentDir)+'/'+name.trim()):null;
|
|
|
|
|
|
if(absPath){
|
|
|
|
|
|
const addAsSpace=await showConfirmDialog({
|
|
|
|
|
|
title:t('folder_add_as_space_title'),
|
|
|
|
|
|
message:t('folder_add_as_space_msg'),
|
|
|
|
|
|
confirmLabel:t('folder_add_as_space_btn'),
|
|
|
|
|
|
focusCancel:true
|
|
|
|
|
|
});
|
|
|
|
|
|
if(addAsSpace){
|
|
|
|
|
|
try{
|
|
|
|
|
|
const data=await api('/api/workspaces/add',{method:'POST',body:JSON.stringify({path:absPath})});
|
|
|
|
|
|
if(typeof _workspaceList!=='undefined')_workspaceList=data.workspaces||_workspaceList||[];
|
|
|
|
|
|
if(typeof renderWorkspacesPanel==='function')renderWorkspacesPanel(_workspaceList);
|
|
|
|
|
|
showToast(t('workspace_added'));
|
|
|
|
|
|
}catch(e2){setStatus((t('error_prefix')||'Error: ')+e2.message);}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-08 14:55:03 +00:00
|
|
|
|
}catch(e){setStatus(t('folder_create_failed')+e.message);}
|
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 03:40:19 +00:00
|
|
|
|
function renderTray(){
|
|
|
|
|
|
const tray=$('attachTray');tray.innerHTML='';
|
feat: polish send button — hidden until content, icon-circle, pop-in animation
- index.html: btnSend hidden by default (display:none), icon-only (upward
arrow SVG, no text label), title attribute for accessibility
- style.css: new send-btn design — 34px circle, blue fill (#7cb9ff),
subtle glow box-shadow, scale() hover/active for tactile feel,
.send-btn.visible with @keyframes send-pop-in (scale+opacity spring
using cubic-bezier(.34,1.56,.64,1) for a satisfying pop). Mobile
override updated to preserve circle dimensions.
- ui.js: updateSendBtn() — shows button with pop-in animation when
textarea has content OR files are attached and agent is not busy;
hides instantly when content is cleared. Hooked into setBusy() and
renderTray() so button state tracks all content sources correctly.
- boot.js: input event listener calls updateSendBtn() on every keystroke.
- messages.js: autoResize() calls updateSendBtn() so button disappears
immediately after send clears the textarea.
- tests/test_sprint21.py: 33 tests covering HTML structure, CSS design
(circle shape, colors, animations, keyframes), JS logic (updateSendBtn,
setBusy, renderTray, autoResize integration), and regressions
(363 total, all pass).
2026-04-03 14:10:59 +00:00
|
|
|
|
if(!S.pendingFiles.length){tray.classList.remove('has-files');updateSendBtn();return;}
|
2026-03-31 03:40:19 +00:00
|
|
|
|
tray.classList.add('has-files');
|
feat: polish send button — hidden until content, icon-circle, pop-in animation
- index.html: btnSend hidden by default (display:none), icon-only (upward
arrow SVG, no text label), title attribute for accessibility
- style.css: new send-btn design — 34px circle, blue fill (#7cb9ff),
subtle glow box-shadow, scale() hover/active for tactile feel,
.send-btn.visible with @keyframes send-pop-in (scale+opacity spring
using cubic-bezier(.34,1.56,.64,1) for a satisfying pop). Mobile
override updated to preserve circle dimensions.
- ui.js: updateSendBtn() — shows button with pop-in animation when
textarea has content OR files are attached and agent is not busy;
hides instantly when content is cleared. Hooked into setBusy() and
renderTray() so button state tracks all content sources correctly.
- boot.js: input event listener calls updateSendBtn() on every keystroke.
- messages.js: autoResize() calls updateSendBtn() so button disappears
immediately after send clears the textarea.
- tests/test_sprint21.py: 33 tests covering HTML structure, CSS design
(circle shape, colors, animations, keyframes), JS logic (updateSendBtn,
setBusy, renderTray, autoResize integration), and regressions
(363 total, all pass).
2026-04-03 14:10:59 +00:00
|
|
|
|
updateSendBtn();
|
2026-03-31 03:40:19 +00:00
|
|
|
|
S.pendingFiles.forEach((f,i)=>{
|
|
|
|
|
|
const chip=document.createElement('div');chip.className='attach-chip';
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
chip.innerHTML=`${li('paperclip',12)} ${esc(f.name)} <button title="${t('remove_title')}">${li('x',12)}</button>`;
|
2026-03-31 03:40:19 +00:00
|
|
|
|
chip.querySelector('button').onclick=()=>{S.pendingFiles.splice(i,1);renderTray();};
|
|
|
|
|
|
tray.appendChild(chip);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
function addFiles(files){for(const f of files){if(!S.pendingFiles.find(p=>p.name===f.name))S.pendingFiles.push(f);}renderTray();}
|
|
|
|
|
|
|
|
|
|
|
|
async function uploadPendingFiles(){
|
|
|
|
|
|
if(!S.pendingFiles.length||!S.session)return[];
|
|
|
|
|
|
const names=[];let failures=0;
|
|
|
|
|
|
const bar=$('uploadBar');const barWrap=$('uploadBarWrap');
|
|
|
|
|
|
barWrap.classList.add('active');bar.style.width='0%';
|
|
|
|
|
|
const total=S.pendingFiles.length;
|
|
|
|
|
|
for(let i=0;i<total;i++){
|
|
|
|
|
|
const f=S.pendingFiles[i];const fd=new FormData();
|
|
|
|
|
|
fd.append('session_id',S.session.session_id);fd.append('file',f,f.name);
|
|
|
|
|
|
try{
|
2026-04-16 18:20:08 +00:00
|
|
|
|
const res=await fetch(new URL('api/upload',location.href).href,{method:'POST',credentials:'include',body:fd});
|
2026-03-31 03:40:19 +00:00
|
|
|
|
if(!res.ok){const err=await res.text();throw new Error(err);}
|
|
|
|
|
|
const data=await res.json();
|
|
|
|
|
|
if(data.error)throw new Error(data.error);
|
2026-04-25 06:09:44 +00:00
|
|
|
|
names.push({name: data.filename, path: data.path});
|
2026-04-08 14:55:03 +00:00
|
|
|
|
}catch(e){failures++;setStatus(`\u274c ${t('upload_failed')}${f.name} \u2014 ${e.message}`);}
|
2026-03-31 03:40:19 +00:00
|
|
|
|
bar.style.width=`${Math.round((i+1)/total*100)}%`;
|
|
|
|
|
|
}
|
|
|
|
|
|
barWrap.classList.remove('active');bar.style.width='0%';
|
|
|
|
|
|
S.pendingFiles=[];renderTray();
|
2026-04-08 14:55:03 +00:00
|
|
|
|
if(failures===total&&total>0)throw new Error(t('all_uploads_failed',total));
|
2026-03-31 03:40:19 +00:00
|
|
|
|
return names;
|
|
|
|
|
|
}
|