# refactor\(web\): composer suggestion lists are built by useComposerMenuItems · PR \#22722 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/22722)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

State: merged

Source repository: pingdotgg/t3code

Source branch: refactor/chat-composer-menu-items

Target branch: refactor/chat-composer-stable-controls

Head commit: 80a98a086e34a13af01787d2c2501b8b681957fc

[Discussion and reviews](https://git.cafe/pingdotgg/t3code/pull/22722)

[Commits](https://git.cafe/pingdotgg/t3code/pull/22722/commits?format=markdown)

[Files changed](https://git.cafe/pingdotgg/t3code/pull/22722/diffs?format=markdown)

## Description

---

Continues the ChatComposer split.

ChatComposer built the @, /, $ and # suggestion lists inline: the path search, the pull request list and exact-number lookups with their debounces, and roughly 150 lines of item mapping. All of that, plus the loading and empty-state text, moves into the `useComposerMenuItems` hook. ChatComposer still owns the trigger, the highlighted item, and keyboard handling. It now gets back `{ items, isLoading, emptyState }`. Behavior is unchanged.

| | before | this PR |
|---|---|---|
| ChatComposer.tsx lines | 6,640 | 6,365 |
| useComposerMenuItems.ts lines | n/a | 344 |
| Renders, 20 typed chars | 352 | 352 |

Renders measured with the shared fiber counter (`rc-init.js`) on a seeded dev server: typing 20 characters into an existing thread's composer, median of 3 runs. This layer is for readability; it does not change renders. I checked by hand that `/` and `@src` still list commands and files.

Tests run: `vp lint` and `vp fmt --check` on the changed files, `tsc-rs --noEmit` for apps/web, and `reactCompilerCoverage.test.ts` for the changed files (0 bailouts).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

