Skip to content

Add light mode theme with --dark CLI flag to switch back - #2

Open
Anurag-Saksena wants to merge 6 commits into
drewburchfield:mainfrom
Anurag-Saksena:add_light_mode
Open

Add light mode theme with --dark CLI flag to switch back#2
Anurag-Saksena wants to merge 6 commits into
drewburchfield:mainfrom
Anurag-Saksena:add_light_mode

Conversation

@Anurag-Saksena

@Anurag-Saksena Anurag-Saksena commented May 2, 2026

Copy link
Copy Markdown

Summary

  • Adds a Claude-inspired light theme (white background + orange accents) as the new default, matching the Claude.ai / Claude Code desktop aesthetic
  • Preserves the original dark theme (slate + teal) behind a --dark / --theme=dark CLI flag at launch time
  • Bundles Chart.js locally (components/chart.min.js) to eliminate the CDN dependency on cdn.jsdelivr.net, so no network requests leave the machine
  • Removes all Cloudflare tunnel code (setupCloudflaredTunnel() and related references) to reduce attack surface
  • Fixes npm start -- --dark by replacing node -e require(...) with a proper src/start.js entry point so CLI flags reach process.argv

Theme architecture

  • All hardcoded colour values extracted to CSS custom properties in :root (light) and [data-theme=dark] (dark)
  • Server injects data-theme=dark on the <html> element when dark mode is active; light mode serves the file unmodified
  • Avatar colours use a per-letter hash into an 8-colour warm palette via getAvatarGradient(letter)
  • Emoji icons (🔍 ⚙️) replaced with inline SVGs that inherit currentColor for correct theming

Usage

npm start              # light mode (default)
npm start -- --dark    # dark mode (original)
npm start -- --theme=dark  # same as --dark

Test plan

  • npm start launches in light mode — white background, orange accents
  • npm start -- --dark launches in dark mode — dark slate background, teal accents
  • Search, conversation list, session details all render correctly in both themes
  • No network requests to external CDNs (Chart.js loaded locally)
  • --tunnel flag no longer exists / tunnel code removed

🤖 Generated with Claude Code


Open in Devin Review

Anurag-Saksena and others added 2 commits May 2, 2026 15:48
- 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>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 4 additional findings in Devin Review.

Open in Devin Review

flex-direction: column;
margin-left: 56px;
transition: margin-left 0.3s ease;
margin-left: 220px;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Suggested change
margin-left: 220px;
margin-left: 0;
Open in Devin Review

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>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 7 additional findings in Devin Review.

Open in Devin Review

Comment thread src/chats-mobile.js
Comment on lines +901 to 915
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);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

Suggested change
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');
}
});
Open in Devin Review

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>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 9 additional findings in Devin Review.

Open in Devin Review

Comment on lines 329 to +333
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Anurag-Saksena and others added 2 commits May 2, 2026 19:03
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant