fix: File Explorer not refreshing after the AI Assistant writes a file#20
Merged
archdex-art merged 1 commit intoJul 17, 2026
Merged
Conversation
Bug report: "claude runs well but it claims that it made changes but i
cant find any" -- screenshot showed a successful write_file tool call
("Created helloworld.py...") and a confirming list_directory call, but
the file never appeared in the Explorer sidebar.
## Root cause (reproduced live, not assumed)
Verified the entire backend chain first: the workspace write API and the
file-listing API both resolve the identical on-disk workspace directory
(a single DB column, `repos.workspace_dir`), and a direct write+list
round trip through the real API confirmed the backend was correct.
Reproduced the actual bug end-to-end in a real browser: stood up a fake
OpenAI-compatible server standing in for the LLM (letting the exact real
frontend code run against a deterministic "write a file" tool call,
without spending real Anthropic credits), drove a real chat turn through
CodeGraph's actual local-model path, and confirmed via the dev server's
own request log that after the chat POST completed, the file explorer
NEVER issued its usual follow-up `GET .../fs?op=list` request. A direct
disk check confirmed the file genuinely existed -- the write succeeded,
the assistant correctly reported success, but the Explorer silently
never refreshed.
Instrumented `AssistantPanel.tsx`'s `applyEvent` directly (writing to a
`window` global read back via the browser tool, since `page.on('console')`
listeners don't persist across separate browser-tool calls) and proved
the exact mechanism: `applyEvent` computed `pathsToTouch`/`shouldMutate`
by mutating `let` variables *inside* the `setTurns` state-updater
callback, then read those same variables immediately after calling
`setTurns()`. React does not guarantee a functional state updater has
already run by the time `setState()` returns -- especially here, since
every event arrives from an async SSE stream-reader loop, not a
synchronous React event handler. The live trace showed exactly this:
a `tool_result` event for a successful write fired with `shouldMutate`
still `false`, because the updater responsible for setting it hadn't
executed yet at the point it was read. `onMutated()` was silently never
called.
## Fix
Track the currently-running tool's input in a `useRef` (synchronous,
no React-scheduling dependency) instead of deriving it from state-updater
side effects. `pathsToTouch`/`shouldMutate` are now computed BEFORE
`setTurns` is even called, from the ref, eliminating the timing hazard
entirely. `setTurns`'s updater now only updates the rendered block list,
with no side-channel data dependency.
## Verification
- Reproduced the exact broken behavior live (pre-fix): a real write_file
call succeeded on disk, but the Explorer's follow-up list request never
fired.
- Re-ran the identical live scenario post-fix: the newly-created file
appeared in the Explorer immediately with zero manual reload.
- New regression guard in assistant.test.ts (source-level, matching this
file's existing convention for frontend logic with no React Testing
Library/jsdom harness in this project): confirms the mutation-flag
computation is ordered before the setTurns call and uses the new ref,
not state-updater side effects.
- Full suite 233/233, tsc --noEmit clean, lint baseline unchanged, next
build clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug report
"claude runs well but it claims that it made changes but i cant find any" — screenshot showed a successful
write_filetool call ("Created helloworld.py...") and a confirminglist_directorycall, but the file never appeared in the Explorer sidebar.Root cause (reproduced live, not assumed)
Verified the backend chain first: the write API and the file-listing API resolve the identical on-disk workspace directory. A direct write+list round trip through the real API confirmed the backend was correct.
Reproduced the actual bug end-to-end in a real browser: stood up a fake OpenAI-compatible server standing in for the LLM (letting the real frontend code run against a deterministic tool call, without spending Anthropic credits), drove a real chat turn through the local-model path, and confirmed via the dev server's request log that after the chat POST completed, the Explorer never issued its usual follow-up
GET .../fs?op=list. A direct disk check confirmed the file genuinely existed — the write succeeded, the assistant correctly reported success, but the Explorer silently never refreshed.Instrumented
AssistantPanel.tsx'sapplyEventdirectly and proved the mechanism: it mutatedpathsToTouch/shouldMutateinside thesetTurnsstate-updater callback, then read those same variables immediately after callingsetTurns(). React does not guarantee a functional state updater has already run by the timesetState()returns — especially here, since every event arrives from an async SSE stream-reader loop, not a synchronous React event handler. The live trace proved it: atool_resultfor a successful write fired withshouldMutatestillfalse.onMutated()was silently never called.Fix
Track the currently-running tool's input in a
useRef(synchronous, no React-scheduling dependency) instead of deriving it from state-updater side effects.pathsToTouch/shouldMutateare computed BEFOREsetTurnsis even called.Verification
tsc --noEmitclean, lint baseline unchanged,next buildclean.