Add light mode theme with --dark CLI flag to switch back - #2
Add light mode theme with --dark CLI flag to switch back#2Anurag-Saksena wants to merge 6 commits into
Conversation
- chats_mobile.html: Replace dark slate+teal theme with light white+orange theme as default. Add [data-theme="dark"] block that restores the original dark slate+teal palette. All hardcoded colours extracted to CSS custom properties (--accent-rgb, --today-color, --progress-start/end) so both themes resolve correctly via data-theme attribute injection. - chats-mobile.js: Parse --dark / --theme=dark from process.argv at startup. serveHtml() helper injects data-theme="dark" on the <html> element when dark mode is active; light mode serves the file unmodified. Theme is logged to the console on startup. - index.html / Sidebar.js: Apply Claude Code dark theme colours and always- expanded sidebar with Claude asterisk logo. - chart.min.js: Bundle Chart.js 3.9.1 locally; remove CDN dependency. Usage: npm start # light mode (white + orange) npm start -- --dark # dark mode (slate + teal, original) npm start -- --theme=dark # same as --dark Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous `node -e require(...)` start script swallowed CLI arguments, so `npm start -- --dark` never reached process.argv. Replaced with a proper `src/start.js` entry point. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| flex-direction: column; | ||
| margin-left: 56px; | ||
| transition: margin-left 0.3s ease; | ||
| margin-left: 220px; |
There was a problem hiding this comment.
🟡 Double margin offset causes 220px gap between sidebar and main content in analytics dashboard
In index.html, the layout uses a flex container (.app) with two children: .app-sidebar (width: 220px as a spacer for the fixed sidebar) and .app-main (flex: 1, margin-left: 220px). In a flex row layout, the margin-left on .app-main adds space after the 220px spacer, so the main content's left edge starts at 220 + 220 = 440px — leaving a 220px empty gap between the fixed sidebar and the content area. The fix is to remove margin-left: 220px from .app-main since the .app-sidebar spacer already provides the offset, or alternatively remove the spacer's width.
Note: This page (index.html) is not currently served by the application routes (only chats_mobile.html is served), but the PR explicitly modifies these CSS values and the layout would be visually broken if the page were ever used.
| margin-left: 220px; | |
| margin-left: 0; |
Was this helpful? React with 👍 or 👎 to provide feedback.
Extract the first user message text from each JSONL during indexing and store it as a summary column in SQLite. The UI now displays this text as the session name, matching the Claude Code desktop sidebar behaviour. - Indexer: capture first user message text (max 80 chars) as summary - DatabaseManager: add summary column + migration + upsert/select - DatabaseBackend: pass summary through _transformConversation - chats_mobile.html: render summary as session name with UUID fallback Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| this.app.get('/', async (req, res) => { | ||
| await this.serveHtml(res); | ||
| }); | ||
|
|
||
| // Fallback for any other routes (but not for API or static files) | ||
| this.app.get('*', (req, res) => { | ||
| // Don't redirect API calls or static files | ||
| if (req.path.startsWith('/api/') || | ||
| req.path.startsWith('/services/') || | ||
| req.path.startsWith('/components/') || | ||
| this.app.get('*', async (req, res) => { | ||
| if (req.path.startsWith('/api/') || | ||
| req.path.startsWith('/services/') || | ||
| req.path.startsWith('/components/') || | ||
| req.path.startsWith('/assets/')) { | ||
| res.status(404).json({ error: 'Not found' }); | ||
| return; | ||
| } | ||
| res.sendFile(path.join(__dirname, 'analytics-web', 'chats_mobile.html')); | ||
| await this.serveHtml(res); | ||
| }); |
There was a problem hiding this comment.
🔴 Unhandled async rejection in serveHtml route handlers causes request to hang in Express 4.x
The '/' and '*' route handlers at lines 901-903 and 906-915 call await this.serveHtml(res) without any try-catch. In Express 4.x (used by this project per package.json:31), rejected promises from async route handlers are not automatically caught — the request will simply hang with no response sent. The serveHtml method (src/chats-mobile.js:921-931) calls await fs.readFile(filePath, 'utf8') when this.theme === 'dark', which can throw on I/O errors. Every other async route handler in this file (e.g., lines 201, 521, 618) wraps its body in try-catch, making this an inconsistency with the established codebase pattern.
| this.app.get('/', async (req, res) => { | |
| await this.serveHtml(res); | |
| }); | |
| // Fallback for any other routes (but not for API or static files) | |
| this.app.get('*', (req, res) => { | |
| // Don't redirect API calls or static files | |
| if (req.path.startsWith('/api/') || | |
| req.path.startsWith('/services/') || | |
| req.path.startsWith('/components/') || | |
| this.app.get('*', async (req, res) => { | |
| if (req.path.startsWith('/api/') || | |
| req.path.startsWith('/services/') || | |
| req.path.startsWith('/components/') || | |
| req.path.startsWith('/assets/')) { | |
| res.status(404).json({ error: 'Not found' }); | |
| return; | |
| } | |
| res.sendFile(path.join(__dirname, 'analytics-web', 'chats_mobile.html')); | |
| await this.serveHtml(res); | |
| }); | |
| // Serve the mobile chats page as default | |
| this.app.get('/', async (req, res) => { | |
| try { | |
| await this.serveHtml(res); | |
| } catch (error) { | |
| console.error('Error serving HTML:', error); | |
| res.status(500).send('Internal Server Error'); | |
| } | |
| }); | |
| // Fallback for any other routes (but not for API or static files) | |
| this.app.get('*', async (req, res) => { | |
| if (req.path.startsWith('/api/') || | |
| req.path.startsWith('/services/') || | |
| req.path.startsWith('/components/') || | |
| req.path.startsWith('/assets/')) { | |
| res.status(404).json({ error: 'Not found' }); | |
| return; | |
| } | |
| try { | |
| await this.serveHtml(res); | |
| } catch (error) { | |
| console.error('Error serving HTML:', error); | |
| res.status(500).send('Internal Server Error'); | |
| } | |
| }); |
Was this helpful? React with 👍 or 👎 to provide feedback.
Background title generator reads first 3 user messages per conversation
and asks a local Ollama model (default: llama3.2:1b) to produce a
5-word session title. Titles are generated after startup and pushed
live to open browser tabs via WebSocket without a page reload.
Falls back gracefully to the first-80-chars summary when Ollama is
not running, and prints install instructions to the console.
- TitleGenerator: new class, Ollama REST API, 3-request concurrency
- Indexer: capture firstMessages (first 3 user texts, max 500 chars each)
- DatabaseManager: first_messages + ai_titled columns, updateSummary(),
getConversationsNeedingTitles(), markAiTitled()
- DatabaseBackend: generateTitlesInBackground(onTitleReady)
- chats-mobile.js: kick off background generation post-startup, broadcast
title_update WebSocket events per title
- chats_mobile.html: handleTitleUpdate() patches DOM labels live;
webSocketService.on('message') listener for title_update events
Usage: install Ollama, then run: ollama pull llama3.2:1b
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| INSERT OR REPLACE INTO conversations ( | ||
| id, file_path, filename, project, message_count, file_size, | ||
| last_modified, created, tokens_total, tokens_input, tokens_output, | ||
| primary_model, indexed_at, is_subagent, parent_id, cwd | ||
| ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | ||
| primary_model, indexed_at, is_subagent, parent_id, cwd, summary, first_messages | ||
| ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
There was a problem hiding this comment.
🔴 INSERT OR REPLACE resets ai_titled flag and overwrites AI-generated summary on re-index
The upsertConversation method uses INSERT OR REPLACE which in SQLite deletes the existing row and inserts a new one. The ai_titled column is not included in the INSERT column list (src/analytics/data/DatabaseManager.js:329-333), so it reverts to its default value of 0 whenever a conversation is re-indexed. Similarly, the summary column is set from the Indexer's fallback value (first 80 chars of first user message from src/analytics/data/Indexer.js:252), overwriting any AI-generated title set by updateSummary() at src/analytics/data/DatabaseManager.js:742.
This creates a cycle: on server startup, runFullIndex() re-indexes changed files → ai_titled resets to 0 and summary reverts to fallback → getConversationsNeedingTitles() returns previously-titled conversations → TitleGenerator wastes Ollama calls regenerating all titles → user sees fallback titles until regeneration completes.
Columns in INSERT vs columns that get reset
The INSERT statement includes: id, file_path, filename, project, message_count, file_size, last_modified, created, tokens_total, tokens_input, tokens_output, primary_model, indexed_at, is_subagent, parent_id, cwd, summary, first_messages
Missing column: ai_titled (defaults to 0 on replacement)
Overwritten column: summary (set to Indexer fallback instead of preserved AI title)
(Refers to lines 328-333)
Prompt for agents
The INSERT OR REPLACE in upsertConversation deletes the old row and inserts a new one, which causes the ai_titled column (not in the INSERT column list) to reset to its default value of 0, and the summary column to be overwritten by the Indexer fallback instead of preserving the AI-generated title.
There are several approaches to fix this:
1. Preserve ai_titled and AI summary during upsert: Before the INSERT OR REPLACE, query the existing row for its ai_titled and summary values. If ai_titled=1, keep the existing summary and set ai_titled=1 in the INSERT. Add ai_titled to the column list in the INSERT statement.
2. Switch from INSERT OR REPLACE to an explicit INSERT ... ON CONFLICT(id) DO UPDATE SET ... approach, which only updates specified columns and leaves ai_titled and summary untouched when ai_titled=1.
3. In the Indexer._indexFile method, check if the conversation already has an AI title before building the conversation object, and if so, preserve the existing summary value.
The key files involved are:
- src/analytics/data/DatabaseManager.js: upsertConversation() method (line ~324)
- src/analytics/data/Indexer.js: _indexFile() method builds the conversation object
- src/analytics/ai/TitleGenerator.js: generateAll() calls updateSummary and markAiTitled
Was this helpful? React with 👍 or 👎 to provide feedback.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- TitleGenerator: 10-word prompt, num_predict 30, word cap 10, no message truncation - Indexer: removed 500-char per-message limit so full text stored in first_messages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
--dark/--theme=darkCLI flag at launch timecomponents/chart.min.js) to eliminate the CDN dependency oncdn.jsdelivr.net, so no network requests leave the machinesetupCloudflaredTunnel()and related references) to reduce attack surfacenpm start -- --darkby replacingnode -e require(...)with a propersrc/start.jsentry point so CLI flags reachprocess.argvTheme architecture
:root(light) and[data-theme=dark](dark)data-theme=darkon the<html>element when dark mode is active; light mode serves the file unmodifiedgetAvatarGradient(letter)currentColorfor correct themingUsage
Test plan
npm startlaunches in light mode — white background, orange accentsnpm start -- --darklaunches in dark mode — dark slate background, teal accents--tunnelflag no longer exists / tunnel code removed🤖 Generated with Claude Code