Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a9469f7
Update Clips recording and agent workflows
shawnmcclelland Sep 8, 2026
ed7f000
Merge branch 'main' into ux-fixes
shawnmcclelland Sep 8, 2026
e814bee
fix: resolve PR review and CI feedback
shawnmcclelland Sep 8, 2026
1f7f0b5
fix: align collapsed Clips sidebar controls
shawnmcclelland Sep 8, 2026
b932fce
chore: remove generated Clips database lock
shawnmcclelland Sep 8, 2026
4a032d6
fix: unify Clips route breadcrumbs
shawnmcclelland Sep 8, 2026
6dcc509
fix: scope folder queries to the active organization
shawnmcclelland Sep 8, 2026
7d1d1cb
Merge origin/main into ux-fixes
shawnmcclelland Sep 8, 2026
c1e77be
fix: resolve remaining Clips review findings
shawnmcclelland Sep 9, 2026
c4c7b8e
Merge origin/main into ux-fixes
shawnmcclelland Sep 9, 2026
1b82d89
fix: rebalance the sidebar feedback action
shawnmcclelland Sep 9, 2026
bfce827
fix: simplify the sidebar feedback action
shawnmcclelland Sep 9, 2026
42f8db5
feat: add library canvas import actions
shawnmcclelland Sep 9, 2026
5324dce
fix: restore the canonical agent chat icon
shawnmcclelland Sep 9, 2026
6f0e032
fix: show recording counts for folders
shawnmcclelland Sep 9, 2026
2a7d3f5
fix: align space pages with library shell
shawnmcclelland Sep 9, 2026
365d138
Improve library navigation and folder recording counts
shawnmcclelland Sep 9, 2026
eee5ffd
fix: restore chromeless desktop popover
shawnmcclelland Sep 9, 2026
464c4f9
Fix dark recorder switches and agent sidebar sizing
shawnmcclelland Sep 9, 2026
d5d5420
fix: use the global Agent panel on recordings
shawnmcclelland Sep 9, 2026
59a602a
Improve Clips recording and transcript workflows
shawnmcclelland Sep 9, 2026
1bf2832
Merge remote-tracking branch 'origin/ux-fixes' into ux-fixes
shawnmcclelland Sep 9, 2026
edd3a97
chore: publish branch work in templates/clips (3 files)
shawnmcclelland Sep 9, 2026
f80ad82
Merge remote-tracking branch 'origin/main' into ux-fixes
shawnmcclelland Sep 9, 2026
2372b56
test: avoid credential-shaped JWT fixture
shawnmcclelland Sep 9, 2026
639e218
test: tolerate transient scaffold cleanup races
shawnmcclelland Sep 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-command-menu-shortcut.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@agent-native/core": patch
---

Prevent Cmd/Ctrl+K from reaching an outer host while a command menu input is focused.
123 changes: 123 additions & 0 deletions docs/command-menu-architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Shared command-menu architecture

Status: planned follow-up. Do not treat the current app-specific command menus
as the long-term architecture.

## Decision

Use `cmdk` as the interaction engine, keep the command-menu shell and registry
contract in shared Agent-Native code, and let each app register its own commands
and searchable resources.

The shared layer should own the behavior that must be identical everywhere:

- Cmd/Ctrl+K opening, toggling, focus, and dismissal
- the dialog, input, list, group, item, shortcut, loading, and empty states
- keyboard navigation, selection, and accessible labeling
- command ranking and the boundary between static commands and async results
- common framework commands such as theme, agent, settings, changelog, and
diagnostics

Apps should own only their domain knowledge:

- localized command labels and descriptions
- icons, keywords, shortcuts, and visibility rules
- route-aware context and permission checks
- command handlers that call the app's existing action/navigation surfaces
- async search providers for resources such as recordings, meetings,
dictations, documents, or CRM records

The shared layer must not become a universal data index or import app routes.
It coordinates registered providers; it does not invent domain results.

## Proposed contract

The core package should expose a registry/provider API along these lines:

```ts
type CommandContext = {
pathname: string;
searchParams: URLSearchParams;
appId: string;
organizationId?: string;
};

type CommandDefinition = {
id: string;
group: string;
label: string;
description?: string;
keywords?: string[];
shortcut?: string;
icon?: React.ComponentType<{ size?: number; className?: string }>;
availableWhen?: (context: CommandContext) => boolean;
run: (context: CommandContext) => void | Promise<void>;
};

type CommandSearchProvider = {
id: string;
search: (
query: string,
context: CommandContext,
) => Promise<CommandDefinition[]>;
};
```

The final names and exact shape should follow the existing core type conventions.
The important boundary is that the menu renders descriptors and provider
results instead of each app manually rebuilding the palette's React tree and
filtering its children.

## Current state

The repository already has the beginnings of this split:

- `packages/toolkit/src/ui/command.tsx` wraps the `cmdk` primitive.
- `packages/core/src/client/CommandMenu.tsx` owns the shared dialog shell,
keyboard hook, framework entries, and composable group/item surface.
- `templates/clips/app/components/clips-command-menu.tsx` currently owns the
Clips registry, route context, navigation handlers, and recording/meeting/
dictation search providers.

The remaining problem is that app registries are still hand-authored JSX. The
same pattern exists in other templates, so improvements currently require
duplicated work and can drift in behavior.

## Migration plan

1. Add the shared descriptor/provider types and registry context in core.
2. Make the shared `CommandMenu` render registered descriptors while keeping
its existing composable API temporarily for compatibility.
3. Move common framework commands into the shared registry.
4. Convert Clips from `ClipsCommandMenu` JSX groups to registered static
commands plus registered search providers. Preserve its route-aware
commands and action-backed searches.
5. Convert the other app menus (including CRM, Forms, Dispatch, and Macros) to
the same registration surface.
6. Remove duplicate per-app shortcut listeners and bespoke static filtering
after all consumers migrate.
7. Add shared contract tests for registration, availability, ranking, async
loading, stale-result suppression, keyboard selection, and contextual
commands; retain app tests for domain-specific handlers and routes.

## Acceptance criteria

- An app can add commands and search providers without copying the command
dialog or keyboard handling.
- A command is hidden when its app-provided context or permission predicate
says it is unavailable.
- Search results can navigate through the app's existing client routing and
actions without raw API calls or a second data model.
- Async providers show loading and empty states consistently and cannot display
stale results from a previous query or route.
- Cmd/Ctrl+K opens exactly one menu in a host containing multiple Agent-Native
surfaces.
- App-local commands remain localized and can link to the current resource,
folder, meeting, or dictation context.

## Non-goals

- Replacing `cmdk` with a second command-palette dependency.
- Putting app route definitions or resource-specific SQL in core.
- Creating one global search endpoint that every app must use.
- Refactoring the current Clips menu as part of an unrelated UX-fixes change.
7 changes: 6 additions & 1 deletion packages/core/src/cli/create-start-shape.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ beforeEach(() => {

afterEach(() => {
process.chdir(originalCwd);
fs.rmSync(parentDir, { recursive: true, force: true });
fs.rmSync(parentDir, {
recursive: true,
force: true,
maxRetries: 3,
retryDelay: 50,
});
vi.clearAllMocks();
});

Expand Down
45 changes: 37 additions & 8 deletions packages/core/src/client/CommandMenu.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,34 @@ describe("CommandMenu docs group", () => {
);
});

it("filters command items nested in fragments", () => {
act(() => {
root.render(
<CommandMenu
open
onOpenChange={() => undefined}
showAgentFallback={false}
>
<CommandMenu.Group heading="Actions">
<>
<CommandMenu.Item onSelect={() => undefined}>
Open comments
</CommandMenu.Item>
<CommandMenu.Item onSelect={() => undefined}>
Open transcript
</CommandMenu.Item>
</>
</CommandMenu.Group>
</CommandMenu>,
);
});

search("transcript");

expect(document.body.textContent).not.toContain("Open comments");
expect(document.body.textContent).toContain("Open transcript");
});

it("offers the shared About Agent-Native surface and matches version searches", () => {
act(() => {
root.render(
Expand Down Expand Up @@ -386,7 +414,7 @@ describe("CommandMenu docs group", () => {
expect(document.body.textContent).toContain("open");
});

it("does not open from native select controls when contenteditable is allowed", () => {
it("claims Cmd+K from native controls without opening", () => {
function ShortcutHarness() {
const [open, setOpen] = React.useState(false);
useCommandMenuShortcut(() => setOpen(true), {
Expand All @@ -408,17 +436,18 @@ describe("CommandMenu docs group", () => {

const select = document.querySelector("select");
expect(select).toBeTruthy();
const event = new KeyboardEvent("keydown", {
key: "k",
metaKey: true,
bubbles: true,
cancelable: true,
});
act(() => {
select!.dispatchEvent(
new KeyboardEvent("keydown", {
key: "k",
metaKey: true,
bubbles: true,
}),
);
select!.dispatchEvent(event);
});

expect(document.body.textContent).toContain("closed");
expect(event.defaultPrevented).toBe(true);
});

it("opens from contenteditable before editor handlers stop propagation", () => {
Expand Down
15 changes: 14 additions & 1 deletion packages/core/src/client/CommandMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,15 @@ export function CommandMenu({
if (!React.isValidElement(child)) return child;
const props = child.props as Record<string, unknown>;

if (child.type === React.Fragment) {
const fragmentChildren = filterChildren(props.children as ReactNode);
if (React.Children.count(fragmentChildren) === 0) return null;
return React.cloneElement(child, {
...props,
children: fragmentChildren,
} as Record<string, unknown>);
}

// If it's a CommandGroup, filter its children
if (child.type === CommandGroup) {
const groupChildren = filterChildren(props.children as ReactNode);
Expand Down Expand Up @@ -667,6 +676,11 @@ export function useCommandMenuShortcut(
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
// Claim the shortcut before checking the focused element so an outer
// host cannot open its own command menu while this one is focused.
e.preventDefault();
e.stopPropagation();

// Don't trigger if user is typing in a native form control.
const target = e.target instanceof HTMLElement ? e.target : null;
const isContentEditable = target?.isContentEditable;
Expand All @@ -678,7 +692,6 @@ export function useCommandMenuShortcut(
) {
return;
}
e.preventDefault();
onOpen();
}
};
Expand Down
12 changes: 9 additions & 3 deletions packages/core/src/email-catalog/redact-body.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,16 @@ describe("redactSensitiveEmailBodyContent", () => {
});

it("redacts a JWT-shaped token", () => {
const text =
"Session token: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U";
const fakeJwt = [
Buffer.from(JSON.stringify({ alg: "HS256" })).toString("base64url"),
Buffer.from(JSON.stringify({ sub: "example-user" })).toString(
"base64url",
),
Buffer.from("not-a-signature").toString("base64url"),
].join(".");
const text = `Session token: ${fakeJwt}`;
const redacted = redactSensitiveEmailBodyContent(text);
expect(redacted).not.toContain("eyJhbGciOiJIUzI1NiJ9");
expect(redacted).not.toContain(fakeJwt);
expect(redacted).toContain("[REDACTED]");
expect(redacted).toContain("Session token:");
});
Expand Down
1 change: 1 addition & 0 deletions templates/clips/.agents/skills/dictate/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Dictate captures **mic only** — system audio is never recorded for dictations.
| Action | What it does |
| -------------------- | ------------------------------------------------------------------------------------------- |
| `list-dictations` | Past dictations, scoped via `accessFilter` |
| `search-dictations` | Search native or cleaned dictation text, with matching snippets |
| `cleanup-dictation` | Polish a single dictation's text (writes `cleanedText`) |
| `cleanup-transcript` | Shared cleanup pipeline (also used by Clips + Meetings); resolves credentials per the order below |

Expand Down
17 changes: 10 additions & 7 deletions templates/clips/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,23 @@ transcript, agent, insights, and settings.

## Progressive disclosure

The viewer presents jobs, not inventories. Sharing starts with one header-level
copy-link action, invitations, and current access. Social destinations and
embed publishing replace the body as focused secondary views; embed
configuration and agent context links stay collapsed until requested. The
The viewer presents jobs, not inventories. Human sharing and agent continuity
are separate jobs: Share owns invitations, durable access policy, password,
expiry, social destinations, and embed publishing; a quiet adjacent Send to
agent action owns ephemeral handoff to an agent destination. Secondary sharing
destinations replace the Share body as focused views. The
overflow leads with recording cleanup actions, while maintenance and
document-generation commands live in named submenus. A generic AI-tools
launcher does not compete with Share. The editor opens in transcript mode and
reveals the precision timeline as a peer mode instead of stacking both
workspaces under the player.

Share is the viewer toolbar's sole labeled primary action and the product-led
growth entry point. Copy link is the first action inside Share, never a competing
toolbar button. Edit, download, and overflow use equal compact icon controls
with accessible names and tooltips; their visual weight must not rival Share.
growth entry point. Send to agent is an accessible secondary icon action that
opens one compact handoff menu; it must not become a second sharing/settings dialog. Copy
link stays inside Share, never as a competing toolbar button. Edit, download,
and overflow use equal compact icon controls with accessible names and tooltips;
their visual weight must not rival Share.

Viewer identity uses one avatar grammar everywhere. People use profile images or
initials; agents use the same circular avatar shape with the assistant mark.
Expand Down
Loading
Loading