Reload open workspace preview when agent mutates that file
Track write/edit tool paths per turn, refresh the open preview on tool_complete and after preservePreview loadDir on stream done, without closing preview for unrelated responses or wiping unsaved local edits.
This commit is contained in:
@@ -3,11 +3,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.51.187] — 2026-05-31 — Release FG (stage-batchG — workspace-preview persistence + repaired-sidecar order + scroll-intent window)
|
||||
## [v0.51.187] — 2026-05-31 — Release FG (stage-batchG — workspace-preview persistence + scroll-intent window)
|
||||
|
||||
### Fixed
|
||||
- Workspace file preview no longer closes when a chat response finishes and the UI refreshes the workspace file tree. Background `loadDir('.')` on stream `done` now preserves an open preview instead of always calling `clearPreview()`, and reloads the open file when a write/edit tool touched that path during the turn (skipping reload while the preview has unsaved local edits) (#3262, @pamnard).
|
||||
- Repaired messaging sidecars now keep their authoritative order when merged with CLI/state rows for display: the sidecar ordering is preserved verbatim and only non-overlapping CLI rows outside the sidecar timestamp window are added, instead of timestamp-sorting the whole union (which replayed older compression-lineage rows into the middle/tail and made repeated user turns reappear after reload) (#3268, @ai-ag2026).
|
||||
- During streaming, scrolling up to read earlier content no longer snaps back to the bottom after a brief pause: the upward-scroll intent window was widened from 450ms to 2000ms so DOM-layout changes from the markdown parser / tool-card insertions are still recognized as co-occurring with user intent and don't re-pin the view. Downward scroll, the scroll-to-bottom button, and trackpad-momentum protection are unaffected (#3250, @emanon312).
|
||||
|
||||
## [v0.51.186] — 2026-05-31 — Release FF (stage-batchF — update-checker ff-reachability fall-through + utf-8 git-output test coverage)
|
||||
|
||||
@@ -684,6 +684,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
}
|
||||
closeOtherLiveStreams(activeSid);
|
||||
closeLiveStream(activeSid);
|
||||
if(!reconnecting&&typeof resetTurnWorkspaceMutations==='function') resetTurnWorkspaceMutations();
|
||||
|
||||
// On reconnect, restore accumulated text from INFLIGHT so we don't lose
|
||||
// progress made before the session switch. Without this the closure starts
|
||||
@@ -1710,8 +1711,10 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
if(d.duration!==undefined) tc.duration=d.duration;
|
||||
S.toolCalls=inflight.toolCalls;
|
||||
persistInflightState();
|
||||
if(typeof noteWorkspaceMutationsFromToolCall==='function') noteWorkspaceMutationsFromToolCall(tc);
|
||||
if(S.session&&S.session.session_id===activeSid&&typeof scheduleRenderSessionArtifacts==='function') scheduleRenderSessionArtifacts();
|
||||
if(!S.session||S.session.session_id!==activeSid) return;
|
||||
if(typeof refreshOpenPreviewIfMutated==='function') refreshOpenPreviewIfMutated();
|
||||
appendLiveToolCard(tc);
|
||||
snapshotLiveTurn();
|
||||
scrollIfPinned();
|
||||
@@ -1974,6 +1977,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
if(isSessionViewed) _markSessionViewed(completedSid, completedSession.message_count ?? S.messages.length);
|
||||
syncTopbar();renderMessages({preserveScroll:true});
|
||||
if(shouldFollowOnDone&&typeof scrollToBottom==='function') scrollToBottom();
|
||||
if(typeof noteWorkspaceMutationsFromToolCalls==='function') noteWorkspaceMutationsFromToolCalls(S.toolCalls);
|
||||
loadDir('.', { preservePreview: true });
|
||||
// TTS auto-read: speak the last assistant response if enabled (#499)
|
||||
if(typeof autoReadLastAssistant==='function') setTimeout(()=>autoReadLastAssistant(), 300);
|
||||
|
||||
@@ -223,6 +223,37 @@ function _artifactCandidatesFromToolCall(tc){
|
||||
return out;
|
||||
}
|
||||
|
||||
const _turnMutatedPreviewPaths = new Set();
|
||||
|
||||
function resetTurnWorkspaceMutations(){
|
||||
_turnMutatedPreviewPaths.clear();
|
||||
}
|
||||
|
||||
function noteWorkspaceMutationsFromToolCall(tc){
|
||||
for(const a of _artifactCandidatesFromToolCall(tc)){
|
||||
const path=_normalizeArtifactPath(a.path);
|
||||
if(path) _turnMutatedPreviewPaths.add(path);
|
||||
}
|
||||
}
|
||||
|
||||
function noteWorkspaceMutationsFromToolCalls(toolCalls){
|
||||
if(!Array.isArray(toolCalls)) return;
|
||||
for(const tc of toolCalls) noteWorkspaceMutationsFromToolCall(tc);
|
||||
}
|
||||
|
||||
function _isOpenPreviewPathMutated(){
|
||||
if(!_previewCurrentPath) return false;
|
||||
const current=_normalizeArtifactPath(_previewCurrentPath);
|
||||
return !!(current&&_turnMutatedPreviewPaths.has(current));
|
||||
}
|
||||
|
||||
async function refreshOpenPreviewIfMutated(){
|
||||
if(typeof _previewDirty!=='undefined'&&_previewDirty) return;
|
||||
if(!_isOpenPreviewPathMutated()) return;
|
||||
if(!_previewCurrentPath||!S.session) return;
|
||||
await openFile(_previewCurrentPath, { bustCache: true });
|
||||
}
|
||||
|
||||
function collectSessionArtifacts(){
|
||||
const items = [];
|
||||
const seen = new Set();
|
||||
@@ -321,6 +352,8 @@ async function loadDir(path, opts={}){
|
||||
}else{
|
||||
clearPreview({keepPanelOpen:true});
|
||||
}
|
||||
}else if(preservePreview){
|
||||
await refreshOpenPreviewIfMutated();
|
||||
}
|
||||
// Fetch git info for workspace root (non-blocking)
|
||||
if(!path||path==='.') _refreshGitBadge();
|
||||
@@ -489,9 +522,11 @@ function cancelEditMode(){
|
||||
updateEditBtn();
|
||||
}
|
||||
|
||||
async function openFile(path){
|
||||
async function openFile(path, opts={}){
|
||||
if(!S.session)return;
|
||||
const ext=fileExt(path);
|
||||
const bustCache=!!(opts&&opts.bustCache);
|
||||
const cacheBust=bustCache?`&_=${Date.now()}`:'';
|
||||
|
||||
// Binary/download-only formats: trigger browser download, don't preview
|
||||
if(DOWNLOAD_EXTS.has(ext)){
|
||||
@@ -508,14 +543,14 @@ async function openFile(path){
|
||||
if(IMAGE_EXTS.has(ext)){
|
||||
// Image: load via raw endpoint, show as <img>
|
||||
showPreview('image');
|
||||
const url=`api/file/raw?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}`;
|
||||
const url=`api/file/raw?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}${cacheBust}`;
|
||||
$('previewImg').alt=path;
|
||||
$('previewImg').src=url;
|
||||
$('previewImg').onerror=()=>setStatus(t('image_load_failed'));
|
||||
} else if(AUDIO_EXTS.has(ext)||VIDEO_EXTS.has(ext)){
|
||||
const mode=VIDEO_EXTS.has(ext)?'video':'audio';
|
||||
showPreview(mode);
|
||||
const url=`api/file/raw?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}&inline=1`;
|
||||
const url=`api/file/raw?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}&inline=1${cacheBust}`;
|
||||
const wrap=$('previewMediaWrap');
|
||||
if(wrap){
|
||||
wrap.innerHTML=(typeof _mediaPlayerHtml==='function')
|
||||
@@ -525,7 +560,7 @@ async function openFile(path){
|
||||
}
|
||||
} else if(PDF_EXTS.has(ext)){
|
||||
showPreview('pdf');
|
||||
const url=`api/file/raw?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}&inline=1`;
|
||||
const url=`api/file/raw?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}&inline=1${cacheBust}`;
|
||||
const frame=$('previewPdfFrame');
|
||||
if(frame){
|
||||
frame.src=''; // clear first to avoid stale content
|
||||
@@ -557,7 +592,7 @@ async function openFile(path){
|
||||
// or reading other origin data. If a stricter mode is needed, remove
|
||||
// allow-scripts (or add sandbox="") to disable all JS execution.
|
||||
showPreview('html');
|
||||
const url=`api/file/raw?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}&inline=1`;
|
||||
const url=`api/file/raw?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}&inline=1${cacheBust}`;
|
||||
const iframe=$('previewHtmlIframe');
|
||||
if(iframe){
|
||||
iframe.src=''; // clear first to avoid stale content
|
||||
|
||||
@@ -50,6 +50,9 @@ def test_load_dir_supports_preserve_preview_option():
|
||||
assert "if(!preservePreview&&typeofclearPreview" in block.replace(" ", ""), (
|
||||
"loadDir() should skip clearPreview() when preservePreview is requested"
|
||||
)
|
||||
assert "awaitrefreshOpenPreviewIfMutated()" in block.replace(" ", ""), (
|
||||
"Background refresh must reload the open preview when a mutation tool touched it"
|
||||
)
|
||||
|
||||
|
||||
def test_load_dir_still_clears_preview_for_directory_navigation():
|
||||
@@ -58,3 +61,25 @@ def test_load_dir_still_clears_preview_for_directory_navigation():
|
||||
assert "clearPreview({keepPanelOpen:true})" in block.replace(" ", ""), (
|
||||
"Directory navigation must still clear previews when preservePreview is not set"
|
||||
)
|
||||
|
||||
|
||||
def test_turn_mutation_tracking_reloads_open_preview():
|
||||
block = _function_block(WORKSPACE_JS, "refreshOpenPreviewIfMutated")
|
||||
assert "openFile(_previewCurrentPath" in block.replace(" ", ""), (
|
||||
"Mutated open previews must reload through openFile()"
|
||||
)
|
||||
assert "_previewDirty" in block, "Reload must be skipped while the preview has unsaved edits"
|
||||
|
||||
|
||||
def test_tool_complete_tracks_workspace_mutations_for_preview_reload():
|
||||
tool_complete_idx = MESSAGES_JS.find("source.addEventListener('tool_complete'")
|
||||
assert tool_complete_idx != -1
|
||||
end = MESSAGES_JS.find("source.addEventListener('approval'", tool_complete_idx)
|
||||
block = MESSAGES_JS[tool_complete_idx:end]
|
||||
assert "noteWorkspaceMutationsFromToolCall" in block
|
||||
assert "refreshOpenPreviewIfMutated" in block
|
||||
|
||||
|
||||
def test_stream_start_resets_turn_mutation_tracking():
|
||||
block = _function_block(MESSAGES_JS, "attachLiveStream")
|
||||
assert "resetTurnWorkspaceMutations" in block
|
||||
|
||||
Reference in New Issue
Block a user