name a chat from its own pane, and name the page after it

Two things, one title.

The pane's header is now editable and calls the same `renameSession` the list's pencil does, so both
surfaces write the transcript's `summary` line and the invalidation that follows refreshes the row. The
id comes from the URL rather than `resumeSessionId`: they agree for an ordinary conversation and not for
a merged `/clear` chain, where the resume target is the tail while the list and the server address the
chain by its head — renaming the tail would have written a title nothing displays. `/chat/new` has no
transcript yet, so there the title is read-only.

And on `/chat/<id>` the conversation names the page, sitting between a typed tab name and the route
default: `label ?? override ?? titleForPath()`. Naming a window is deliberate and must still win. Not
gated on full screen, though that is where it earns its keep — the nav header is hidden there, so the
browser tab strip is the only thing telling two side-by-side windows apart. Tiled, the same value fills
the header's centre.

The edit interaction is now one `EditableTitle` shared with the nav header instead of a second copy of
it. `allowEmpty` is what keeps the header's "clear it to hand the tab back to the route name" working;
everywhere else empty means keep, since the rename endpoint 400s on it. `SessionList`'s row rename is
deliberately NOT folded in — it opens from a pencil and confirms with a check, so it is a different
interaction wearing the same styling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-08 22:28:29 +01:00
co-authored by Claude Opus 5
parent c79b5a287b
commit 6a74b3d198
6 changed files with 186 additions and 34 deletions
+7 -1
View File
@@ -1,5 +1,6 @@
import { useCallback, useEffect } from 'react';
import { useLocation } from 'react-router';
import { usePageTitleOverride } from 'officerdev';
import { useSessionState, writeSessionValue } from 'hooks/useSessionState';
type TitleRule = { match: (p: string) => boolean; title: string };
@@ -150,16 +151,21 @@ claimTabIdentity();
* refresh: you named this window to find it again among a dozen others, and wiping it because you opened
* a different screen would defeat the point. Clear the field to hand the tab back to the route's own
* name — that is the only way out, and there is no third state to get stuck in.
*
* Between the two sits a screen's own name for itself, published by whatever is showing (see
* `usePublishPageTitle`): on `/chat/<id>` that is the conversation's title. It ranks under a typed name
* for the reason above, and over the route default because "Chat" says less than the chat's name does.
*/
export function usePageTitle() {
const { pathname } = useLocation();
const override = usePageTitleOverride();
const [label, setLabel] = useSessionState<string | null>(TAB_LABEL_KEY, null);
useEffect(() => onTabLabelDropped(() => setLabel(null)), [setLabel]);
const rename = useCallback((next: string) => setLabel(next.trim() || null), [setLabel]);
return [label ?? titleForPath(pathname), rename] as const;
return [label ?? override ?? titleForPath(pathname), rename] as const;
}
/**