Your first change
Add a view to a module — where its id lives, which files change with it, and how to check it in the browser.
A module never touches window.location. The shell owns the address bar and hands each module
its own slice through props — view, the path segment after the module id, and setView to
move within it. So "add a screen" is not a route you register anywhere central. It's one more
value the module accepts for view, and a component to render when it arrives.
Here is that change end to end, in contacts, adding a view called timeline. Every file below
is under src/modules/contacts/ in the app the wizard wrote.
Name the view
The module's addresses are parsed and written in one pure file, lib/contactsParams.ts. Three
lines at the top of it are the whole vocabulary:
export type ContactsView = 'list' | 'fields' | 'audience';
export const VIEWS: readonly ContactsView[] = ['list', 'fields', 'audience'];
export const DEFAULT_VIEW: ContactsView = 'list';Add 'timeline' to the type and to the array. Both, not one: the type is what the compiler
walks, and the array is what the parser accepts off the address bar.
DEFAULT_VIEW stays list, and it is the one view with no segment of its own — /contacts
is the list, and /contacts/timeline is yours.
Write the component
Views live in views/. The props are frozen: a view takes ContactsViewProps and nothing else,
owns its own data, its own toolbar and its own live channel, and reports its count and busy
state upward. The audience breakdown is a short one to start from:
export function AudienceView({ team, catalog, refreshToken, onCount, onBusy }: ContactsViewProps) {Copy that file to views/TimelineView.tsx, rename the function, and empty the body. Anything a
view wants that is not in the contract is a sign the view should own it, not that the contract
should grow.
Register it
One record in ContactsApp.tsx maps a view id to its component, and the workspace picks out of
it with VIEWS[parsed.view]:
const VIEWS: Record<ContactsView, ComponentType<ContactsViewProps>> = {
list: ListView,
fields: FieldsView,
audience: AudienceView,
};Add timeline: TimelineView and import it. This record is keyed on the union, so if you skip
this step the compiler says so by name.
Let people reach it
Two places, because there are two ways into a view. The header tabs, in
components/ContactsHeader.tsx:
const VIEW_TABS: { id: ContactsView; label: string }[] = [
{ id: 'list', label: 'Contacts' },
{ id: 'fields', label: 'Fields' },
{ id: 'audience', label: 'Audience' },
];And the command palette, in lib/commands.ts, which builds its destination group from three
records keyed on the union — VIEW_LABELS, VIEW_KEYWORDS and VIEW_SHORTCUT — plus its own
array of the views to offer.
Those three records are compiler-checked, so npm run check names them for you. The plain
arrays are not: VIEW_TABS above, the palette's list beside those records, and the one in step
one.
Check it
npm run check
npm run devOpen localhost:5173/contacts. The new tab is in the header;
clicking it puts /contacts/timeline in the address bar and pushes a history entry, so the back
button returns you to the list. Reload on /contacts/timeline and your view comes back. Press
⌘K and type the view's name — it should be in the destinations.
Forget the array in step one and nothing breaks loudly. An address the parser does not recognise falls back to the default view in silence — that rule exists so a stale link renders the list instead of a white screen — so the tab appears to do nothing and the URL rewrites itself back. If a new view will not open, the array is the first place to look.
Ask your agent instead
The wizard installed a skill per module: .claude/skills/chatfuel-contacts/ for Claude Code,
.agents/skills/chatfuel-contacts/ for Codex CLI. Each one carries that module's structure and
the traps in the API behind it, which is why this is a sentence rather than a file hunt. Open
your agent in the project and type:
Add a "timeline" view to the contacts module: the view id, a component under views/,
the tab in the header and the command-palette entry. Then run npm run check.Next: deploy, or what the wizard writes for the rest of the tree.
What the wizard writes
The tree the wizard leaves on disk, why the design system and the API client are copied in as source, and what that costs you.
How it fits together
Why the token never reaches the browser, why the code is copied in rather than installed, and how a module, a route and a store fit together.