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.
التثبيت
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
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:
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:
// In a page component or its sub-tree
<CommandPaletteSlot name="page">
<CMDKActiondisplay={{label:t('ResolveIssue')}} onAction={handleResolve} />
</CommandPaletteSlot>
Global actions are registered once in GlobalCommandPaletteActions — add to that component rather than creating a new global slot consumer.
CMDKAction Props
interfaceCMDKActionProps {
// Required: what the user seesdisplay: {
label: string; // primary textdetails?: string; // secondary description lineicon?: 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 recallkeywords?: 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. Navigateto?: LocationDescriptor;
// 2. CallbackonAction?: () =>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;
}
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:
// Icon reflects current priority — user sees state at a glance
<CMDKAction
display={{
label: t('Set Priority'),
icon: <IconCellSignalbars={PRIORITY_BARS[group.priority ?? PriorityLevel.MEDIUM]} />,
}}
>
<CMDKActiondisplay={{label:t('High'), icon: <IconCellSignalbars={3} />}}
onAction={() => setPriority('high')}
/><CMDKActiondisplay={{label:t('Medium'), icon: <IconCellSignalbars={2} />}}
onAction={() => setPriority('medium')}
/><CMDKActiondisplay={{label:t('Low'), icon: <IconCellSignalbars={1} />}}
onAction={() => setPriority('low')}
/>
</CMDKAction>;
// Icon reflects current assignee — avatar when assigned, generic icon when notconst assigneeIcon = group.assignedTo ? (
<ActorAvataractor={group.assignedTo}size={16}hasTooltip={false} />
) : (
<IconUser />
);
<CMDKActiondisplay={{label:t('Assignto'), 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.
import {cmdkQueryOptions} from'sentry/components/commandPalette/types';
import {apiOptions} from'sentry/utils/api/apiOptions';
import {ProjectAvatar} from'@sentry/scraps/avatar';
<CMDKActiondisplay={{label:t('SwitchProject')}}
prompt={t('Selectaproject...')}
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: <ProjectAvatarproject={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:
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:
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
Use trailingItem with a "Current" badge only for entity selections where the user is switching between distinct objects — projects, organizations, users, environments, and similar. The badge answers the question "which one am I on right now?" when the items are otherwise indistinguishable.
Do not use "Current" for settings or modes (sort order, theme, display density, etc.). Those have a single correct value at any time, and the group's own label or icon already reflects it (see the group-icon-as-state-indicator pattern above). A "Current" badge on a settings option duplicates information without adding clarity.
import {Tag} from'@sentry/scraps/badge';
// ✅ Entity selection — badge makes sense: user sees which project they're on<CMDKActiondisplay={{label:t('SwitchProject')}}
prompt={t('Selectaproject...')}
resource={(query,context) => cmdkQueryOptions({...})}
>
{/* Current project rendered statically so it always appears first */}
<CMDKActiondisplay={{label:currentProject.slug,
icon: <ProjectAvatarproject={currentProject}size={16} />,
trailingItem: <Tagvariant="muted">{t('Current')}</Tag>,
}}
to={`/organizations/${org.slug}/projects/${currentProject.slug}/`}
/>
</CMDKAction>// ❌ Settings/mode — do not use a badge; the group label already shows the active value<CMDKActiondisplay={{label:t('Sortby: %s', getSortLabel(sort)), // labelreflectscurrentstateicon: <IconSort />,
}}
>
{sortKeys.map(key => (
<CMDKActionkey={key}display={{label:getSortLabel(key),
// trailingItem:key === sort ? <Tagvariant="muted">{t('Current')}</Tag> : undefined
// ❌ Don't do this — the group label already communicates the active sort
}}
onAction={() => onSortChange(key)}
/>
))}
</CMDKAction>
7. Query-content-gated resource
A resource can activate based on what the user has typed, not just drill-in state. Use this for contextual lookup tools that only make sense for a specific query shape:
constDSN_PATTERN = /^https?:\/\/.+@.+\/.+/;
<CMDKActiondisplay={{label:t('DSNLookup')}}
prompt={t('PasteaDSN...')}
resource={(query,context) =>
cmdkQueryOptions({
...apiOptions.as<DsnLookupResponse>()(/* ... */),
// Only fetch when the query looks like a DSN
enabled: context.state === 'selected' && DSN_PATTERN.test(query),
select: result => result.navTargets.map(/* ... */),
})
}
/>;
8. State-conditional actions
Render different actions based on current entity state — not just feature flags. Actions that don't apply to the current state should simply not render:
Prefix id with cmdk:supplementary: to sort the section after all other results, regardless of search score. Reserved for content like Help links that should never surface above real actions.
When a page's action set is complex, split it across multiple components. Child components that register actions do not need their own slot — they inherit the slot context from the parent that established it. Just emit <CMDKAction> nodes directly:
// views/issueDetails/groupPriorityActions.tsx// No slot here — registers under whatever parent mounts thisfunctionGroupPriorityActions({group}: {group: Group}) {
return (
<CMDKActiondisplay={{label:t('SetPriority')}}><CMDKActiondisplay={{label:t('High')}} onAction={() => setPriority('high')} />
<CMDKActiondisplay={{label:t('Medium')}} onAction={() => setPriority('medium')} />
<CMDKActiondisplay={{label:t('Low')}} onAction={() => setPriority('low')} />
</CMDKAction>
);
}
// views/issueDetails/seerActions.tsx// Returns a Fragment of siblings — adds actions into the parent group without// creating an extra nesting levelfunctionSeerActions({group}: {group: Group}) {
if (!canShowSeer) returnnull;
return (
<Fragment><CMDKActiondisplay={{label:t('FixwithSeer'), icon: <IconSeer />}}
onAction={startAutofix}
/>
</Fragment>
);
}
// views/issueDetails/issueCommandPaletteActions.tsx// Only this component owns the slotfunctionIssueCommandPaletteActions({group, issue}: Props) {
return (
<CommandPaletteSlotname="page"><CMDKActiondisplay={{label:issue.title,
icon: <ProjectAvatarproject={project}size={16} />,
}}
>
<GroupPriorityActionsgroup={group} /><SeerActionsgroup={group} /></CMDKAction></CommandPaletteSlot>
);
}
Use <Fragment> (not a wrapping <CMDKAction>) when a child component contributes flat siblings into an existing parent group.
Registering Global Actions
Add to GlobalCommandPaletteActions in commandPaletteGlobalActions.tsx. Don't create a second global slot consumer — there is one slot outlet in the navigation shell, so a second consumer would compete with it rather than extend it. It's a JSX component — just insert a new CMDKAction in the relevant group or create a new named group:
Create a component that wraps actions in <CommandPaletteSlot name="page"> and mount it inside the relevant page component. The actions register and deregister automatically with the page's mount/unmount lifecycle.
Gate the entire slot when a page is disabled — don't render individual disabled actions; don't render the slot at all:
// ✅ Gate at the slot level
{
!disabled && (
<CommandPaletteSlotname="page"><CMDKActiondisplay={{label:entity.title}}>{/* all actions */}</CMDKAction></CommandPaletteSlot>
);
}
Capability Config
When an entity type determines which actions are available, derive that from a config object rather than inline conditionals. getConfigForIssueType(group, project) returns per-action capability flags:
For new entity types, follow the same pattern: define a config shape that carries capability flags, then gate rendering on those flags rather than scattered group.type === '...' checks.
Workflow / Sequential State Machine
When actions represent steps in a multi-stage workflow, show only the next valid action — not all possible steps at once. Gate each step on the previous step being complete and the next not yet started:
// Extract state into a dedicated hook in the same filefunctionuseSeerState(group: Group, project: Project) {
const autofix = useExplorerAutofix(group.id);
const sections = getOrderedAutofixSections(autofix.runState);
return {
autofix,
completedRootCause: sections.some(
s =>isRootCauseSection(s) && s.status === 'completed'
),
completedSolution: sections.some(
s =>isSolutionSection(s) && s.status === 'completed'
),
completedCodeChanges: sections.some(
s =>isCodeChangesSection(s) && s.status === 'completed'
),
hasPR: sections.some(isPullRequestsSection),
runId: autofix.runState?.run_id,
isPolling: autofix.isPolling,
};
}
functionWorkflowActions({group, project}: Props) {
const {
autofix,
completedRootCause,
completedSolution,
completedCodeChanges,
hasPR,
runId,
isPolling,
} = useSeerState(group, project);
// Guard: can only advance the workflow when not mid-operation and run existsconst canContinue = !isPolling && defined(runId);
return (
<Fragment>
{(!autofix.runState || autofix.runState.status === 'error') && (
<CMDKActiondisplay={{label:t('FixwithSeer')}} onAction={startFix} />
)}
{canContinue && completedRootCause && !completedSolution && (
<CMDKActiondisplay={{label:t('Generatesolution')}}
onAction={() => nextStep('solution', runId)}
/>
)}
{canContinue && completedSolution && !completedCodeChanges && (
<CMDKActiondisplay={{label:t('Generatecodechanges')}}
onAction={() => nextStep('code_changes', runId)}
/>
)}
{canContinue && completedCodeChanges && !hasPR && (
<CMDKActiondisplay={{label:t('Openpullrequest')}}
onAction={() => createPR(runId)}
/>
)}
</Fragment>
);
}
Key points:
Extract the state logic into a dedicated use*State hook within the action component file — keeps the JSX clean.
Use a canContinue guard to prevent showing progress actions while an async operation is in flight.
Return null early at the top of the component when the feature isn't applicable:
// Guard clause — return null before any hooks if possible, else afterif (!aiConfig.areAiFeaturesAllowed || !isExplorer || !issueTypeSupportsSeer || !event) {
returnnull;
}
Dynamic Labels
Embed the current value in an action label to give context without requiring the user to drill in first:
// Shows who is currently assigned
<CMDKAction
display={{label: t('Unassign from %s', currentAssigneeName)}}
onAction={() =>handleAssigneeChange(null)}
/>
// Shows the current value being changed<CMDKActiondisplay={{label:t('Changetheme: %s', currentTheme)}}
onAction={openThemePicker}
/>
Use t('... %s', value) (printf-style) rather than template literals so strings remain translatable.
Checklist
Wrap page-level actions in <CommandPaletteSlot name="page">; add global actions to GlobalCommandPaletteActions
Child components that split a page action set do not add their own slot — they inherit from the parent
All resource functions use cmdkQueryOptions(...)
resource functions set enabled: context.state === 'selected' to defer fetching (or a query-content check for contextual resources)
select in resource options returns CommandPaletteAction[]
prompt is set on any drill-target that replaces the search placeholder
limit is set on resource nodes to avoid overwhelming the list (default 4 only applies when resource AND children is a render-prop function; auto-render mode has no default)
staleTime: Infinity for stable lists (projects, nav items); staleTime: 30_000 for dynamic data
id="cmdk:supplementary:..." on any section that should always sort last
keywords added for non-obvious actions to improve search recall
Section/group icons use default size; avatar icons (ProjectAvatar, ActorAvatar, TeamAvatar) use size={16}
State-conditional actions (resolved, archived, etc.) are rendered conditionally rather than disabled
disabled state gates the entire <CommandPaletteSlot>, not individual actions
Group icon reflects the current value of the setting it controls (priority, assignee, theme)
Dynamic action labels use t('... %s', value) not template literals, so strings stay translatable
Workflow action components extract state logic into a dedicated use*State hook and use a canContinue guard
Components that are not applicable return null early via a guard clause before rendering any JSX
Entity capability config (e.g. getConfigForIssueType) drives action availability rather than scattered type checks
Dynamic list keys use type-id format (member-${id}, team-${id}) to prevent cross-type collisions