Skip to main content

cmdk-actions

Guide for adding new actions to Sentry's Command+K palette. Use when implementing new cmdk actions, registering page-level or global actions, building async resource pickers, or adding contextual actions to a view.

Informations de source

Dépôt
getsentry/sentry
Dernière activité de la source
30 avril 2026 à 16:32
Langue détectée de SKILL.md
anglais
Étoiles
44 880
Forks
4 869

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
cmdk-actions
description
Guide for adding new actions to Sentry's Command+K palette. Use when implementing new cmdk actions, registering page-level or global actions, building async resource pickers, or adding contextual actions to a view.
# Adding Actions to the Command Palette (cmdk) Sentry's Command+K palette is built on a tree-collection system where `CMDKAction` components register themselves via React context. Actions render wherever in the component tree they live — no central registry to update. ## Core files - **`static/app/components/commandPalette/ui/cmdk.tsx`** — `CMDKAction` component (the only primitive you need) - **`static/app/components/commandPalette/types.tsx`** — public types + `cmdkQueryOptions` helper - **`static/app/components/commandPalette/ui/commandPaletteSlot.tsx`** — `CommandPaletteSlot` for scoping - **`static/app/components/commandPalette/ui/commandPaletteGlobalActions.tsx`** — always-on global actions --- ## The Three Slots Slots control sort order and lifetime. Import from `commandPaletteSlot.tsx`: ```tsx import {CommandPaletteSlot} from 'sentry/components/commandPalette/ui/commandPaletteSlot'; ``` | Slot | Order in palette | Lifetime | Use for | | -------- | ------------------------ | ------------------------------------------ | ----------------------------------------------------------------------- | | `task` | First (highest priority) | Reserved — not yet used in production | Future transient workflow steps | | `page` | Second | Tied to the page component's mount/unmount | Contextual actions for the current view (issue details, settings pages) | | `global` | Last | Always present for any org | Org-wide navigation, create actions, help | Wrap page-level actions in the slot provider: ```tsx // In a page component or its sub-tree <CommandPaletteSlot name="page"> <CMDKAction display={{label: t('Resolve Issue')}} onAction={handleResolve} /> </CommandPaletteSlot> ``` Global actions are registered once in `GlobalCommandPaletteActions` — add to that component rather than creating a new global slot consumer. --- ## `CMDKAction` Props ```ts interface CMDKActionProps { // Required: what the user sees display: { label: string; // primary text details?: string; // secondary description line icon?: React.ReactNode; // icon on the left — use default size for section icons, // size={16} for avatars (ProjectAvatar, ActorAvatar, TeamAvatar) trailingItem?: React.ReactNode; // right-side decoration (overrides link indicator) }; // Optional: improve search recall keywords?: string[]; // Optional stable key. Prefix with "cmdk:supplementary:" to sort last in // search results regardless of fuzzy score (used for the Help section). id?: string; // --- Choose one action type (TypeScript union enforces mutual exclusivity) --- // 1. Navigate to?: LocationDescriptor; // 2. Callback onAction?: () => void; // 3. Group/resource — requires children or resource to render anything. // Without at least one of those the component returns null. resource?: (query: string, context: CMDKResourceContext) => CMDKQueryOptions; children?: React.ReactNode | ((data: CommandPaletteAction[]) => React.ReactNode); // --- Group display --- // Overrides the input placeholder when the user drills into this action. // Has no effect without children or resource — the node still needs content // to drill into. prompt?: string; // Max results shown before a "See all" expansion item appears. // Default: 4 when resource is set and children is a render-prop function. // No default for static children. limit?: number; } ``` --- ## Action Patterns ### 1. Navigation link ```tsx import {CMDKAction} from 'sentry/components/commandPalette/ui/cmdk'; import {IconIssues} from 'sentry/icons'; <CMDKAction display={{ label: t('Go to Issues'), icon: <IconIssues />, }} keywords={['bugs', 'errors', 'problems']} to={`/organizations/${org.slug}/issues/`} />; ``` ### 2. Callback action ```tsx <CMDKAction display={{label: t('Resolve Issue'), details: t('Mark as resolved')}} onAction={() => handleResolve(group.id)} /> ``` ### 3. Static group Nest `CMDKAction` children to create a drillable group. The parent label appears as a breadcrumb prefix in search results (e.g. `Set Priority > High`), so use a label that identifies the context. **Group icon as current-state indicator**: set the group's own icon to reflect the current value so the user can see the state before drilling in. Both the priority and assignee selectors do this: ```tsx // Icon reflects current priority — user sees state at a glance <CMDKAction display={{ label: t('Set Priority'), icon: <IconCellSignal bars={PRIORITY_BARS[group.priority ?? PriorityLevel.MEDIUM]} />, }} > <CMDKAction display={{label: t('High'), icon: <IconCellSignal bars={3} />}} onAction={() => setPriority('high')} /> <CMDKAction display={{label: t('Medium'), icon: <IconCellSignal bars={2} />}} onAction={() => setPriority('medium')} /> <CMDKAction display={{label: t('Low'), icon: <IconCellSignal bars={1} />}} onAction={() => setPriority('low')} /> </CMDKAction>; // Icon reflects current assignee — avatar when assigned, generic icon when not const assigneeIcon = group.assignedTo ? ( <ActorAvatar actor={group.assignedTo} size={16} hasTooltip={false} /> ) : ( <IconUser /> ); <CMDKAction display={{label: t('Assign to'), icon: assigneeIcon}}> {/* children */} </CMDKAction>; ``` ### 4. Async resource picker Use `resource` + `cmdkQueryOptions` to load items from an API. The user types to filter. The loading spinner activates automatically while the query is in flight. Note: when the user drills into a resource node the palette clears the query. Your `resource` function will initially receive an empty string — design your query params accordingly. ```tsx import {cmdkQueryOptions} from 'sentry/components/commandPalette/types'; import {apiOptions} from 'sentry/utils/api/apiOptions'; import {ProjectAvatar} from '@sentry/scraps/avatar'; <CMDKAction display={{label: t('Switch Project')}} prompt={t('Select a project...')} limit={5} resource={(query, context) => cmdkQueryOptions({ ...apiOptions.as<Project[]>()('/organizations/$organizationIdOrSlug/projects/', { path: {organizationIdOrSlug: org.slug}, query: {query, per_page: 20}, staleTime: 30_000, }), // Only fetch once the user has drilled into this node enabled: context.state === 'selected', select: projects => projects.map(project => ({ display: { label: project.slug, icon: <ProjectAvatar project={project} size={16} />, }, to: `/organizations/${org.slug}/projects/${project.slug}/`, })), }) } />; ``` **Rules for `resource`:** - **Always** wrap with `cmdkQueryOptions(...)` — this injects `meta: { cmdk: true }` so the palette's loading spinner tracks the request via `useIsFetching`. - Use `enabled: context.state === 'selected'` to defer fetching until the user actually drills in. - The `select` field must transform the API response into `CommandPaletteAction[]`. - `query` is the live search input value (not debounced) — pass it through as a search param. - Use `staleTime: Infinity` for data that rarely changes (project lists, settings nav items). Use `staleTime: 30_000` for user/session data. ### 5. Resource with render-prop children Use a render-prop when you need custom rendering or want to mix static and async items. `CommandPaletteAction` is a union that includes groups (which have `actions`, not `children`). Don't blindly spread items into `CMDKAction` — type-narrow to only handle `to` and `onAction` variants, as the codebase's own `renderAsyncResult` helper does: ```tsx // Type-safe helper — skip groups, which can't be spread into CMDKAction function renderAsyncResult(item: CommandPaletteAction, index: number) { if ('to' in item) return <CMDKAction key={index} {...item} />; if ('onAction' in item) return <CMDKAction key={index} {...item} />; return null; } <CMDKAction display={{label: t('Assign to')}} prompt={t('Search teammates...')} resource={(query, context) => cmdkQueryOptions({ queryKey: ['members', org.slug, query], queryFn: () => fetchMembers(org.slug, query), enabled: context.state === 'selected', select: members => members.map(m => ({ display: {label: m.name, details: m.email}, onAction: () => assignTo(m.id), })), }) } > {members => ( <> {/* Static first entry */} <CMDKAction display={{label: t('Assign to me')}} onAction={assignToMe} /> {/* Dynamic entries from resource */} {members.map(renderAsyncResult)} </> )} </CMDKAction>; ``` **Auto-render limitation**: when `children` is _not_ a render-prop (static children + `resource`), resource results that are `CommandPaletteActionGroup` items are silently skipped. Only `to` and `onAction` results are auto-rendered. Use the render-prop pattern if you need groups from a resource. ### 6. Static async children via hook When a dataset is small and already cached, fetch it with a hook and render it as static JSX children. The palette's built-in fuzzy search handles filtering client-side — no `resource` prop needed: ```tsx // useProjectMembers fetches once and caches; palette fuzzy-searches the results const {data: members = []} = useProjectMembers(project.id); const assignableUsers = members.filter(m => m.id !== currentUser.id); <CMDKAction display={{label: t('Assign to'), icon: assigneeIcon}}> <CMDKAction display={{label: t('Assign to me')}} onAction={() => handleAssign(currentUser)} /> {assignableUsers.map(member => ( <CMDKAction key={`member-${member.id}`} display={{ label: member.name || member.email, icon: ( <ActorAvatar actor={{id: member.id, name: member.name, type: 'user'}} size={16} hasTooltip={false} /> ), }} onAction={() => handleAssign(member)} /> ))} {teams.map(team => ( <CMDKAction key={`team-${team.id}`} display={{ label: `#${team.slug}`, icon: <TeamAvatar team={team} size={16} hasTooltip={false} />, }} onAction={() => handleAssign(team)} /> ))} </CMDKAction>; ``` **When to use static children vs `resource`:** | | Static children via hook | `resource` prop | | ------------- | -------------------------- | ----------------------- | | Dataset size | Small, bounded | Large or unbounded | | Filtering | Client-side fuzzy search | Server-side search | | Fetch timing | Eager (on component mount) | Deferred (on drill-in) | | Query updates | Fixed at render | Responds to typed query | **Key naming for mixed entity lists**: prefix keys with the entity type to prevent collisions — `member-${id}`, `team-${id}`, `${owner.type}-${owner.id}`, `coding-agent:${id}`. ### 7. `trailingItem` — marking the active item
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub