Skip to content

Commit 68176e3

Browse files
committed
fix: resolve 12 session/file management bugs + add collab editing plan
- Fix URL hash not clearing when creating/switching files from shared doc - Fix cloud auto-save overwriting all files to same Firebase doc - Fix all flat disk API calls (readFile/writeFile/deleteFile) to use path-based variants for subdirectory support - Fix boot-sequence content flicker when opening shared links - Fix cloud timer not stopped on folder disconnect - Add localStorage quota exceeded user toast - Add persistent sidebar reconnect banner for expired folder permissions - Add resetCloudForFileSwitch() API for per-file cloud doc isolation - Add collaborative editing implementation plan (upgradePlan/)
1 parent c6dd481 commit 68176e3

6 files changed

Lines changed: 364 additions & 16 deletions

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# CHANGELOG — Session & File Management Fixes
2+
3+
**Date:** 2026-03-16
4+
5+
## Summary
6+
7+
Comprehensive audit and fix of TextAgent's file management, cloud session, and disk workspace code. Resolved 12 bugs across session state management, disk file I/O paths, and cloud auto-save behavior.
8+
9+
## Changes
10+
11+
### Cloud Session & URL Hash Fixes (`js/cloud-share.js`)
12+
13+
- **Fixed:** Cloud auto-save no longer overwrites all files to the same Firebase doc — added `resetCloudForFileSwitch()` to unbind cloud doc on file switch
14+
- **Fixed:** Boot-sequence content flicker — deferred localStorage restore when a share hash is present, eliminating restore → shared overwrite double render
15+
- **Fixed:** Autosave disk write now uses `writeFileToPath()` instead of flat `writeFile()` for correct subdirectory writes
16+
17+
### File Switching & Shared Doc Cleanup (`js/workspace.js`)
18+
19+
- **Fixed:** `wsCreateFile()`, `wsOpenFile()`, and `openDiskFile()` now call `clearCloudSession()` when leaving a shared doc, preventing stale `#id=...&k=...` URLs
20+
- **Fixed:** All three file-switch paths call `resetCloudForFileSwitch()` so each workspace file gets its own independent cloud doc
21+
- **Fixed:** `getFileContentAsync()` uses `readFileFromPath()` for correct subdirectory file reads
22+
- **Fixed:** `setFileContent()` uses `writeFileToPath()` for correct subdirectory disk writes
23+
- **Fixed:** `removeFileContent()` uses `deleteFileFromPath()` for correct subdirectory file deletes
24+
- **Fixed:** `wsConnectFolder()`, `wsReconnectFolder()`, `wsRefreshFromDisk()` all use `readFileFromPath()` for initial file loads
25+
- **Fixed:** `wsDisconnectFolder()` now calls `clearCloudSession()` to stop the cloud timer
26+
- **Fixed:** `setFileContent()` shows a user-facing toast when localStorage quota is exceeded instead of silently failing
27+
28+
### Disk Reconnection UX (`js/disk-workspace.js`)
29+
30+
- **Improved:** Replaced auto-dismissing toast with a persistent amber "Reconnect" sidebar banner when folder permission expires
31+
- **Added:** Clickable "Reconnect" button in sidebar that triggers `requestPermission()` directly
32+
33+
### Reconnect Banner Styling (`css/workspace.css`)
34+
35+
- **Added:** CSS for `.ws-reconnect-notice` and `.ws-reconnect-btn` (amber-themed persistent banner)
36+
37+
### Collaborative Editing Plan (`upgradePlan/collaborative-editing.md`)
38+
39+
- **Added:** Implementation plan for future real-time collaborative editing using Yjs + WebRTC + CodeMirror 6
40+
41+
## Files Modified
42+
43+
- `js/workspace.js` — 8 bug fixes across file CRUD, disk I/O, and cloud session management
44+
- `js/cloud-share.js` — 3 bug fixes + new `resetCloudForFileSwitch()` API
45+
- `js/disk-workspace.js` — Persistent reconnect banner replacing transient toast
46+
- `css/workspace.css` — Reconnect banner styles
47+
48+
## Files Added
49+
50+
- `upgradePlan/collaborative-editing.md` — Future collab editing implementation plan

css/workspace.css

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,42 @@
382382
padding: 0;
383383
}
384384

385+
/* --- Reconnect Notice (persistent sidebar banner) --- */
386+
.ws-reconnect-notice {
387+
display: flex;
388+
align-items: center;
389+
gap: 6px;
390+
padding: 8px 12px;
391+
background: rgba(234, 179, 8, 0.1);
392+
border-bottom: 1px solid rgba(234, 179, 8, 0.25);
393+
font-size: 12px;
394+
color: var(--text-color);
395+
flex-shrink: 0;
396+
}
397+
.ws-reconnect-notice i {
398+
color: #eab308;
399+
font-size: 14px;
400+
flex-shrink: 0;
401+
}
402+
.ws-reconnect-btn {
403+
background: rgba(234, 179, 8, 0.2);
404+
border: 1px solid rgba(234, 179, 8, 0.4);
405+
color: #eab308;
406+
font-size: 11px;
407+
font-weight: 600;
408+
padding: 3px 10px;
409+
border-radius: 4px;
410+
cursor: pointer;
411+
transition: all 0.15s ease;
412+
margin-left: auto;
413+
white-space: nowrap;
414+
font-family: inherit;
415+
}
416+
.ws-reconnect-btn:hover {
417+
background: rgba(234, 179, 8, 0.35);
418+
border-color: #eab308;
419+
}
420+
385421
/* Folder label in header */
386422
#ws-folder-label {
387423
overflow: hidden;

js/cloud-share.js

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@
115115
if (M.wsDiskMode && M._disk && M._disk.isConnected() && M.wsActiveFileId) {
116116
var file = M._wsFindFileById ? M._wsFindFileById(M.wsActiveFileId) : null;
117117
if (file) {
118-
M._disk.writeFile(file.name, M.markdownEditor.value).then(function () {
118+
M._disk.writeFileToPath(file.name, M.markdownEditor.value).then(function () {
119119
showAutosaveIndicator('💾 Saved to disk');
120120
}).catch(function (e) {
121121
console.warn('Disk autosave failed:', e);
@@ -185,6 +185,7 @@
185185
var content = M.markdownEditor.value;
186186
if (!content.trim() || content === lastCloudContent) { cloudSaveDirty = false; return; }
187187
if (M.markdownEditor.readOnly) return;
188+
// Don't auto-save if we're on someone else's shared URL and haven't established our own cloud doc
188189
var hash = window.location.hash;
189190
if (hash && (hash.includes('id=') || hash.includes('d=')) && !localStorage.getItem(CLOUD_DOC_KEY)) return;
190191
try {
@@ -383,6 +384,24 @@
383384
}
384385
M.clearCloudSession = clearCloudSession;
385386

387+
/**
388+
* Reset cloud state for file switching (lighter than clearCloudSession).
389+
* Each workspace file should get its own cloud doc — clear the current
390+
* cloud doc binding so the next auto-save creates a fresh one.
391+
*/
392+
function resetCloudForFileSwitch() {
393+
localStorage.removeItem(CLOUD_DOC_KEY);
394+
localStorage.removeItem(CLOUD_KEY_KEY);
395+
localStorage.removeItem(CLOUD_WT_KEY);
396+
lastCloudContent = '';
397+
cloudSaveDirty = false;
398+
// Clear hash so it doesn't show stale cloud doc URL
399+
if (window.location.hash) {
400+
window.history.replaceState({}, document.title, window.location.pathname);
401+
}
402+
}
403+
M.resetCloudForFileSwitch = resetCloudForFileSwitch;
404+
386405
document.getElementById('shared-banner-edit').addEventListener('click', function () {
387406
clearCloudSession();
388407
M.setViewMode('split');
@@ -692,14 +711,18 @@
692711
}
693712

694713
// --- Restore Auto-Saved Content or Load Default ---
695-
var wasRestored = restoreFromLocalStorage();
696-
if (wasRestored) {
697-
M.renderMarkdown();
698-
} else if (!window.location.hash || (!window.location.hash.includes('d=') && !window.location.hash.includes('id='))) {
699-
// No autosave and no share link — load Feature Showcase as default
700-
if (M.getDefaultContent) {
701-
M.markdownEditor.value = M.getDefaultContent();
714+
// Only restore if we are NOT loading a shared link (which will overwrite the editor anyway)
715+
var hasShareHash = window.location.hash && (window.location.hash.includes('d=') || window.location.hash.includes('id='));
716+
if (!hasShareHash) {
717+
var wasRestored = restoreFromLocalStorage();
718+
if (wasRestored) {
702719
M.renderMarkdown();
720+
} else {
721+
// No autosave and no share link — load Feature Showcase as default
722+
if (M.getDefaultContent) {
723+
M.markdownEditor.value = M.getDefaultContent();
724+
M.renderMarkdown();
725+
}
703726
}
704727
}
705728

js/disk-workspace.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,27 @@
379379
// Successfully reconnected — load workspace from disk
380380
if (M.wsReconnectFolder) M.wsReconnectFolder();
381381
} else if (result === 'needs-permission') {
382-
M.showToast('Folder reconnection needs permission. Use the folder button to reconnect.', 'info');
382+
// Show a persistent notice in the sidebar instead of a transient toast
383+
var sidebar = document.getElementById('workspace-sidebar');
384+
if (sidebar) {
385+
var notice = document.createElement('div');
386+
notice.className = 'ws-reconnect-notice';
387+
notice.innerHTML =
388+
'<i class="bi bi-folder-symlink"></i> ' +
389+
'<span>Folder access expired. </span>' +
390+
'<button id="ws-reconnect-btn" class="ws-reconnect-btn">Reconnect</button>';
391+
sidebar.insertBefore(notice, sidebar.querySelector('.ws-file-list'));
392+
document.getElementById('ws-reconnect-btn').addEventListener('click', function () {
393+
disk.requestPermission().then(function (granted) {
394+
if (granted) {
395+
notice.remove();
396+
if (M.wsReconnectFolder) M.wsReconnectFolder();
397+
} else {
398+
M.showToast('Permission denied. Try using Open Folder instead.', 'warning');
399+
}
400+
});
401+
});
402+
}
383403
}
384404
});
385405
}

js/workspace.js

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,20 +61,24 @@
6161
function getFileContentAsync(id) {
6262
if (diskMode && M._disk && M._disk.isConnected()) {
6363
var file = findFileById(id);
64-
if (file) return M._disk.readFile(file.name);
64+
// Use readFileFromPath so subdirectory files (e.g. "notes/ideas.md") resolve correctly
65+
if (file) return M._disk.readFileFromPath(file.name);
6566
}
6667
return Promise.resolve(getFileContent(id));
6768
}
6869

6970
function setFileContent(id, content) {
7071
try {
7172
localStorage.setItem(FILE_PREFIX + id, content);
72-
} catch (e) { console.warn('File save failed:', e); }
73+
} catch (e) {
74+
console.warn('File save failed:', e);
75+
if (M.showToast) M.showToast('⚠️ Save failed — browser storage is full. Free space or connect a folder.', 'error');
76+
}
7377
// Also write to disk when in disk mode
7478
if (diskMode && M._disk && M._disk.isConnected()) {
7579
var file = findFileById(id);
7680
if (file) {
77-
M._disk.writeFile(file.name, content).catch(function (e) {
81+
M._disk.writeFileToPath(file.name, content).catch(function (e) {
7882
console.warn('Disk file write failed:', e);
7983
});
8084
}
@@ -86,7 +90,7 @@
8690
if (diskMode && M._disk && M._disk.isConnected()) {
8791
var file = findFileById(id);
8892
if (file) {
89-
M._disk.deleteFile(file.name).catch(function (e) {
93+
M._disk.deleteFileFromPath(file.name).catch(function (e) {
9094
console.warn('Disk file delete failed:', e);
9195
});
9296
}
@@ -335,6 +339,10 @@
335339
function openDiskFile(entry) {
336340
if (!M._disk || !M._disk.isConnected()) return;
337341

342+
// If we were viewing a shared doc, break out of that session
343+
if (M.isViewingSharedDoc) M.clearCloudSession();
344+
// Reset cloud doc binding so each file gets its own cloud doc
345+
else if (M.resetCloudForFileSwitch) M.resetCloudForFileSwitch();
338346
// Save current file
339347
M.wsSaveCurrent();
340348

@@ -535,6 +543,10 @@
535543
M.wsDiskMode = false;
536544

537545
M.wsCreateFile = function (name) {
546+
// If we were viewing a shared doc, break out of that session
547+
if (M.isViewingSharedDoc) M.clearCloudSession();
548+
// Reset cloud doc binding so new file gets its own cloud doc
549+
else if (M.resetCloudForFileSwitch) M.resetCloudForFileSwitch();
538550
// Save current document first
539551
M.wsSaveCurrent();
540552
var id = generateId();
@@ -572,6 +584,10 @@
572584
if (id === workspace.activeFileId) return;
573585
var file = findFileById(id);
574586
if (!file) return;
587+
// If we were viewing a shared doc, break out of that session
588+
if (M.isViewingSharedDoc) M.clearCloudSession();
589+
// Reset cloud doc binding so each file gets its own cloud doc
590+
else if (M.resetCloudForFileSwitch) M.resetCloudForFileSwitch();
575591
// Save current
576592
M.wsSaveCurrent();
577593
// Switch
@@ -802,7 +818,7 @@
802818
// Load active file content
803819
var activeFile = findFileById(workspace.activeFileId);
804820
if (activeFile) {
805-
var content = await M._disk.readFile(activeFile.name);
821+
var content = await M._disk.readFileFromPath(activeFile.name);
806822
M.markdownEditor.value = content;
807823
// Cache in localStorage
808824
localStorage.setItem(FILE_PREFIX + workspace.activeFileId, content);
@@ -829,6 +845,8 @@
829845
if (!M._disk) return;
830846
// Save current to localStorage before disconnecting
831847
M.wsSaveCurrent();
848+
// Stop cloud auto-save timer so it doesn't create unwanted cloud docs
849+
if (M.clearCloudSession) M.clearCloudSession();
832850
await M._disk.disconnect();
833851
diskMode = false;
834852
M.wsDiskMode = false;
@@ -874,7 +892,7 @@
874892
// Reload active file content from disk
875893
var activeFile = findFileById(workspace.activeFileId);
876894
if (activeFile) {
877-
var content = await M._disk.readFile(activeFile.name);
895+
var content = await M._disk.readFileFromPath(activeFile.name);
878896
M.markdownEditor.value = content;
879897
localStorage.setItem(FILE_PREFIX + workspace.activeFileId, content);
880898
M.renderMarkdown();
@@ -918,7 +936,7 @@
918936
saveWorkspace();
919937
var activeFile = findFileById(workspace.activeFileId);
920938
if (activeFile) {
921-
var content = await M._disk.readFile(activeFile.name);
939+
var content = await M._disk.readFileFromPath(activeFile.name);
922940
M.markdownEditor.value = content;
923941
localStorage.setItem(FILE_PREFIX + workspace.activeFileId, content);
924942
M.renderMarkdown();

0 commit comments

Comments
 (0)