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
@@ -1,12 +1,15 @@
import { useEffect, useRef } from 'react';
import { useParams, useNavigate } from 'react-router';
import type { SelectedSession } from 'officerdev';
import { ChatTabs, chatListPath, cwdFromSplat, useSelectedChatSession } from 'officerdev';
import type { LayoutNode, SelectedSession } from 'officerdev';
import { WorkspaceView, chatListPath, cwdFromSplat, useSelectedChatSession } from 'officerdev';
import { toast } from '@/components/ui/sonner';
import { useIsMobile } from 'hooks/useIsMobile';
import { useClient } from 'hooks/useClient';
import { serverClient } from 'hooks/useServerClient';
import { errorText } from 'helpers/error-text';
import { useDashboardState } from 'state/useDashboardState';
import type { ClaudeSessionDetail } from 'state/useClaudeSessions';
import { defaultLayout, hasAppType } from './defaultLayout';
// How many messages to render on first open (anchored to the bottom); scroll-up pages older ones in.
const CHAT_TAIL = 20;
@@ -25,7 +28,26 @@ export const SessionListPage = ({ isNew }: SessionListPageProps) => {
selectedRef.current = selected;
// A layout persisted before the chat panels were renamed still names `officerdev/chat`, which no
// longer resolves; `appTypes` lands anything unknown on the detail panel.
const workspace = useDashboardState<LayoutNode>('screens/chat', defaultLayout);
const isMobile = useIsMobile();
const navigate = useNavigate();
const mobilePanelId = isMobile && (sessionId || isNew) ? 'chat-detail' : undefined;
// Adopt a structural change to this screen's layout.
//
// `useDashboardState` seeds its default ONLY when the key is absent, so anyone who has ever opened
// /chat keeps the shape it had then — for good. `appTypes`/`normalizeLayout` does not help: it repairs
// which app a panel runs, never the tree, so adding the Live panel above the list would have been
// invisible to every existing user and visible only on a fresh account.
//
// Replacing outright is safe *here* specifically because the screen is `locked`: its structure is
// dictated by code and the only thing a user can have contributed is the column sizes, which is a
// cheap thing to lose once. Terminates because the replacement contains the panel it tests for.
useEffect(() => {
if (!workspace.isLoaded) return;
if (hasAppType(workspace.value, 'chat-live')) return;
workspace.setValue(defaultLayout);
}, [workspace.isLoaded, workspace.value, workspace.setValue]);
// Retire a legacy `?cwd=`. Nothing reads it any more and nothing writes it, but a refresh re-requests
// the address bar verbatim — so one left over from before the path-based groups sits there forever,
@@ -98,15 +120,20 @@ export const SessionListPage = ({ isNew }: SessionListPageProps) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sessionId, isNew, groupCwd]);
// The tabbed, multi-pane chat replaces the fixed three-panel workspace. The panels themselves are
// unchanged and still registered for the dashboard; what changes is that a PANE owns its conversation
// rather than the whole screen sharing one, which is what lets two machines be live side by side.
//
// `useDashboardState`/`WorkspaceView` are no longer used here. The layout that matters now is the tab
// blob in localStorage, because a tab spanning two servers cannot be stored per server.
return (
<div className="h-full w-full pt-2">
<ChatTabs />
<WorkspaceView
workspace={workspace}
locked
appTypes={{ allowed: ['chat-session-list', 'chat-live', 'chat-detail'], fallback: 'chat-detail' }}
mobilePanelId={mobilePanelId}
onMobilePanelChange={(id) => {
// Back goes to the group's list, not the default one. On /chat/g/* that group is in the URL;
// on /chat/<id> it isn't (deliberately — see chat-routes.ts), so fall back to the open
// session's own directory, which the resolve above put on the selection.
if (!id) navigate(chatListPath(groupCwd ?? selectedRef.current?.cwd ?? null), { replace: true });
}}
/>
</div>
);
};