The Setup

I've been building a practice management system for a law firm — think contacts, matters, conflict checks, engagement letters, the whole intake-to-close flow.

The app needed an AI assistant baked in.

So I built one.

What I Built First (And Shouldn't Have)

A slide-out drawer.

Persistent. Always visible. Accessible from the side panel at all times.

It seemed reasonable at the time. The AI should live somewhere obvious, right?

Wrong.

The drawer ate screen real estate. It competed with the matter detail. It was always there, which meant you stopped seeing it. Nobody uses the thing that's always in your peripheral vision.

The wall was subtle — not a crash, not a build error. Just… nobody opened it.

The Pattern I Borrowed

I've been obsessed with how Wisprflow (the voice-dictation app with the floating pill UI) handles presence — it exists when you call it, then vanishes.

That's the right model for an in-app assistant.

Summon it. Use it. Dismiss it. Gone.

So I ripped out the slide-out and replaced it with a ⌘K command palette using cmdk (the headless command-menu library) and motion (the Framer Motion animation lib, now just "motion") for the entrance/exit animation.

The palette floats. It's centered on the viewport. It opens on ⌘K from anywhere in the app. It closes on Escape or click-outside.

The difference in feel was IMMEDIATE.

What Actually Made It Useful

Here's the part people skip when they write about command palettes — the UI is nothing without the query layer.

Before the redesign, the AI assistant was doing natural-language handwaving over vague data. After, I wired it to structured field-query tools so it could actually filter contacts and matters by any column.

Then I fixed search itself: tokenized multi-word matching, so "Smith LLC" hit on both tokens, not just the exact string.

Now when you open ⌘K and type "show me all open matters for Smith" — it just works.

The Gotcha That Burned Me

Radix's CommandDialog component renders an always-present, screen-reader-only [data-slot=dialog-title] with the text "Command Palette" even when the dialog is completely closed.

I was querying dialog titles to detect open state and kept getting a false positive.

Took me longer than I'd like to admit to realize the component was technically "in the DOM" the whole time.

Fix: filter by the specific dialog element you expect, not just any dialog title. The sr-only ghost will fool you every time.

Why This Matters to Me

There's a version of "AI in your app" that's a permanent fixture demanding your attention.

And there's a version that waits to be asked.

The second one actually gets used.

If you're embedding an assistant into a real workflow tool — a CRM, a PM app, a legal platform, anything — don't give it a permanent home. Give it a summon key. The users who figure out that ⌘K is there will never stop using it.

P.S. The Radix sr-only ghost is documented nowhere obvious. Now you know.