fix: tab folder visibility controlled by Tall Tabs setting + cache busting

- Renamed "Two-Row Tabs" to "Tall Tabs (Name + Folder)" for clarity
- Tab folder path now only rendered in JS when setting is ON (not just CSS hidden)
- CSS specificity fix: mobile.css now matches .tabs-two-rows specificity (0,2,0)
- Server settings sync: display keys seed from server on fresh localStorage
- Bumped cache buster versions to 0.1534 to force browser reload

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-02-18 11:15:15 +01:00
co-authored by Claude Opus 4.6
parent 6fd6ff770f
commit d5a3806aa3
7 changed files with 437 additions and 24 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ When user says "COM":
1. Increment version in BOTH `package.json` AND `CLAUDE.md` (verify they match with `grep version package.json && grep Version CLAUDE.md`)
2. Run: `git add -A && git commit -m "chore: bump version to X.XXXX" && git push && npm run build && systemctl --user restart claudeman-web`
**Version**: 0.1534 (must match `package.json` for npm publish)
**Version**: 0.1535 (must match `package.json` for npm publish)
## Project Overview
+388
View File
@@ -0,0 +1,388 @@
# Performance Audit: First Page Load
**Date**: 2026-02-18
**Scope**: Browser first-load of Claudeman web UI (`/`)
**Method**: Static analysis by 4 parallel audit agents (server, frontend, SSE/xterm, asset pipeline)
---
## Current State Summary
### Payload Sizes (measured from live server, port 3000)
| Asset | Raw Size | Gzip | Brotli | Lines | Render-Blocking? |
|-------|----------|------|--------|-------|-----------------|
| `index.html` | 82 KB | 15 KB | 15 KB | 1,479 | N/A (document) |
| `app.js` | 562 KB | 126 KB | 125 KB | 15,354 | No (`defer`) |
| `styles.css` | 154 KB | 25 KB | 27 KB | 8,199 | **YES** |
| `mobile.css` | 34 KB | 7 KB | 7 KB | 1,493 | **YES** (no media query!) |
| `xterm.css` (CDN) | 2 KB | 2 KB | — | — | **YES** (external CDN) |
| `xterm.min.js` (CDN) | 67 KB | 65 KB | — | — | No (`defer`) |
| `xterm-addon-fit` (CDN) | 1 KB | 1 KB | — | — | No (`defer`) |
| **Total local** | **832 KB** | **173 KB** | **174 KB** | | |
| **Total w/ CDN** | **~902 KB** | **~241 KB** | | | |
**Server compression**: Brotli preferred (`Content-Encoding: br`), via `@fastify/compress` with threshold 1024. Compression is **on-the-fly per request** — no pre-compressed files exist.
**HTTP headers verified**: `Cache-Control: public, max-age=3600`, weak ETags auto-generated by `@fastify/static`, `Vary: accept-encoding`, CSP + security headers present.
### Request Waterfall on First Load (6-7 API calls!)
```
Browser hits /
├── index.html ............................ (82 KB document)
├── styles.css?v=0.1533 .................. (render-blocking CSS, 154 KB)
├── mobile.css?v=0.1533 .................. (render-blocking CSS, 34 KB — wasted on desktop!)
├── xterm.css (CDN) ...................... (render-blocking CSS — external!)
├── xterm.min.js (CDN, defer) ........... (67 KB, parallel download)
├── xterm-addon-fit.min.js (CDN, defer) .. (1 KB, parallel download)
├── app.js?v=0.1533 (defer) ............. (562 KB, parallel download)
│
│ [FIRST PAINT blocked until ALL CSS downloaded + parsed]
│
├── JS executes: new ClaudemanApp().init()
│ ├── initTerminal() ................... (SYNC: new Terminal() + terminal.open() → canvas creation)
│ ├── connectSSE() → /api/events ....... (SSE → fires 'init' with getLightState())
│ ├── loadState() → /api/status ........ (DUPLICATE #1: same data as SSE init!)
│ ├── loadQuickStartCases()
│ │ ├── /api/settings ................ (settings fetch #1)
│ │ └── /api/cases?_t=<timestamp> ... (case list, cache-busted!)
│ ├── startSystemStatsPolling() → /api/system/stats (every 2s, starts immediately)
│ └── loadAppSettingsFromServer() → /api/settings (DUPLICATE #2: settings fetched again!)
```
**Total init API calls**: 6-7 requests, with **2 duplicates** (`/api/status` = SSE init, `/api/settings` fetched twice).
### Critical Path Bottlenecks
1. **3 render-blocking CSS files** (one from CDN, one wasted on desktop)
2. **Synchronous `terminal.open()`** blocks main thread during init (canvas creation)
3. **Double `handleInit()` execution** — SSE init + `/api/status` both call it, causing full state reset + cleanup twice within ~100ms
4. **`/api/settings` fetched twice** — once in `loadQuickStartCases()`, once in `loadAppSettingsFromServer()`
5. **No loading skeleton** — blank `#0a0a0a` screen until CSS+JS fully loaded
6. **12 modals pre-rendered** in HTML — ~600+ DOM elements, ~60KB of invisible HTML
7. **562KB monolith `app.js`** unminified — 1,525 comment lines (10%), 89 `console.*` statements, 23% whitespace
8. **No minification in build** — `cp -r` copies raw source to dist
9. **Stats polling starts immediately** — 2s interval even with no sessions
10. **Version query strings stale** — HTML has `?v=0.1533`, package.json is `0.1534`
### What's Already Good
- Only **1 xterm Terminal instance** shared across all sessions (buffer swapping on tab switch)
- Teammate terminals created **lazily** on window open (with `requestAnimationFrame` defer)
- Subagent windows use **HTML activity logs**, not additional Terminal instances
- `getLightState()` has a **1-second TTL cache** — no duplicate server-side computation
- SSE init sends **lightweight state** (no terminal buffers) — buffers fetched on-demand per tab
- Buffer hydration uses **chunked writes** (128KB chunks via `requestAnimationFrame`) — no UI jank
- `selectSession()` defers secondary panels via **`requestIdleCallback`**
- Buffer fetch is **tail-mode** (last 256KB only, not full 2MB)
- **System fonts only** — no web font downloads blocking paint
- All JS scripts use **`defer`**
- SSE reconnection has **proper exponential backoff** with timeout cleanup
---
## Optimization Plan
### Phase 1: Quick Wins (High Impact, Low Effort)
#### 1.1 Add `media` attribute to mobile.css
**Impact**: HIGH — 34KB CSS stops blocking render on desktop
**Effort**: 1 line change
**File**: `src/web/public/index.html:14`
```html
<!-- Before -->
<link rel="stylesheet" href="mobile.css?v=...">
<!-- After -->
<link rel="stylesheet" href="mobile.css?v=..." media="(max-width: 1023px)">
```
The browser still downloads it (for potential resize) but won't block rendering on desktop. The `mobile.css` comment on line 4 says this was *intended* but never implemented.
#### 1.2 Eliminate duplicate `/api/status` fetch + double `handleInit()`
**Impact**: HIGH — removes 1 redundant API call + eliminates double state reset (clearing 15+ Maps, 7+ timers, `cleanupAllFloatingWindows()`, double `renderSessionTabs()`, double async subagent restore chain)
**Effort**: Small
**Files**: `src/web/public/app.js:1554`, `app.js:3566-3574`
The SSE `init` event (`server.ts:618`) already sends `getLightState()`. The `loadState()` at `app.js:1554` fetches identical data from `/api/status`. Both call `handleInit()` which does a full state reset — whichever arrives second **wipes all state from the first** and rebuilds from scratch.
The `_initGeneration` guard (line 3373/3549) only protects the session-restore at the end, NOT the expensive full cleanup (lines 3389-3503).
**Approach**: Remove `this.loadState()` from `init()`. Add a fallback timeout:
```js
// In init():
this.connectSSE();
// Remove: this.loadState();
this._initFallbackTimer = setTimeout(() => {
if (this._initGeneration === 0) this.loadState();
}, 3000);
```
Clear the timer in `handleInit()`:
```js
handleInit(data) {
if (this._initFallbackTimer) {
clearTimeout(this._initFallbackTimer);
this._initFallbackTimer = null;
}
// ... rest of handleInit
}
```
#### 1.3 Deduplicate `/api/settings` fetch
**Impact**: MEDIUM — removes 1 redundant API call
**Effort**: Small
**Files**: `src/web/public/app.js:7341` (in `loadQuickStartCases`), `app.js:9964` (in `loadAppSettingsFromServer`)
Both fetch `/api/settings`. Fetch it once, pass the result to both consumers:
```js
// In init():
const settingsPromise = fetch('/api/settings').then(r => r.json());
this.loadQuickStartCases(null, settingsPromise);
this.loadAppSettingsFromServer(settingsPromise);
```
#### 1.4 Defer system stats polling
**Impact**: MEDIUM — removes 1 API call every 2s when idle
**Effort**: Small
**Files**: `src/web/public/app.js:1567`, `app.js:15261-15271`
`fetchSystemStats()` already has a visibility guard (line 15282: skips if `#headerSystemStats` is `display: none`), but the interval still ticks. Move `startSystemStatsPolling()` out of `init()` — start it in `handleInit()` only when `data.sessions.length > 0`.
#### 1.5 Preload xterm.css to unblock render
**Impact**: MEDIUM — external CDN CSS currently blocks first paint
**Effort**: 2 line change
**File**: `src/web/public/index.html:15`
```html
<!-- Before -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css">
<!-- After -->
<link rel="preload" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css"></noscript>
```
Terminal won't display until xterm.js executes anyway, so the CSS doesn't need to block initial paint.
#### 1.6 Fix stale version query strings
**Impact**: LOW — prevents serving cached stale assets after deploy
**Effort**: Small
**File**: COM script in CLAUDE.md
The HTML references `?v=0.1533` while package.json is already at `0.1534`. The COM workflow should auto-update HTML version strings. Add to the COM script:
```bash
# After incrementing version in package.json + CLAUDE.md:
sed -i "s/?v=[0-9.]*/?v=$NEW_VERSION/g" src/web/public/index.html
```
#### 1.7 Remove cache-busting from `/api/cases`
**Impact**: LOW — allows HTTP caching of case list
**Effort**: 1 line change
**File**: `src/web/public/app.js:7351`
```js
// Before:
const res = await fetch('/api/cases?_t=' + Date.now());
// After:
const res = await fetch('/api/cases');
```
The case list rarely changes during a session. Let the browser cache it.
---
### Phase 2: Medium Effort (High Impact)
#### 2.1 Add loading skeleton
**Impact**: MEDIUM-HIGH — perceived performance improvement (instant visual structure)
**Effort**: Small-Medium
**File**: `src/web/public/index.html`
Add minimal inline `<style>` + skeleton HTML in `<body>` showing a dark header bar + terminal placeholder. Hidden by `app.js` once init completes:
```html
<style>
.skeleton { display: flex; flex-direction: column; height: 100vh; }
.skeleton-header { height: 40px; background: #111; border-bottom: 1px solid #222; }
.skeleton-terminal { flex: 1; background: #0d0d0d; }
.app-loaded .skeleton { display: none; }
</style>
<div class="skeleton">
<div class="skeleton-header"></div>
<div class="skeleton-terminal"></div>
</div>
```
In `app.js` init(), add `document.body.classList.add('app-loaded')` at the end.
#### 2.2 Defer xterm.js terminal creation to after first paint
**Impact**: MEDIUM-HIGH — `terminal.open()` is the heaviest synchronous call in init
**Effort**: Medium
**Files**: `src/web/public/app.js:1545`, `app.js:1578-1639`
```js
init() {
// ... mobile detection, visibility settings ...
document.documentElement.classList.remove('mobile-init');
// Show skeleton/header immediately, defer heavy terminal init
requestAnimationFrame(() => {
this.initTerminal();
this.connectSSE();
// ... rest of init
});
}
```
Lets the browser paint the header/tabs before the terminal canvas is created.
#### 2.3 Batch initial API calls into one endpoint
**Impact**: MEDIUM — reduces 4+ API calls to 1
**Effort**: Medium
**Files**: `src/web/server.ts`, `src/web/public/app.js`
Create `/api/init-bundle`:
```json
{
"status": { /* getLightState() */ },
"cases": [ /* case list */ ],
"settings": { /* user settings */ }
}
```
Replaces `/api/status` (fallback), `/api/cases`, `/api/settings`. Saves HTTP round trips and server-side work.
#### 2.4 Lazy-create modals on first open
**Impact**: HIGH — removes ~600+ DOM elements from initial parse (~60KB of HTML)
**Effort**: Medium-High
**Files**: `src/web/public/index.html`, `src/web/public/app.js`
12 modals pre-rendered in `index.html`:
- `helpModal` (lines 227-447)
- `sessionOptionsModal` (lines 448-714) — **266 lines alone**
- `appSettingsModal` (lines 715-900+)
- `createCaseModal`, `mobileCasePickerModal`, `ralphWizardModal`, `killAllModal`, `closeConfirmModal`, `savePresetModal`, `tokenStatsModal`, `filePreviewModal`, notification drawer
**Approach**: Replace each modal's HTML with `<div id="helpModal" class="modal"></div>`. On first open, inject full HTML via `createModalContent()`. Cache after creation.
---
### Phase 3: Build Pipeline (Highest Impact)
#### 3.1 Self-host xterm.js assets
**Impact**: MEDIUM — eliminates CDN dependency + latency, enables local caching
**Effort**: Low-Medium
**Files**: `src/web/public/index.html`, `package.json` build script
```bash
# Build script addition:
mkdir -p dist/web/public/vendor
cp node_modules/xterm/css/xterm.css dist/web/public/vendor/
cp node_modules/xterm/lib/xterm.min.js dist/web/public/vendor/
cp node_modules/@xterm/addon-fit/lib/xterm-addon-fit.min.js dist/web/public/vendor/
```
Update HTML to reference `/vendor/xterm.min.js` etc. Removes render-blocking CDN CSS entirely.
#### 3.2 Add esbuild minification to build
**Impact**: HIGH — ~38 KB compressed savings (16% of local payload)
**Effort**: Medium
**Files**: `package.json` (build script)
Current build just does `cp -r src/web/public dist/web/`. No minification at all.
**app.js specifics**: 1,525 comment lines (10%), 89 `console.*` statements, 23% whitespace.
```bash
# Add to build script:
npx esbuild dist/web/public/app.js --minify --drop:console --outfile=dist/web/public/app.js --allow-overwrite
npx esbuild dist/web/public/styles.css --minify --outfile=dist/web/public/styles.css --allow-overwrite
npx esbuild dist/web/public/mobile.css --minify --outfile=dist/web/public/mobile.css --allow-overwrite
```
Expected: `app.js` 562KB → ~350KB minified → ~90KB gzip (from 126KB). `--drop:console` removes all 89 debug statements.
Note: `app.js` is vanilla JS (not modules), so esbuild works directly as a minifier.
#### 3.3 Pre-compress static assets at build time
**Impact**: MEDIUM — eliminates per-request CPU compression work
**Effort**: Low
**Files**: `package.json` build script, `src/web/server.ts`
Currently `@fastify/compress` compresses on-the-fly for every request. Pre-compress at build time:
```bash
# Build script:
for f in dist/web/public/*.{js,css,html}; do
gzip -9 -k "$f"
brotli -9 -k "$f"
done
```
Then configure `@fastify/static` with `preCompressed: true` (if supported) or serve pre-compressed files via custom logic.
#### 3.4 Extract critical CSS inline
**Impact**: MEDIUM — eliminates render-blocking `styles.css` for first paint
**Effort**: Medium-High
**Files**: `src/web/public/styles.css`, `src/web/public/index.html`
Identify ~2-3KB of CSS needed for first paint (body, header, tab bar, terminal container) and inline it in `<head>`. Load full `styles.css` asynchronously:
```html
<style>/* ~50 lines of critical CSS */</style>
<link rel="preload" href="styles.css?v=..." as="style" onload="this.onload=null;this.rel='stylesheet'">
```
---
## Impact Estimates
| # | Optimization | First Paint | TTI | Effort |
|---|-------------|-------------|-----|--------|
| 1.1 | mobile.css media query | -50ms | — | 1 min |
| 1.2 | Remove duplicate fetch + double handleInit | — | -100-200ms | 15 min |
| 1.3 | Deduplicate settings fetch | — | -50ms | 10 min |
| 1.4 | Defer stats polling | — | -20ms | 10 min |
| 1.5 | Preload xterm.css | -100-300ms | — | 5 min |
| 1.6 | Fix stale version strings | cache correctness | — | 5 min |
| 1.7 | Remove cases cache-bust | — | -10ms | 1 min |
| 2.1 | Loading skeleton | perceived -500ms | — | 30 min |
| 2.2 | Defer terminal init | -50-100ms | -50ms | 30 min |
| 2.3 | Batch API endpoint | — | -100-200ms | 1 hr |
| 2.4 | Lazy modals | -30-50ms parse | -50ms | 2-3 hrs |
| 3.1 | Self-host xterm | -100-300ms | — | 20 min |
| 3.2 | Minify JS/CSS | -50-100ms parse | — | 30 min |
| 3.3 | Pre-compress assets | -10-30ms TTFB | — | 20 min |
| 3.4 | Critical CSS inline | -200-400ms | — | 2 hrs |
**Combined estimate**: First paint **300-800ms faster**, TTI **200-500ms faster**.
---
## Implementation Order (for implementation agent)
Do these in order — each step is independently testable:
1. **1.1** — mobile.css media query (1 line, instant win)
2. **1.5** — Preload xterm.css (2 lines, big render-blocking fix)
3. **1.2** — Remove duplicate `/api/status` + double handleInit
4. **1.3** — Deduplicate `/api/settings` fetch
5. **1.7** — Remove cache-busting from `/api/cases`
6. **3.1** — Self-host xterm.js (removes CDN dependency entirely)
7. **3.2** — Add esbuild minification to build
8. **1.4** — Defer stats polling
9. **1.6** — Fix stale version strings in COM workflow
10. **2.1** — Loading skeleton
11. **2.2** — Defer terminal init after first paint
12. **2.3** — Batch init API endpoint
13. **2.4** — Lazy modals (biggest refactor, do last)
14. **3.3** — Pre-compress assets (nice-to-have)
15. **3.4** — Critical CSS extraction (only if still needed after above)
**Verification after each step**: Use Playwright to load the page with `waitUntil: 'domcontentloaded'`, measure first paint timing, check that the UI renders correctly with 3-4s wait for async data.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claudeman",
"version": "0.1534",
"version": "0.1535",
"description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence",
"type": "module",
"main": "dist/index.js",
+28 -9
View File
@@ -3721,9 +3721,10 @@ class ClaudemanApp {
const minimizedCount = minimizedAgents?.size || 0;
const subagentBadge = minimizedCount > 0 ? this.renderSubagentTabBadge(id, minimizedAgents) : '';
// Show folder name if session has a custom name (to still show the directory context)
// Show folder name if session has a custom name AND tall tabs setting is enabled
const folderName = session.workingDir ? session.workingDir.split('/').pop() || '' : '';
const showFolder = session.name && folderName && folderName !== name;
const tallTabsEnabled = this._tallTabsEnabled ?? false;
const showFolder = tallTabsEnabled && session.name && folderName && folderName !== name;
parts.push(`<div class="session-tab ${isActive ? 'active' : ''}${alertClass}" data-id="${id}" data-color="${color}" onclick="app.selectSession('${this.escapeHtml(id)}')" oncontextmenu="event.preventDefault(); app.startInlineRename('${this.escapeHtml(id)}')" tabindex="0" role="tab" aria-selected="${isActive ? 'true' : 'false'}" aria-label="${this.escapeHtml(name)} session" ${session.workingDir ? `title="${this.escapeHtml(session.workingDir)}"` : ''}>
<span class="tab-status ${status}" aria-hidden="true"></span>
@@ -9821,10 +9822,21 @@ class ClaudemanApp {
applyTabWrapSettings() {
const settings = this.loadAppSettingsFromStorage();
const defaults = this.getDefaultSettings();
const twoRows = settings.tabTwoRows ?? defaults.tabTwoRows ?? false;
const deviceType = MobileDetection.getDeviceType();
// Two-row tabs disabled on mobile/tablet — not enough screen space
const twoRows = deviceType === 'desktop'
? (settings.tabTwoRows ?? defaults.tabTwoRows ?? false)
: false;
const prevTallTabs = this._tallTabsEnabled;
this._tallTabsEnabled = twoRows;
const tabsEl = document.getElementById('sessionTabs');
if (tabsEl) {
tabsEl.classList.toggle('tabs-two-rows', twoRows);
tabsEl.classList.toggle('tabs-show-folder', twoRows);
}
// Re-render tabs if folder visibility changed (folder spans are generated in JS)
if (prevTallTabs !== undefined && prevTallTabs !== twoRows) {
this._fullRenderSessionTabs();
}
}
@@ -9970,15 +9982,22 @@ class ClaudemanApp {
'subagentTrackingEnabled', 'subagentActiveTabOnly',
'imageWatcherEnabled', 'ralphTrackerEnabled', 'tabTwoRows',
]);
const filteredAppSettings = {};
// Merge settings: non-display keys always sync from server,
// display keys only seed from server when localStorage has no value
// (prevents cross-device overwrite while fixing settings re-enabling on fresh loads)
const localSettings = this.loadAppSettingsFromStorage();
const merged = { ...localSettings };
for (const [key, value] of Object.entries(appSettings)) {
if (!displayKeys.has(key)) {
filteredAppSettings[key] = value;
if (displayKeys.has(key)) {
// Display keys: only use server value as initial seed
if (!(key in localSettings)) {
merged[key] = value;
}
} else {
// Non-display keys: server always wins
merged[key] = value;
}
}
// Merge non-display settings with localStorage (server takes precedence)
const localSettings = this.loadAppSettingsFromStorage();
const merged = { ...localSettings, ...filteredAppSettings };
this.saveAppSettingsToStorage(merged);
// Apply notification prefs from server if present (only if localStorage has none)
+5 -5
View File
@@ -10,8 +10,8 @@
<!-- DNS preconnect for CDN — saves ~100ms on first load -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
<link rel="dns-prefetch" href="https://cdn.jsdelivr.net">
<link rel="stylesheet" href="styles.css?v=0.1533">
<link rel="stylesheet" href="mobile.css?v=0.1533">
<link rel="stylesheet" href="styles.css?v=0.1534">
<link rel="stylesheet" href="mobile.css?v=0.1534">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css">
<!-- Defer CDN scripts — they no longer block initial HTML parsing/paint.
'defer' preserves execution order (xterm loads before fit addon). -->
@@ -792,8 +792,8 @@
<!-- Tab Bar Section -->
<div class="settings-section-header">Tab Bar</div>
<div class="settings-item" title="Allow tabs to wrap into two rows when there are many sessions">
<span class="settings-item-label">Two-Row Tabs</span>
<div class="settings-item" title="Show folder path below tab name and allow tab bar to wrap into multiple rows">
<span class="settings-item-label">Tall Tabs (Name + Folder)</span>
<label class="switch switch-sm">
<input type="checkbox" id="appSettingsTabTwoRows">
<span class="slider"></span>
@@ -1474,6 +1474,6 @@
<!-- Lines drawn dynamically -->
</svg>
<script defer src="app.js?v=0.1533"></script>
<script defer src="app.js?v=0.1534"></script>
</body>
</html>
+10 -4
View File
@@ -77,10 +77,13 @@ html.mobile-init .file-browser-panel {
gap: 0.35rem;
}
/* Compact session tabs */
.session-tabs {
/* Compact session tabs — .tabs-two-rows override needed to match
specificity of .session-tabs.tabs-two-rows in styles.css (0,2,0) */
.session-tabs,
.session-tabs.tabs-two-rows {
flex-wrap: nowrap;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
max-height: 52px;
@@ -344,11 +347,14 @@ html.mobile-init .file-browser-panel {
position: relative;
}
/* Ultra-compact session tabs */
.session-tabs {
/* Ultra-compact session tabs — .tabs-two-rows override needed to match
specificity of .session-tabs.tabs-two-rows in styles.css (0,2,0) */
.session-tabs,
.session-tabs.tabs-two-rows {
flex: 1;
flex-wrap: nowrap;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
max-height: 36px;
+4 -4
View File
@@ -309,6 +309,7 @@ body {
text-overflow: ellipsis;
}
/* Tab folder path — hidden by default, shown via .tabs-show-folder on container */
.session-tab .tab-folder {
font-size: 0.6rem;
color: var(--text-muted);
@@ -316,12 +317,11 @@ body {
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.2;
display: none;
}
@media (max-width: 768px) {
.session-tab .tab-folder {
display: none;
}
.tabs-show-folder .session-tab .tab-folder {
display: block;
}
.session-tab .tab-close {