Commit graph

52 commits

Author SHA1 Message Date
nesquena-hermes
d67036db24
v0.50.220: workspace panel collapse + project color dot fix (#1090)
* fix(ui): workspace panel collapse priority + visible project color dot

Two related sidebar UI bugs from project-ui-bugs.md:

1. Workspace panel header had no collapse priority. As the right panel
   narrowed, all three header children (Workspace label, git badge,
   icon buttons) compressed at the same rate because `.panel-header`
   used `justify-content:space-between` with no flex-shrink ratios.
   The icon buttons -- the actual primary controls -- could disappear
   before the git badge (which is least-essential metadata).

   Fix: declare `.rightpanel` as a `container-type:inline-size` container.
   Replace `justify-content:space-between` with `gap:6px` plus
   `margin-left:auto` on `.panel-actions`. Set flex-shrink:0 on
   `.panel-actions` (icons never shrink), flex-shrink:2 on the label,
   flex-shrink:3 on `.git-badge` (shrinks fastest), and
   `min-width:0;text-overflow:ellipsis` for graceful intermediate
   shrink. Add @container queries that crisply set `display:none` on
   the git badge below 220px and on the label below 160px.

2. Project color dot was appended INSIDE the `.session-title` span,
   which is `overflow:hidden;text-overflow:ellipsis`. Long titles
   clipped the dot off entirely -- hiding the project marker exactly
   when it was most needed. The timestamp was also `position:absolute`,
   so the title's `flex:1` ran underneath it and there was nowhere
   coherent to anchor the dot.

   Fix: in sessions.js, append the dot to `titleRow` between title and
   timestamp (a flex sibling, not inside the truncating title span).
   In style.css, move `.session-time` from absolute positioning to
   `margin-left:auto` in the flex row. Drop the
   `margin-left:4px/vertical-align:middle` from
   `.session-project-dot` (gap:6px on the row handles spacing).
   Reduce `.session-item` padding-right at rest from 86px (which was
   reserving space for the absolute timestamp) to 8px; expand to 40px
   on hover/streaming/unread/menu-open/focus-within so the absolute
   action button + attention indicator still have room.

Tests:
  - tests/test_workspace_panel_session_list.py (14 new tests)
  - tests/test_issue856_pinned_indicator_layout.py updated to reflect
    the new flex-flow timestamp + reduced rest-padding

Full suite: 2433 passed, 47 skipped, 0 PR-related failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ui): remove duplicate margin-left:auto from .git-badge

With .panel-actions already carrying margin-left:auto, both .git-badge
and .panel-actions having auto margins split the free space equally,
centering the badge instead of keeping it adjacent to the label.

Remove margin-left:auto and margin-right:4px from .git-badge. The
panel-header gap:6px handles label→badge spacing; panel-actions
margin-left:auto owns the right-push. Layout: [label][badge][→][actions].

* fix(ui): mobile session-item padding 86px → 40px + git-badge margin fix

Two fixes from Opus independent review of #1089:

1. Mobile padding regression: .session-item mobile override had
   padding:10px 86px 10px 12px — the 86px was reserving space for the
   old position:absolute timestamp. Since the timestamp now lives in the
   flex flow of .session-title-row (margin-left:auto), that 86px
   reservation is wasted and pushes the timestamp ~76px from the right
   edge, leaving dead space between it and the always-visible action
   button. Fixed: 86px → 40px (matching desktop hover/attention rule,
   only enough for the absolute action button at right:6px + 26px wide).

2. Duplicate margin-left:auto on .git-badge: the old rule from master
   had margin-left:auto on .git-badge (for the old space-between layout).
   With .panel-actions also having margin-left:auto, the two auto margins
   split free space equally, floating the badge to the middle of the header
   instead of keeping it flush against the label. Removed margin-left:auto
   and margin-right:4px from .git-badge; gap:6px on .panel-header handles
   label→badge spacing; .panel-actions margin-left:auto owns the right-push.

Updated tests:
- test_workspace_panel_session_list.py: assert 40px mobile padding
- test_issue856_pinned_indicator_layout.py: assert 40px mobile padding

Verified by Playwright visual QA:
- Desktop 250px: badge hidden, Workspace label visible, icons visible ✓
- Desktop 150px: badge hidden, label hidden, icons only ✓
- Project dots visible on long-title sessions (outside truncating title span) ✓
- Mobile: padding-right=40px, no layout overflow ✓

* docs: v0.50.220 release notes, test count 2481, roadmap

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 00:19:05 -07:00
nesquena-hermes
d625bac6d4
v0.50.219: project chip context menu + input auto-sizing (#1087)
* fix(projects): opaque context menu + auto-sizing rename/create input

Two project chip UI bugs reported in project-ui-bugs.md:

1. Right-click context menu was transparent and the session list bled
   through it. Root cause: _showProjectContextMenu set
   background: var(--panel), but --panel is not defined anywhere in
   style.css, so the menu fell back to transparent. Fix: use
   var(--surface) -- the same opaque variable used by
   .session-action-menu and other floating popovers.

2. The rename and new-project input field was hard-coded to 100px
   regardless of the project name being edited (a 3-letter name got
   the same field size as a 20-letter name). Fix: drop width:100px
   from .project-create-input, replace with
   min-width:40px / max-width:180px / width:auto. Add a
   _resizeProjectInput() helper that measures the current value with
   a hidden span and sets pixel width inside those bounds. Wired into
   both _startProjectRename (called once on focus, again on every
   input event) and _startProjectCreate (same pattern).

Tests: 9 new static-source tests in tests/test_project_chip_ui.py
that pin (a) var(--panel) is undefined in style.css so the fallback
trap doesn't return; (b) menu uses var(--surface); (c) the fixed
width:100px is gone and min/max bounds are present; (d) the
_resizeProjectInput helper is defined and called from both flows.

Full suite: 2419 passed, 47 skipped, 0 PR-related failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(projects): use getComputedStyle in _resizeProjectInput sizer span

Switch the hidden sizer span from hardcoded font-size:10px / font-family:inherit
to reading the live values from getComputedStyle(inp). This keeps the sizer
calibrated if the CSS rule ever changes, rather than silently drifting.

Also update test_resize_helper_uses_hidden_span to assert getComputedStyle
is used rather than the old literal font-size check.

Suggested by Opus independent review of #1086.

* docs: v0.50.219 release notes, test count 2467, roadmap update

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-25 23:28:29 -07:00
nesquena-hermes
498b51bfc6
v0.50.218: chat bubble overflow, project color picker, blockquote renderer (#1085)
* fix(css): add overflow-wrap:anywhere to chat bubbles — prevents long URL overflow (#1080)

* fix(projects): rename now works via dblclick timer guard + right-click color picker (#1078)

* fix(renderer): block-level constructs inside blockquotes now render

Fenced code blocks, headings, horizontal rules, and ordered lists inside
blockquotes now render correctly. Six related bugs documented in
blockquote-rendering-bugs.md were collapsed into one architectural fix
in renderMd().

Bugs fixed (all 6):

1. Fenced code blocks inside blockquotes -- > prefixes leaked into the
   <pre> body and the blockquote got fragmented around the rendered
   code, sometimes leaving raw <pre>/<div class="pre-header"> as
   visible text.
2. Blank > continuation lines fragmented multi-paragraph blockquotes
   into separate <blockquote> elements with literal > between them.
3. ## headings inside blockquotes rendered as literal "##" text.
4. Numbered lists inside blockquotes rendered as plain prose.
5. Complex blockquote (mixed headings + code + list + inline code)
   collapsed into a monospace blob with raw markdown syntax leaking
   everywhere.
6. Horizontal rules (---) inside blockquotes rendered as literal text.

Root cause:

The per-line passes for fenced code, headings, hr, ordered lists all ran
BEFORE the blockquote handler and could not match lines that started
with >, so by the time blockquote stripping ran those constructs had
already been mishandled.

Fix:

A new blockquote pre-pass at the top of renderMd():

- Walks lines fence-aware so > -prefixed lines inside non-blockquote
  code fences (e.g. shell prompts in bash code blocks) are not
  miscaptured as a blockquote.
- Groups consecutive > -prefixed lines, strips the > prefix, and
  recursively calls renderMd() on the stripped content. The recursive
  call handles all block-level constructs (fenced code, headings, hr,
  ordered/unordered lists, nested blockquotes) using the same pipeline.
- Wraps the rendered HTML in <blockquote> and stashes it with a \x00Q
  token. Restored at the very end of renderMd() so no later pass can
  mangle the inner HTML.

The old _applyBlockquotes regex-replace is removed entirely along with
its limited inline branches for nested blockquotes and unordered lists.

Behaviour change:

Blockquotes now produce CommonMark-compliant <p> wrapping for text
content (was: bare text directly inside <blockquote>). The visual
output is the same in browsers but the HTML structure is now standard.

Tests:

- 14 new behavioural tests in tests/test_renderer_js_behaviour.py
  drive the actual renderMd() via node and lock all 6 bug fixes.
- .local-review/test_blockquote_bugs.js -- node harness covering the
  same scenarios, runnable manually for fast iteration.
- 2407/2408 tests pass (1 pre-existing macOS-only failure deselected).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(renderer): entity decode before blockquote pre-pass + CSS margin fix

- Move the &gt;/&lt;/&amp; entity-decode to run at the very top of
  renderMd(), before the blockquote pre-pass. Previously decode() ran
  at line 756 (after the pre-pass at line 697), so LLM output containing
  &gt;-encoded blockquotes was never matched by the pre-pass.

- Add .msg-body blockquote p{margin:0} and .preview-md blockquote p{margin:0}
  so the new CommonMark-compliant <p> wrapping inside blockquotes doesn't
  add extra vertical spacing. Prior shape (bare text) had no default p-margins.

- Add Node-driven tests: TestBlockquoteEntityEncodedInput covers &gt; prefix
  and &gt;-encoded fenced code inside blockquotes.

- Add struct test: TestBlockquotePrePassOrdering::test_entity_decode_runs_before_blockquote_pre_pass
  locks decode < _bq_stash ordering in ui.js.

Fixes found during Opus independent review of #1083.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs: v0.50.218 release notes, test count 2458, roadmap update

---------

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-25 23:08:59 -07:00
nesquena-hermes
360463dd8e
v0.50.212: model cache perf (~30s→~1ms), session switch UX, cache isolation fix (#1063)
* fix(models): disk cache now used on restart, cold path locked, 24h TTL

Root causes fixed:
- reload_config() was deleting disk cache on every server start (cfg_mtime 0.0 vs real mtime).
  Now saves old mtime before update and skips cache deletion on first-ever load.
- Cold path was running outside the lock causing thundering herd on startup.
  Now extracted to _build_available_models_uncached() helper running inside RLock.
- Disk cache was never being checked before lock acquisition.
  Now loads from disk BEFORE acquiring lock; cache hit returns without lock contention.
- Credential pool load_pool() was called per-provider per-request (~10s for zai).
  Now cached in _CREDENTIAL_POOL_CACHE with 24h TTL.

Result: /api/models returns in ~1ms on restart instead of ~30s.

* fix(ui): block stale SSE events, cancel old stream on switch, clear pending files after send, focus textarea after switch, instant click for inactive sessions, rename session via titlebar dblclick

Key UX improvements:
- Block stale SSE responses from old sessions reaching new session DOM after switch
- Cancel in-flight streaming when switching sessions
- Clear pending files after send (prevents ghost attachments in tray)
- Auto-focus message textarea after session switch
- Instant click for inactive sessions (no loading spinner blocking)
- Double-click app titlebar to rename active session
- Persist/restore composer draft across session switches

* style: add user-select:none to session titles to prevent accidental text selection

* fix(models): prevent concurrent cold path runs with _cache_build_in_progress guard

Thread 2 was re-entering the cold path (via RLock) while Thread 1 was
still inside it, causing duplicate 10s zai load_pool() calls. The RLock
allows re-entry from the same thread, defeating the 'only one cold path'
guarantee. Now threads wait on _cache_build_cv instead of re-entering.

* fix(models): add missing global declarations, move mtime check to outer scope for test

* fix(models): attach _cache_build_cv to the RLock so notify_all() is safe

* fix(models): evict _CREDENTIAL_POOL_CACHE entries when provider cache is invalidated

Without this, invalidate_provider_models_cache(provider_id) cleared the
models cache but left stale CredentialPool objects in _CREDENTIAL_POOL_CACHE
for up to 24h.  The next get_available_models() cold path would re-use the
stale pool instead of re-loading, meaning new credentials added by the user
wouldn't show up until the pool TTL expired.

Now evicts both provider_id and its canonical alias from the pool cache
so the next cold path re-loads from disk.

* fix(merge): restore #1024/#1025 work in static/sessions.js after rebase

The merge of master (commit 05d1ba9) resolved the static/sessions.js
conflict by keeping the contributor's version, which silently dropped
several pieces of work that had landed via PR #1024 and #1025:

  PR #1024 (session attention indicators):
    - _renderOneSession(s, isPinnedGroup=false) signature
    - body.appendChild(_renderOneSession(s, Boolean(g.isPinned)))
    - pinned-group dedup: if(s.pinned&&!isPinnedGroup) ...
    - last_message_at preference in _sessionTimestampMs
    - Right-slot attention indicator + hide-timestamp-when-attentive

  PR #1025 (session restore speed):
    - &resolve_model=0 on the loadSession metadata fetch
    - S.session._modelResolutionDeferred=true after assignment
    - _resolveSessionModelForDisplaySoon(sid) helper + invocation
    - &resolve_model=0 on the lazy full-message fetch

Restoration approach: reset sessions.js to current master, then layer
the contributor's #1060 additions on top:
  - _loadingSessionId global for stale-response discard
  - composer draft persistence on session switch (via S.composerDrafts)
  - _loadingSessionId !== sid bail-outs at every async await point
  - Cleanup _loadingSessionId = null at all exit paths

Test outcome:
  - tests/test_issue856_pinned_indicator_layout.py: 5/5 (was 5/5 fail)
  - tests/test_session_metadata_fast_path.py: 5/5 (was 3/5 fail)
  - tests/test_session_sidebar_relative_time.py: 5/5 (was 1/5 fail)
  - Full suite: 2233 passed, 0 failed

fix(models): clear _CREDENTIAL_POOL_CACHE in invalidate_models_cache

The 24h-TTL credential pool cache introduced in this PR was keyed by
provider_id only, so when a user added/changed credentials, or when
tests called invalidate_models_cache() between cases with different
auth payloads, the cached CredentialPool from the prior payload leaked
into the new run.

Two complementary fixes:
  1. invalidate_models_cache() now also clears _CREDENTIAL_POOL_CACHE
  2. invalidate_provider_models_cache(provider_id) pops just that
     provider's entry — surgical eviction for live key edits

Pinned by tests/test_credential_pool_providers.py — 23/23 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: invalidate disk cache in invalidate_models_cache(); reset _cache_build_in_progress on exception

1. invalidate_models_cache() now calls _delete_models_cache_on_disk() so that the
   on-disk snapshot at /dev/shm is removed alongside the memory cache. Without this,
   _load_models_cache_from_disk() serves a stale prior-test result immediately after
   invalidation, breaking all test_credential_pool_providers and test_model_resolver
   tests that rely on get_available_models() returning fresh mocked data.

2. Wrap _build_available_models_uncached() in try/except so _cache_build_in_progress
   is always reset (+ notify_all) even if the rebuild raises unexpectedly, preventing
   waiting threads from being stuck at wait_for() for the full 60s timeout.

3. Fix misleading comment: "avoid deadlock" → "file I/O outside the lock".

Co-authored-by: JKJameson <JKJameson@users.noreply.github.com>

* docs: v0.50.212 release notes and version bump

Model cache perf, session switch UX improvements, cache isolation fixes.

---------

Co-authored-by: Josh <josh@fyul.link>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: JKJameson <JKJameson@users.noreply.github.com>
2026-04-25 18:24:30 -07:00
nesquena-hermes
01404ac062
v0.50.211: compact timestamps, adaptive title refresh, settings picker fix (#1061)
* Shorten session sidebar relative time labels

* feat: adaptive session title refresh based on conversation evolution

Addresses #869 — the 'Optional' part: adapt session names to current
conversation context instead of only generating once from the first exchange.

Backend (api/streaming.py):
- Add _latest_exchange_snippets() to extract last user+assistant pair
- Add _count_exchanges() to count user messages
- Add _get_title_refresh_interval() to read the setting
- Add _run_background_title_refresh() — refreshes title from latest exchange
  with LLM, skips if title is unchanged or user manually renamed
- Add _maybe_schedule_title_refresh() — checks exchange count and schedules
  refresh after stream_end (non-blocking)

Config (api/config.py):
- Add auto_title_refresh_every setting (default '0' = off)
- Enum validation: {'0', '5', '10', '20'}

Frontend:
- Settings UI dropdown (static/index.html)
- Wire up load/save in panels.js
- i18n keys for all 6 locales (en/ru/es/de/zh/zh-Hant)

Default: off. Opt-in via Settings > Conversation > Adaptive title refresh.

* test: add 37 tests for adaptive title refresh helpers

Covers all five new functions introduced in this PR:
  _count_exchanges, _latest_exchange_snippets, _get_title_refresh_interval,
  _run_background_title_refresh, _maybe_schedule_title_refresh

Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>

* fix(settings): show selected state on theme/skin/font-size picker cards

The CSS rule `#mainSettings .theme-pick-btn { border-color: var(--border) !important }` was
overriding the inline `style.borderColor = "var(--accent)"` set by `_syncThemePicker()` and
siblings — `!important` beats inline styles. Active cards showed no visual highlight.

Fix: move to `.active` CSS class with `border-color:var(--accent)!important` so the active
rule wins over the base rule, and clear the stale inline borderColor/boxShadow from the
sync functions. 5 regression tests added.

Closes #1057

* fix: rename test file to match PR number, fix stale issue reference

* docs: v0.50.211 release notes and version bump

Compact sidebar timestamps, adaptive title refresh (opt-in), settings picker fix.

* docs(changelog): correct settings tab for adaptive title refresh

The v0.50.211 entry for #1058 said "Settings → Appearance" but the
toggle is actually rendered inside settingsPanePreferences (the
Preferences tab) per static/index.html:604+. The commit message also
had the wrong tab ("Conversation"). Updated CHANGELOG to match the
actual UI surface so users can find the toggle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: create state dir before writing settings file

save_settings() called SETTINGS_FILE.write_text() without ensuring the
parent directory exists. In fresh environments (CI, first run without
HERMES_WEBUI_STATE_DIR set) this raised FileNotFoundError.
Add mkdir(parents=True, exist_ok=True) before the write.

---------

Co-authored-by: Pavol Biely <biely@webtec.sk>
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
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-25 17:50:58 -07:00
nesquena-hermes
3ce7844a7a
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 14:21:50 -07:00
nesquena-hermes
ad8e10304c
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 13:07:35 -07:00
nesquena-hermes
116a510ed3
i18n: add complete Traditional Chinese (zh-Hant) translations (#954)
* i18n: add complete Traditional Chinese (zh-Hant) translations

- Add 300+ zh-Hant translation entries covering all UI sections:
  onboarding, settings/Control Center, session actions, cron jobs,
  providers panel, workspace management, skills, profiles, todos, BTW
- Fix existing zh-Hant translations: remove mixed Simplified Chinese
  characters, fix typos (e.g. 皮膚→佈景, 待踩→待辦, 新存對話→新對話)
- Update zh locale: fix 需要审批→需要审核 (Simplified Chinese correction)
- Add data-i18n attributes to Control Center HTML (index.html) for
  heading, subtitle, tab names, dropdown, and section titles
- Migrate session action menu (sessions.js) from hardcoded English to
  t() function calls for full i18n support

* fix: translate remaining English entries to Traditional Chinese in zh-Hant locale

- settings_heading_title: 'Control Center' → '控制中心'
- settings_dropdown_providers: 'Providers' → '供應商'
- providers_section_title: 'Providers' → '供應商'
- providers_tab_title: 'Providers' → '供應商'

* fix: add missing locale keys to zh/ru/es/de + restore zh approval_heading

- zh (Simplified): reverted approval_heading to 需要审批 (matches master)
  PR had changed it to 需要审核 which broke the representative-translation test
- zh/ru/es/de: added 39 new session management + settings keys as English
  fallback strings (session_archive, session_pin, settings_dropdown_*, etc.)
  These keys were added to English in this PR but missing from other locales
- es: added cmd_status (English fallback) to fix coverage gap
- Fixes all locale coverage test failures

---------

Co-authored-by: 陳俊宇 <chenjunyu@chenjunyudeMacBook-Air-7.local>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-24 11:36:41 -07:00
Josh Jameson
7e17ec497c
fix: fast conversation switching with metadata-first load (#959)
- Backend: save session JSON with metadata fields before messages array
  so load_metadata_only() reads only ~1KB without parsing the full session
- Backend: add GET /api/session?messages=0 for metadata-only responses
  (~1KB vs ~400KB), enabling instant sidebar switching
- Backend: add POST /api/admin/reload to hot-reload models without restart
- Backend: gzip compress JSON API responses (>1KB) for 70-80% bandwidth reduction
- Frontend: show Loading indicator immediately on session switch, replacing
  old DOM before API call to prevent stale content flash
- Frontend: clear S.messages before API call so _ensureMessagesLoaded
  always fetches fresh data for the target session
- Frontend: wrap both Phase 1 (messages=0) and Phase 2 (_ensureMessagesLoaded)
  in try/catch to prevent permanently stuck loading state on network/server errors
2026-04-24 11:35:14 -07:00
Aron Prins
970bc1d3fd
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 09:05:25 -07:00
nesquena-hermes
498156a3e8
fix(settings): show live models in default model picker and apply to new chats (#872) (#900)
* fix(settings): show live models in default model picker and apply to new chats (#872)

Two related bugs:
1. Settings > Preferences > Default Model dropdown only showed static models
   from /api/models — live-fetched models (e.g. @nous:anthropic/claude-opus-4.7)
   were missing. Now calls _fetchLiveModels() on the settings picker too.
2. New chats ignored the saved default model preference — they always used the
   chat-header dropdown value (which reflects the previous session's model).
   Now newSession() uses the saved default_model and syncs the dropdown.

Extracted _addLiveModelsToSelect() from _fetchLiveModels() so cached live models
can be applied to any <select> element (chat-header or settings picker).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(tests): update live-model prefix tests for _addLiveModelsToSelect extraction

The tests searched for og.dataset.provider, _isPortalFetch, and openrouter
exclusion patterns inside _fetchLiveModels(). These were extracted into
_addLiveModelsToSelect() as part of the #872 fix. Updated regex targets to
check _addLiveModelsToSelect first, falling back to _fetchLiveModels.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: add multi-tab note on window._defaultModel

Clarifies that window._defaultModel is per-page-load and not synced
across browser tabs, following maintainer feedback on #889.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: CHANGELOG for v0.50.170

* chore: trigger PR refresh after rebase

---------

Co-authored-by: fr33m1nd <bergeouss@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-23 09:58:15 -07:00
nesquena-hermes
b82954ee70
feat(ui): session attention indicators — streaming spinner, unread dot, timestamps (#856)
Closes #856. Co-authored-by: Frank Song <138988108+franksong2702@users.noreply.github.com>
Reviewed-by: nesquena (709bd37 — test isolation fix also included)
2026-04-23 09:05:57 -07:00
nesquena-hermes
d4a3adb7b1
fix(sessions): surface gateway SSE failures and add polling fallback (#828)
* fix(sessions): surface gateway SSE failures and add polling fallback

- add a JSON probe mode for the gateway SSE endpoint
- detect watcher-unavailable 503s from the browser
- fall back to periodic session refresh with a toast
- add probe payload tests and endpoint coverage

Fixes #635

* fix(sessions): surface gateway SSE failures and add polling fallback (#826)

Absorbed from PR #826 by @cloudyun888 (fixes #635).

When the gateway watcher thread is not running, the browser now shows a
toast notification and falls back to 30-second periodic polling for session
sync. Previously the SSE failure was completely silent with no user feedback.

Changes from original PR:
- Deleted misplaced test_gateway_sse_probe_unit.py (was at repo root, not
  discovered by `pytest tests/`); unit tests moved into tests/test_gateway_sync.py
- _gateway_sse_probe_payload now checks watcher._thread.is_alive() rather
  than just watcher is not None — a watcher instance with a dead poll thread
  now correctly reports unavailable and activates the polling fallback
- probeGatewaySSEStatus catch(e) now starts the polling fallback on network
  error rather than silently swallowing the failure
- Added 5 unit tests covering all watcher-alive/dead/missing/disabled branches

Co-authored-by: cloudyun888 <269269188+86cloudyun-afk@users.noreply.github.com>

* cleanup(gateway): public is_alive() + dedup probe/live watcher-alive check + changelog

Three small cleanups on top of @cloudyun888's PR #826 absorption:

1. Add GatewayWatcher.is_alive() public accessor so routes.py doesn't
   reach into the private _thread attribute.  The existing private-
   attribute check stays as a defensive fallback for any older in-
   memory instance or test double that doesn't implement the full API.

2. Dedupe the watcher_alive computation in _handle_gateway_sse_stream:
   the live-SSE path now calls _gateway_sse_probe_payload(...) and reads
   its watcher_running field instead of re-deriving the same logic
   inline.  Keeps probe and SSE in sync automatically.

3. CHANGELOG trailer was (#826, fixes #635, @cloudyun888) — this PR is
   #828, so updated to (#828, absorbs PR #826 by @cloudyun888, fixes
   #635) matching the repo convention for absorbed PRs (see #805).

Added two regression tests:
- test_gateway_watcher_is_alive_public_method — covers the three
  lifecycle states (before start, while running, after stop).
- test_probe_payload_prefers_public_is_alive — asserts the probe
  uses watcher.is_alive() rather than poking _thread when the
  public method exists.

Full suite: 1735 passed, 0 new failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: cloudyun888 <269269188+86cloudyun-afk@users.noreply.github.com>
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-21 21:18:55 -07:00
nesquena-hermes
d41d05ea36
fix(workspace): _profileDefaultWorkspace persists after newSession() (#823)
Closes #823.

Separates two conflated semantics in S._profileDefaultWorkspace:
- Persistent blank-page default (set by boot/settings, never nulled)
- Profile-switch one-shot (now S._profileSwitchWorkspace, consumed by newSession())

newSession() priority: switchWs → current session → _profileDefaultWorkspace.
switchToWorkspace() clears _profileSwitchWorkspace on explicit switch.

9 new tests. 1777/1777 suite. Browser-verified.
2026-04-21 19:14:31 -07:00
nesquena-hermes
cbb4ba3f28
fix(profiles): profile isolation — new_session uses per-request profile, not process global (#800)
Fixes the multi-client profile isolation bug (#798).

- get_hermes_home_for_profile(): pure path resolver, validates name against
  _PROFILE_ID_RE (rejects path traversal), never mutates os.environ or globals
- new_session() accepts explicit profile= param from POST body (S.activeProfile),
  short-circuits the process-level _active_profile global
- streaming handler resolves HERMES_HOME from s.profile instead of the global
- sessions.js sends profile: S.activeProfile in every new-session POST

10 tests in tests/test_issue798.py including concurrency and traversal coverage.

Co-authored-by: nesquena <nesquena@users.noreply.github.com>
2026-04-21 16:16:51 +00:00
nesquena-hermes
94a04ddd40
fix(ui): persist session queue to sessionStorage across page refresh (#768)
Queued follow-up messages now survive page refresh. Persisted atomically in queueSessionMessage/shiftQueuedSessionMessage. On reload: if agent still active, queue is silently hydrated (done handler drains it); if idle, first entry is restored as a composer draft with a toast. Stale entries discarded.

Fixes #660
2026-04-20 23:04:09 +00:00
Frank Song
0dd5d6f21c
feat(ui): add sidebar density mode to session list (#764)
Adds compact/detailed toggle for the session list sidebar. Compact is the default (no behavior change for existing users). Detailed mode shows message count and model; profile names only appear when mixing sessions across profiles.

Fixes #673

Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-04-20 19:43:40 +00:00
nesquena-hermes
067d96bb30
feat: add full Russian (ru-RU) localization — v0.50.95 (PR #713)
Full Russian locale — 389/389 English keys, Slavic plural forms, native Cyrillic. Rebased from PR #605 with rebase artifacts fixed. Login page Russian added to api/routes.py. Credits: @DrMaks22 (translation), @renheqiang (PR #605 author).

Co-authored-by: DrMaks22 <DrMaks22@users.noreply.github.com>
Co-authored-by: renheqiang <renheqiang@users.noreply.github.com>
2026-04-19 06:47:24 +00:00
nesquena-hermes
66fbfbaa2b
fix: gateway sync race condition + hybrid session data loss — v0.50.93 (PR #714)
Fixes and extends PR #676 (yunyunyunyun-yun). Race guard in sessions.js SSE handler; prefix-equality check in routes.py _handle_session_import_cli. Closes #676.

Co-authored-by: yunyunyunyun-yun <yunyunyunyun-yun@users.noreply.github.com>
2026-04-19 06:18:28 +00:00
Aron Prins
9a3dc10d93
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 14:04:42 -07:00
nesquena-hermes
54e83fb8b6
feat: support subpath mount via reverse proxy — v0.50.67 (PR #588 by @vcavichini)
Squash-merges feature from PR #588 by @vcavichini. Dynamic <base href> injection + api() helper slash-stripping enables deploying hermes-webui behind a reverse proxy at any subpath without configuration. Also fixes pre-existing bug: api/upload was using location.origin instead of location.href (closes #596). Co-authored-by: vcavichini <vcavichini@users.noreply.github.com>
2026-04-16 11:20:08 -07:00
Aron Prins
a9a22ee751
fix(sidebar): declutter session items — drop message count, model, and source-tag badges (v0.50.64)
Squash-merges PR #584 by @aronprins.

Drops the meta row (message count, model slug, source-tag badge) from every sidebar session item. Each session now renders as a single title line — visible session count roughly doubles at typical viewport height.

Changes merged verbatim from contributor branch, plus maintainer additions:
- CHANGELOG entry for v0.50.64
- Version badge bump to v0.50.64
- New test: test_relative_time_today_bucket (closes minor coverage gap from code review)

Co-authored-by: aronprins <aronprins@users.noreply.github.com>
2026-04-16 09:58:53 -07:00
vansour
74dee6b665 fix: respect IME composition in Enter submit flows 2026-04-15 23:12:47 +08:00
Frank Song
ccba2f5c01 feat: harden clarify dialog flow and refresh recovery 2026-04-15 13:10:50 +08:00
Hermes Agent
7b9f08c774 fix: suppress N/A source_tag in session list sidebar (#429)
- sessions.js _formatSourceTag(): return null for unrecognised tags
  instead of raw string — prevents legacy 'N/A' values from surfacing
- sessions.js metaBits push: guarded with _stLabel null check so only
  known platform labels appear in the session metadata line
- sessions.js [SYSTEM:] title fallback: drop raw s.source_tag middle
  term, fall back directly to 'Gateway' for unknown sources

7 new tests in test_issue429.py.
1 updated test in test_sprint40_ui_polish.py (new guarded push pattern).

Closes #429
2026-04-14 22:14:31 +00:00
Hermes Agent
3c5ca2db62 fix(sessions): replace [SYSTEM: titles with platform name for gateway sessions (fixes #441) 2026-04-14 20:56:52 +00:00
Hermes Agent
c7381ee3f1 fix(ui): context indicator prefers latest usage over stale session data (fixes #437) 2026-04-14 20:56:50 +00:00
Nathan Esquenazi
85d8aad0ae fix(ux): mute Telegram badge color and format source tag as display name (fixes #442) 2026-04-14 19:06:33 +00:00
Hermes Agent
2400e14a31 fix(sidebar): hide session timestamps entirely to give titles full width 2026-04-14 19:04:49 +00:00
Nathan Esquenazi
69b0a905a4 fix(sidebar): move session timestamp below title to prevent truncation 2026-04-14 19:04:49 +00:00
nesquena-hermes
d3fea34c41
fix: correct tool call card rendering on session load after context compaction (#408)
* fix: correct tool call card rendering on session load

Two bugs caused duplicate/incorrect tool call cards when loading
sessions (especially after context compaction):

1. loadSession() sanitized messages (B9 filter) but did NOT update
   the session-level tool_calls array's assistant_msg_idx references.
   Since compact() returns only sanitized messages and recomputes
   tool_calls with indices into the compacted array, the original
   assistant_msg_idx values became stale/misaligned.

2. loadSession() then assigned the broken session-level tool_calls
   directly to S.toolCalls. This prevented renderMessages()'s fallback
   path (which derives tool_calls from per-message tool_calls using
   correct sanitized-array indices) from ever running.

Fix:
- Keep full sanitization loop with index remapping for session-level
  tool_calls (in case they're needed by other code paths).
- Instead of assigning broken session-level tool_calls to S.toolCalls,
  set S.toolCalls=[] so renderMessages() uses the fallback derivation
  from per-message tool_calls, which already have correct indices.

* test: add 8 regression tests for issue #401 tool call index remapping

* docs: v0.50.29 release — version badge and CHANGELOG

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 22:41:31 -07:00
nesquena-hermes
1345ccccee
feat: relative time labels in session sidebar (#406)
* feat: add relative time to session sidebar

(cherry picked from commit 272be9787fdff75d3da2dbc73175820477a3390e)

* fix: address session sidebar relative-time review feedback

* docs: v0.50.27 release — version badge and CHANGELOG

---------

Co-authored-by: Jordan SkyLF <jordan@skylinkfiber.net>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 22:26:05 -07:00
nesquena-hermes
9542639a90
fix: live reasoning, tool progress, in-flight session recovery (#367)
* fix: preserve live session output across chat switches

(cherry picked from commit 401e3b643d25e8dad8c06883b478b3c3073f07a5)

* fix: preserve todo state after session reload

(cherry picked from commit 7ee093ba19978af23b79148df2f2347e2f1e5bde)

* fix: preserve live assistant anchor across rerenders

* fix: stream live reasoning and tool progress

* fix: recover inflight session state after reload

* fix: add loadInflightState stub + CHANGELOG v0.50.21

- static/ui.js: add loadInflightState() function (currently returns null —
  the typeof guard in sessions.js means reload recovery works via the
  else-path attachLiveStream call; this stub satisfies the guard cleanly
  and documents the extension point for future localStorage-backed state)
- CHANGELOG.md: v0.50.21 entry; 960 tests (up from 949)

---------

Co-authored-by: Jordan SkyLF <jordan@skylinkfiber.net>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 16:18:15 -07:00
nesquena-hermes
ede1a5fc50
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 11:55:40 -07:00
nesquena-hermes
711bb5a6c9
feat: real-time gateway session sync (Phase 1) (#274)
* feat: add real-time gateway session sync (Phase 1)

- Add gateway_watcher.py: background daemon polling state.db every 5s
  for gateway session changes (telegram, discord, slack, etc.)
- Extend get_cli_sessions() to include all non-webui sources
- Add SSE endpoint /api/sessions/gateway/stream for real-time push
- Add dynamic source badges (telegram=blue, discord=purple, slack=dark purple)
- Rename 'Show CLI sessions' to 'Show agent sessions'
- Wire watcher lifecycle into server start/stop
- 10 tests covering metadata, filtering, SSE, and watcher lifecycle
- Activated via the same checkbox as CLI session import

Addresses GitHub issue #272

* fix: SSE event name mismatch, TLS attribute, remove PLAN.md

- Fix critical SSE bug: frontend listened for 'gateway_session_update'
  but backend sends 'sessions_changed' -- events were silently dropped
- Fix frontend field check: data.changed -> data.sessions (matches
  the actual payload structure from gateway_watcher)
- Fix TLS: ssl.TLSv1_2 -> ssl.TLSVersion.TLSv1_2 (the bare attribute
  does not exist, would crash TLS setup and silently fall back to HTTP)
- Remove PLAN.md: implementation plan should not be committed to repo

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: test isolation and slow-consumer sentinel in gateway sync

tests/test_gateway_sync.py:
- Fix _get_test_state_dir() path mismatch: the function was computing
  HERMES_HOME/webui-mvp-test but conftest.py sets HERMES_HOME=TEST_STATE_DIR,
  so state.db was written to a double-nested path the server never read.
  Now uses HERMES_WEBUI_STATE_DIR first (which conftest sets directly to
  TEST_STATE_DIR), fixing the 7/10 test failures in full-suite ordering.
- Fix conn cleanup: removed conn.close() from inside try blocks so the
  connection stays valid for _remove_test_sessions() in the finally block.
  Previously the closed conn caused ProgrammingError in finally (swallowed
  by bare except), leaving ghost sessions in state.db on test failure.

api/gateway_watcher.py:
- Fix slow-consumer queue eviction: when a subscriber queue fills (>10 events)
  and is removed from _subscribers, now puts a None sentinel into it so the
  SSE handler unblocks and closes the connection, letting EventSource
  auto-reconnect. Without this the connection stayed open but received no
  further events.

* fix: test isolation — set HERMES_WEBUI_TEST_STATE_DIR in conftest

The gateway sync tests write directly to state.db and must use the same
path the test server reads from.  Previously they computed the path
independently, which broke when test_auth_sessions.py set a different
HERMES_WEBUI_STATE_DIR in the test-process environment at import time.

tests/conftest.py:
- Set HERMES_WEBUI_TEST_STATE_DIR=TEST_STATE_DIR in the test process's
  os.environ (via setdefault) so gateway tests can read it reliably.
  Using setdefault preserves any explicit override the caller may pass.

tests/test_gateway_sync.py:
- Simplify _get_test_state_dir(): check HERMES_WEBUI_TEST_STATE_DIR first
  (now reliably set by conftest), fall back to HERMES_HOME/webui-mvp-test.
  Remove the workaround that tried to snapshot HERMES_HOME at import time.

Result: 658/658 tests pass in full-suite ordering (was 651 pass / 7 fail).

---------

Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 20:53:12 -07:00
nesquena-hermes
b86ace6ce3
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 12:19:12 -07:00
TaraTheStar
e8a8fceb26 feat: make bot name configurable 2026-04-06 05:14:31 +00:00
Nathan Esquenazi
a2243f4c4f fix: remove unused ordered variable, add hoisting note
Agent review feedback: ordered array was constructed but never iterated
(the new code uses groups[] instead). Removed the dead variable.
Added comment noting function hoisting for _renderOneSession.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 14:24:04 -07:00
Nathan Esquenazi
42590fceb3 feat: collapsible date groups in session sidebar
Date group headers (Pinned, Today, Yesterday, Earlier) are now clickable
to collapse/expand their session lists. Collapsed state persists to
localStorage across page reloads.

- Refactored renderSessionListFromCache to group sessions first, then
  render groups with collapsible wrappers
- Extracted _renderOneSession() helper for reuse within group bodies
- Chevron indicator rotates -90deg when collapsed
- Pinned group header keeps its gold color

Inspired by PR #75 (@MartinNielsenDev).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 14:05:00 -07:00
nesquena-hermes
15fde033c3
fix: wire up CLI session display in sidebar (3 frontend gaps) (#58)
The backend CLI session bridge (PR #56) was complete but the frontend
never connected to it:

1. css class never applied -- el.className never included 'cli-session'
   so the gold border and 'cli' badge CSS was dead code. Fixed: append
   ' cli-session' when s.is_cli_session is true.

2. import never triggered -- click handler always called loadSession()
   directly, never POST /api/session/import_cli. Fixed: for CLI sessions,
   call import_cli first (idempotent -- safe to call on every click),
   then fall through to loadSession() which now finds the imported copy.

3. profile filter silently hid CLI sessions -- filter required
   s.profile === S.activeProfile, but CLI sessions may have profile=null
   if the SQLite DB has no profile column. Fixed: CLI sessions always
   pass the filter (s.is_cli_session || s.profile === S.activeProfile).

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-03 20:55:29 -07:00
Nathan Esquenazi
da43a6a09a fix: switching profiles mid-conversation starts a new session instead of cross-tagging
A session with messages belongs to the profile it was created under. Switching
profiles while a conversation is in progress should not retag that session or
update its workspace/model in place — that would corrupt the session's context.

New behavior:
- Session has NO messages (empty): profile switch updates it in place (model,
  workspace). Works exactly as before — nothing was started yet.
- Session HAS messages (in progress): profile switch calls newSession() to
  start a fresh session tagged to the new profile. The old session is left
  untouched. Toast: 'Switched to profile: X — new conversation started'.
- Agent busy: blocked as before, no change.

Also: S._profileDefaultWorkspace is now consumed (set to null) inside
newSession() after the first use, so it doesn't keep forcing the same
workspace on every subsequent new session after a switch.
2026-04-03 20:27:50 +00:00
Nathan Esquenazi
f75e17c912 fix: legacy sessions (profile=null) leak into all profiles' session lists
Root cause: sessions created before Sprint 22 have no profile tag (profile=None).
The client filter was '!s.profile || s.profile === S.activeProfile' -- the
'!s.profile' guard made ALL 33 legacy sessions visible under every profile,
so switching to Camanji still showed the entire default session history.

Fix:
- api/models.py all_sessions(): backfill profile='default' on sessions with
  no profile tag before returning. This is in-memory only (no disk writes) --
  legacy sessions just get attributed to the default profile at read time.
  Applied to both the index-path and the full-scan fallback path.
- static/sessions.js: tighten the client filter to s.profile === S.activeProfile
  (remove the '!s.profile' escape hatch -- now redundant since server fills it).
  Every session now has an explicit profile, so the filter is precise.

Result: switching to Camanji shows only Camanji sessions. Default profile shows
legacy + default-tagged sessions. 'All profiles' toggle still shows everything.
S.activeProfile defaults to 'default' in the S object so first render is safe.

Tests: 426 passed, 0 failed.
2026-04-03 19:50:08 +00:00
Nathan Esquenazi
d4ab01c152 fix: workspace updates on profile switch; remove redundant topbar workspace chip
Two changes:

1. Workspace updates correctly on profile switch
   switchToProfile() now applies data.default_workspace from the switch
   response to the current session via /api/session/update, updates
   S.session.workspace in-memory, and stores S._profileDefaultWorkspace
   so the next new session also inherits the profile's workspace.
   newSession() in sessions.js picks up S._profileDefaultWorkspace when
   creating a new session after a profile switch.

2. Workspace chip removed from topbar
   The workspace was shown in two places: the topbar chip (wsChip) AND
   the sidebar bottom display (sidebarWsDisplay with name + full path).
   The topbar chip was redundant, cluttered the topbar, and pushed other
   chips (profile, model, clear, settings) off screen.
   Removed wsChip from the topbar entirely. The sidebar display is now
   the sole workspace UI, consistent and unambiguous.
   Moved wsDropdown to live inside the sidebar position:relative wrapper
   so it opens downward from sidebarWsDisplay. Updated the click-outside
   listener to close on clicks outside sidebarWsDisplay/wsDropdown.
   Removed stale wsChip update code from syncTopbar() in ui.js.

Tests: 426 passed, 0 failed.
2026-04-03 19:38:33 +00:00
Nathan Esquenazi
3520fa5643 feat: Sprint 23 -- profile/workspace/model coherence
Fix five coherence bugs in profile switching:
1. Model picker ignored profile default (localStorage stale key)
2. Workspace list was global (not profile-scoped)
3. DEFAULT_WORKSPACE was a boot-time singleton
4. Session list showed all profiles (no filtering)
5. switchToProfile() didn't refresh workspaces or sessions

Backend: workspace storage is now profile-local for named profiles,
switch_profile() returns default_model and default_workspace.
Frontend: switchToProfile() clears stale model pref, refreshes
workspace list and session list, sessions.js filters by active profile
with 'Show N from other profiles' toggle.

8 new tests. 400 pass / 23 fail (identical to baseline).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 11:46:15 -07:00
Nathan Esquenazi
d278563e00 feat: Sprint 21 — mobile responsive layout + Docker support
Mobile responsive (Issue #21):
- Hamburger sidebar: slide-in overlay on mobile (<640px) with backdrop.
  Tap hamburger in topbar to open, tap outside to close. Full session
  list, project chips, all panel content accessible.
- Bottom navigation bar: 5-tab fixed bar (Chat, Tasks, Skills, Memory,
  Spaces) replaces sidebar nav tabs on mobile. iOS-style layout.
  Tapping a tab opens the sidebar overlay with that panel active.
- Right panel slide-over: Files button in topbar chips opens workspace
  panel as a slide-over from the right on mobile/tablet.
- Touch targets: all interactive elements get min 44x44px touch areas.
  Session items, approval buttons, composer buttons all sized for fingers.
- Composer positioned above bottom nav bar with proper spacing.
- Sidebar nav tabs and bottom section hidden on mobile (replaced by
  bottom nav + topbar chips).
- Clicking a session auto-closes the sidebar overlay.
- Desktop layout completely unchanged — all mobile elements are
  display:none by default, only shown inside @media(max-width:640px).

Docker (Issue #7):
- Dockerfile: python:3.12-slim, HERMES_WEBUI_HOST=0.0.0.0, port 8787.
- docker-compose.yml: named volume for state persistence, optional
  ~/.hermes mount for agent features, password env var documented.
- README: Docker quick start section with compose and manual commands.

Tests: 392 passed, 23 pre-existing failures, 0 regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:09:36 -07:00
Hermes
0be7ccde4c 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 &lt;strong&gt; 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
Nathan Esquenazi
d2bcd2b2f7 feat: Sprint 16 — session sidebar visual polish
- Action buttons overlay: wrap pin/move/archive/dup/trash in a
  .session-actions container with position:absolute. Titles now use
  full available width. Actions appear on hover with gradient fade
  from the right edge. Overlay auto-hides during inline rename.

- SVG line icons: replace all emoji HTML entities with monochrome
  SVGs that inherit currentColor. Consistent across all platforms.

- Pin indicator: small gold star rendered inline only when pinned.
  Unpinned sessions get full title width (zero space reservation).

- Project border: sessions assigned to a project show a colored
  left border matching the project color, replacing the old
  always-visible blue folder button.

Fixes both BUGS.md items (title truncation + sticky folder button).
Tests: 214 passed, 23 pre-existing failures, 0 regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 11:49:24 -07:00
Nathan Esquenazi
e59eb8bb5b fix: project picker clipped, full-screen width bug, New Project shortcut
Five fixes to the Sprint 15 Move to Project picker:

1. CRITICAL: Picker was invisible (overflow:hidden clipping)
   Appended to document.body + positioned with fixed/getBoundingClientRect
   instead of inside .session-item (overflow:hidden). Flips above button
   when near bottom of viewport.

2. CRITICAL: Picker stretched full screen width
   position:fixed removed the containing block width constraint. Added
   max-width:220px; width:max-content to .project-picker.

3. UX: No way to create a project from the picker
   Added '+ New project': creates project and moves session in one click.

4. UX: Feature was undiscoverable
   Folder button shows persistently (blue, 60% opacity) when session
   has a project.

5. Minor: Event listener leak
   removeEventListener was missing from picker item onclick handlers.

Tests: 237 passed (7 pre-existing failures from unrelated logger bug).
2026-04-02 18:18:20 +00:00
Nathan Esquenazi
2f281cbbd7 fix: project picker clipping, create-from-picker, button visibility, listener leak
- Picker dropdown: append to document.body with fixed positioning instead
  of inside the session-item (which has overflow:hidden). Flips above
  when near bottom of viewport.
- Add "+ New project" item at bottom of picker so users can create a
  project and assign in one flow.
- Folder button stays visible (blue, 60% opacity) when session belongs
  to a project, instead of only appearing on hover.
- Clean up document click listener in all picker item onclick handlers
  to prevent stale listener accumulation.

Tests: 214 passed, 23 pre-existing failures, 0 regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 01:12:14 -07:00
Nathan Esquenazi
1a4793848e feat: Sprint 15 — session projects, code copy button, tool card toggle
Session projects: named groups for organizing sessions. Project filter
bar with chips between search and session list. Create/rename/delete
projects, assign sessions via folder icon dropdown. Stored in
projects.json, project_id on Session model. 5 new API endpoints.

Code block copy button: every code block gets a Copy button in the
language header (or top-right for plain blocks). Clipboard API with
"Copied!" feedback.

Tool card expand/collapse: messages with 2+ tool cards get an
"Expand all / Collapse all" toggle above the card group.

13 new tests (237 total), all passing. No regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 00:11:49 -07:00