From 28f38331cc71323cb48bcbbd44ec718a120c66aa Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Thu, 2 Apr 2026 23:27:07 +0200 Subject: [PATCH] docs: implementation plan for context-aware cmd-k search Co-Authored-By: Claude Opus 4.6 (1M context) --- .../plans/2026-04-02-context-aware-cmdk.md | 510 ++++++++++++++++++ 1 file changed, 510 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-02-context-aware-cmdk.md diff --git a/docs/superpowers/plans/2026-04-02-context-aware-cmdk.md b/docs/superpowers/plans/2026-04-02-context-aware-cmdk.md new file mode 100644 index 00000000..f16a573a --- /dev/null +++ b/docs/superpowers/plans/2026-04-02-context-aware-cmdk.md @@ -0,0 +1,510 @@ +# 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: + +```json +"@cameleer/design-system": "^0.1.26", +``` + +- [ ] **Step 2: Install and verify** + +Run: +```bash +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: +```bash +npx tsc --noEmit +``` +Expected: Clean compile (existing code still valid since old categories are still valid strings). + +- [ ] **Step 4: Commit** + +```bash +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`): + +```typescript +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): + +```typescript +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: + +```typescript + // --- 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: + +```typescript + 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): + +```typescript + 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)) { + 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: +```bash +cd ui && npx tsc --noEmit +``` +Expected: Clean compile. + +- [ ] **Step 7: Commit** + +```bash +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: + +```typescript + const ADMIN_CATEGORIES = new Set(['user', 'group', 'role']); + const ADMIN_TAB_MAP: Record = { 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 = { 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: + +```typescript + 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: +```bash +cd ui && npx tsc --noEmit +``` +Expected: Clean compile. + +- [ ] **Step 4: Commit** + +```bash +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: + +```typescript +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(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 ( +
+
+ + + +
+ +
+ {tab === 'users' && setHighlightId(null)} />} + {tab === 'groups' && setHighlightId(null)} />} + {tab === 'roles' && setHighlightId(null)} />} +
+
+ ); +} +``` + +- [ ] **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: +```typescript +export default function UsersTab() { +``` +to: +```typescript +export default function UsersTab({ highlightId, onHighlightConsumed }: { highlightId?: string | null; onHighlightConsumed?: () => void }) { +``` + +Then inside the component, after the existing `useState` declarations, add: + +```typescript + // 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): +```typescript +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: +```typescript +export default function GroupsTab() { +``` +to: +```typescript +export default function GroupsTab({ highlightId, onHighlightConsumed }: { highlightId?: string | null; onHighlightConsumed?: () => void }) { +``` + +Add after existing `useState` declarations: + +```typescript + // 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: +```typescript +export default function RolesTab() { +``` +to: +```typescript +export default function RolesTab({ highlightId, onHighlightConsumed }: { highlightId?: string | null; onHighlightConsumed?: () => void }) { +``` + +Add after existing `useState` declarations: + +```typescript + // 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: +```bash +cd ui && npx tsc --noEmit +``` +Expected: Clean compile. + +- [ ] **Step 6: Commit** + +```bash +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** + +```bash +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)** + +```bash +git add -A && git commit -m "fix: cmd-k context-aware search adjustments" +```