batch3 T7 review fix: serialize open_file_in_browser to avoid double pane

When no file_browser pane exists, two rapid open_file_in_browser events
could both trigger create() since the ref check happens before the first
create resolves. Add a creating flag/promise so the second event waits
for the first create then updates the newly-created pane's state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-15 15:50:30 +00:00
parent fb31e63d10
commit 60a0036850

View File

@@ -72,12 +72,36 @@ export function Workspace({ sessionId, projectId }: Props) {
}
}, [panes, activeId]);
// Tracks an in-flight create() call so rapid open_file_in_browser events
// don't race to each spawn a new file_browser pane. While a create is in
// progress the subsequent events wait for it and update the same pane.
const creatingRef = useRef<{ id: string; promise: Promise<string> } | null>(
null
);
// Subscribe to open_file_in_browser events: focus an existing file_browser
// pane (updating its open_file) or spawn one if room is available.
useEffect(() => {
return sessionEvents.subscribe((event) => {
if (event.type !== 'open_file_in_browser') return;
void (async () => {
// If a create is already in flight, wait for it to finish then update
// the newly-created pane rather than spawning a second one.
if (creatingRef.current) {
const { id: pendingId, promise } = creatingRef.current;
const resolvedId = await promise;
const targetId = resolvedId || pendingId;
const current = panesRef.current;
const fb = current?.find((p) => p.id === targetId);
const nextState: FileBrowserPaneState = {
...(fb?.kind === 'file_browser' ? fb.state : {}),
open_file: event.path,
};
await update(targetId, { state: nextState });
setActiveId(targetId);
return;
}
const current = panesRef.current;
if (!current) return;
const fb = current.find(
@@ -92,14 +116,32 @@ export function Workspace({ sessionId, projectId }: Props) {
await update(fb.id, { state: nextState });
setActiveId(fb.id);
} else if (current.length < MAX_PANES) {
const newPane = await create({ kind: 'file_browser' });
const nextState: FileBrowserPaneState = {
open_file: event.path,
filter: '',
expanded_dirs: [],
// Reserve the slot immediately so concurrent events see the flag.
const createPromise = (async (): Promise<string> => {
const newPane = await create({ kind: 'file_browser' });
return newPane.id;
})();
// Use a stable object; id is filled in once resolved.
const entry: { id: string; promise: Promise<string> } = {
id: '',
promise: createPromise,
};
await update(newPane.id, { state: nextState });
setActiveId(newPane.id);
creatingRef.current = entry;
try {
const newId = await createPromise;
entry.id = newId;
const nextState: FileBrowserPaneState = {
open_file: event.path,
filter: '',
expanded_dirs: [],
};
await update(newId, { state: nextState });
setActiveId(newId);
} finally {
if (creatingRef.current === entry) {
creatingRef.current = null;
}
}
}
})();
});