Skip to content

feat: reload Kaoto editor on external file changes#1330

Open
bricefrisco wants to merge 3 commits into
KaotoIO:mainfrom
bricefrisco:main
Open

feat: reload Kaoto editor on external file changes#1330
bricefrisco wants to merge 3 commits into
KaotoIO:mainfrom
bricefrisco:main

Conversation

@bricefrisco
Copy link
Copy Markdown

@bricefrisco bricefrisco commented Mar 26, 2026

Fixes #1192 by implementing an ExternalFileChangeWatcher.

This is especially useful when using AI agents to update route files, as the file must currently be closed and reopened to see the changes.

Demo video:
https://github.com/user-attachments/assets/229004c0-7c93-4d41-8eac-a5d7b7ec0a4b

Summary by CodeRabbit

  • New Features

    • Editor now auto-reloads diagram content when the underlying YAML file is changed externally, with debouncing to avoid redundant updates and suppression of reloads triggered by saves from the editor.
  • Tests

    • Added end-to-end and unit tests validating external-change detection, auto-reload behavior, suppression for editor-initiated saves, and proper cleanup of watchers.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 26, 2026

📝 Walkthrough

Walkthrough

Adds a debounced file watcher that detects external edits to a single YAML file (ignoring VS Code self-saves), integrates it into the Kaoto editor channel to auto-reload editor content on external changes, and includes unit and end-to-end tests validating the behavior.

Changes

Cohort / File(s) Summary
File System Watcher Implementation
src/helpers/ExternalFileChangeWatcher.ts
New exported class that watches a single vscode.Uri via workspace.createFileSystemWatcher, debounces change events (default 300ms), reads UTF‑8 file contents after debounce, suppresses callbacks when content equals the last VS Code self-save (tracked via onDidSaveTextDocument), and exposes dispose() to clean up watcher, listener, and timers.
Watcher Unit Tests
src/test/helpers/ExternalFileChangeWatcher.test.ts
New Mocha/TypeScript tests that create a temp YAML in the workspace and verify: external fs writes invoke the callback with new content; VS Code self-saves are suppressed while subsequent external writes are observed; and disposing the watcher prevents further callbacks.
Watcher Integration into Webview API
src/webview/VSCodeKaotoEditorChannelApi.ts
Adds a private externalFileChangeWatcher field and constructs it for the current document URI; on external change, computes workspace-relative or basename path, calls editor.setContent(normalizedPath, content), logs an info message, and disposes the watcher when the panel is disposed.
End-to-End Test
it-tests/ExternalFileChange.test.ts
New E2E test that opens a Kaoto editor for a YAML fixture, copies it to a temp file (including special characters), asserts initial diagram state, externally overwrites the file, waits for the editor to auto-reload and reflect the new route, and performs cleanup (remove temp file, close editor).

Sequence Diagram

sequenceDiagram
    actor External as External Process (e.g., git pull)
    participant FSW as FileSystemWatcher
    participant Watcher as ExternalFileChangeWatcher
    participant API as VSCodeKaotoEditorChannelApi
    participant Editor as Kaoto Editor

    External->>FSW: Modify YAML file on disk
    FSW->>Watcher: onDidChange event
    Watcher->>Watcher: Start debounce timer (300ms)
    Watcher->>Watcher: Read file contents from disk (UTF-8)
    Watcher->>Watcher: Compare with lastSelfSavedContent
    alt External change detected
        Watcher->>API: onExternalChange(newContent)
        API->>API: Normalize path (workspace-relative or basename)
        API->>Editor: setContent(normalizedPath, content)
        Editor->>Editor: Refresh diagram
    else Self-save detected
        Watcher->>Watcher: Suppress callback
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I nibble lines where YAML sleeps,

I wait a moment, debounce my peeps,
If hands outside repaint the page,
I twitch and wake the editor's stage,
Hop—diagram refreshed—another happy leap.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: reload Kaoto editor on external file changes' directly and clearly summarizes the main change: implementing automatic editor reloading when files are externally modified.
Linked Issues check ✅ Passed The PR implements external file change detection and automatic editor reloading, fulfilling issue #1192's requirement for automatic diagram updates after external changes like git pull.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the external file watcher and editor reload functionality: the watcher class, integration into the editor channel API, and corresponding tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@lhein
Copy link
Copy Markdown
Contributor

lhein commented Mar 26, 2026

Looks good. Thanks for also providing the video. Have you tested the case when you have conflicting changes? Means when I open the Kaoto editor and have unsaved changes and then the process outside changes the file too?

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
it-tests/ExternalFileChange.test.ts (1)

54-64: Consider wrapping closeEditor in try-catch for robustness.

If closeEditor throws (e.g., editor already closed or unexpected modal state), the afterEach hook would fail. Wrapping it would make cleanup more resilient across various failure modes.

♻️ Proposed defensive cleanup
 afterEach(async function () {
 	if (globalKaotoWebView !== undefined) {
 		try {
 			await globalKaotoWebView.switchBack();
 		} catch {
 			// editor may already be closed, continue
 		}
 		globalKaotoWebView = undefined;
 	}
-	await closeEditor(testFileName, false);
+	try {
+		await closeEditor(testFileName, false);
+	} catch {
+		// editor may already be closed or unavailable, continue
+	}
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@it-tests/ExternalFileChange.test.ts` around lines 54 - 64, The afterEach
cleanup should not fail if closeEditor throws; wrap the existing await
closeEditor(testFileName, false) call inside a try-catch so any errors (e.g.,
editor already closed or modal issues) are caught and ignored or logged for
debugging; update the afterEach block that references globalKaotoWebView,
switchBack(), and closeEditor(testFileName, false) to perform the closeEditor
call inside a try { await closeEditor(...) } catch (e) { /* swallow or log e */
} to make cleanup robust.
src/webview/VSCodeKaotoEditorChannelApi.ts (1)

41-46: Consider wrapping editor.setContent in try-catch.

If editor.setContent throws, the error may surface as an unhandled promise rejection. Wrapping the call would allow for graceful error handling and logging.

♻️ Proposed defensive error handling
 this.externalFileChangeWatcher = new ExternalFileChangeWatcher(docUri, async (content) => {
 	const normalizedPath = workspaceFolder
 		? path.relative(workspaceFolder.uri.fsPath, docUri.fsPath).split(path.sep).join('/')
 		: path.basename(docUri.fsPath);
-	await editor.setContent(normalizedPath, content);
-	KaotoOutputChannel.logInfo(`Kaoto editor reloaded after external change to: ${docUri.fsPath}`);
+	try {
+		await editor.setContent(normalizedPath, content);
+		KaotoOutputChannel.logInfo(`Kaoto editor reloaded after external change to: ${docUri.fsPath}`);
+	} catch (ex) {
+		KaotoOutputChannel.logError(`Failed to reload Kaoto editor after external change to: ${docUri.fsPath}`, ex);
+	}
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/webview/VSCodeKaotoEditorChannelApi.ts` around lines 41 - 46, The async
callback passed to ExternalFileChangeWatcher can cause an unhandled rejection if
editor.setContent throws; wrap the await editor.setContent(normalizedPath,
content) in a try-catch inside that callback, on success keep the
KaotoOutputChannel.logInfo(...) call, and on catch call
KaotoOutputChannel.logError(...) (including the error and docUri.fsPath or
normalizedPath) so failures are logged and don't surface as unhandled promise
rejections.
src/helpers/ExternalFileChangeWatcher.ts (1)

56-60: Potential for callback invocation after dispose.

If dispose() is called while handleFileChange() is mid-execution (after the timer fires but before onExternalChange completes), the callback may still run. Consider adding a disposed flag to guard the callback invocation.

♻️ Proposed fix with disposed guard
 export class ExternalFileChangeWatcher implements vscode.Disposable {
 	private readonly didSaveDisposable: vscode.Disposable;
 	private readonly fileWatcher: vscode.FileSystemWatcher;
 	private lastSelfSavedContent: string | undefined;
 	private debounceTimer: NodeJS.Timeout | undefined;
+	private disposed = false;
 
 	// ... constructor ...
 
 	private async handleFileChange(): Promise<void> {
+		if (this.disposed) {
+			return;
+		}
 		const content = await fs.readFile(this.docUri.fsPath, 'utf8');
 		// ...
 	}
 
 	dispose(): void {
+		this.disposed = true;
 		this.fileWatcher.dispose();
 		// ...
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/helpers/ExternalFileChangeWatcher.ts` around lines 56 - 60, Add a
disposed guard to prevent callbacks running after disposal: introduce a boolean
(e.g., this.disposed) set to true in dispose(), ensure dispose clears any
pending this.debounceTimer, and modify the timeout callback and the async
handleFileChange() to check this.disposed before proceeding (and again before
calling onExternalChange) so that if dispose() was called while the timer fired
or while handleFileChange is mid-execution the callback short-circuits and does
not call onExternalChange.
🤖 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/helpers/ExternalFileChangeWatcher.ts`:
- Around line 63-73: In handleFileChange, wrap the
fs.readFile(this.docUri.fsPath, 'utf8') call in a try/catch so file read errors
(e.g., file deleted/renamed/permission denied) do not cause unhandled
rejections; on error set this.lastSelfSavedContent = undefined and bail out (or
optionally log the error) instead of letting the exception propagate, then only
call this.onExternalChange(content) when the read succeeded and content differs
from this.lastSelfSavedContent.

---

Nitpick comments:
In `@it-tests/ExternalFileChange.test.ts`:
- Around line 54-64: The afterEach cleanup should not fail if closeEditor
throws; wrap the existing await closeEditor(testFileName, false) call inside a
try-catch so any errors (e.g., editor already closed or modal issues) are caught
and ignored or logged for debugging; update the afterEach block that references
globalKaotoWebView, switchBack(), and closeEditor(testFileName, false) to
perform the closeEditor call inside a try { await closeEditor(...) } catch (e) {
/* swallow or log e */ } to make cleanup robust.

In `@src/helpers/ExternalFileChangeWatcher.ts`:
- Around line 56-60: Add a disposed guard to prevent callbacks running after
disposal: introduce a boolean (e.g., this.disposed) set to true in dispose(),
ensure dispose clears any pending this.debounceTimer, and modify the timeout
callback and the async handleFileChange() to check this.disposed before
proceeding (and again before calling onExternalChange) so that if dispose() was
called while the timer fired or while handleFileChange is mid-execution the
callback short-circuits and does not call onExternalChange.

In `@src/webview/VSCodeKaotoEditorChannelApi.ts`:
- Around line 41-46: The async callback passed to ExternalFileChangeWatcher can
cause an unhandled rejection if editor.setContent throws; wrap the await
editor.setContent(normalizedPath, content) in a try-catch inside that callback,
on success keep the KaotoOutputChannel.logInfo(...) call, and on catch call
KaotoOutputChannel.logError(...) (including the error and docUri.fsPath or
normalizedPath) so failures are logged and don't surface as unhandled promise
rejections.
🪄 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: 1812f034-21b1-4213-a59f-93839b11247d

📥 Commits

Reviewing files that changed from the base of the PR and between 5c0f53a and b34b7f5.

📒 Files selected for processing (4)
  • it-tests/ExternalFileChange.test.ts
  • src/helpers/ExternalFileChangeWatcher.ts
  • src/test/helpers/ExternalFileChangeWatcher.test.ts
  • src/webview/VSCodeKaotoEditorChannelApi.ts

Comment thread src/helpers/ExternalFileChangeWatcher.ts
@bricefrisco
Copy link
Copy Markdown
Author

bricefrisco commented Mar 26, 2026

Looks good. Thanks for also providing the video. Have you tested the case when you have conflicting changes? Means when I open the Kaoto editor and have unsaved changes and then the process outside changes the file too?

Ah, good thought.. Unfortunately if another process overwrites the file, the unsaved local changes will be lost.
I may need to think about that some more. This is where things could get tricky, if conflict resolution needs to happen.

@lhein
Copy link
Copy Markdown
Contributor

lhein commented Mar 26, 2026

Looks good. Thanks for also providing the video. Have you tested the case when you have conflicting changes? Means when I open the Kaoto editor and have unsaved changes and then the process outside changes the file too?

Ah, good thought.. Unfortunately if another process overwrites the file, the unsaved local changes will be lost. I may need to think about that some more. This is where things could get tricky, if conflict resolution needs to happen.

Just throwing an idea...
I think the editor changes should have priority over the external changes. We could however in the case of detection show a warning dialog to the user and ask what to do.

  • Discard local changes and reload
  • Keep local changes and not reload (which would be same as Cancel)

@bricefrisco
Copy link
Copy Markdown
Author

Looks good. Thanks for also providing the video. Have you tested the case when you have conflicting changes? Means when I open the Kaoto editor and have unsaved changes and then the process outside changes the file too?

Ah, good thought.. Unfortunately if another process overwrites the file, the unsaved local changes will be lost. I may need to think about that some more. This is where things could get tricky, if conflict resolution needs to happen.

Just throwing an idea... I think the editor changes should have priority over the external changes. We could however in the case of detection show a warning dialog to the user and ask what to do.

  • Discard local changes and reload
  • Keep local changes and not reload (which would be same as Cancel)

Great idea, I'll get started on this. Thanks!

@lhein
Copy link
Copy Markdown
Contributor

lhein commented Mar 26, 2026

But I would only show the dialog if the current Kaoto Editor is marked dirty and then an external change is detected. Otherwise it will end up with a lot of annoying dialogs for the user :D

@bricefrisco
Copy link
Copy Markdown
Author

This change is a bit harder than I expected because the editor is being marked as dirty even when no local modifications are made but background updates happen.

This complicates the logic because subsequent updates will trigger the prompt each time even if no local changes were made.

I'm thinking through how to solve this, will look more into it once I get some time.

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Apr 9, 2026

@lhein
Copy link
Copy Markdown
Contributor

lhein commented Apr 9, 2026

rebased the PR and also comitted the suggested try/catch change from coderabbit

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/webview/VSCodeKaotoEditorChannelApi.ts (1)

24-24: Mark the field as readonly since it's never reassigned.

SonarCloud correctly identified that externalFileChangeWatcher is only assigned in the constructor and never reassigned.

♻️ Proposed fix
-	private externalFileChangeWatcher: ExternalFileChangeWatcher | undefined;
+	private readonly externalFileChangeWatcher: ExternalFileChangeWatcher | undefined;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/webview/VSCodeKaotoEditorChannelApi.ts` at line 24, The field
externalFileChangeWatcher on class VSCodeKaotoEditorChannelApi is only assigned
in the constructor and should be declared readonly; update the declaration
"private externalFileChangeWatcher: ExternalFileChangeWatcher | undefined" to
"private readonly externalFileChangeWatcher: ExternalFileChangeWatcher |
undefined" and keep the existing assignment in the constructor (no other code
changes needed).
src/helpers/ExternalFileChangeWatcher.ts (1)

17-18: Consider using Node.js protocol prefixes for built-in modules.

SonarCloud flagged the use of fs and path without the node: prefix. Using the node: prefix is a modern best practice that makes it explicit these are Node.js built-in modules and avoids potential naming conflicts with npm packages.

♻️ Proposed fix
-import { promises as fs } from 'fs';
-import * as path from 'path';
+import { promises as fs } from 'node:fs';
+import * as path from 'node:path';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/helpers/ExternalFileChangeWatcher.ts` around lines 17 - 18, Update the
built-in module imports to use Node.js protocol prefixes: replace the current
imports of fs and path (the lines importing "{ promises as fs } from 'fs'" and
"import * as path from 'path'") with their node: counterparts so the module
resolution is explicit (use node:fs and node:path); keep the same imported
symbols (fs and path) so usages throughout ExternalFileChangeWatcher (e.g., any
functions/methods referencing fs.promises or path.*) continue to work unchanged.
🤖 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/helpers/ExternalFileChangeWatcher.ts`:
- Around line 17-18: Update the built-in module imports to use Node.js protocol
prefixes: replace the current imports of fs and path (the lines importing "{
promises as fs } from 'fs'" and "import * as path from 'path'") with their node:
counterparts so the module resolution is explicit (use node:fs and node:path);
keep the same imported symbols (fs and path) so usages throughout
ExternalFileChangeWatcher (e.g., any functions/methods referencing fs.promises
or path.*) continue to work unchanged.

In `@src/webview/VSCodeKaotoEditorChannelApi.ts`:
- Line 24: The field externalFileChangeWatcher on class
VSCodeKaotoEditorChannelApi is only assigned in the constructor and should be
declared readonly; update the declaration "private externalFileChangeWatcher:
ExternalFileChangeWatcher | undefined" to "private readonly
externalFileChangeWatcher: ExternalFileChangeWatcher | undefined" and keep the
existing assignment in the constructor (no other code changes needed).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 81174ee2-b15c-472e-a349-59a349612033

📥 Commits

Reviewing files that changed from the base of the PR and between c9b32d0 and 3c5bcd2.

📒 Files selected for processing (4)
  • it-tests/ExternalFileChange.test.ts
  • src/helpers/ExternalFileChangeWatcher.ts
  • src/test/helpers/ExternalFileChangeWatcher.test.ts
  • src/webview/VSCodeKaotoEditorChannelApi.ts
✅ Files skipped from review due to trivial changes (1)
  • src/test/helpers/ExternalFileChangeWatcher.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • it-tests/ExternalFileChange.test.ts

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.

Update route diagram after a git pull

2 participants