revert the chat tabs and panes work, back to one conversation
Andre asked for zero, not another fix on top. Reverts ec4f06a..7726c9f — the ten commits from "tabs and panes" onward: the tab bar and pane splitting, tab renaming and its page title, the per-server directory picker, the render-loop fix, pane transcript resolution, the send queue, the two socket fixes from the other session, the pane-socket notes, and my own socket-set change from tonight. He is rebuilding from here. Deliberately KEPT:dc6b623, "talk to two officers at once from one browser". That was a separate ask that predates the tabs one, and the multi-server client, the server chips and the connections store stand on their own without panes. Reverting it too is one more command if that was the intent. Collateral, worth naming:cb7ab55carried an unrelated MusicPlayerHost change alongside its socket instrumentation, so that came out with it. Reverts, not a reset — every one of these is pushed and a second session is live in this repo. Typecheck clean. 600 pass, 2 fail — cliamp path-escape and the pty transport test, both failing identically before this and unrelated to chat. What is NOT explained by this revert: the browser symptoms tonight. The server was verified good throughout — two real turns streamed back through the public URL on both models, and the full 2,281-message history came through nginx intact. Whatever the client fault is, it is still unfound, and the pre-tabs code is where it now has to be looked for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,22 +20,6 @@ export const useChatWebSocket = ({ url, onMessage, onOpen }: UseChatWebSocketPar
|
||||
const onOpenRef = useRef(onOpen);
|
||||
onOpenRef.current = onOpen;
|
||||
|
||||
/**
|
||||
* Messages typed before the socket was ready.
|
||||
*
|
||||
* `send` used to drop them: `readyState !== OPEN` returned, silently, with no error and no retry — so
|
||||
* pressing enter did nothing and the turn never happened. That window is not rare. React's dev
|
||||
* StrictMode double-invokes effects, so every socket is created, closed and recreated on mount, and a
|
||||
* reconnect after a drop reopens it again; with several chat panes on screen there are several sockets
|
||||
* doing this at once. One of them is always briefly not OPEN.
|
||||
*
|
||||
* Queued and flushed on open, in order. The mobile chat app does exactly this and for exactly this
|
||||
* reason — the composer is allowed to fire before the transport is ready.
|
||||
*/
|
||||
const pendingRef = useRef<string[]>([]);
|
||||
/** A deferred teardown, cancelled when the effect re-runs — see the cleanup below. */
|
||||
const closeTimerRef = useRef<number | null>(null);
|
||||
|
||||
const connect = () => {
|
||||
if (isCleaningUpRef.current) return;
|
||||
if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) return;
|
||||
@@ -43,30 +27,15 @@ export const useChatWebSocket = ({ url, onMessage, onOpen }: UseChatWebSocketPar
|
||||
const socket = new WebSocket(url);
|
||||
socketRef.current = socket;
|
||||
|
||||
// Temporary, and deliberately loud. A pane connected and then sat silent, and reasoning from this
|
||||
// hook's source three times running did not explain it — the console says a socket closed, never who
|
||||
// closed it or whether the message went out. `window.__officerWs = false` turns it off.
|
||||
const host = new URL(url).host;
|
||||
const log = (what: string, extra?: unknown) =>
|
||||
(window as any).__officerWs !== false && console.log(`[ws ${host}] ${what}`, extra ?? '');
|
||||
log('creating');
|
||||
|
||||
socket.addEventListener('open', () => {
|
||||
if (socketRef.current !== socket) return;
|
||||
setIsConnected(true);
|
||||
retryRef.current = 0;
|
||||
log('OPEN');
|
||||
// BEFORE onOpen, deliberately: onOpen sends the resume/attach handshake, and anything the user
|
||||
// typed while connecting belongs after that, not in front of it.
|
||||
const queued = pendingRef.current;
|
||||
pendingRef.current = [];
|
||||
onOpenRef.current?.();
|
||||
for (const message of queued) socket.send(message);
|
||||
});
|
||||
|
||||
socket.addEventListener('message', (ev) => {
|
||||
try {
|
||||
log('recv', String(ev.data).slice(0, 120));
|
||||
const data = JSON.parse(typeof ev.data === 'string' ? ev.data : new TextDecoder().decode(ev.data));
|
||||
onMessageRef.current(data);
|
||||
} catch {
|
||||
@@ -74,13 +43,7 @@ export const useChatWebSocket = ({ url, onMessage, onOpen }: UseChatWebSocketPar
|
||||
}
|
||||
});
|
||||
|
||||
socket.addEventListener('close', (ev) => {
|
||||
log('close', {
|
||||
code: ev.code,
|
||||
reason: ev.reason,
|
||||
stale: socketRef.current !== socket,
|
||||
tearingDown: isCleaningUpRef.current,
|
||||
});
|
||||
socket.addEventListener('close', () => {
|
||||
if (isCleaningUpRef.current) return;
|
||||
if (socketRef.current !== socket) return;
|
||||
setIsConnected(false);
|
||||
@@ -96,60 +59,25 @@ export const useChatWebSocket = ({ url, onMessage, onOpen }: UseChatWebSocketPar
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// A pending teardown from a remount that is about to be undone — see below.
|
||||
if (closeTimerRef.current !== null) {
|
||||
clearTimeout(closeTimerRef.current);
|
||||
closeTimerRef.current = null;
|
||||
}
|
||||
isCleaningUpRef.current = false;
|
||||
connect();
|
||||
|
||||
return () => {
|
||||
/**
|
||||
* Close LATER, not now.
|
||||
*
|
||||
* Closing here directly is correct for a real unmount and disastrous for a remount, and this hook
|
||||
* cannot tell them apart at the moment it runs. React's dev StrictMode double-invokes every effect
|
||||
* (mount → unmount → mount), and a pane whose subtree is re-created — a tab re-render, a resolved
|
||||
* transcript, a parent key change — does the same. Each time, the socket was closed while still
|
||||
* CONNECTING, the browser logged "closed before the connection is established", and the replacement
|
||||
* was closed in turn. A pane could churn forever and never hold a connection: exactly what a fresh
|
||||
* remote pane did.
|
||||
*
|
||||
* Deferring by a tick makes the two distinguishable. A remount re-runs the effect immediately and
|
||||
* cancels this timer, so the live socket is kept and the handshake completes. A real unmount has
|
||||
* nobody to cancel it and the socket closes a frame later, which costs nothing.
|
||||
*/
|
||||
isCleaningUpRef.current = true;
|
||||
if (retryTimeoutRef.current !== null) {
|
||||
clearTimeout(retryTimeoutRef.current);
|
||||
retryTimeoutRef.current = null;
|
||||
}
|
||||
const socket = socketRef.current;
|
||||
closeTimerRef.current = window.setTimeout(() => {
|
||||
closeTimerRef.current = null;
|
||||
if (!isCleaningUpRef.current) return; // remounted: the effect above already reclaimed it
|
||||
if (socket) socket.close();
|
||||
if (socketRef.current === socket) socketRef.current = null;
|
||||
}, 0);
|
||||
if (socketRef.current) {
|
||||
socketRef.current.close();
|
||||
socketRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [url]);
|
||||
|
||||
const send = (data: Record<string, unknown>) => {
|
||||
const socket = socketRef.current;
|
||||
const message = JSON.stringify(data);
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
if ((window as any).__officerWs !== false) console.log(`[ws ${new URL(url).host}] send`, message.slice(0, 120));
|
||||
socket.send(message);
|
||||
return;
|
||||
}
|
||||
if ((window as any).__officerWs !== false)
|
||||
console.log(`[ws ${new URL(url).host}] QUEUED (socket ${socket?.readyState ?? 'none'})`, message.slice(0, 80));
|
||||
// Not open yet, or reconnecting. Hold it rather than dropping it — see `pendingRef`. Bounded so a
|
||||
// socket that never comes back cannot grow this without limit; the oldest go first, because the
|
||||
// newest message is the one the user is still waiting on.
|
||||
pendingRef.current.push(message);
|
||||
if (pendingRef.current.length > 50) pendingRef.current.shift();
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) return;
|
||||
socket.send(JSON.stringify(data));
|
||||
};
|
||||
|
||||
return { isConnected, send };
|
||||
|
||||
Reference in New Issue
Block a user