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:
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { Link, NavLink } from 'react-router';
|
||||
import { Menu } from 'lucide-react';
|
||||
import { EditableTitle } from '@/components/EditableTitle';
|
||||
// import { Menu, Terminal } from 'lucide-react'; // Terminal used by the commented-out web inspector button
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||
import type { DockItem } from '../Dock';
|
||||
@@ -12,42 +13,21 @@ import { usePageTitle } from '@/state/usePageTitle';
|
||||
// import { BugReportButton } from '../BugReport/BugReportButton';
|
||||
|
||||
// Center title: click to rename this browser tab. The name is kept in sessionStorage, so it survives a
|
||||
// refresh and navigation and dies with the tab — see usePageTitle.
|
||||
// refresh and navigation and dies with the tab — see usePageTitle. `allowEmpty` is what makes clearing
|
||||
// the field hand the tab back to the route's own name.
|
||||
function EditablePageTitle() {
|
||||
const [title, rename] = usePageTitle();
|
||||
const [draft, setDraft] = useState<string | null>(null);
|
||||
|
||||
// The edit is a draft committed on blur/Enter rather than a live write. Writing every keystroke was
|
||||
// fine while the title was a plain string, but an empty field now means "use the route name", so
|
||||
// deleting the last character would snap the input to "Chat" underneath the cursor.
|
||||
if (draft !== null) {
|
||||
return (
|
||||
<input
|
||||
autoFocus
|
||||
value={draft}
|
||||
onChange={(ev) => setDraft(ev.target.value)}
|
||||
onBlur={() => {
|
||||
rename(draft);
|
||||
setDraft(null);
|
||||
}}
|
||||
onKeyDown={(ev) => {
|
||||
if (ev.key === 'Enter') ev.currentTarget.blur();
|
||||
if (ev.key === 'Escape') setDraft(null); // discard, keeping whatever the tab was called
|
||||
}}
|
||||
aria-label="Tab name"
|
||||
placeholder={title}
|
||||
className="pointer-events-auto max-w-[50vw] border-b border-[#1d2724]/40 bg-transparent text-center text-base font-bold text-[#1d2724] outline-none md:text-xl"
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<button
|
||||
onClick={() => setDraft(title)}
|
||||
title="Click to rename this tab — clear it to go back to the page name"
|
||||
<EditableTitle
|
||||
value={title}
|
||||
onCommit={rename}
|
||||
allowEmpty
|
||||
ariaLabel="Tab name"
|
||||
hint="Click to rename this tab — clear it to go back to the page name"
|
||||
className="pointer-events-auto max-w-[50vw] cursor-text truncate text-base font-bold text-[#1d2724] transition-opacity hover:opacity-70 md:text-xl"
|
||||
>
|
||||
{title}
|
||||
</button>
|
||||
inputClassName="pointer-events-auto max-w-[50vw] border-b border-[#1d2724]/40 bg-transparent text-center text-base font-bold text-[#1d2724] outline-none md:text-xl"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user