Skip to content

Fix/issue 142 settings not applied on startup - #161

Merged
matt1398 merged 1 commit into
matt1398:mainfrom
nevdelap:fix/issue-142-settings-not-applied-on-startup
Apr 4, 2026
Merged

matt1398 merged 1 commit into
matt1398:mainfrom
nevdelap:fix/issue-142-settings-not-applied-on-startup

Conversation

@nevdelap

@nevdelap nevdelap commented Apr 3, 2026 •

Copy link
Copy Markdown
Contributor

I am not a Typescript guy. This is done by Claude. I use it with Docker and that works. Cheers!

Claude says:

Description:

Settings saved to ~/.claude/claude-devtools-config.json were not applied on startup — the app always launched with defaults despite the file being correctly persisted.

Three bugs combined to cause this:

1. Standalone mode never loaded config from disk
standalone.ts never called ConfigManager.initializeInstance(), so registerConfigRoutes created a default ConfigManager instance via getInstance() and the config file was never read. Added await ConfigManager.initializeInstance(configPath) at startup, using CLAUDE_ROOT to resolve the correct path when set (e.g. Docker).

2. Race condition in ConfigManager singleton initialisation
initializeInstance() created a new instance and set ConfigManager.instance only after await initialize() completed. getInstance() (called synchronously on the next line) saw instance as null and created a second default instance. The two exports referred to different objects, so awaiting the promise didn't help the synchronous export. Fixed by setting ConfigManager.instance synchronously before the await, so both exports share the same object.

3. Electron entry point didn't wait for config before use
app.whenReady() was synchronous and called initializeServices() / getConfig() while the async disk read was still in flight. Made the callback async and added await configManagerPromise before initializeServices().

Changes:

  • src/main/standalone.ts: initialise ConfigManager from disk before starting the HTTP server; derive config path from CLAUDE_ROOT when set
  • src/main/services/infrastructure/ConfigManager.ts: set singleton instance synchronously before await initialize() to eliminate the two-instance race
  • src/main/index.ts: await configManagerPromise in app.whenReady() before initialising services
  • src/main/services/discovery/ProjectScanner.ts: remove references to undeclared sessionPreviewCache (TypeScript compile error)

Summary by CodeRabbit

  • Chores

    • Enhanced startup sequence to ensure configuration loads before service initialization begins
    • Refined service initialization timing for improved startup reliability
  • Changes

    • Adjusted cache refresh behavior for project updates; preview data refresh timing may differ

@coderabbitai coderabbitai Bot added the bug Something isn't working label Apr 3, 2026

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a mechanism to ensure configuration is loaded from disk before services are initialized in the main process. It also removes redundant cache clearing logic in the ProjectScanner. A critical issue was identified regarding a potential race condition where the imported configManager instance might not be synchronized with the state of the configManagerPromise, and a suggestion was provided to initialize the instance directly to ensure consistency.

Comment thread src/main/index.ts
logger.info('App ready, initializing...');
try {
// Wait for config to finish loading from disk before using it
await configManagerPromise;

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.

high

There is a potential race condition in ConfigManager.ts that may cause the configManager instance imported at line 63 to be different from the one initialized by configManagerPromise.

Specifically, ConfigManager.getInstance() (which populates the configManager export) may create a default instance before ConfigManager.initializeInstance() (which populates configManagerPromise) finishes and overwrites the static singleton. Since the configManager export is a const binding to the object returned by getInstance() at load time, it will not be updated when the singleton is overwritten. Consequently, initializeServices() might still use default settings even after the await.

To fix this properly, ConfigManager.ts should be updated to ensure both exports share the same instance. As a workaround in this file, you can initialize the imported instance directly to ensure it has the latest data from disk.

Suggested change
await configManagerPromise;
await configManager.initialize();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Told Claude about this. Update coming.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude's update is the change in src/main/services/infrastructure/ConfigManager.ts. I republished and redeployed and it is still working.

@nevdelap nevdelap Apr 3, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I told Claude: Gemini suggested await configManagerPromise; -> await configManager.initialize();

Image

@coderabbitai

coderabbitai Bot commented Apr 3, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Await configuration loading at startup by awaiting configManagerPromise before config-dependent initialization; publish the ConfigManager singleton earlier during initialization; stop removing sessionPreviewCache entries in project cache invalidation; add explicit config initialization in the standalone startup path.

Changes

Cohort / File(s) Summary
Startup & config initialization
src/main/index.ts, src/main/standalone.ts, src/main/services/infrastructure/ConfigManager.ts
Made the app.whenReady() handler async and await configManagerPromise before any config-dependent initialization; added explicit config load in standalone (ConfigManager.initializeInstance(configPath)); ConfigManager.initializeInstance() now assigns ConfigManager.instance before awaiting instance.initialize(), exposing the singleton earlier.
Project cache invalidation
src/main/services/discovery/ProjectScanner.ts
invalidateCachesForProject(projectId) no longer deletes entries from sessionPreviewCache; it only invalidates contentPresenceCache and sessionMetadataCache for the project's path prefix. Potential for stale session previews after project changes.

Possibly related PRs


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/main/services/infrastructure/ConfigManager.ts (1)

970-974: Dual export pattern is appropriate; consider clarifying usage in the comment.

The pattern correctly provides both a promise for initialization sequencing and synchronous access to the same instance. The comment on line 973 accurately states the instance "uses defaults until initialized."

For maintainability, consider expanding the comment to explicitly state that callers should await configManagerPromise before using configManager:

📝 Suggested clarification
 /** Singleton instance promise - resolves to an initialized ConfigManager */
 export const configManagerPromise = ConfigManager.initializeInstance();

-/** Singleton instance for convenience (synchronous access, uses defaults until initialized) */
+/** 
+ * Singleton instance for convenience (synchronous access, uses defaults until initialized).
+ * Callers must await configManagerPromise before relying on disk-loaded config.
+ */
 export const configManager = ConfigManager.getInstance();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/services/infrastructure/ConfigManager.ts` around lines 970 - 974,
Update the comment above the dual exports to explicitly document usage: state
that configManagerPromise is the initialization promise from
ConfigManager.initializeInstance() and that callers should await
configManagerPromise before relying on runtime-loaded configuration from the
synchronous export configManager (from ConfigManager.getInstance()), otherwise
configManager will return default values until initialization completes; mention
both exports reference the same singleton instance so awaiting the promise
ensures the same instance is fully initialized.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/main/services/infrastructure/ConfigManager.ts`:
- Around line 970-974: Update the comment above the dual exports to explicitly
document usage: state that configManagerPromise is the initialization promise
from ConfigManager.initializeInstance() and that callers should await
configManagerPromise before relying on runtime-loaded configuration from the
synchronous export configManager (from ConfigManager.getInstance()), otherwise
configManager will return default values until initialization completes; mention
both exports reference the same singleton instance so awaiting the promise
ensures the same instance is fully initialized.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1005c645-cc47-4e0e-bddf-6bd26840b6ef

📥 Commits

Reviewing files that changed from the base of the PR and between 38213f1 and 86b849a.

📒 Files selected for processing (1)
  • src/main/services/infrastructure/ConfigManager.ts

for (const key of this.sessionMetadataCache.keys()) {
if (key.startsWith(prefix)) this.sessionMetadataCache.delete(key);
}
for (const key of this.sessionPreviewCache.keys()) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude says: Removed two references to sessionPreviewCache which was never declared, causing a TypeScript compile error.

Comment thread src/main/index.ts
logger.info('App ready, initializing...');
try {
// Wait for config to finish loading from disk before using it
await configManagerPromise;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Told Claude about this. Update coming.

Comment thread src/main/index.ts
logger.info('App ready, initializing...');
try {
// Wait for config to finish loading from disk before using it
await configManagerPromise;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude's update is the change in src/main/services/infrastructure/ConfigManager.ts. I republished and redeployed and it is still working.

@nevdelap
nevdelap marked this pull request as draft April 3, 2026 04:27
@nevdelap

nevdelap commented Apr 3, 2026

Copy link
Copy Markdown
Contributor Author

Actually, I am not sure it is working. Converted to draft.

- Await configManagerPromise in app.whenReady() so config is loaded
  from disk before initializeServices() and getConfig() are called
- Fix ConfigManager singleton race: set instance synchronously before
  awaiting initialize() so getInstance() returns the same object
- Remove undeclared sessionPreviewCache references (TS error)
- Load config from disk in standalone/Docker mode; use CLAUDE_ROOT
  path when set so Docker mounts are respected

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread src/main/standalone.ts
@@ -13,14 +13,15 @@
*/

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixes for running in Docker.

Comment thread src/main/index.ts
@@ -61,6 +61,7 @@ process.on('uncaughtException', (error) => {
import { HttpServer } from './services/infrastructure/HttpServer';
import {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixes for running in Electron.

@nevdelap
nevdelap marked this pull request as ready for review April 3, 2026 05:19

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/services/infrastructure/ConfigManager.ts`:
- Around line 357-362: Make initializeInstance single-flight and guard against
conflicting configPath values: add a static initializingPromise (or similar)
that you set before calling instance.initialize() so concurrent callers await
the same promise instead of starting parallel initialize() flows; if
initializeInstance is called while initializingPromise exists, compare the
incoming configPath to the already-registered instance.configPath (or the
initial configPath passed when creating the first ConfigManager) and reject or
normalize if they differ to avoid out-of-order side effects (e.g.,
setClaudeBasePathOverride). Ensure you clear initializingPromise on
completion/failure and keep the assignment to ConfigManager.instance only once
(first-writer wins) so subsequent callers get the same instance.

In `@src/main/standalone.ts`:
- Around line 90-99: The startup config initialization in async function start
currently awaits ConfigManager.initializeInstance(...) without catching errors,
which can leave the process partially started; wrap the initializeInstance call
in a try/catch inside start and on error log the error (use console.error and/or
logger.error) and exit the process with a non-zero code (e.g., process.exit(1));
ensure you reference the ConfigManager.initializeInstance call and preserve
existing logging around logger.info('Starting standalone server...') while
failing fast when initialization throws.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 36328b3e-5f9e-4bad-9417-11bb793bf63c

📥 Commits

Reviewing files that changed from the base of the PR and between 86b849a and e55deaa.

📒 Files selected for processing (4)
  • src/main/index.ts
  • src/main/services/discovery/ProjectScanner.ts
  • src/main/services/infrastructure/ConfigManager.ts
  • src/main/standalone.ts
💤 Files with no reviewable changes (1)
  • src/main/services/discovery/ProjectScanner.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/index.ts

Comment on lines 357 to 362
static async initializeInstance(configPath?: string): Promise<ConfigManager> {
const instance = new ConfigManager(configPath);
await instance.initialize();
ConfigManager.instance = instance;
await instance.initialize();
return instance;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Prevent concurrent initializeInstance() calls from racing with different config paths.

Publishing the instance before await fixes the previous duplication bug, but this still allows parallel initializations (e.g., module-level configManagerPromise plus standalone Line 98). That can run two initialize() flows and apply side effects out of order (setClaudeBasePathOverride), making startup nondeterministic.

💡 Suggested fix (single-flight initialization + path consistency guard)
 export class ConfigManager {
   private config: AppConfig;
   private readonly configPath: string;
   private static instance: ConfigManager | null = null;
+  private static initializePromise: Promise<ConfigManager> | null = null;
+  private static initializePath: string | undefined;
   private triggerManager: TriggerManager;
@@
   static async initializeInstance(configPath?: string): Promise<ConfigManager> {
+    if (ConfigManager.initializePromise) {
+      if (ConfigManager.initializePath !== configPath) {
+        throw new Error(
+          `ConfigManager already initializing with a different path: ${ConfigManager.initializePath ?? '<default>'}`
+        );
+      }
+      return ConfigManager.initializePromise;
+    }
+
     const instance = new ConfigManager(configPath);
     ConfigManager.instance = instance;
-    await instance.initialize();
-    return instance;
+    ConfigManager.initializePath = configPath;
+    ConfigManager.initializePromise = instance
+      .initialize()
+      .then(() => instance)
+      .catch((error) => {
+        ConfigManager.instance = null;
+        throw error;
+      })
+      .finally(() => {
+        ConfigManager.initializePromise = null;
+      });
+    return ConfigManager.initializePromise;
   }
@@
   static resetInstance(): void {
     ConfigManager.instance = null;
+    ConfigManager.initializePromise = null;
+    ConfigManager.initializePath = undefined;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/services/infrastructure/ConfigManager.ts` around lines 357 - 362,
Make initializeInstance single-flight and guard against conflicting configPath
values: add a static initializingPromise (or similar) that you set before
calling instance.initialize() so concurrent callers await the same promise
instead of starting parallel initialize() flows; if initializeInstance is called
while initializingPromise exists, compare the incoming configPath to the
already-registered instance.configPath (or the initial configPath passed when
creating the first ConfigManager) and reject or normalize if they differ to
avoid out-of-order side effects (e.g., setClaudeBasePathOverride). Ensure you
clear initializingPromise on completion/failure and keep the assignment to
ConfigManager.instance only once (first-writer wins) so subsequent callers get
the same instance.

Comment thread src/main/standalone.ts
Comment on lines 90 to +99
async function start(): Promise<void> {
logger.info('Starting standalone server...');

// Load config from disk before anything else uses it.
// When CLAUDE_ROOT is set (e.g. Docker), config lives there rather than ~/.claude.
const configPath = CLAUDE_ROOT
? path.join(CLAUDE_ROOT, 'claude-devtools-config.json')
: undefined;
await ConfigManager.initializeInstance(configPath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Wrap startup config initialization in try/catch and fail fast.

If initializeInstance() rejects, void start() falls through to global unhandled rejection logging and can leave the process in a partial startup state. Handle this locally and exit with a non-zero code.

🛠️ Suggested patch
 async function start(): Promise<void> {
-  logger.info('Starting standalone server...');
+  try {
+    logger.info('Starting standalone server...');
@@
-  const configPath = CLAUDE_ROOT
-    ? path.join(CLAUDE_ROOT, 'claude-devtools-config.json')
-    : undefined;
-  await ConfigManager.initializeInstance(configPath);
+    const configPath = CLAUDE_ROOT
+      ? path.join(CLAUDE_ROOT, 'claude-devtools-config.json')
+      : undefined;
+    await ConfigManager.initializeInstance(configPath);
@@
-  const port = await httpServer.start(services, modeSwitchHandler, PORT, HOST);
-  logger.info(`Standalone server running at http://${HOST}:${port}`);
-  logger.info('Open in your browser to view Claude Code sessions');
+    const port = await httpServer.start(services, modeSwitchHandler, PORT, HOST);
+    logger.info(`Standalone server running at http://${HOST}:${port}`);
+    logger.info('Open in your browser to view Claude Code sessions');
+  } catch (error) {
+    console.error('Standalone startup failed:', error);
+    logger.error('Standalone startup failed:', error);
+    process.exit(1);
+  }
 }

As per coding guidelines "Implement error handling with try/catch in main process and console.error for debugging".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/standalone.ts` around lines 90 - 99, The startup config
initialization in async function start currently awaits
ConfigManager.initializeInstance(...) without catching errors, which can leave
the process partially started; wrap the initializeInstance call in a try/catch
inside start and on error log the error (use console.error and/or logger.error)
and exit the process with a non-zero code (e.g., process.exit(1)); ensure you
reference the ConfigManager.initializeInstance call and preserve existing
logging around logger.info('Starting standalone server...') while failing fast
when initialization throws.

@nevdelap

nevdelap commented Apr 3, 2026 •

Copy link
Copy Markdown
Contributor Author

Fixes it in Docker and the AppImage running on Linux. I don't know Typescript so I don't know if coderabbitai's comments are valid.

Docker tested in Linux with...

docker run --rm --name claude-devtools -p 3456:3456 -v /home/nevd/.claude:/data/.claude claude-devtools

App tested in Linux with...

> curl -L https://github.com/nevdelap/claude-devtools/releases/download/v0.4.10/claude-devtools-0.4.10.AppImage -o /tmp/claude-devtools.AppImage
> nix-shell -p appimage-run --run "appimage-run /tmp/claude-devtools.AppImage"

Both read and save the config and retain the config changes between restarts, tested by switching between light and dark modes.

@matt1398
matt1398 merged commit 4a0274f into matt1398:main Apr 4, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants