[feat] Make browser tabs describe the active page - #5704
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe PR adds frontend browser page-title documentation, shared title formatting, reactive agent-session titles, workflow-aware playground titles, and static titles across project and observability routes. It also removes competing demo title metadata. ChangesBrowser page-title system
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant PlaygroundPageTitle
participant ActiveSessionState
participant AgentTitleSelector
participant PageTitle
participant Browser
PlaygroundPageTitle->>ActiveSessionState: Read active session title data
ActiveSessionState->>AgentTitleSelector: Provide session title and first user message
AgentTitleSelector->>PageTitle: Provide selected title part
PageTitle->>Browser: Set formatted document title
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 9a23e50f-7bb4-4319-b606-bd629c802505
📒 Files selected for processing (30)
docs/design/browser-page-titles/README.mddocs/design/browser-page-titles/context.mddocs/design/browser-page-titles/plan.mddocs/design/browser-page-titles/research.mddocs/design/browser-page-titles/status.mdweb/ee/src/components/Scripts/assets/CloudScripts.tsxweb/oss/src/components/AgentChatSlice/assets/pageTitle.test.tsweb/oss/src/components/AgentChatSlice/assets/pageTitle.tsweb/oss/src/components/AgentChatSlice/state/sessions.pageTitle.test.tsweb/oss/src/components/AgentChatSlice/state/sessions.tsweb/oss/src/components/PageTitle/WorkflowPageTitle.tsxweb/oss/src/components/PageTitle/index.tsxweb/oss/src/components/PageTitle/utils.test.tsweb/oss/src/components/PageTitle/utils.tsweb/oss/src/components/Playground/Playground.tsxweb/oss/src/components/Playground/PlaygroundPageTitle.tsxweb/oss/src/components/PlaygroundRouter/index.tsxweb/oss/src/components/Scripts/GlobalScripts.tsxweb/oss/src/components/pages/observability/index.tsxweb/oss/src/pages/w/[workspace_id]/p/[project_id]/agents/index.tsxweb/oss/src/pages/w/[workspace_id]/p/[project_id]/annotations/index.tsxweb/oss/src/pages/w/[workspace_id]/p/[project_id]/apps/[app_id]/evaluations/index.tsxweb/oss/src/pages/w/[workspace_id]/p/[project_id]/apps/[app_id]/overview/index.tsxweb/oss/src/pages/w/[workspace_id]/p/[project_id]/apps/[app_id]/variants/index.tsxweb/oss/src/pages/w/[workspace_id]/p/[project_id]/apps/index.tsxweb/oss/src/pages/w/[workspace_id]/p/[project_id]/evaluations/index.tsxweb/oss/src/pages/w/[workspace_id]/p/[project_id]/evaluators/index.tsxweb/oss/src/pages/w/[workspace_id]/p/[project_id]/prompts/index.tsxweb/oss/src/pages/w/[workspace_id]/p/[project_id]/settings/index.tsxweb/oss/src/pages/w/[workspace_id]/p/[project_id]/testsets/index.tsx
| | Context | Title | | ||
| | --- | --- | | ||
| | Home | `Home | Agenta` | | ||
| | Empty agent chat | `<Agent name> | Agenta` | | ||
| | Agent chat after the session starts | `<Session title, at most 60 characters> | Agenta` | | ||
| | Project observability | `Observability | Agenta` | | ||
| | Agent observability | `Observability | <Agent name>` | | ||
| | Settings | `Settings | Agenta` | |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Keep each title example in one table cell.
The literal | characters in lines 13-18 are parsed as extra table separators. markdownlint reports MD056, and rendered documentation can lose part of each example. Encode or escape the separator in every value.
Proposed fix
-| Home | `Home | Agenta` |
+| Home | `Home` &`#124`; `Agenta` |Apply the same change to all rows containing |.
🧰 Tools
🪛 markdownlint-cli2 (0.23.1)
[warning] 13-13: Table column count
Expected: 2; Actual: 3; Too many cells, extra data will be missing
(MD056, table-column-count)
[warning] 14-14: Table column count
Expected: 2; Actual: 3; Too many cells, extra data will be missing
(MD056, table-column-count)
[warning] 15-15: Table column count
Expected: 2; Actual: 3; Too many cells, extra data will be missing
(MD056, table-column-count)
[warning] 16-16: Table column count
Expected: 2; Actual: 3; Too many cells, extra data will be missing
(MD056, table-column-count)
[warning] 17-17: Table column count
Expected: 2; Actual: 3; Too many cells, extra data will be missing
(MD056, table-column-count)
[warning] 18-18: Table column count
Expected: 2; Actual: 3; Too many cells, extra data will be missing
(MD056, table-column-count)
Source: Linters/SAST tools
| export const truncateTitlePart = (value: string, maxLength: number): string => { | ||
| const normalized = normalizeTitlePart(value) | ||
| const characters = Array.from(normalized) | ||
| if (characters.length <= maxLength) return normalized | ||
| return `${characters | ||
| .slice(0, Math.max(0, maxLength - 1)) | ||
| .join("") | ||
| .trimEnd()}…` | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Handle non-positive maxLength explicitly.
For maxLength equal to 0 or a negative value, the current code returns …. That result has one code point and exceeds the requested maximum. Validate the limit or return an empty string for zero before adding the ellipsis. Add boundary tests.
Proposed fix
export const truncateTitlePart = (value: string, maxLength: number): string => {
+ if (!Number.isInteger(maxLength) || maxLength < 0) {
+ throw new RangeError("maxLength must be a non-negative integer")
+ }
+ if (maxLength === 0) return ""
const normalized = normalizeTitlePart(value)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const truncateTitlePart = (value: string, maxLength: number): string => { | |
| const normalized = normalizeTitlePart(value) | |
| const characters = Array.from(normalized) | |
| if (characters.length <= maxLength) return normalized | |
| return `${characters | |
| .slice(0, Math.max(0, maxLength - 1)) | |
| .join("") | |
| .trimEnd()}…` | |
| } | |
| export const truncateTitlePart = (value: string, maxLength: number): string => { | |
| if (!Number.isInteger(maxLength) || maxLength < 0) { | |
| throw new RangeError("maxLength must be a non-negative integer") | |
| } | |
| if (maxLength === 0) return "" | |
| const normalized = normalizeTitlePart(value) | |
| const characters = Array.from(normalized) | |
| if (characters.length <= maxLength) return normalized | |
| return `${characters | |
| .slice(0, Math.max(0, maxLength - 1)) | |
| .join("") | |
| .trimEnd()}…` | |
| } |
| const WorkflowPageTitle = ({title}: {title: string}) => { | ||
| const workflow = useAtomValue(currentWorkflowContextAtom).workflow | ||
| const workflowName = workflow?.name || workflow?.slug | ||
|
|
||
| return <PageTitle title={title} context={workflowName} /> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Preserve the global fallback while workflow context loads.
When currentWorkflowContextAtom has no workflow, this component still renders PageTitle with title. The formatter then sets a value such as Evaluations | Agenta instead of the documented global Agenta loading fallback. Render the fallback until a workflow name or slug is available.
Proposed fix
const workflow = useAtomValue(currentWorkflowContextAtom).workflow
const workflowName = workflow?.name || workflow?.slug
+ if (!workflowName) return <PageTitle />
return <PageTitle title={title} context={workflowName} />📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const WorkflowPageTitle = ({title}: {title: string}) => { | |
| const workflow = useAtomValue(currentWorkflowContextAtom).workflow | |
| const workflowName = workflow?.name || workflow?.slug | |
| return <PageTitle title={title} context={workflowName} /> | |
| const WorkflowPageTitle = ({title}: {title: string}) => { | |
| const workflow = useAtomValue(currentWorkflowContextAtom).workflow | |
| const workflowName = workflow?.name || workflow?.slug | |
| if (!workflowName) return <PageTitle /> | |
| return <PageTitle title={title} context={workflowName} /> |
| import WorkflowPageTitle from "@/oss/components/PageTitle/WorkflowPageTitle" | ||
| import RequireWorkflowKind from "@/oss/components/RequireWorkflowKind" | ||
| import {useAppId} from "@/oss/hooks/useAppId" | ||
|
|
||
| const AppEvaluationsPage = () => { | ||
| const appId = useAppId() | ||
| return ( | ||
| <RequireWorkflowKind allowed={["app", "evaluator"]} currentRoute="evaluations"> | ||
| <EvaluationsView scope="app" appId={appId} /> | ||
| </RequireWorkflowKind> | ||
| <> | ||
| <WorkflowPageTitle title="Evaluations" /> | ||
| <RequireWorkflowKind allowed={["app", "evaluator"]} currentRoute="evaluations"> | ||
| <EvaluationsView scope="app" appId={appId} /> | ||
| </RequireWorkflowKind> | ||
| </> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Render WorkflowPageTitle inside RequireWorkflowKind.
At Line [10], WorkflowPageTitle is a sibling of the guard. At Lines [11]-[13], the guard can render loading, not-found, or redirect states without its children. The title can therefore use an empty or previous workflow context during navigation instead of the global fallback title.
Move the title into the guarded fragment.
Proposed fix
return (
- <>
- <WorkflowPageTitle title="Evaluations" />
- <RequireWorkflowKind allowed={["app", "evaluator"]} currentRoute="evaluations">
+ <RequireWorkflowKind allowed={["app", "evaluator"]} currentRoute="evaluations">
+ <>
+ <WorkflowPageTitle title="Evaluations" />
<EvaluationsView scope="app" appId={appId} />
- </RequireWorkflowKind>
- </>
+ </>
+ </RequireWorkflowKind>
)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import WorkflowPageTitle from "@/oss/components/PageTitle/WorkflowPageTitle" | |
| import RequireWorkflowKind from "@/oss/components/RequireWorkflowKind" | |
| import {useAppId} from "@/oss/hooks/useAppId" | |
| const AppEvaluationsPage = () => { | |
| const appId = useAppId() | |
| return ( | |
| <RequireWorkflowKind allowed={["app", "evaluator"]} currentRoute="evaluations"> | |
| <EvaluationsView scope="app" appId={appId} /> | |
| </RequireWorkflowKind> | |
| <> | |
| <WorkflowPageTitle title="Evaluations" /> | |
| <RequireWorkflowKind allowed={["app", "evaluator"]} currentRoute="evaluations"> | |
| <EvaluationsView scope="app" appId={appId} /> | |
| </RequireWorkflowKind> | |
| </> | |
| import WorkflowPageTitle from "`@/oss/components/PageTitle/WorkflowPageTitle`" | |
| import RequireWorkflowKind from "`@/oss/components/RequireWorkflowKind`" | |
| import {useAppId} from "`@/oss/hooks/useAppId`" | |
| const AppEvaluationsPage = () => { | |
| const appId = useAppId() | |
| return ( | |
| <RequireWorkflowKind allowed={["app", "evaluator"]} currentRoute="evaluations"> | |
| <> | |
| <WorkflowPageTitle title="Evaluations" /> | |
| <EvaluationsView scope="app" appId={appId} /> | |
| </> | |
| </RequireWorkflowKind> | |
| ) |
Railway Preview Environment
|
EE ships its own copies of the project- and app-scoped evaluations pages instead of re-exporting the OSS twins, so the new titles never reached them. Both kept the marketing fallback on cloud. The session auto-title cut at 60 UTF-16 units, so an emoji straddling the cap left a lone surrogate that rendered as a replacement character in the tab. Cut on code points instead, and drop a trailing lone surrogate when the title arrives already cut from the server.
Context
Agenta showed the same marketing title across product pages, agents, and chats. Multiple open tabs were hard to distinguish because the title did not follow the active page or session.
Changes
Pages now own semantic titles through a small shared
PageTitleformatter.Before:
Agenta – the open-source workspace for building and running agentsAfter examples:
Home | AgentaMarketing Coworker | Agentafor an empty agent chatDraft the launch plan | Agentaafter the first messageObservability | Marketing Coworkerinside an agentThe active chat title follows the selected session and reacts to its first message or rename. Session titles are normalized and capped at 60 Unicode code points with an ellipsis. Primary project and agent navigation pages also receive stable titles.
The global marketing title remains a synchronous loading and unknown-route fallback. The asynchronous cloud scripts no longer write a competing title, so they cannot overwrite page-owned titles after mount.
Scope and risk
This is frontend-only. It adds no API, backend, storage, analytics, or data-fetching changes. Agent names come from the already-loaded current workflow artifact.
Authentication, workspace selection, archive, and deep-detail routes remain on the safe fallback. They are documented as a separate follow-up so this PR stays reviewable.
The main regression risk is title precedence between the global fallback and page heads. The fallback now mounts synchronously before page content, and live EE verification covered both project and agent contexts.
How to review
web/oss/src/components/PageTitle/for formatting and fallback behavior.PlaygroundPageTitle.tsxandactiveSessionTitleAtomFamilyfor empty-session, active-session, and rename behavior.GlobalScriptsowns the only fallback title andCloudScriptsno longer competes.Tests / notes
pnpm --filter @agenta/oss exec vitest run --reporter=default src/components/PageTitle/utils.test.ts src/components/AgentChatSlice/assets/pageTitle.test.ts src/components/AgentChatSlice/state/sessions.pageTitle.test.ts(12 passed)pnpm lint-fix(passed; two existing TanStack Virtual compiler warnings)pnpm type-check(OSS and EE passed)git diff --check(passed)What to QA
| Agenta.| Agenta.| Agenta; a long title should end with an ellipsis and stay within 60 characters before the separator.