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.

来源信息

仓库
getsentry/sentry
最近来源活动
2026年4月30日 16:32
检测到的 SKILL.md 语言
英语
星标
44,880
分支
4,869

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
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/issues'; <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
在 GitHub 查看
这个 SKILL.md 很大,SkillsMP 这里只预览前一段内容。 在 GitHub 查看