Compare commits

...

6 Commits

Author SHA1 Message Date
Nathan Esquenazi
fd993c5513 Merge pull request #20 from nesquena/sprint-16-sidebar-polish
Sprint 16: Session sidebar visual polish — overlay actions, SVG icons, project borders
2026-04-02 11:50:28 -07: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
5e4645ee05 Merge pull request #25 from nesquena/fix/project-picker-overflow
fix: project picker clipped and width issues
2026-04-02 11:46:56 -07:00
Nathan Esquenazi
ae1faa7252 Merge pull request #24 from nesquena/fix/model-discovery-logger-crash
fix: NameError on logger in custom endpoint model discovery
2026-04-02 11:46:48 -07:00
Nathan Esquenazi
6c7fb4ee44 fix: NameError on logger in custom endpoint model discovery
get_available_models() references 'logger' in the except block of
the custom endpoint fetch (added in PR #18), but 'logger' is never
imported or defined in api/config.py. When the custom endpoint is
unreachable (the normal case -- most users don't have a local LLM),
the except handler raises NameError: name 'logger' is not defined,
which propagates as a 500 on every GET /api/models request.

This broke 7 test_sprint11 tests and caused the model dropdown to
fail for all users regardless of whether they have a custom endpoint.

Fix: replace logger.debug() with a silent pass -- the exception is
expected when no local LLM is configured and needs no logging.

Tests: 237 passed, 0 failed.
2026-04-02 11:36:01 -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
6 changed files with 150 additions and 94 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

@@ -510,8 +510,8 @@ def get_available_models() -> dict:
if model_id and model_name:
auto_detected_models.append({'id': model_id, 'label': model_name})
detected_providers.add(provider.lower())
except Exception as e:
logger.debug(f"Failed to fetch models from custom endpoint: {e}")
except Exception:
pass # custom endpoint unreachable or misconfigured -- fail silently
# 5. Build model groups
if detected_providers:

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,25 +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,
@@ -308,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;
@@ -360,15 +392,13 @@ function _showProjectPicker(session, anchorEl){
document.querySelectorAll('.project-picker').forEach(p=>p.remove());
const picker=document.createElement('div');
picker.className='project-picker';
// Close on outside click
const close=(e)=>{if(!picker.contains(e.target)&&e.target!==anchorEl){picker.remove();document.removeEventListener('click',close);}};
// "No project" option
const none=document.createElement('div');
none.className='project-picker-item'+(!session.project_id?' active':'');
none.textContent='No project';
none.onclick=async()=>{
document.removeEventListener('click',close);
picker.remove();
document.removeEventListener('click',close);
await api('/api/session/move',{method:'POST',body:JSON.stringify({session_id:session.session_id,project_id:null})});
session.project_id=null;
renderSessionListFromCache();
@@ -389,8 +419,8 @@ function _showProjectPicker(session, anchorEl){
name.textContent=p.name;
item.appendChild(name);
item.onclick=async()=>{
document.removeEventListener('click',close);
picker.remove();
document.removeEventListener('click',close);
await api('/api/session/move',{method:'POST',body:JSON.stringify({session_id:session.session_id,project_id:p.project_id})});
session.project_id=p.project_id;
renderSessionListFromCache();
@@ -398,19 +428,21 @@ function _showProjectPicker(session, anchorEl){
};
picker.appendChild(item);
}
// "+ New project" item
// "+ New project" shortcut at the bottom
const createItem=document.createElement('div');
createItem.className='project-picker-item project-picker-create';
createItem.textContent='+ New project';
createItem.onclick=async()=>{
picker.remove();
document.removeEventListener('click',close);
// Prompt for name inline
const name=prompt('Project name:');
if(!name||!name.trim()) return;
const color=PROJECT_COLORS[_allProjects.length%PROJECT_COLORS.length];
const res=await api('/api/projects/create',{method:'POST',body:JSON.stringify({name:name.trim(),color})});
if(res.project){
_allProjects.push(res.project);
// Now move session into it
await api('/api/session/move',{method:'POST',body:JSON.stringify({session_id:session.session_id,project_id:res.project.project_id})});
session.project_id=res.project.project_id;
await renderSessionList();
@@ -418,11 +450,13 @@ function _showProjectPicker(session, anchorEl){
}
};
picker.appendChild(createItem);
// Position picker on document.body to avoid overflow:hidden clipping
// Append to body and position using getBoundingClientRect so it isn't clipped
// by overflow:hidden on .session-item ancestors
document.body.appendChild(picker);
const rect=anchorEl.getBoundingClientRect();
picker.style.position='fixed';
picker.style.zIndex='999';
// Prefer opening below; flip above if too close to bottom of viewport
const spaceBelow=window.innerHeight-rect.bottom;
if(spaceBelow<160&&rect.top>160){
picker.style.bottom=(window.innerHeight-rect.top+4)+'px';
@@ -431,10 +465,13 @@ function _showProjectPicker(session, anchorEl){
picker.style.top=(rect.bottom+4)+'px';
picker.style.bottom='auto';
}
const pickerW=160;
// Align right edge of picker with right edge of button; keep within viewport
const pickerW=Math.min(220,Math.max(160,picker.scrollWidth||160));
let left=rect.right-pickerW;
if(left<8) left=8;
picker.style.left=left+'px';
// Close on outside click
const close=(e)=>{if(!picker.contains(e.target)&&e.target!==anchorEl){picker.remove();document.removeEventListener('click',close);}};
setTimeout(()=>document.addEventListener('click',close),0);
}

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;}
@@ -540,7 +540,7 @@ body.resizing{user-select:none;cursor:col-resize;}
.project-create-btn{font-size:10px;padding:3px 6px;border-radius:12px;cursor:pointer;border:1px dashed var(--border2);background:none;color:var(--muted);opacity:.6;transition:all .15s;}
.project-create-btn:hover{opacity:1;border-color:var(--blue);color:var(--blue);}
.project-create-input{font-size:10px;padding:3px 8px;border-radius:12px;border:1px solid rgba(124,185,255,.6);background:rgba(20,32,60,.9);color:var(--text);outline:none;width:100px;font-family:inherit;box-shadow:0 0 0 2px rgba(124,185,255,.15);}
.project-picker{position:absolute;right:0;top:100%;background:var(--sidebar);border:1px solid var(--border2);border-radius:8px;padding:4px;z-index:30;min-width:140px;max-width:220px;width:max-content;box-shadow:0 4px 16px rgba(0,0,0,.3);}
.project-picker{position:absolute;right:0;top:100%;background:var(--sidebar);border:1px solid var(--border2);border-radius:8px;padding:4px;z-index:30;min-width:160px;max-width:220px;width:max-content;box-shadow:0 4px 16px rgba(0,0,0,.3);}
.project-picker-item{padding:5px 10px;font-size:11px;border-radius:6px;cursor:pointer;color:var(--muted);transition:all .1s;display:flex;align-items:center;gap:6px;}
.project-picker-item:hover{background:rgba(255,255,255,.08);color:var(--text);}
.project-picker-item.active{color:var(--blue);}