feat(coach): move conversations into a collapsible right panel

The Coach page gets a conversations panel on the right of the thread. From
1280 px it docks beside the thread and slides open and shut; the reading
column keeps its width either way and only its centring moves. It is open
on a first visit and remembers the last choice per device. Below 1280 px it
is a sheet from the right. The toggle sits at the trailing edge of the top
bar, through a small page-owned slot the top bar now renders.

The panel lists every conversation grouped by recency, with a row menu to
rename in place (Enter saves, Escape cancels without closing the panel) and
delete with the existing undo toast. The undo window now survives the sheet
closing. New chat is the round button at the panel's bottom right, and
picking or starting a conversation keeps ?c= in the URL.

The gear in the panel header opens quick settings: a popover from md up, a
bottom sheet on a phone. It switches the answering provider (chain reordered
through the existing route), the model and reasoning level where the person
owns them, and shows what the Coach can see. Keys, the Codex sign-in and the
fallback chain stay in Settings > AI. The drawer's gear opens the same
overlay. The composer's + menu is gone.

Settings > Coach links to /coach?settings=data, which opens the overlay on
what the Coach can see instead of a rail that no longer exists.
This commit is contained in:
Marc-André Bombeck committed 2026-10-04 20:00:07 +02:00
1 parent 1cb50ed21d
commit 12153fe143
33 files changed
+2074 -428

No files matched your search

@@ -54,16 +54,16 @@ beforeEach(() => {
});
describe("Coach links follow the Coach's capability", () => {
it("the sources pointer links to the Coach while it is available", () => {
it("the sources pointer opens the Coach's settings on what it can see", () => {
expect(render(<CoachPrefsSection isAuthenticated />)).toContain(
'href="/coach"',
'href="/coach?settings=data"',
);
});
it("the sources pointer offers no dead link without the Coach", () => {
coachState.available = false;
const html = render(<CoachPrefsSection isAuthenticated />);
expect(html).not.toContain('href="/coach"');
expect(html).not.toContain('href="/coach?settings=data"');
expect(html).not.toContain("coach-prefs-sources-pointer");
});
@@ -223,8 +223,9 @@ export function CoachPrefsSection({ isAuthenticated }: CoachPrefsSectionProps) {
</div>
</div>
{/* Data clusters + analysis window live on ONE owner: the
sources rail inside the chat ("What I draw on"). This card
{/* Data clusters + analysis window live on ONE owner: "What I
can see" behind the Coach's settings gear, which
`/coach?settings=data` opens directly. This card
used to render its own copies of both controls; two writable
surfaces for the same persisted fields is exactly the
redundancy the rail was built to end. A pointer replaces
@@ -237,7 +238,7 @@ export function CoachPrefsSection({ isAuthenticated }: CoachPrefsSectionProps) {
>
{t("insights.coach.settingsSourcesPointer")}{" "}
<Link
href="/coach"
href="/coach?settings=data"
className="text-foreground underline underline-offset-2"
>
{t("insights.coach.settingsSourcesPointerLink")}