brainHero.test.tsxfeat(agents): AOP-name picker on the Brain hero, defaulting to Deals
Adds a second picker beside the administered-user bar (AdministeredUserBar)
on the Brain hero, matching the Settings/Playbooks pattern: pick a
conversation type by name, and the Agents grid narrows to just the agents
filed under it. Applies whether viewing your own agents or, combined with
the admin picker, a teammate's -- BrainHeroBelowChat reads useTargetUserId()
and hands it into the AOP query the same way brain/playbooks/page.tsx does,
never through useAOPs() (that hook writes into the shared Zustand AOP store
every CRM surface reads, so scoping IT to a teammate would swap conversation
types app-wide for the admin, not just narrow this filter -- its own doc
comment says so).
No backend change: AgentSummary carries no aopId (the merge across AOPs
deliberately discards which one a merged agent was resolved through), but
its documentPath does encode where the document is actually filed --
user/playbooks/{aopId}/subagents/... or organisation/playbooks/{orgAopId}/
subagents/.... agentAopIdFromDocumentPath (agent-paths.ts, mirroring the
server's userSubagentPath/orgSubagentPath convention) parses it back out.
The filter matches against both a user AOP's own id and its orgAopId, since
an org-scoped agent's document lives under the org id, not the user one.
Defaults to Deals once the AOP list loads, not "every agent" -- a seeded
account's classification buckets and background agents otherwise bury the
one AOP everyone actually works in. A ref, not a null-check, gates this: the
filter is also null right after a user explicitly picks "All conversation
types", and re-defaulting there would fight that choice. Resets (both the
selection and whether the default has fired) whenever the administered
person changes, since a conversation type only makes sense for the person it
belongs to.
The "New agent" card and the true-empty-state (as opposed to
filter-matched-zero) messaging are withheld while a filter or a teammate is
selected, for the same reason `duplicate` stays self-only in the org-admin
PR this builds on: a create landing wherever firstAopIdForUser picks, not
necessarily the filtered AOP, would be a create button that lies about where
its result appears.
Co-Authored-By: Claude Sonnet 5 <<email>>
Claude-Session: https://claude.ai/code/session_01SfYKPYHXdMgrPLuKhXvWedSep 11, 2026, 10:43 AMplaybookAopSection.test.tsxfix(lint): clear the four oxlint findings this branch was carrying
CI lints with oxlint under --deny-warnings, so a warning fails the branch the
same as an error. Two dead imports, a `filter(Boolean).at(-1)` that is
`findLast(Boolean)`, and an `?? {}` fallback inside a spread that never had
anything to fall back from.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 1, 2026, 12:44 AMplaybookFileTree.test.tsxfeat(brain): the Brain lands like the agent home, and its playbooks stop drawing their own files
`/brain` was a two-column page: a bespoke home in the wide column, chat pinned to the side, and
one colour-washed card per conversation type drawing its files with a private tree. Three things
came out of that. The Brain looked like a different product from `/home`. The same document
looked like a different object here than in the file explorer — a fourth row renderer with its
own indent step, icons and type scale. And the card made the COLOUR the object, when the colour
is a two-pixel label on it.
`/brain` is now the same machinery as `/home`: chat-dominant over the sunlit backdrop, a greeting
above the composer, its own context below it, a rail beside it. Everything that dresses a hero —
the backdrop, the translucent left rail, the transparent chat column, the suppressed suggestions
strip — already read one boolean, so generalising `selectHomeHero` into `selectHeroSurface`
('home' | 'brain' | null) was the whole of it; only the chat's three content slots branch.
Below the composer: two destinations (Playbook, Knowledge base) and every agent rendered in
place, because a card whose only job is to reveal a grid charges a click for nothing. The rail
holds recent files rather than widgets — the question this screen answers is what Cedar has been
reading and writing, and a pipeline metric is not that.
navigation.section — `page` is derived from the FIRST path segment, so `/brain` and
`/brain/knowledge` are the same page while wanting opposite arrangements. The second segment is
now mirrored into the store, and `computeArrangement` reads it: the Brain ROOT is the hero,
anything under it is a screen. Deliberately not folded into `setNavigationPage`, which clears
the open artifact on every change — moving between two sections of one page must not.
The file surface moved to `/brain/knowledge`, so `/brain?documentId=` had to move with it. Every
in-app call site goes through `brainDocumentPath` now; `BrainLegacyAddressRedirect` forwards the
links already in the wild, and is mounted at the ROOT layout because a chat-dominant route
renders no page — a redirect inside `/brain` would never run.
FileBrowser left modules/agents. It was the agent Output tab; the playbooks wanted the same
thing, and a component two surfaces share does not live in one of them. Its scope is now id-free
(user / org / conversation, resolved inside), which fixed a live bug: the old mapping fell
through `org` to `user`, so an org-namespaced agent's Output tab was rooted in a workspace
holding none of its files. HEAD needed this anyway — the rename was swept into 00b5021ba
without the import updates, leaving FileBrowser at the new path and two files importing the old.
searchForLink now selects `documents.emoji`. Every row has one, and without it the recents rail
drew a generic page glyph for a document the file tree drew 🔥 for.
A playbook's PLAYBOOK.md is pinned as the first row and opens the COMPOSITE editor; neither the
User nor the Org root lists its own copy. The two halves are one document with one editor, and
listing them invites editing a half — the old tree hid the file entirely, which hid the most
important document in the folder.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 1, 2026, 12:21 AM