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
Conversation
…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>
This was referenced May 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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,setActiveTenantalso writes?tenant=to the current URL viahistory.replaceState, preserving every other query param.Deliverable C — Active-tenant tab-title prefix.
document.titlenow syncs from the in-page header + active-tenant prefix ([marvin]/[all tenants]). Hidden on single-tenant + default-active.What changed
web/src/components/TenantPicker.tsxonListKeyDown— ↑/↓ 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<TenantOption>extracted to takehighlighted(keyboard) separate fromactive(selected); hover sets highlight so a stray mouseover doesn't strand keyboard navTENANT_PICKER_KEYBOARD_SHORTCUTSexported constant — drift-guard pinweb/src/hooks/useActiveTenant.tsTENANT_PICKER_URL_TOGGLE_STORAGE_KEY = \"kora_tenant_picker_update_url\"+useTenantUrlToggle()hook (read/write + cross-tab sync via storage +kora:tenant-url-toggle-changedcustom event)updateUrlTenantParam(next)helper:history.replaceStatewrite (no history-stack pollution); preserves other query params;defaultremoves the param entirely so the URL stays cleansetActiveTenantreads 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 writeweb/src/contexts/PageHeaderProvider.tsxformatBrowserTabTitle(displayTitle, {activeTenant, isAllTenants, isMultiTenant})exported pure helper — single source of truth for the tab-title formatuseEffectinPageHeaderProvidersyncsdocument.title = formatBrowserTabTitle(...)whenever the page title or active tenant changesTAB_TITLE_SUFFIX = \"Hermes Agent\"mirrorsweb/index.html's static<title>— single change point if the brand suffix ever movesdefault(no value to surface)tests/test_tenants_endpoint.pyNew
test_fe_keyboard_nav_and_url_toggle_and_tab_title_pinspinning:TENANT_PICKER_KEYBOARD_SHORTCUTSconstant existence + ArrowUp/Down/Enter/Escape literal greps +cycleLetterJumpcycling-behavior pinTENANT_PICKER_URL_TOGGLE_STORAGE_KEY = \"kora_tenant_picker_update_url\"literal pin +useTenantUrlToggleexistence in both hook + pickerTAB_TITLE_SUFFIX = \"Hermes Agent\"literal pin +formatBrowserTabTitleexistence +[all tenants]format pin +document.title = formatBrowserTabTitle(grepDemo
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)
Existing query params preserved:
Tab-title prefix (C)
document.titleProbe Investigations · Hermes AgentdefaultProbe Investigations · Hermes Agentmarvin[marvin] Probe Investigations · Hermes Agentall tenants[all tenants] Probe Investigations · Hermes AgentOperator 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✓.venv); CI runs it on PRTest plan
m→ highlight first "m*" tenant; pressmagain → cycle to next?after=…query params preserveddefault→?tenantparam removed (URL stays clean)defaultactive: tab title unchanged (no prefix)marvinactive:[marvin] <title> · Hermes Agentpytest 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. Checkgh pr view 447 --json state,mergedbefore 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-livefor selection-changed) and high-contrast theme verification. ~2-3 hours.🤖 Generated with Claude Code