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: cb7ab55 carried 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:
2026-08-10 23:29:18 +01:00
co-authored by Claude Opus 5
parent 7726c9fc71
commit 0a4ff548b9
19 changed files with 77 additions and 749 deletions
+9 -15
View File
@@ -158,10 +158,8 @@ export function close(ws: ServerWebSocket<WSData>): void {
const sessionId = wsToSessionMap.get(ws);
if (sessionId) {
sessionManager.detachWs(sessionId, ws);
// Only once nothing is watching. Another pane or another device still attached means the
// conversation is live, and arming the idle GC here would collect it out from under them.
if (!sessionManager.hasSockets(sessionId)) sessionManager.setIdleTimeout(sessionId, IDLE_TIMEOUT_MS);
sessionManager.detachWs(sessionId);
sessionManager.setIdleTimeout(sessionId, IDLE_TIMEOUT_MS);
}
}
@@ -246,7 +244,7 @@ function createMessageHandler(sessionId: string, model: string) {
const session = sessionManager.getSession(sessionId);
if (!session) return;
foldIntoSession(session, msg, model);
for (const socket of session.sockets) sendToClient(socket as ServerWebSocket<WSData>, msg, seq);
sendToClient(session.ws as ServerWebSocket<WSData> | null, msg, seq);
};
}
@@ -653,7 +651,7 @@ async function handleResumeCursor(
// the Claude sidecar about an OpenCode session, hear "not generating", and write "the agent went
// away" into a turn that was running perfectly well.
if (msg.generating && decision.kind !== 'assume') {
await endTurnIfAgentIsGone([ws], sessionId, decision.model);
await endTurnIfAgentIsGone(ws, sessionId, decision.model);
}
}
@@ -823,7 +821,7 @@ async function handleAttach(ws: ServerWebSocket<WSData>, msg: { claudeSessionId:
* so its sessions are left alone rather than guessed at.
*/
async function endTurnIfAgentIsGone(
targets: Iterable<ServerWebSocket<WSData> | null>,
ws: ServerWebSocket<WSData> | null,
sessionId: string,
model: string,
): Promise<void> {
@@ -836,11 +834,11 @@ async function endTurnIfAgentIsGone(
const event: ServerMessage = { type: 'cut-off' };
try {
const seq = await appendChatEvent(sessionId, event);
for (const target of targets) sendToClient(target, event, seq);
sendToClient(ws, event, seq);
} catch (err) {
// Still tell every client — an un-replayable explanation beats a spinner that never stops.
// Still tell this client — an un-replayable explanation beats a spinner that never stops.
logger.error('Failed to persist cut-off notice', { sessionId, error: String(err) });
for (const target of targets) sendToClient(target, event);
sendToClient(ws, event);
}
logger.info('Ended a turn whose agent had gone', { sessionId });
}
@@ -852,11 +850,7 @@ async function endTurnIfAgentIsGone(
sidecar.onClaudeSidecarStarted(() => {
for (const session of sessionManager.getAllSessions()) {
if (!session.isGenerating) continue;
void endTurnIfAgentIsGone(
session.sockets as Set<ServerWebSocket<WSData>>,
session.sessionId,
session.model,
);
void endTurnIfAgentIsGone(session.ws as ServerWebSocket<WSData> | null, session.sessionId, session.model);
}
});