Skip to content
This repository was archived by the owner on May 26, 2026. It is now read-only.

feat(kora): KR-FE-TENANT-PICKER-KEYBOARD-NAV-AND-URL-TOGGLE-AND-TAB-TITLE — picker keyboard nav + URL-toggle + tab-title prefix#210

Merged
rafe-walker merged 1 commit into
feature/phase2-upgradesfrom
feat/kora-KR-FE-TENANT-PICKER-KEYBOARD-NAV-AND-URL-TOGGLE-AND-TAB-TITLE-MEGABUCKET
May 24, 2026
Merged

feat(kora): KR-FE-TENANT-PICKER-KEYBOARD-NAV-AND-URL-TOGGLE-AND-TAB-TITLE — picker keyboard nav + URL-toggle + tab-title prefix#210
rafe-walker merged 1 commit into
feature/phase2-upgradesfrom
feat/kora-KR-FE-TENANT-PICKER-KEYBOARD-NAV-AND-URL-TOGGLE-AND-TAB-TITLE-MEGABUCKET

Conversation

@rafe-walker

Copy link
Copy Markdown
Owner

Summary

Three small multi-tenant UX polish items off the #208 follow-on list. Audit-filter verification (the #1 recommendation) still waits on CC#1 NousResearch#447 landing the per-tenant audit JSONL paths; this bucket bundles the #2 recommendation (keyboard nav + URL-toggle) with one more small ergonomics polish (tab-title prefix).

Deliverable A — TenantPicker keyboard nav. ↑/↓ wrap-around nav, Enter selects, Esc closes + returns focus to trigger, Home/End, letter-jump with cycling on repeated press. Picker opens with highlight on the currently-active tenant.

Deliverable B — Opt-in URL-toggle in picker. "Also update URL" checkbox at picker footer (localStorage kora_tenant_picker_update_url, default off). When on, setActiveTenant also writes ?tenant= to the current URL via history.replaceState, preserving every other query param.

Deliverable C — Active-tenant tab-title prefix. document.title now syncs from the in-page header + active-tenant prefix ([marvin] / [all tenants]). Hidden on single-tenant + default-active.

What changed

web/src/components/TenantPicker.tsx

  • Trigger button (sidebar): Enter/Space/ArrowDown open the picker
  • Listbox: full keyboard contract via onListKeyDown — ↑/↓ wrap, Enter selects, Escape closes (returns focus to trigger via a deferred focus restoration that doesn't fight React's commit ordering), Home/End jump first/last, single-char keys trigger letter-jump
  • Letter-jump: cycles through matches on repeated press of the same letter (§4 STOP-ASK resolved in favor of cycling, the option marked preferred). Modifier-key guard avoids hijacking Cmd-A / Ctrl-A etc.
  • On open: highlight starts at the currently-active option; on close: highlight resets and trigger refocuses
  • Listbox div auto-focuses on mount so keyboard nav works without a second click
  • <TenantOption> extracted to take highlighted (keyboard) separate from active (selected); hover sets highlight so a stray mouseover doesn't strand keyboard nav
  • New TENANT_PICKER_KEYBOARD_SHORTCUTS exported constant — drift-guard pin
  • URL-toggle checkbox rendered at picker footer

web/src/hooks/useActiveTenant.ts

  • TENANT_PICKER_URL_TOGGLE_STORAGE_KEY = \"kora_tenant_picker_update_url\" + useTenantUrlToggle() hook (read/write + cross-tab sync via storage + kora:tenant-url-toggle-changed custom event)
  • updateUrlTenantParam(next) helper: history.replaceState write (no history-stack pollution); preserves other query params; default removes the param entirely so the URL stays clean
  • setActiveTenant reads the toggle live (not via the hook) so the write path doesn't need extra plumbing — if the toggle is on, the URL gets updated as a side-effect of the localStorage write

web/src/contexts/PageHeaderProvider.tsx

  • formatBrowserTabTitle(displayTitle, {activeTenant, isAllTenants, isMultiTenant}) exported pure helper — single source of truth for the tab-title format
  • New useEffect in PageHeaderProvider syncs document.title = formatBrowserTabTitle(...) whenever the page title or active tenant changes
  • TAB_TITLE_SUFFIX = \"Hermes Agent\" mirrors web/index.html's static <title> — single change point if the brand suffix ever moves
  • Hidden on single-tenant deployments and when active tenant is default (no value to surface)

tests/test_tenants_endpoint.py

New test_fe_keyboard_nav_and_url_toggle_and_tab_title_pins pinning:

  • TENANT_PICKER_KEYBOARD_SHORTCUTS constant existence + ArrowUp/Down/Enter/Escape literal greps + cycleLetterJump cycling-behavior pin
  • TENANT_PICKER_URL_TOGGLE_STORAGE_KEY = \"kora_tenant_picker_update_url\" literal pin + useTenantUrlToggle existence in both hook + picker
  • TAB_TITLE_SUFFIX = \"Hermes Agent\" literal pin + formatBrowserTabTitle existence + [all tenants] format pin + document.title = formatBrowserTabTitle( grep

Demo

Keyboard nav (A)

With sidebar picker focused: Enter opens → ↓ highlights next tenant → Enter selects + closes + refocuses trigger. Esc cancels without selection. Type "m" → highlight jumps to first tenant starting with "m"; press "m" again → cycles to the next.

URL-toggle (B)

/probe-investigations
↓ open picker, check \"Also update URL\", select marvin
/probe-investigations?tenant=marvin
↓ select default
/probe-investigations            (param removed, URL stays clean)
↓ select all
/probe-investigations?tenant=all (operator-readable alias from #208)

Existing query params preserved:

/probe-investigations?after=2026-05-25
↓ select marvin (with toggle on)
/probe-investigations?after=2026-05-25&tenant=marvin

Tab-title prefix (C)

Active tenant document.title
single-tenant deployment Probe Investigations · Hermes Agent
multi-tenant, default Probe Investigations · Hermes Agent
multi-tenant, marvin [marvin] Probe Investigations · Hermes Agent
multi-tenant, all tenants [all tenants] Probe Investigations · Hermes Agent

Operator with 3 tabs open (probe investigations for Kora / probe for Marvin / cost-state aggregate) can distinguish them in the browser tab strip without switching focus.

Build

  • tsc -b && vite build ✓ clean (one pre-existing chunk-size warning; not introduced by this bucket)
  • python3 -m py_compile tests/test_tenants_endpoint.py
  • Pytest runtime not available in CC#2 worktree (no .venv); CI runs it on PR

Test plan

  • Keyboard nav from sidebar trigger: Enter opens, ↓ highlights, Enter selects, Esc closes + refocuses trigger
  • Letter-jump: press m → highlight first "m*" tenant; press m again → cycle to next
  • Wrap-around: ↑ from top jumps to last option; ↓ from last jumps to first
  • Picker opens with highlight on currently-active tenant
  • URL-toggle off + setActiveTenant → localStorage updates, URL unchanged
  • URL-toggle on + setActiveTenant → both update; existing ?after=… query params preserved
  • URL-toggle on + select default?tenant param removed (URL stays clean)
  • URL-toggle persists across tab reloads via localStorage
  • Single-tenant deployment: tab title unchanged (no prefix)
  • Multi-tenant + default active: tab title unchanged (no prefix)
  • Multi-tenant + marvin active: [marvin] <title> · Hermes Agent
  • Switching tenant in one tab updates tab title immediately
  • pytest tests/test_tenants_endpoint.py — 7 tests pass (3 endpoint + 4 drift-guard incl. new one)

Recommendation for next CC#2 dispatch

Per-tenant audit BE filter verification (once CC#1 NousResearch#447 lands). The original #1 recommendation from the #208 hand-off. CC#1 NousResearch#447 ships per-tenant audit JSONL paths + ?tenant_id= BE filter; CC#2 verifies each FE audit page (Probe / Alert / Email Intent / Outbound / Autofix / KoraActions) renders only the matching subset, tightening the contract from "BE may ignore the param" to "BE filters correctly." ~½ day. Check gh pr view 447 --json state,merged before dispatch.

Lighter alternative if NousResearch#447 is still in flight: TenantPicker accessibility audit. Run axe-core / Lighthouse against the picker chrome; the keyboard nav added in this bucket is the foundation but there's likely follow-on work on screen-reader announcements (e.g., aria-live for selection-changed) and high-contrast theme verification. ~2-3 hours.

🤖 Generated with Claude Code

…ITLE — picker keyboard nav + URL-toggle + tab-title prefix

Deliverable A — TenantPicker keyboard nav:
  * Trigger: Enter/Space/ArrowDown opens the picker
  * Listbox: ArrowUp/ArrowDown wrap-around nav, Enter selects,
    Escape closes + returns focus to the trigger, Home/End jump
    to first/last
  * Letter-jump: any single-char key jumps to the first matching
    tenant; repeated press on the same letter cycles through
    matches (resolves dup-prefix §4 STOP-ASK in favor of cycling)
  * Picker opens with highlight on the currently-active tenant
    so arrow-nav starts from where the operator already is
  * Highlighted option scrolls into view (large tenant lists)
  * TENANT_PICKER_KEYBOARD_SHORTCUTS exported constant — pinned
    by the cross-stack drift-guard test

Deliverable B — opt-in URL-toggle in picker:
  * "Also update URL" checkbox at picker footer; localStorage
    persisted under kora_tenant_picker_update_url; default off
    (preserves #207 picker-vs-URL precedence for everyone who
    doesn't opt in)
  * useTenantUrlToggle hook reads + writes the preference, with
    cross-tab + in-tab sync via custom + storage events
  * When on, setActiveTenant also mirrors the pick into the URL
    via history.replaceState (preserves every other query param;
    "default" removes the ?tenant param entirely so the URL
    stays clean)

Deliverable C — active-tenant tab-title prefix:
  * PageHeaderProvider now syncs document.title from the in-page
    displayTitle + active-tenant prefix
  * formatBrowserTabTitle exported pure helper:
    - single-tenant or default-active → "<title> · Hermes Agent"
    - tenant-active → "[marvin] <title> · Hermes Agent"
    - aggregate-active → "[all tenants] <title> · Hermes Agent"
  * Lets operators distinguish ProbeInvestigations-for-Kora vs
    ProbeInvestigations-for-Marvin tabs at a glance in their
    browser tab strip

Drift-guard extension (tests/test_tenants_endpoint.py):
  * TENANT_PICKER_KEYBOARD_SHORTCUTS literal pin + ArrowUp/Down/
    Enter/Escape implementation grep
  * cycleLetterJump grep (asserts letter-cycling behavior chose
    over first-match-only)
  * TENANT_PICKER_URL_TOGGLE_STORAGE_KEY = "kora_tenant_picker_update_url"
    pin + useTenantUrlToggle existence in both hook + picker
  * TAB_TITLE_SUFFIX = "Hermes Agent" pin +
    formatBrowserTabTitle existence + "[all tenants]" format pin

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rafe-walker rafe-walker merged commit ba7e486 into feature/phase2-upgrades May 24, 2026
2 of 4 checks passed
@rafe-walker rafe-walker deleted the feat/kora-KR-FE-TENANT-PICKER-KEYBOARD-NAV-AND-URL-TOGGLE-AND-TAB-TITLE-MEGABUCKET branch May 24, 2026 18:51
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant