Files
cameleer-server/docs/superpowers/plans/2026-04-02-context-aware-cmdk.md
hsiegeln 28f38331cc docs: implementation plan for context-aware cmd-k search
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:27:07 +02:00

16 KiB

Context-Aware Cmd-K Search Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Make cmd-k search context-aware — show users/groups/roles when on admin pages instead of operational data.

Architecture: Route-based data swap in LayoutShell.tsx. When isAdminPage is true, build search data from admin RBAC hooks instead of catalog/agents/exchanges. RbacPage reads navigation state to auto-switch tab and highlight the selected item.

Tech Stack: React, @cameleer/design-system v0.1.26, react-router location state


File Structure

File Responsibility
ui/package.json Bump DS to v0.1.26
ui/src/components/LayoutShell.tsx Import admin hooks, build admin search data, swap based on isAdminPage, update select/submit handlers for admin categories
ui/src/pages/Admin/RbacPage.tsx Read location state { tab, highlight }, switch tab and scroll to highlighted item

Task 1: Bump Design System to v0.1.26

Files:

  • Modify: ui/package.json

  • Step 1: Update DS version

In ui/package.json, change the @cameleer/design-system dependency:

"@cameleer/design-system": "^0.1.26",
  • Step 2: Install and verify

Run:

cd ui && npm install

Expected: Clean install, no errors. node_modules/@cameleer/design-system/package.json shows version 0.1.26.

  • Step 3: Verify SearchCategory is now open

Check that SearchCategory accepts arbitrary strings by running:

npx tsc --noEmit

Expected: Clean compile (existing code still valid since old categories are still valid strings).

  • Step 4: Commit
git add ui/package.json ui/package-lock.json
git commit -m "chore: bump @cameleer/design-system to v0.1.26"

Task 2: Build Admin Search Data in LayoutShell

Files:

  • Modify: ui/src/components/LayoutShell.tsx

This task adds the admin search data builder function and wires it into the search data flow, swapping based on isAdminPage.

  • Step 1: Add admin search data builder function

Add this function after the existing buildSearchData function (after line 96 in LayoutShell.tsx):

function buildAdminSearchData(
  users: UserDetail[] | undefined,
  groups: GroupDetail[] | undefined,
  roles: RoleDetail[] | undefined,
): SearchResult[] {
  const results: SearchResult[] = [];

  if (users) {
    for (const u of users) {
      results.push({
        id: `user:${u.userId}`,
        category: 'user',
        title: u.displayName || u.userId,
        meta: u.userId,
        path: '/admin/rbac',
      });
    }
  }

  if (groups) {
    for (const g of groups) {
      results.push({
        id: `group:${g.id}`,
        category: 'group',
        title: g.name,
        meta: g.parentGroupId ? `parent: ${g.parentGroupId}` : 'top-level group',
        path: '/admin/rbac',
      });
    }
  }

  if (roles) {
    for (const r of roles) {
      results.push({
        id: `role:${r.id}`,
        category: 'role',
        title: r.name,
        meta: r.scope,
        path: '/admin/rbac',
      });
    }
  }

  return results;
}
  • Step 2: Add admin hook imports

Add to the existing import from '../../api/queries/admin/rbac' — but this file doesn't import from rbac yet. Add a new import near the top of the file (after the existing query imports around line 21):

import { useUsers, useGroups, useRoles } from '../api/queries/admin/rbac';
import type { UserDetail, GroupDetail, RoleDetail } from '../api/queries/admin/rbac';
  • Step 3: Call admin hooks in LayoutContent

Inside LayoutContent(), after the existing useAttributeKeys() call (line 228), add:

  // --- Admin search data (only fetched on admin pages) ----------------
  const { data: adminUsers } = useUsers();
  const { data: adminGroups } = useGroups();
  const { data: adminRoles } = useRoles();

Note: These hooks use useQuery which caches results. When the user is on admin pages and switches to cmd-k, the data is already warm from the RBAC page itself. When not on admin pages, these queries still fire but are lightweight (small payloads, cached by react-query). If we want to avoid the extra fetches when not on admin, we can add enabled: isAdminPage — but this is an optimization and the hooks in rbac.ts don't support an enabled parameter currently. The simplest approach is to let them fire; react-query deduplication handles the rest.

  • Step 4: Build admin search data with useMemo

After the catalogRef block (around line 366), add:

  const adminSearchData: SearchResult[] = useMemo(
    () => buildAdminSearchData(adminUsers, adminGroups, adminRoles),
    [adminUsers, adminGroups, adminRoles],
  );
  • Step 5: Swap search data based on isAdminPage

Replace the existing searchData useMemo block (lines 368-402):

  const operationalSearchData: SearchResult[] = useMemo(() => {
    if (isAdminPage) return [];

    const exchangeItems: SearchResult[] = (exchangeResults?.data || []).map((e: any) => ({
      id: e.executionId,
      category: 'exchange' as const,
      title: e.executionId,
      badges: [{ label: e.status, color: statusToColor(e.status) }],
      meta: `${e.routeId} · ${e.applicationId ?? ''} · ${formatDuration(e.durationMs)}`,
      path: `/exchanges/${e.applicationId ?? ''}/${e.routeId}/${e.executionId}`,
      serverFiltered: true,
      matchContext: e.highlight ?? undefined,
    }));

    const attributeItems: SearchResult[] = [];
    if (debouncedQuery) {
      const q = debouncedQuery.toLowerCase();
      for (const e of exchangeResults?.data || []) {
        if (!e.attributes) continue;
        for (const [key, value] of Object.entries(e.attributes as Record<string, string>)) {
          if (key.toLowerCase().includes(q) || String(value).toLowerCase().includes(q)) {
            attributeItems.push({
              id: `${e.executionId}-attr-${key}`,
              category: 'attribute' as const,
              title: `${key} = "${value}"`,
              badges: [{ label: e.status, color: statusToColor(e.status) }],
              meta: `${e.executionId} · ${e.routeId} · ${e.applicationId ?? ''}`,
              path: `/exchanges/${e.applicationId ?? ''}/${e.routeId}/${e.executionId}`,
              serverFiltered: true,
            });
          }
        }
      }
    }

    return [...catalogRef.current, ...exchangeItems, ...attributeItems];
  }, [isAdminPage, catalogRef.current, exchangeResults, debouncedQuery]);

  const searchData = isAdminPage ? adminSearchData : operationalSearchData;
  • Step 6: Verify compilation

Run:

cd ui && npx tsc --noEmit

Expected: Clean compile.

  • Step 7: Commit
git add ui/src/components/LayoutShell.tsx
git commit -m "feat: build admin search data for cmd-k on admin pages"

Task 3: Update Palette Select and Submit Handlers

Files:

  • Modify: ui/src/components/LayoutShell.tsx

This task updates handlePaletteSelect to navigate with tab/highlight state for admin results, and handlePaletteSubmit to navigate to the top result when on admin pages.

  • Step 1: Update handlePaletteSelect for admin categories

Replace the existing handlePaletteSelect callback (lines 443-459) with:

  const ADMIN_CATEGORIES = new Set(['user', 'group', 'role']);
  const ADMIN_TAB_MAP: Record<string, string> = { user: 'users', group: 'groups', role: 'roles' };

  const handlePaletteSelect = useCallback((result: any) => {
    if (result.path) {
      if (ADMIN_CATEGORIES.has(result.category)) {
        const itemId = result.id.split(':').slice(1).join(':');
        navigate(result.path, {
          state: { tab: ADMIN_TAB_MAP[result.category], highlight: itemId },
        });
      } else {
        const state: Record<string, unknown> = { sidebarReveal: result.path };
        if (result.category === 'exchange' || result.category === 'attribute') {
          const parts = result.path.split('/').filter(Boolean);
          if (parts.length === 4 && parts[0] === 'exchanges') {
            state.selectedExchange = {
              executionId: parts[3],
              applicationId: parts[1],
              routeId: parts[2],
            };
          }
        }
        navigate(result.path, { state });
      }
    }
    setPaletteOpen(false);
  }, [navigate, setPaletteOpen]);
  • Step 2: Update handlePaletteSubmit for admin context

Replace the existing handlePaletteSubmit callback (lines 461-466) with:

  const handlePaletteSubmit = useCallback((query: string) => {
    if (isAdminPage) {
      // Navigate to top result — CommandPalette calls onSelect for the top result
      // when Enter is pressed, so this is only reached if there are no results.
      // Navigate to RBAC page as fallback.
      navigate('/admin/rbac');
    } else {
      const baseParts = ['/exchanges'];
      if (scope.appId) baseParts.push(scope.appId);
      if (scope.routeId) baseParts.push(scope.routeId);
      navigate(`${baseParts.join('/')}?text=${encodeURIComponent(query)}`);
    }
  }, [isAdminPage, navigate, scope.appId, scope.routeId]);
  • Step 3: Verify compilation

Run:

cd ui && npx tsc --noEmit

Expected: Clean compile.

  • Step 4: Commit
git add ui/src/components/LayoutShell.tsx
git commit -m "feat: handle admin cmd-k selection with tab navigation state"

Task 4: RbacPage Reads Location State for Tab + Highlight

Files:

  • Modify: ui/src/pages/Admin/RbacPage.tsx

This task makes RbacPage read { tab, highlight } from location state, switch to the correct tab, and pass the highlight ID down.

  • Step 1: Update RbacPage to read location state

Replace the entire RbacPage.tsx with:

import { useState, useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router';
import { StatCard, Tabs } from '@cameleer/design-system';
import { useRbacStats } from '../../api/queries/admin/rbac';
import styles from './UserManagement.module.css';
import UsersTab from './UsersTab';
import GroupsTab from './GroupsTab';
import RolesTab from './RolesTab';

const TABS = [
  { label: 'Users', value: 'users' },
  { label: 'Groups', value: 'groups' },
  { label: 'Roles', value: 'roles' },
];

const VALID_TABS = new Set(TABS.map((t) => t.value));

export default function RbacPage() {
  const { data: stats } = useRbacStats();
  const location = useLocation();
  const navigate = useNavigate();
  const [tab, setTab] = useState('users');
  const [highlightId, setHighlightId] = useState<string | null>(null);

  // Read tab + highlight from location state (set by cmd-k navigation)
  useEffect(() => {
    const state = location.state as { tab?: string; highlight?: string } | null;
    if (!state) return;

    if (state.tab && VALID_TABS.has(state.tab)) {
      setTab(state.tab);
    }
    if (state.highlight) {
      setHighlightId(state.highlight);
    }

    // Consume the state so back-navigation doesn't re-trigger
    navigate(location.pathname, { replace: true, state: null });
  }, [location.state]); // eslint-disable-line react-hooks/exhaustive-deps

  return (
    <div>
      <div className={styles.statStrip}>
        <StatCard label="Users" value={stats?.userCount ?? 0} />
        <StatCard label="Groups" value={stats?.groupCount ?? 0} />
        <StatCard label="Roles" value={stats?.roleCount ?? 0} />
      </div>
      <Tabs tabs={TABS} active={tab} onChange={setTab} />
      <div className={styles.tabContent}>
        {tab === 'users' && <UsersTab highlightId={highlightId} onHighlightConsumed={() => setHighlightId(null)} />}
        {tab === 'groups' && <GroupsTab highlightId={highlightId} onHighlightConsumed={() => setHighlightId(null)} />}
        {tab === 'roles' && <RolesTab highlightId={highlightId} onHighlightConsumed={() => setHighlightId(null)} />}
      </div>
    </div>
  );
}
  • Step 2: Update UsersTab to accept and use highlightId

In ui/src/pages/Admin/UsersTab.tsx, update the component signature and add highlight logic. At the top of the file, update the default export:

Change the function signature from:

export default function UsersTab() {

to:

export default function UsersTab({ highlightId, onHighlightConsumed }: { highlightId?: string | null; onHighlightConsumed?: () => void }) {

Then inside the component, after the existing useState declarations, add:

  // Auto-select highlighted item from cmd-k navigation
  useEffect(() => {
    if (highlightId && users) {
      const match = users.find((u) => u.userId === highlightId);
      if (match) {
        setSelectedId(match.userId);
        onHighlightConsumed?.();
      }
    }
  }, [highlightId, users]); // eslint-disable-line react-hooks/exhaustive-deps

Add useEffect to the import from react if not already there (line 1):

import { useState, useMemo, useEffect } from 'react';
  • Step 3: Update GroupsTab to accept and use highlightId

In ui/src/pages/Admin/GroupsTab.tsx, apply the same pattern. Update the function signature from:

export default function GroupsTab() {

to:

export default function GroupsTab({ highlightId, onHighlightConsumed }: { highlightId?: string | null; onHighlightConsumed?: () => void }) {

Add after existing useState declarations:

  // Auto-select highlighted item from cmd-k navigation
  useEffect(() => {
    if (highlightId && groups) {
      const match = groups.find((g) => g.id === highlightId);
      if (match) {
        setSelectedId(match.id);
        onHighlightConsumed?.();
      }
    }
  }, [highlightId, groups]); // eslint-disable-line react-hooks/exhaustive-deps

Ensure useEffect is in the react import.

  • Step 4: Update RolesTab to accept and use highlightId

In ui/src/pages/Admin/RolesTab.tsx, apply the same pattern. Update the function signature from:

export default function RolesTab() {

to:

export default function RolesTab({ highlightId, onHighlightConsumed }: { highlightId?: string | null; onHighlightConsumed?: () => void }) {

Add after existing useState declarations:

  // Auto-select highlighted item from cmd-k navigation
  useEffect(() => {
    if (highlightId && roles) {
      const match = roles.find((r) => r.id === highlightId);
      if (match) {
        setSelectedId(match.id);
        onHighlightConsumed?.();
      }
    }
  }, [highlightId, roles]); // eslint-disable-line react-hooks/exhaustive-deps

Ensure useEffect is in the react import.

  • Step 5: Verify compilation

Run:

cd ui && npx tsc --noEmit

Expected: Clean compile.

  • Step 6: Commit
git add ui/src/pages/Admin/RbacPage.tsx ui/src/pages/Admin/UsersTab.tsx ui/src/pages/Admin/GroupsTab.tsx ui/src/pages/Admin/RolesTab.tsx
git commit -m "feat: RBAC page reads cmd-k navigation state for tab switch and highlight"

Task 5: Manual Verification

  • Step 1: Start dev server
cd ui && npm run dev
  • Step 2: Test operational search
  1. Navigate to /exchanges
  2. Press Cmd-K (or Ctrl-K)
  3. Type a query — verify applications, routes, agents, exchanges appear as before
  4. Select a result — verify navigation works
  • Step 3: Test admin search
  1. Navigate to /admin/rbac
  2. Press Cmd-K
  3. Verify the palette shows users, groups, and roles (not applications/exchanges)
  4. Type a username — verify filtering works
  5. Select a user — verify RBAC page switches to Users tab and selects that user
  • Step 4: Test tab switch via search
  1. While on /admin/rbac (Users tab active), press Cmd-K
  2. Search for a role name
  3. Select the role — verify RBAC page switches to Roles tab and selects that role
  • Step 5: Test context switching
  1. From admin, click "Applications" in sidebar to leave admin
  2. Press Cmd-K — verify operational search is back (apps, routes, exchanges)
  3. Click "Admin" in sidebar
  4. Press Cmd-K — verify admin search is back (users, groups, roles)
  • Step 6: Final commit (if any fixups needed)
git add -A && git commit -m "fix: cmd-k context-aware search adjustments"