Merge pull request #20 from nesquena/sprint-16-sidebar-polish

Sprint 16: Session sidebar visual polish — overlay actions, SVG icons, project borders
This commit is contained in:
Nathan Esquenazi
2026-04-02 11:50:28 -07:00
committed by GitHub
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.
> 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
> 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)
**Claude parity impact:** Very High (projects are a core Claude concept)
### Candidates for next sprints
- Workspace reorder (drag-and-drop)
- View skill linked files
### Candidates for later sprints
- Artifacts + code execution (HTML/SVG preview, inline Python execution)
- Voice input via Whisper
- 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 --
code runs inline, HTML renders in a sandboxed iframe, SVGs show as images.
This is the largest single capability gap between what we have and what Claude
feels like. It also directly enables the Hermes "code execution cell" feature
(Jupyter-style in-browser execution).
**Why now:** The session sidebar had two visible UX bugs: titles truncated
unnecessarily because action icons reserved space even when hidden, and
the project folder icon felt "sticky" and awkward. Emoji icons rendered
inconsistently across platforms. These were the most common visual complaints.
### Track A: Bugs
- Prism.js autoloader makes one CDN request per language encountered. On a
code-heavy session this causes noticeable latency. Bundle the top 10 languages
(Python, JS, bash, JSON, SQL, YAML, TypeScript, CSS, HTML, Rust) locally.
- Code blocks in long responses sometimes re-highlight on every renderMessages()
call. Debounce highlightCode() with requestAnimationFrame.
### Track A: Bugs (from BUGS.md)
- **Session title truncation.** Action icons (pin, move, archive, dup, trash)
were always in the DOM with `flex-shrink:0`, reserving ~30px even when
invisible. Fix: wrapped all actions 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 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
- **Artifact panel:** When Hermes produces a code block tagged as `html`, `svg`,
or `react`, a "Preview" button appears on that code block. Clicking it opens
a sandboxed `<iframe>` in the right panel showing the rendered output. The
preview updates live if Hermes edits the artifact in a follow-up.
- **Code execution cell:** A "Run" button on Python code blocks. Sends the code
to a new server endpoint (`POST /api/execute`) which runs it in a subprocess
with a 30-second timeout and streams stdout/stderr back as SSE. Output appears
below the code block inline. This is the Jupyter cell experience without
needing a kernel.
- **Mermaid diagram rendering:** Mermaid.js CDN (deferred). Code blocks tagged
as `mermaid` render as flow/sequence/gantt diagrams inline.
- **SVG action icons.** Replaced all emoji HTML entities (★, 📂, 📦, ⊕, 🗑)
with monochrome SVG line icons that inherit `currentColor`. Consistent
rendering across macOS, Linux, and Windows. Icons: pin (star), folder,
archive (box), duplicate (overlapping squares), trash (bin with lines).
- **Pin indicator.** Small gold filled-star icon rendered inline before the
title only when the session is actually pinned. Unpinned sessions get
full title width with zero space reservation.
- **Project border indicator.** Sessions assigned to a project show a
colored left border matching the project color, replacing the old
always-visible blue folder button.
- **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
- Sandbox safety: `/api/execute` runs in a restricted subprocess (no network,
limited filesystem via a temp directory). Returns exit code, stdout, stderr,
and execution time.
- Artifact state: artifacts are tracked in `S.artifacts = {}` (code block hash
-> rendered content). Persisted in session JSON as `artifacts` array.
### Deferred to Sprint 17
- Slash commands (basic set with `commands.js` module)
- Thinking/reasoning display for extended-thinking models
- Slash command autocomplete popup
**Tests:** ~18 new. Total: ~259.
**Hermes CLI parity impact:** High (code execution closes the Jupyter gap)
**Claude parity impact:** Very High (artifacts are Claude's signature feature)
**Tests:** 0 new (pure CSS/DOM changes). Total: 237.
**Hermes CLI parity impact:** Low
**Claude parity impact:** Medium (sidebar polish matches Claude's quality bar)
---
@@ -403,7 +404,7 @@ address.
| Settings persistence | Done (Sprint 12) |
| Subagent visibility | Sprint 18 |
| Background task monitor | Sprint 18 |
| Code execution (Jupyter) | Sprint 16 |
| Code execution (Jupyter) | Sprint 17+ |
| Cron completion alerts | Done (Sprint 13) |
| Virtual scroll (perf) | Deferred |
@@ -419,12 +420,12 @@ address.
| Tool use visibility | Done (v0.11) |
| Edit/regenerate messages | Done (v0.10) |
| Session management | Done (v0.6) |
| Artifacts (HTML/SVG preview) | Sprint 16 |
| Code execution inline | Sprint 16 |
| Artifacts (HTML/SVG preview) | Sprint 17+ |
| Code execution inline | Sprint 17+ |
| Mermaid diagrams | Done (Sprint 14) |
| Projects / folders | Done (Sprint 15) |
| Pinned/starred sessions | Done (Sprint 12) |
| Reasoning display | Sprint 18 |
| Reasoning display | Sprint 16 |
| Voice input | Sprint 17 |
| TTS playback | Sprint 17 |
| Notifications | Done (Sprint 13) |
@@ -448,6 +449,6 @@ address.
---
*Last updated: April 1, 2026*
*Current version: v0.17 | 237 tests*
*Next sprint: Sprint 16 (Artifacts + Code Execution)*
*Last updated: April 2, 2026*
*Current version: v0.18 | 237 tests*
*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){
MSG_QUEUE.length=0;updateQueueBadge();
S.toolCalls=[];
@@ -242,11 +253,35 @@ function renderSessionListFromCache(){
setTimeout(()=>{inp.focus();inp.select();},10);
};
const pin=document.createElement('span');
pin.className='session-pin'+(s.pinned?' pinned':'');
pin.innerHTML=s.pinned?'&#9733;':'&#9734;';
pin.title=s.pinned?'Unpin':'Pin to top';
pin.onclick=async(e)=>{
// Pin indicator (inline, only when pinned — no space reserved otherwise)
if(s.pinned){
const pinInd=document.createElement('span');
pinInd.className='session-pin-indicator';
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();
const newPinned=!s.pinned;
try{
@@ -256,8 +291,15 @@ function renderSessionListFromCache(){
renderSessionList();
}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');
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.onclick=async(e)=>{
e.stopPropagation();e.preventDefault();
@@ -269,8 +311,10 @@ function renderSessionListFromCache(){
showToast(s.archived?'Session archived':'Session restored');
}catch(err){showToast('Archive failed: '+err.message);}
};
actions.appendChild(archive);
// Duplicate
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)=>{
e.stopPropagation();e.preventDefault();
try{
@@ -282,26 +326,13 @@ function renderSessionListFromCache(){
}
}catch(err){showToast('Duplicate failed: '+err.message);}
};
actions.appendChild(dup);
// Trash
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);};
// Project move button (folder icon)
const move=document.createElement('button');
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);
actions.appendChild(trash);
el.appendChild(actions);
// 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,
@@ -309,7 +340,7 @@ function renderSessionListFromCache(){
let _clickTimer=null;
el.onclick=async(e)=>{
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);
_clickTimer=setTimeout(async()=>{
_clickTimer=null;

View File

@@ -20,13 +20,21 @@
.session-search input::placeholder{color:var(--muted);opacity:.7;}
/* 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-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.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-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-item:hover .session-trash{opacity:1;}
.session-trash:hover{color:var(--accent)!important;}
/* ── Session action button overlay ── */
.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-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);}}
.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;}
@@ -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:hover{color:var(--text);background:rgba(255,255,255,.08);}
/* ── Session pin star ── */
.session-pin{font-size:12px;cursor:pointer;opacity:0;transition:opacity .15s;padding:2px 4px;flex-shrink:0;}
.session-item:hover .session-pin,.session-pin.pinned{opacity:1;}
.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;}
/* ── Session pin indicator (inline, only when pinned) ── */
.session-pin-indicator{flex-shrink:0;color:#f5c542;line-height:1;display:flex;align-items:center;}
.session-pin-indicator svg{width:10px;height:10px;}
/* ── 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;}