I asked deepseek for help:
Letβs compare all possible ways of watching a pc folder for new files from a browser tab, comparing the size and complexity of any necessary intermediaries. These must all be local-only solutions, no cloud server. Ideally the user should not have to run a separate app on their pc but if they do it should be as small as possible. New tech is ok!
https://chat.deepseek.com/share/dqq85dbt3lrib4qmp0
We ended up with this demo. It doesnβt work in Firefox, deepseek was a little confused. Works great in chrome!

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Firefox Folder Watcher</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
button {
background: #0066cc;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
margin: 10px 0;
}
button:hover {
background: #0052a3;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
.status {
padding: 15px;
border-radius: 6px;
margin: 15px 0;
background: #e8f4fd;
border-left: 4px solid #0066cc;
}
.file-list {
border: 1px solid #ddd;
border-radius: 6px;
padding: 15px;
max-height: 300px;
overflow-y: auto;
background: #fafafa;
}
.file-item {
padding: 8px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
}
.file-item:last-child {
border-bottom: none;
}
.new-file {
background: #e8ffe8;
animation: highlight 2s ease;
}
@keyframes highlight {
from { background: #a8ffa8; }
to { background: #e8ffe8; }
}
.error {
background: #ffe8e8;
border-left: 4px solid #cc0000;
color: #cc0000;
}
</style>
</head>
<body>
<div class="container">
<h1>π Firefox Folder Watcher</h1>
<p>Experimental proof of concept using FileSystemObserver API</p>
<div class="status" id="status">
Click "Select Folder" to begin watching for new files...
</div>
<button id="selectFolder">Select Folder</button>
<button id="stopWatching" disabled>Stop Watching</button>
<h3>Files in selected folder:</h3>
<div class="file-list" id="fileList">
<div class="file-item">No folder selected</div>
</div>
<h3>File Events:</h3>
<div class="file-list" id="eventLog">
<div class="file-item">Events will appear here...</div>
</div>
</div>
<script>
class FolderWatcher {
constructor() {
this.directoryHandle = null;
this.observer = null;
this.isWatching = false;
this.elements = {
status: document.getElementById('status'),
fileList: document.getElementById('fileList'),
eventLog: document.getElementById('eventLog'),
selectFolder: document.getElementById('selectFolder'),
stopWatching: document.getElementById('stopWatching')
};
this.bindEvents();
this.checkBrowserSupport();
}
bindEvents() {
this.elements.selectFolder.addEventListener('click', () => this.selectFolder());
this.elements.stopWatching.addEventListener('click', () => this.stopWatching());
}
async checkBrowserSupport() {
if (!('showDirectoryPicker' in window)) {
this.showError('File System Access API not supported in this browser');
this.elements.selectFolder.disabled = true;
return;
}
if (!('FileSystemObserver' in window)) {
this.showError('FileSystemObserver API not available. This proof of concept requires Firefox with experimental APIs enabled.');
this.elements.selectFolder.disabled = true;
return;
}
this.updateStatus('Browser support detected! Ready to select folder.', 'success');
}
async selectFolder() {
try {
this.directoryHandle = await window.showDirectoryPicker();
this.updateStatus(`Watching folder: ${this.directoryHandle.name}`, 'success');
await this.loadExistingFiles();
await this.startWatching();
this.elements.selectFolder.disabled = true;
this.elements.stopWatching.disabled = false;
} catch (error) {
if (error.name === 'AbortError') {
this.updateStatus('Folder selection cancelled');
} else {
this.showError(`Error selecting folder: ${error.message}`);
}
}
}
async loadExistingFiles() {
const files = [];
for await (const entry of this.directoryHandle.values()) {
if (entry.kind === 'file') {
const file = await entry.getFile();
files.push({
name: entry.name,
handle: entry,
size: file.size,
lastModified: file.lastModified
});
}
}
this.displayFiles(files);
}
displayFiles(files) {
this.elements.fileList.innerHTML = '';
if (files.length === 0) {
this.elements.fileList.innerHTML = '<div class="file-item">No files found</div>';
return;
}
files.forEach(file => {
const fileElement = document.createElement('div');
fileElement.className = 'file-item';
fileElement.innerHTML = `
<span><strong>${file.name}</strong></span>
<span>${this.formatFileSize(file.size)} β’ ${new Date(file.lastModified).toLocaleTimeString()}</span>
`;
this.elements.fileList.appendChild(fileElement);
});
}
async startWatching() {
if (!this.directoryHandle) {
throw new Error('No directory selected');
}
// Create the observer
this.observer = new FileSystemObserver((records, observer) => {
this.handleFileChanges(records);
});
// Start observing the directory
await this.observer.observe(this.directoryHandle);
this.isWatching = true;
this.updateStatus(`Watching for file changes in: ${this.directoryHandle.name}`, 'success');
}
handleFileChanges(records) {
records.forEach(record => {
const timestamp = new Date().toLocaleTimeString();
let message = '';
switch (record.type) {
case 'created':
message = `π NEW FILE: ${record.changedHandle.name}`;
this.addFileToList(record.changedHandle);
break;
case 'modified':
message = `βοΈ MODIFIED: ${record.changedHandle.name}`;
break;
case 'moved':
message = `π MOVED: ${record.relativePathComponents?.join('/') || record.changedHandle.name}`;
break;
case 'deleted':
message = `ποΈ DELETED: ${record.changedHandle.name}`;
break;
default:
message = `π CHANGE: ${record.changedHandle.name} (${record.type})`;
}
this.logEvent(`${timestamp} - ${message}`);
});
}
async addFileToList(fileHandle) {
try {
const file = await fileHandle.getFile();
const fileElement = document.createElement('div');
fileElement.className = 'file-item new-file';
fileElement.innerHTML = `
<span><strong>${file.name}</strong> π</span>
<span>${this.formatFileSize(file.size)} β’ ${new Date(file.lastModified).toLocaleTimeString()}</span>
`;
// Add to top of list
this.elements.fileList.insertBefore(fileElement, this.elements.fileList.firstChild);
// Remove highlight animation after 2 seconds
setTimeout(() => {
fileElement.classList.remove('new-file');
}, 2000);
} catch (error) {
console.error('Error reading new file:', error);
}
}
logEvent(message) {
const eventElement = document.createElement('div');
eventElement.className = 'file-item';
eventElement.textContent = message;
this.elements.eventLog.insertBefore(eventElement, this.elements.eventLog.firstChild);
// Keep only last 50 events
if (this.elements.eventLog.children.length > 50) {
this.elements.eventLog.removeChild(this.elements.eventLog.lastChild);
}
}
async stopWatching() {
if (this.observer) {
this.observer.disconnect();
this.observer = null;
}
this.isWatching = false;
this.directoryHandle = null;
this.updateStatus('Stopped watching folder');
this.elements.selectFolder.disabled = false;
this.elements.stopWatching.disabled = true;
this.elements.fileList.innerHTML = '<div class="file-item">No folder selected</div>';
}
updateStatus(message, type = 'info') {
this.elements.status.textContent = message;
this.elements.status.className = 'status';
if (type === 'error') {
this.elements.status.classList.add('error');
} else if (type === 'success') {
this.elements.status.style.borderLeftColor = '#00cc00';
}
}
showError(message) {
this.updateStatus(message, 'error');
console.error(message);
}
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
}
// Initialize the folder watcher when page loads
document.addEventListener('DOMContentLoaded', () => {
new FolderWatcher();
});
</script>
</body>
</html>