- ${this.openFiles.length === 0 ? html`
+ ${this.showDiffView && this.diffViewConfig ? html`
+
{ this.showDiffView = false; this.diffViewConfig = null; }}
+ >
+ ` : this.openFiles.length === 0 ? html`
Select a file to edit
@@ -992,6 +1021,9 @@ testSmartPromise();
.executionEnvironment=${this.executionEnvironment}
@run-process=${this.handleRunProcess}
>
+
+
+
`;
}
@@ -1016,6 +1048,7 @@ testSmartPromise();
this.autoSaveInterval = null;
}
this.stopNodeModulesWatcher();
+ this.stopAllFileWatchers();
}
public async firstUpdated() {
@@ -1023,6 +1056,9 @@ testSmartPromise();
this.currentFileTreeWidth = this.fileTreeWidth;
this.currentTerminalHeight = this.terminalHeight;
+ // Get actionbar reference for file change notifications
+ this.actionbarElement = this.shadowRoot?.querySelector('dees-actionbar') as DeesActionbar;
+
if (this.executionEnvironment) {
await this.initializeWorkspace();
}
@@ -1186,6 +1222,191 @@ testSmartPromise();
}
}
+ // ========== Open File Watching for External Changes ==========
+
+ /**
+ * Start watching an open file for external changes
+ */
+ private startWatchingFile(path: string): void {
+ if (!this.executionEnvironment || this.openFileWatchers.has(path)) return;
+
+ try {
+ const watcher = this.executionEnvironment.watch(
+ path,
+ (_event, _filename) => {
+ // Debounce to avoid multiple rapid triggers
+ const existingTimeout = this.fileChangeDebounce.get(path);
+ if (existingTimeout) {
+ clearTimeout(existingTimeout);
+ }
+ const timeout = setTimeout(() => {
+ this.handleExternalFileChange(path);
+ this.fileChangeDebounce.delete(path);
+ }, 300);
+ this.fileChangeDebounce.set(path, timeout);
+ }
+ );
+ this.openFileWatchers.set(path, watcher);
+ } catch (error) {
+ console.warn(`Could not watch file ${path}:`, error);
+ }
+ }
+
+ /**
+ * Stop watching a file when it's closed
+ */
+ private stopWatchingFile(path: string): void {
+ const watcher = this.openFileWatchers.get(path);
+ if (watcher) {
+ watcher.stop();
+ this.openFileWatchers.delete(path);
+ }
+ const timeout = this.fileChangeDebounce.get(path);
+ if (timeout) {
+ clearTimeout(timeout);
+ this.fileChangeDebounce.delete(path);
+ }
+ }
+
+ /**
+ * Stop all file watchers
+ */
+ private stopAllFileWatchers(): void {
+ for (const watcher of this.openFileWatchers.values()) {
+ watcher.stop();
+ }
+ this.openFileWatchers.clear();
+
+ for (const timeout of this.fileChangeDebounce.values()) {
+ clearTimeout(timeout);
+ }
+ this.fileChangeDebounce.clear();
+ }
+
+ /**
+ * Handle external file change - show actionbar if file has local changes,
+ * otherwise silently update with cursor preservation
+ */
+ private async handleExternalFileChange(path: string): Promise
{
+ const file = this.openFiles.find(f => f.path === path);
+ if (!file || !this.executionEnvironment) return;
+
+ try {
+ // Read the new content from disk
+ const newContent = await this.executionEnvironment.readFile(path);
+
+ // If content is same as what we have, no action needed
+ if (newContent === file.content) return;
+
+ if (file.modified) {
+ // File has unsaved local changes AND disk changed - conflict!
+ const result = await this.actionbarElement?.show({
+ message: `"${file.name}" changed on disk. What do you want to do?`,
+ type: 'question',
+ icon: 'lucide:gitMerge',
+ actions: [
+ { id: 'load-disk', label: 'Load from Disk', primary: true },
+ { id: 'save-local', label: 'Save Local to Disk' },
+ { id: 'compare', label: 'Compare' },
+ ],
+ timeout: { duration: 15000, defaultActionId: 'load-disk' },
+ dismissible: true,
+ });
+
+ if (result?.actionId === 'load-disk') {
+ // Discard local changes, load disk version
+ await this.updateFileContent(path, newContent, false);
+ } else if (result?.actionId === 'save-local') {
+ // Keep local changes and save to disk (overwrite external)
+ await this.executionEnvironment.writeFile(path, file.content);
+ // Mark as saved
+ this.openFiles = this.openFiles.map(f =>
+ f.path === path ? { ...f, modified: false } : f
+ );
+ } else if (result?.actionId === 'compare') {
+ // Open diff view
+ this.openDiffView(path, file.content, newContent);
+ }
+ // If dismissed, do nothing - user can manually resolve later
+ } else {
+ // No local changes - silently update with cursor preservation
+ await this.updateFileContent(path, newContent, true);
+ }
+ } catch (error) {
+ console.warn(`Failed to handle external change for ${path}:`, error);
+ }
+ }
+
+ /**
+ * Update file content in state and optionally in the editor
+ */
+ private async updateFileContent(
+ path: string,
+ newContent: string,
+ preserveCursor: boolean
+ ): Promise {
+ // Update internal state
+ this.openFiles = this.openFiles.map(f =>
+ f.path === path ? { ...f, content: newContent, modified: false } : f
+ );
+
+ // If this is the active file, update Monaco editor
+ if (path === this.activeFilePath) {
+ const editor = this.shadowRoot?.querySelector('dees-workspace-monaco') as DeesWorkspaceMonaco;
+ if (editor) {
+ await editor.setContentExternal(newContent, preserveCursor);
+ }
+ }
+ }
+
+ /**
+ * Open the diff view to compare local and disk versions
+ */
+ private openDiffView(path: string, localContent: string, diskContent: string): void {
+ this.diffViewConfig = {
+ filePath: path,
+ originalContent: diskContent,
+ modifiedContent: localContent,
+ language: this.getLanguageFromPath(path),
+ };
+ this.showDiffView = true;
+ }
+
+ /**
+ * Handle diff view resolution
+ */
+ private async handleDiffResolved(e: CustomEvent): Promise {
+ const { action, content } = e.detail;
+ const path = this.diffViewConfig?.filePath;
+
+ if (!path || !this.executionEnvironment) {
+ this.showDiffView = false;
+ this.diffViewConfig = null;
+ return;
+ }
+
+ if (action === 'use-local') {
+ // Save local content to disk
+ await this.executionEnvironment.writeFile(path, content);
+ this.openFiles = this.openFiles.map(f =>
+ f.path === path ? { ...f, content, modified: false } : f
+ );
+ // Update editor if active
+ if (path === this.activeFilePath) {
+ const editor = this.shadowRoot?.querySelector('dees-workspace-monaco') as DeesWorkspaceMonaco;
+ if (editor) {
+ await editor.setContentExternal(content, false);
+ }
+ }
+ } else if (action === 'use-disk') {
+ // Update editor with disk content
+ await this.updateFileContent(path, content, false);
+ }
+
+ this.showDiffView = false;
+ this.diffViewConfig = null;
+ }
+
private async handleFileSelect(e: CustomEvent<{ path: string; name: string }>) {
const { path, name } = e.detail;
await this.openFile(path, name);
@@ -1210,6 +1431,9 @@ testSmartPromise();
];
this.activeFilePath = path;
+ // Start watching for external changes
+ this.startWatchingFile(path);
+
// Initialize IntelliSense lazily after first file opens (Monaco loads on demand)
if (!this.intelliSenseInitialized) {
// Wait for Monaco editor to mount and load Monaco from CDN
@@ -1246,6 +1470,9 @@ testSmartPromise();
if (!confirmed) return;
}
+ // Stop watching this file
+ this.stopWatchingFile(path);
+
this.openFiles = this.openFiles.filter(f => f.path !== path);
// If closing the active file, activate another one
diff --git a/ts_web/elements/00group-workspace/index.ts b/ts_web/elements/00group-workspace/index.ts
index 5b217f5..261227a 100644
--- a/ts_web/elements/00group-workspace/index.ts
+++ b/ts_web/elements/00group-workspace/index.ts
@@ -7,3 +7,4 @@ export * from './dees-workspace-terminal-preview/index.js';
export * from './dees-workspace-markdown/index.js';
export * from './dees-workspace-markdownoutlet/index.js';
export * from './dees-workspace-bottombar/index.js';
+export * from './dees-workspace-diff-editor/index.js';