# 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.

Page: https://sdk.chatfuel.com/docs/first-change
Markdown: https://sdk.chatfuel.com/docs/first-change.md

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.

<Steps>
  <Step>
    ### Name the view [#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:

    ```ts title="src/modules/contacts/lib/contactsParams.ts"
    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.
  </Step>

  <Step>
    ### Write the component [#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:

    ```tsx title="src/modules/contacts/views/AudienceView.tsx"
    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.
  </Step>

  <Step>
    ### Register it [#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]`:

    ```tsx title="src/modules/contacts/ContactsApp.tsx"
    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.
  </Step>

  <Step>
    ### Let people reach it [#let-people-reach-it]

    Two places, because there are two ways into a view. The header tabs, in
    `components/ContactsHeader.tsx`:

    ```tsx title="src/modules/contacts/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.
  </Step>

  <Step>
    ### Check it [#check-it]

    ```bash
    npm run check
    npm run dev
    ```

    Open [localhost:5173/contacts](http://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.
  </Step>
</Steps>

<Callout type="warn">
  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.
</Callout>

## Ask your agent instead [#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.
```

<Accordions>
  <Accordion title="Renaming what the rail says">
    Every module directory exports one descriptor from its `index.tsx`, and the sidebar is built out
    of it:

    ```tsx title="src/modules/deals/index.tsx"
    import { lazy } from 'react';
    import { IconKanban } from '~ui';
    import type { ModuleDescriptor } from '../types';

    export const moduleDescriptor: ModuleDescriptor = {
      id: 'deals',
      title: 'Deals',
      icon: <IconKanban />,
      Component: lazy(() => import('./DealsApp').then((m) => ({ default: m.DealsApp }))),
    };
    ```

    `title` is the sidebar tooltip and the topbar label. Change it to `'Pipeline'` and both follow.
    Leave `id` alone — it is the module's first path segment, so changing it changes every address
    that module owns.
  </Accordion>
</Accordions>

Next: [deploy](/docs/deploy), or [what the wizard writes](/docs/what-you-get) for the rest of the
tree.
