diff --git a/CHANGELOG.md b/CHANGELOG.md
index c3adfbe2..f2db32e3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,12 @@ contract — modules that are not exported are internal and may change in any re
now an explicit `panel` slot with no fallback — pass ``,
``, or any other content. A caller that omitted `panel` to
get the dither canvas for free now gets a bare panel instead.
+- **`ui/command` is removed.** `Command` was a second, weaker inline command surface next
+ to `CommandPaletteInline` — index-based keyboard navigation (which loses the highlighted
+ row when the list re-sorts under a keystroke) and self-filtering, where the palette
+ family deliberately leaves matching to the caller. `CommandAction` moves to
+ `lib/command-registry`, its only consumer; `commandMatches` is gone with the component.
+ Root-barrel imports of `CommandAction` still work; the `./ui/command` subpath is gone.
## [0.1.0] - 2026-09-17
diff --git a/package.json b/package.json
index 28d24833..1d9551d5 100644
--- a/package.json
+++ b/package.json
@@ -393,10 +393,6 @@
"types": "./dist/ui/checkbox.d.ts",
"default": "./dist/ui/checkbox.js"
},
- "./ui/command": {
- "types": "./dist/ui/command.d.ts",
- "default": "./dist/ui/command.js"
- },
"./ui/command-palette": {
"types": "./dist/ui/command-palette.d.ts",
"default": "./dist/ui/command-palette.js"
diff --git a/src/index.ts b/src/index.ts
index 68ae9ffd..e1a934d0 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -82,7 +82,6 @@ export * from "./ui/chat-input.js";
export * from "./ui/chat-panel.js";
export * from "./ui/chat-thread.js";
export * from "./ui/checkbox.js";
-export * from "./ui/command.js";
export * from "./ui/command-palette.js";
export * from "./ui/command-queue.js";
export * from "./ui/compare-body.js";
diff --git a/src/lib/command-registry.tsx b/src/lib/command-registry.tsx
index e9abe9ca..508c9cff 100644
--- a/src/lib/command-registry.tsx
+++ b/src/lib/command-registry.tsx
@@ -1,7 +1,19 @@
import { createContext, useContext, useEffect, useMemo, useState } from "react";
import type { ReactNode } from "react";
-import type { CommandAction } from "../ui/command.js";
+/** One thing the user can do. */
+export type CommandAction = {
+ readonly id: string;
+ readonly label: string;
+ /** Section heading. Actions with no group render before any grouped ones. */
+ readonly group?: string;
+ /** Extra words that should find this action — synonyms, the old name for it. */
+ readonly keywords?: readonly string[];
+ readonly icon?: ReactNode;
+ /** Displayed hint like "⌘K". Purely a label; this file binds nothing. */
+ readonly shortcut?: string;
+ readonly run: () => void;
+};
/**
* Lets a page contribute commands to the palette without the palette importing
diff --git a/src/ui/command.tsx b/src/ui/command.tsx
deleted file mode 100644
index 56f03279..00000000
--- a/src/ui/command.tsx
+++ /dev/null
@@ -1,218 +0,0 @@
-import { useEffect, useId, useMemo, useRef, useState } from "react";
-import type { ReactNode } from "react";
-
-import { cn } from "../lib/utils.js";
-
-/** One thing the user can do. */
-export type CommandAction = {
- readonly id: string;
- readonly label: string;
- /** Section heading. Actions with no group render before any grouped ones. */
- readonly group?: string;
- /** Extra words that should find this action — synonyms, the old name for it. */
- readonly keywords?: readonly string[];
- readonly icon?: ReactNode;
- /** Displayed hint like "⌘K". Purely a label; this file binds nothing. */
- readonly shortcut?: string;
- readonly run: () => void;
-};
-
-/**
- * Subsequence matching, case-insensitive: "wfr" finds "Workflow runs".
- *
- * Deliberately not fuzzy-with-scoring. A scored ranker reorders results as you
- * type, so the row under your cursor moves out from under it — and in a command
- * palette, muscle memory beats cleverness. Order here is the order given.
- */
-export function commandMatches(action: CommandAction, query: string): boolean {
- const needle = query.trim().toLowerCase();
- if (needle.length === 0) return true;
-
- const haystacks = [action.label, action.group ?? "", ...(action.keywords ?? [])];
- return haystacks.some((haystack) => {
- const text = haystack.toLowerCase();
- let index = 0;
- for (const char of needle) {
- index = text.indexOf(char, index);
- if (index === -1) return false;
- index += 1;
- }
- return true;
- });
-}
-
-export type CommandProps = {
- readonly actions: readonly CommandAction[];
- /** Accessible name for the surface, e.g. "Commands". */
- readonly label: string;
- /** Called after an action runs — a palette closes itself here. */
- readonly onRun?: (action: CommandAction) => void;
- readonly placeholder?: string;
- readonly empty?: ReactNode;
- readonly className?: string;
-};
-
-/**
- * A filterable, keyboard-driven action list — the primitive behind a command
- * palette, a picker, or any "type to narrow, arrow to choose" surface.
- *
- * Hand-rolled rather than wrapping cmdk. This file is copied into a consumer's
- * repo and owned by them, and the behaviour worth changing — the matching — is
- * one exported function they can rewrite. A dependency would make the one thing
- * every app customises the one thing they cannot touch.
- *
- * Actions are a prop rather than something items self-register: a flat list the
- * component filters is testable as a pure function of its input, and a
- * registration protocol would put the render order at the mercy of mount order.
- *
- * ARIA combobox: focus stays in the input and the highlight is carried by
- * `aria-activedescendant`. Moving real focus onto options would take it out of
- * the input and stop typing, which is the entire interaction.
- */
-export function Command({
- actions,
- label,
- onRun,
- placeholder = "Type a command…",
- empty = "No matching commands.",
- className,
-}: CommandProps) {
- const [query, setQuery] = useState("");
- const [activeIndex, setActiveIndex] = useState(0);
- const baseId = useId();
- const listRef = useRef(null);
- const inputRef = useRef(null);
-
- const visible = useMemo(() => actions.filter((action) => commandMatches(action, query)), [actions, query]);
-
- // The highlight is an index into a list that shrinks as you type; clamp it
- // rather than letting aria-activedescendant point at an id that is gone.
- const active = Math.min(activeIndex, Math.max(visible.length - 1, 0));
-
- useEffect(() => {
- inputRef.current?.focus();
- }, []);
-
- useEffect(() => {
- listRef.current?.querySelector('[data-active="true"]')?.scrollIntoView({ block: "nearest" });
- }, [active, query]);
-
- const run = (action: CommandAction) => {
- action.run();
- onRun?.(action);
- };
-
- const optionId = (index: number) => `${baseId}-option-${index}`;
-
- // Grouped for display, but the keyboard walks `visible` in order — so the
- // index the user is on always means the same row they can see.
- const groups = useMemo(() => {
- const order: string[] = [];
- const byGroup = new Map();
- visible.forEach((action, index) => {
- const key = action.group ?? "";
- if (!byGroup.has(key)) {
- byGroup.set(key, []);
- order.push(key);
- }
- byGroup.get(key)?.push({ action, index });
- });
- return order.map((key) => ({ heading: key, items: byGroup.get(key) ?? [] }));
- }, [visible]);
-
- return (
-