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>
This commit is contained in:
Nathan Esquenazi
2026-04-02 11:08:34 -07:00
parent 5e4645ee05
commit d2bcd2b2f7
5 changed files with 135 additions and 85 deletions

18
BUGS.md Normal file
View File

@@ -0,0 +1,18 @@
# Bugs Backlog
This file tracks UI bugs and polish items to address in a future sprint.
## ~~Conversation list title truncation / hover actions~~ — Fixed (Sprint 16)
- **Was:** Action icons reserved ~30px of space even when invisible, truncating titles.
- **Fix:** Wrapped all action buttons in a `.session-actions` overlay container with `position:absolute`. Titles now use full available width. Actions appear on hover with a gradient fade from the right edge.
## ~~Folder/project assignment interaction feels sticky~~ — Fixed (Sprint 16)
- **Was:** Folder icon stayed permanently visible (blue, 60% opacity) when a session belonged to a project.
- **Fix:** Replaced `.has-project` persistent button with a colored left border matching the project color. The folder button now only appears in the hover overlay like all other actions.
## Notes
- Both issues resolved in Sprint 16 (Session Sidebar Visual Polish).
- Icons replaced from inconsistent emoji HTML entities to monochrome SVG line icons.

View File

@@ -3,7 +3,7 @@
> Goal: Full 1:1 parity with the Hermes CLI experience via a clean dark web UI. > Goal: Full 1:1 parity with the Hermes CLI experience via a clean dark web UI.
> Everything you can do from the CLI terminal, you can do from this UI. > Everything you can do from the CLI terminal, you can do from this UI.
> >
> Last updated: Sprint 15 (April 1, 2026) > Last updated: Sprint 15 / v0.17.1 (April 2, 2026)
> Tests: 237 passing > Tests: 237 passing
> Source: <repo>/ > Source: <repo>/

View File

@@ -217,54 +217,55 @@ organizational gap vs. Claude's project folders.
**Hermes CLI parity impact:** Low (CLI has no session organization) **Hermes CLI parity impact:** Low (CLI has no session organization)
**Claude parity impact:** Very High (projects are a core Claude concept) **Claude parity impact:** Very High (projects are a core Claude concept)
### Candidates for next sprints ### Candidates for later sprints
- Workspace reorder (drag-and-drop) - Artifacts + code execution (HTML/SVG preview, inline Python execution)
- View skill linked files
- Voice input via Whisper - Voice input via Whisper
- Subagent delegation cards (enhanced tool card rendering) - Subagent delegation cards (enhanced tool card rendering)
--- ---
## Sprint 16 -- Artifacts + Code Execution ## Sprint 16 -- Session Sidebar Visual Polish (COMPLETED)
**Theme:** See outputs, not just text. **Theme:** Make the session list feel high-quality and delightful.
**Why now:** Claude's most distinctive feature is the artifact panel -- **Why now:** The session sidebar had two visible UX bugs: titles truncated
code runs inline, HTML renders in a sandboxed iframe, SVGs show as images. unnecessarily because action icons reserved space even when hidden, and
This is the largest single capability gap between what we have and what Claude the project folder icon felt "sticky" and awkward. Emoji icons rendered
feels like. It also directly enables the Hermes "code execution cell" feature inconsistently across platforms. These were the most common visual complaints.
(Jupyter-style in-browser execution).
### Track A: Bugs ### Track A: Bugs (from BUGS.md)
- Prism.js autoloader makes one CDN request per language encountered. On a - **Session title truncation.** Action icons (pin, move, archive, dup, trash)
code-heavy session this causes noticeable latency. Bundle the top 10 languages were always in the DOM with `flex-shrink:0`, reserving ~30px even when
(Python, JS, bash, JSON, SQL, YAML, TypeScript, CSS, HTML, Rust) locally. invisible. Fix: wrapped all actions in a `.session-actions` overlay
- Code blocks in long responses sometimes re-highlight on every renderMessages() container with `position:absolute`. Titles now use full available width.
call. Debounce highlightCode() with requestAnimationFrame. Actions appear on hover with a gradient fade from the right edge.
- **Folder button feels sticky.** Replaced `.has-project` persistent blue
button with a colored left border matching the project color. The folder
button now only appears in the hover overlay like all other actions.
### Track B: Features ### Track B: Features
- **Artifact panel:** When Hermes produces a code block tagged as `html`, `svg`, - **SVG action icons.** Replaced all emoji HTML entities (★, 📂, 📦, ⊕, 🗑)
or `react`, a "Preview" button appears on that code block. Clicking it opens with monochrome SVG line icons that inherit `currentColor`. Consistent
a sandboxed `<iframe>` in the right panel showing the rendered output. The rendering across macOS, Linux, and Windows. Icons: pin (star), folder,
preview updates live if Hermes edits the artifact in a follow-up. archive (box), duplicate (overlapping squares), trash (bin with lines).
- **Code execution cell:** A "Run" button on Python code blocks. Sends the code - **Pin indicator.** Small gold filled-star icon rendered inline before the
to a new server endpoint (`POST /api/execute`) which runs it in a subprocess title only when the session is actually pinned. Unpinned sessions get
with a 30-second timeout and streams stdout/stderr back as SSE. Output appears full title width with zero space reservation.
below the code block inline. This is the Jupyter cell experience without - **Project border indicator.** Sessions assigned to a project show a
needing a kernel. colored left border matching the project color, replacing the old
- **Mermaid diagram rendering:** Mermaid.js CDN (deferred). Code blocks tagged always-visible blue folder button.
as `mermaid` render as flow/sequence/gantt diagrams inline. - **Hover overlay polish.** Actions container uses a gradient background
that fades from transparent to the sidebar color, creating a smooth
emergence effect. Overlay hides automatically during inline rename.
### Track C: Architecture ### Deferred to Sprint 17
- Sandbox safety: `/api/execute` runs in a restricted subprocess (no network, - Slash commands (basic set with `commands.js` module)
limited filesystem via a temp directory). Returns exit code, stdout, stderr, - Thinking/reasoning display for extended-thinking models
and execution time. - Slash command autocomplete popup
- Artifact state: artifacts are tracked in `S.artifacts = {}` (code block hash
-> rendered content). Persisted in session JSON as `artifacts` array.
**Tests:** ~18 new. Total: ~259. **Tests:** 0 new (pure CSS/DOM changes). Total: 237.
**Hermes CLI parity impact:** High (code execution closes the Jupyter gap) **Hermes CLI parity impact:** Low
**Claude parity impact:** Very High (artifacts are Claude's signature feature) **Claude parity impact:** Medium (sidebar polish matches Claude's quality bar)
--- ---
@@ -403,7 +404,7 @@ address.
| Settings persistence | Done (Sprint 12) | | Settings persistence | Done (Sprint 12) |
| Subagent visibility | Sprint 18 | | Subagent visibility | Sprint 18 |
| Background task monitor | Sprint 18 | | Background task monitor | Sprint 18 |
| Code execution (Jupyter) | Sprint 16 | | Code execution (Jupyter) | Sprint 17+ |
| Cron completion alerts | Done (Sprint 13) | | Cron completion alerts | Done (Sprint 13) |
| Virtual scroll (perf) | Deferred | | Virtual scroll (perf) | Deferred |
@@ -419,12 +420,12 @@ address.
| Tool use visibility | Done (v0.11) | | Tool use visibility | Done (v0.11) |
| Edit/regenerate messages | Done (v0.10) | | Edit/regenerate messages | Done (v0.10) |
| Session management | Done (v0.6) | | Session management | Done (v0.6) |
| Artifacts (HTML/SVG preview) | Sprint 16 | | Artifacts (HTML/SVG preview) | Sprint 17+ |
| Code execution inline | Sprint 16 | | Code execution inline | Sprint 17+ |
| Mermaid diagrams | Done (Sprint 14) | | Mermaid diagrams | Done (Sprint 14) |
| Projects / folders | Done (Sprint 15) | | Projects / folders | Done (Sprint 15) |
| Pinned/starred sessions | Done (Sprint 12) | | Pinned/starred sessions | Done (Sprint 12) |
| Reasoning display | Sprint 18 | | Reasoning display | Sprint 16 |
| Voice input | Sprint 17 | | Voice input | Sprint 17 |
| TTS playback | Sprint 17 | | TTS playback | Sprint 17 |
| Notifications | Done (Sprint 13) | | Notifications | Done (Sprint 13) |
@@ -448,6 +449,6 @@ address.
--- ---
*Last updated: April 1, 2026* *Last updated: April 2, 2026*
*Current version: v0.17 | 237 tests* *Current version: v0.18 | 237 tests*
*Next sprint: Sprint 16 (Artifacts + Code Execution)* *Next sprint: Sprint 17 (Slash Commands + Thinking Display)*

View File

@@ -1,3 +1,14 @@
// ── Session action icons (SVG, monochrome, inherit currentColor) ──
const ICONS={
pin:'<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" stroke="none"><polygon points="8,1.5 9.8,5.8 14.5,6.2 11,9.4 12,14 8,11.5 4,14 5,9.4 1.5,6.2 6.2,5.8"/></svg>',
unpin:'<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3"><polygon points="8,2 9.8,6.2 14.2,6.2 10.7,9.2 12,13.8 8,11 4,13.8 5.3,9.2 1.8,6.2 6.2,6.2"/></svg>',
folder:'<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3"><path d="M2 4.5h4l1.5 1.5H14v7H2z"/></svg>',
archive:'<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3"><rect x="1.5" y="2" width="13" height="3" rx="1"/><path d="M2.5 5v8h11V5"/><line x1="6" y1="8.5" x2="10" y2="8.5"/></svg>',
unarchive:'<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3"><rect x="1.5" y="2" width="13" height="3" rx="1"/><path d="M2.5 5v8h11V5"/><polyline points="6.5,7 8,5.5 9.5,7"/></svg>',
dup:'<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3"><rect x="4.5" y="4.5" width="8.5" height="8.5" rx="1.5"/><path d="M3 11.5V3h8.5"/></svg>',
trash:'<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3"><path d="M3.5 4.5h9M6.5 4.5V3h3v1.5M4.5 4.5v8.5h7v-8.5"/><line x1="7" y1="7" x2="7" y2="11"/><line x1="9" y1="7" x2="9" y2="11"/></svg>',
};
async function newSession(flash){ async function newSession(flash){
MSG_QUEUE.length=0;updateQueueBadge(); MSG_QUEUE.length=0;updateQueueBadge();
S.toolCalls=[]; S.toolCalls=[];
@@ -242,11 +253,35 @@ function renderSessionListFromCache(){
setTimeout(()=>{inp.focus();inp.select();},10); setTimeout(()=>{inp.focus();inp.select();},10);
}; };
const pin=document.createElement('span'); // Pin indicator (inline, only when pinned — no space reserved otherwise)
pin.className='session-pin'+(s.pinned?' pinned':''); if(s.pinned){
pin.innerHTML=s.pinned?'&#9733;':'&#9734;'; const pinInd=document.createElement('span');
pin.title=s.pinned?'Unpin':'Pin to top'; pinInd.className='session-pin-indicator';
pin.onclick=async(e)=>{ pinInd.innerHTML=ICONS.pin;
el.appendChild(pinInd);
}
// Project indicator: colored left border
if(s.project_id){
const proj=_allProjects.find(p=>p.project_id===s.project_id);
if(proj){
el.style.borderLeftColor=proj.color||'var(--blue)';
const dot=document.createElement('span');
dot.className='session-project-dot';
dot.style.background=proj.color||'var(--blue)';
dot.title=proj.name;
title.appendChild(dot);
}
}
el.appendChild(title);
// Action buttons overlay (appears on hover with gradient fade)
const actions=document.createElement('div');
actions.className='session-actions';
// Pin toggle
const pinBtn=document.createElement('button');
pinBtn.className='act-pin'+(s.pinned?' pinned':'');
pinBtn.innerHTML=s.pinned?ICONS.pin:ICONS.unpin;
pinBtn.title=s.pinned?'Unpin':'Pin to top';
pinBtn.onclick=async(e)=>{
e.stopPropagation();e.preventDefault(); e.stopPropagation();e.preventDefault();
const newPinned=!s.pinned; const newPinned=!s.pinned;
try{ try{
@@ -256,8 +291,15 @@ function renderSessionListFromCache(){
renderSessionList(); renderSessionList();
}catch(err){showToast('Pin failed: '+err.message);} }catch(err){showToast('Pin failed: '+err.message);}
}; };
actions.appendChild(pinBtn);
// Move to project
const move=document.createElement('button');
move.className='act-move';move.innerHTML=ICONS.folder;move.title='Move to project';
move.onclick=async(e)=>{e.stopPropagation();e.preventDefault();_showProjectPicker(s,move);};
actions.appendChild(move);
// Archive
const archive=document.createElement('button'); const archive=document.createElement('button');
archive.className='session-action-btn';archive.innerHTML=s.archived?'&#9993;':'&#128230;'; archive.className='act-archive';archive.innerHTML=s.archived?ICONS.unarchive:ICONS.archive;
archive.title=s.archived?'Unarchive':'Archive'; archive.title=s.archived?'Unarchive':'Archive';
archive.onclick=async(e)=>{ archive.onclick=async(e)=>{
e.stopPropagation();e.preventDefault(); e.stopPropagation();e.preventDefault();
@@ -269,8 +311,10 @@ function renderSessionListFromCache(){
showToast(s.archived?'Session archived':'Session restored'); showToast(s.archived?'Session archived':'Session restored');
}catch(err){showToast('Archive failed: '+err.message);} }catch(err){showToast('Archive failed: '+err.message);}
}; };
actions.appendChild(archive);
// Duplicate
const dup=document.createElement('button'); const dup=document.createElement('button');
dup.className='session-dup';dup.innerHTML='&#10697;';dup.title='Duplicate'; dup.className='act-dup';dup.innerHTML=ICONS.dup;dup.title='Duplicate';
dup.onclick=async(e)=>{ dup.onclick=async(e)=>{
e.stopPropagation();e.preventDefault(); e.stopPropagation();e.preventDefault();
try{ try{
@@ -282,26 +326,13 @@ function renderSessionListFromCache(){
} }
}catch(err){showToast('Duplicate failed: '+err.message);} }catch(err){showToast('Duplicate failed: '+err.message);}
}; };
actions.appendChild(dup);
// Trash
const trash=document.createElement('button'); const trash=document.createElement('button');
trash.className='session-trash';trash.innerHTML='&#128465;';trash.title='Delete'; trash.className='act-trash';trash.innerHTML=ICONS.trash;trash.title='Delete';
trash.onclick=async(e)=>{e.stopPropagation();e.preventDefault();await deleteSession(s.session_id);}; trash.onclick=async(e)=>{e.stopPropagation();e.preventDefault();await deleteSession(s.session_id);};
// Project move button (folder icon) actions.appendChild(trash);
const move=document.createElement('button'); el.appendChild(actions);
move.className='session-action-btn'+(s.project_id?' has-project':'');
move.innerHTML='&#128194;';move.title='Move to project';
move.onclick=async(e)=>{e.stopPropagation();e.preventDefault();_showProjectPicker(s,move);};
// Project dot indicator
if(s.project_id){
const proj=_allProjects.find(p=>p.project_id===s.project_id);
if(proj){
const dot=document.createElement('span');
dot.className='session-project-dot';
dot.style.background=proj.color||'var(--blue)';
dot.title=proj.name;
title.appendChild(dot);
}
}
el.appendChild(pin);el.appendChild(title);el.appendChild(move);el.appendChild(archive);el.appendChild(dup);el.appendChild(trash);
// Use a click timer to distinguish single-click (navigate) from double-click (rename). // Use a click timer to distinguish single-click (navigate) from double-click (rename).
// This prevents loadSession from firing on the first click of a double-click, // This prevents loadSession from firing on the first click of a double-click,
@@ -309,7 +340,7 @@ function renderSessionListFromCache(){
let _clickTimer=null; let _clickTimer=null;
el.onclick=async(e)=>{ el.onclick=async(e)=>{
if(_renamingSid) return; // ignore while any rename is active if(_renamingSid) return; // ignore while any rename is active
if([trash,dup,archive,move].some(b=>e.target===b||b.contains(e.target))) return; if(actions.contains(e.target)) return;
clearTimeout(_clickTimer); clearTimeout(_clickTimer);
_clickTimer=setTimeout(async()=>{ _clickTimer=setTimeout(async()=>{
_clickTimer=null; _clickTimer=null;

View File

@@ -20,13 +20,21 @@
.session-search input::placeholder{color:var(--muted);opacity:.7;} .session-search input::placeholder{color:var(--muted);opacity:.7;}
/* Inline session title edit */ /* Inline session title edit */
.session-title-input{flex:1;background:rgba(20,32,60,.9);border:1px solid rgba(124,185,255,.6);border-radius:6px;color:var(--text);padding:3px 8px;font-size:13px;outline:none;min-width:0;box-shadow:0 0 0 2px rgba(124,185,255,.15);font-family:inherit;} .session-title-input{flex:1;background:rgba(20,32,60,.9);border:1px solid rgba(124,185,255,.6);border-radius:6px;color:var(--text);padding:3px 8px;font-size:13px;outline:none;min-width:0;box-shadow:0 0 0 2px rgba(124,185,255,.15);font-family:inherit;}
.session-item{padding:8px 10px 8px 8px;border-radius:8px;cursor:pointer;font-size:13px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:background .15s,color .15s,border-color .15s;display:flex;align-items:center;gap:6px;min-width:0;border-left:2px solid transparent;} .session-item{padding:8px 10px 8px 8px;border-radius:8px;cursor:pointer;font-size:13px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:background .15s,color .15s,border-color .15s;display:flex;align-items:center;gap:6px;min-width:0;border-left:2px solid transparent;position:relative;}
.session-item:hover{background:rgba(255,255,255,0.06);color:var(--text);} .session-item:hover{background:rgba(255,255,255,0.06);color:var(--text);}
.session-item.active{background:rgba(124,185,255,0.1);color:var(--blue);border-left:2px solid var(--blue);padding-left:8px;} .session-item.active{background:rgba(124,185,255,0.1);color:var(--blue);border-left:2px solid var(--blue);padding-left:8px;}
.session-title{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} .session-title{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.session-trash{flex-shrink:0;opacity:0;font-size:13px;color:var(--muted);background:none;border:none;cursor:pointer;padding:0 2px;line-height:1;transition:opacity .15s,color .15s;} /* ── Session action button overlay ── */
.session-item:hover .session-trash{opacity:1;} .session-actions{position:absolute;right:0;top:0;bottom:0;display:flex;align-items:center;gap:2px;padding:0 6px 0 16px;background:linear-gradient(to right,transparent,var(--sidebar) 12px);opacity:0;pointer-events:none;transition:opacity .15s ease;border-radius:0 8px 8px 0;}
.session-trash:hover{color:var(--accent)!important;} .session-item:hover .session-actions{opacity:1;pointer-events:auto;}
.session-item.active .session-actions{background:linear-gradient(to right,transparent,rgba(16,33,62,.95) 12px);}
.session-actions button{background:none;border:none;color:var(--muted);cursor:pointer;padding:2px 3px;line-height:1;transition:color .12s;display:flex;align-items:center;}
.session-actions button:hover{color:var(--text);}
.session-actions .act-trash:hover{color:var(--accent);}
.session-actions .act-pin.pinned{color:#f5c542;}
.session-actions .act-pin.pinned:hover{color:#d4a017;}
/* Hide overlay during inline rename */
.session-item:has(.session-title-input) .session-actions{display:none;}
@keyframes newflash{0%{background:rgba(124,185,255,0.22);color:var(--blue);}100%{background:transparent;color:var(--muted);}} @keyframes newflash{0%{background:rgba(124,185,255,0.22);color:var(--blue);}100%{background:transparent;color:var(--muted);}}
.session-item.new-flash{animation:newflash 1.4s ease-out forwards;} .session-item.new-flash{animation:newflash 1.4s ease-out forwards;}
.toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:rgba(20,30,50,.95);backdrop-filter:blur(12px);border:1px solid rgba(124,185,255,0.25);color:var(--text);font-size:13px;padding:10px 20px;border-radius:12px;pointer-events:none;opacity:0;transition:opacity .2s,transform .2s;z-index:100;box-shadow:0 4px 20px rgba(0,0,0,.3);letter-spacing:.01em;} .toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:rgba(20,30,50,.95);backdrop-filter:blur(12px);border:1px solid rgba(124,185,255,0.25);color:var(--text);font-size:13px;padding:10px 20px;border-radius:12px;pointer-events:none;opacity:0;transition:opacity .2s,transform .2s;z-index:100;box-shadow:0 4px 20px rgba(0,0,0,.3);letter-spacing:.01em;}
@@ -495,17 +503,9 @@ body.resizing{user-select:none;cursor:col-resize;}
.gear-btn{font-size:13px;cursor:pointer;transition:color .15s,background .15s;} .gear-btn{font-size:13px;cursor:pointer;transition:color .15s,background .15s;}
.gear-btn:hover{color:var(--text);background:rgba(255,255,255,.08);} .gear-btn:hover{color:var(--text);background:rgba(255,255,255,.08);}
/* ── Session pin star ── */ /* ── Session pin indicator (inline, only when pinned) ── */
.session-pin{font-size:12px;cursor:pointer;opacity:0;transition:opacity .15s;padding:2px 4px;flex-shrink:0;} .session-pin-indicator{flex-shrink:0;color:#f5c542;line-height:1;display:flex;align-items:center;}
.session-item:hover .session-pin,.session-pin.pinned{opacity:1;} .session-pin-indicator svg{width:10px;height:10px;}
.session-pin.pinned{color:#f5c542;}
/* ── Session duplicate button ── */
.session-dup,.session-action-btn{background:none;border:none;color:var(--muted);font-size:11px;cursor:pointer;opacity:0;transition:opacity .15s;padding:2px 4px;flex-shrink:0;}
.session-item:hover .session-dup,.session-item:hover .session-action-btn{opacity:1;}
.session-dup:hover,.session-action-btn:hover{color:var(--text);}
.session-action-btn.has-project{opacity:.6;color:var(--blue);}
.session-item:hover .session-action-btn.has-project{opacity:1;}
/* ── Cron alert badge ── */ /* ── Cron alert badge ── */
.cron-badge{position:absolute;top:2px;right:2px;background:#e53e3e;color:#fff;font-size:9px;font-weight:700;min-width:14px;height:14px;line-height:14px;text-align:center;border-radius:7px;padding:0 3px;} .cron-badge{position:absolute;top:2px;right:2px;background:#e53e3e;color:#fff;font-size:9px;font-weight:700;min-width:14px;height:14px;line-height:14px;text-align:center;border-radius:7px;padding:0 3px;}