Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codbash-app",
"version": "7.5.2",
"version": "7.5.3",
"description": "Dashboard + CLI for AI coding agents — Claude Code, Codex, Cursor, OpenCode, Kiro. View, search, resume, convert, sync sessions.",
"bin": {
"codbash": "./bin/cli.js",
Expand Down
10 changes: 10 additions & 0 deletions src/changelog.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
'use strict';

const CHANGELOG = [
{
version: '7.5.3',
date: '2026-07-19',
title: 'Sidebar hover tooltips',
changes: [
'Every sidebar item now shows a one-line "what it is & why" tooltip on hover — easier to learn what each view and agent filter does',
'Tooltips render as a single shared element positioned outside the sidebar, so they are never clipped, and adapt to dark / light / monokai themes',
'Tooltip text doubles as an aria-label for screen-reader accessibility',
],
},
{
version: '7.5.2',
date: '2026-07-19',
Expand Down
63 changes: 63 additions & 0 deletions src/frontend/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -2513,6 +2513,68 @@ function applySidebarConfig() {
_updateSidebarEmptySectionHints();
}

// Attach a one-line "what it is & why" tooltip to every sidebar item, keyed by
// data-key (preferred) or data-view. The sidebar has overflow-x:hidden, so a
// pure-CSS ::after tooltip would be clipped — instead we render a single shared
// element appended to <body> and position it fixed on hover (same approach as
// the agent-picker popover). aria-label keeps it accessible to screen readers.
var _navTooltipEl = null;
var _navTooltipTimer = null;

function _getNavTooltipEl() {
if (_navTooltipEl) return _navTooltipEl;
var el = document.createElement('div');
el.className = 'nav-tooltip';
el.setAttribute('role', 'tooltip');
el.setAttribute('aria-hidden', 'true');
document.body.appendChild(el);
_navTooltipEl = el;
return el;
}

function _hideNavTooltip() {
clearTimeout(_navTooltipTimer);
if (!_navTooltipEl) return;
_navTooltipEl.classList.remove('visible');
_navTooltipEl.setAttribute('aria-hidden', 'true');
}

function _onNavItemEnter(e) {
var item = e.currentTarget;
var help = item.getAttribute('data-tooltip');
if (!help) return;
clearTimeout(_navTooltipTimer);
_navTooltipTimer = setTimeout(function() {
var tip = _getNavTooltipEl();
tip.textContent = help;
// Show invisibly first to measure height, then clamp within the viewport.
tip.style.visibility = 'hidden';
tip.classList.add('visible');
var r = item.getBoundingClientRect();
var th = tip.offsetHeight;
var top = r.top + (r.height - th) / 2;
top = Math.max(8, Math.min(top, window.innerHeight - th - 8));
tip.style.top = top + 'px';
tip.style.left = (r.right + 10) + 'px';
tip.style.visibility = '';
tip.setAttribute('aria-hidden', 'false');
}, 350);
}

function applyNavTooltips() {
if (typeof SidebarConfig === 'undefined' || !SidebarConfig.navHelpFor) return;
document.querySelectorAll('.sidebar-item').forEach(function(el) {
var key = el.getAttribute('data-key') || el.getAttribute('data-view');
var help = SidebarConfig.navHelpFor(key);
if (!help) return;
el.setAttribute('data-tooltip', help);
if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', help);
el.addEventListener('mouseenter', _onNavItemEnter);
el.addEventListener('mouseleave', _hideNavTooltip);
el.addEventListener('click', _hideNavTooltip);
});
}

// When a user hides every togglable item inside a section the body becomes
// visually empty. Inject a small inline hint so the section doesn't look
// broken. The hint is removed automatically when any item becomes visible
Expand Down Expand Up @@ -4256,6 +4318,7 @@ function _onProjectsHashChange() {
// user never sees a flash of hidden items.
applySidebarConfig();
_bindSidebarHeaders();
applyNavTooltips();

// Load data
loadSessions();
Expand Down
50 changes: 50 additions & 0 deletions src/frontend/sidebar-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,54 @@
'install-agents': true
};

// One-line "what it is & why" help for each sidebar item, keyed by the item's
// data-key (preferred) or data-view (fallback). Surfaced as a hover tooltip
// in the browser (see applyNavTooltips in app.js). Kept here so it's covered
// by the sidebar-config unit test (every KNOWN_ITEM_KEY must have help).
var NAV_HELP = {
// Workspace
'sessions': 'Every AI coding session across all agents, in one place',
'projects': 'Browse sessions grouped by project folder',
'timeline': 'Your sessions laid out on a chronological timeline',
'activity': 'GitHub-style heatmap of your coding activity and streaks',
'running': 'Live sessions right now — CPU, memory, uptime',
'analytics': 'Token cost & usage broken down by day, project and agent',
'starred': 'Sessions you’ve pinned for quick access',
'leaderboard': 'Ranking of projects and agents by activity',
'cloud': 'Sync your sessions across machines',
// Agents (filter the session list to one agent)
'claude-only': 'Show only Claude Code sessions',
'codex-only': 'Show only Codex sessions',
'qwen-only': 'Show only Qwen Code sessions',
'pi-original-only': 'Show only Pi sessions',
'ohmypi-only': 'Show only Oh My Pi sessions',
'kiro-only': 'Show only Kiro sessions',
'cursor-only': 'Show only Cursor sessions',
'copilot-chat-only': 'Show only Copilot Chat sessions',
'copilot-only': 'Show only Copilot CLI sessions',
'opencode-only': 'Show only OpenCode sessions',
'kilo-only': 'Show only Kilo sessions',
// Tools
'export-import': 'Back up or restore your sessions as an archive',
'changelog': 'What’s new in each codbash release',
'settings': 'Themes, sidebar layout and other preferences',
// Install agents (copy the install command)
'install:claude': 'Copy the install command for Claude Code',
'install:codex': 'Copy the install command for Codex CLI',
'install:qwen': 'Copy the install command for Qwen Code',
'install:pi': 'Copy the install command for Pi',
'install:ohmypi': 'Copy the install command for Oh My Pi',
'install:kiro': 'Copy the install command for Kiro CLI',
'install:opencode': 'Copy the install command for OpenCode',
'install:kilo': 'Copy the install command for Kilo CLI',
'install:copilot': 'Copy the install command for Copilot CLI'
};

function navHelpFor(key) {
if (typeof key !== 'string') return '';
return Object.prototype.hasOwnProperty.call(NAV_HELP, key) ? NAV_HELP[key] : '';
}

function defaults() {
return { v: CURRENT_VERSION, hidden: {}, collapsed: {} };
}
Expand Down Expand Up @@ -168,6 +216,8 @@
return {
STORAGE_KEY: STORAGE_KEY,
KNOWN_ITEM_KEYS: KNOWN_ITEM_KEYS,
NAV_HELP: NAV_HELP,
navHelpFor: navHelpFor,
SETTINGS_KEY: SETTINGS_KEY,
parseSidebarConfig: parseSidebarConfig,
serializeSidebarConfig: serializeSidebarConfig,
Expand Down
24 changes: 24 additions & 0 deletions src/frontend/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,30 @@ body {

.sidebar-divider { height: 1px; background: var(--border); margin: 12px 20px; }

/* Sidebar hover tooltips — a single shared element positioned fixed by JS
(see applyNavTooltips in app.js), so it escapes the sidebar's overflow. */
.nav-tooltip {
position: fixed;
z-index: 200;
max-width: 240px;
padding: 8px 11px;
background: var(--bg-card);
color: var(--text-primary);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
font-size: 12px;
line-height: 1.4;
pointer-events: none;
opacity: 0;
transform: translateX(-4px);
transition: opacity 0.12s ease, transform 0.12s ease;
}
.nav-tooltip.visible {
opacity: 1;
transform: translateX(0);
}

.sidebar-section {
padding: 8px 20px 6px;
font-size: 10px;
Expand Down
26 changes: 26 additions & 0 deletions test/sidebar-config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -365,3 +365,29 @@ test('clearStorage tolerates a throwing removeItem', () => {
const throwingStorage = { removeItem() { throw new Error('boom'); } };
assert.doesNotThrow(() => m.clearStorage(throwingStorage));
});

// ── NAV_HELP tooltips ───────────────────────────────────────────

test('every known item key (plus settings) has a NAV_HELP entry', () => {
const m = loadModule();
const keys = m.KNOWN_ITEM_KEYS.concat([m.SETTINGS_KEY]);
for (const key of keys) {
const help = m.navHelpFor(key);
assert.ok(typeof help === 'string' && help.length > 0,
`missing NAV_HELP for "${key}"`);
}
});

test('navHelpFor returns empty string for unknown or non-string keys', () => {
const m = loadModule();
assert.equal(m.navHelpFor('does-not-exist'), '');
assert.equal(m.navHelpFor(undefined), '');
assert.equal(m.navHelpFor(null), '');
assert.equal(m.navHelpFor(42), '');
});

test('navHelpFor does not leak inherited Object.prototype keys', () => {
const m = loadModule();
assert.equal(m.navHelpFor('toString'), '');
assert.equal(m.navHelpFor('constructor'), '');
});
Loading