This repository was archived by the owner on May 26, 2026. It is now read-only.
feat(kora): KR-FE-TENANT-PICKER-COCKPIT-CHROME-AND-WIZARD-RESUME — multi-tenant cockpit chrome + wizard resume#207
Merged
Conversation
…lti-tenant cockpit chrome + wizard resume
Deliverable A — tenant-picker cockpit chrome:
* /api/tenants/list BE endpoint (wraps list_cost_holder_tenants;
always default-first, synthesizes default when registry is
named-only)
* useActiveTenant() hook with URL ?tenant= → localStorage →
"default" resolution; cross-tab + in-tab event-driven re-render
* TenantPicker dropdown in sidebar header chrome; renders nothing
when <2 tenants observed (A.6 single-tenant degradation)
* /api/cost-state extended with optional ?tenant_id= → per-tenant
holder lookup; CostStatePage + DashboardPage CostCardBody read
snap.cost_ladder_by_tenant[activeTenant] when non-default
* Audit + promotion pages forward ?tenant_id= (Probe + Alert
Investigations, Email Intent + Outbound + Autofix logs, Kora
Actions, PromotionReviewPage); BE-side filter lands with CC#1
NousResearch#444 — FE wires the param + graceful fallback today
* Drift-guard pins: TENANT_ID_QUERY_PARAM_NAME (BE) ↔
TENANT_ID_QUERY_PARAM (FE) cross-stack test in
tests/test_tenants_endpoint.py + storage-key + default-id pins
Deliverable B — wizard sessionStorage resume:
* sessionStorage["kora_wizard_state"] persists step + non-cred
config on every change
* On mount past step 0, render Resume? prompt (yes restores +
jumps to saved step; no clears + fresh start)
* Credentials NEVER stored (anthropicApiKey,
substrateServiceRoleKey, slackBotToken stripped before
persist); validation results also reset on resume so the
operator re-validates re-entered creds
* Cleared on wizard complete + skip (terminal states)
Note: PM bucket attributed cost_ladder consumption to
CostTelemetryPage; in HEAD the cost_ladder block is consumed by
DashboardPage CostCardBody + CostStatePage (CostTelemetryPage
reads per-route /api/cost_telemetry, not per-tenant cost_ladder).
A.4 applied to the actual cost_ladder consumers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9 tasks
rafe-walker
added a commit
that referenced
this pull request
May 24, 2026
…ITLE — picker keyboard nav + URL-toggle + tab-title prefix (#210) 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: CC#2 Kora Web <kora-pm@stormhavenenterprises.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
CC#2 megabucket — two deliverables on the multi-tenant cockpit-chrome arc that CC#1 + CC#3 are also pushing on.
Deliverable A — Tenant-picker cockpit chrome (primary). Sidebar-header dropdown that re-scopes cost / audit / promotion telemetry to the operator-selected tenant. New BE endpoint
/api/tenants/listenumerates registered cost-holder tenants;useActiveTenant()hook resolves URL?tenant=→localStorage→ "default"; FE forwards?tenant_id=to cost + audit + promotion endpoints. Single-tenant deployments (Joshua today) see no picker chrome — it appears automatically when a second tenant emerges.Deliverable B — Wizard
sessionStorageresume. Wizard state (minus credentials) persists on every step transition; on mount past step 0, an inline Resume? prompt offers to restore. Credentials (Anthropic key / IsoKron service-role key / Slack bot token) are never stored — operator re-enters + re-validates on resume.What changed
Backend (
kora_cli/web_server.py)GET /api/tenants/list— wrapslist_cost_holder_tenants(); always default-first; synthesizes "default" when the registry is named-only.GET /api/cost-state?tenant_id=<id>— optional param; routes toget_cost_holder(tenant_id=tenant_id). Omitted ≡ pre-feat(kora): KR-FE-COCKPIT-NAV-RESTRUCTURE-AND-ALERT-VIEWER-VERIFICATION — sidebar groups + alert viewer aligned to real #197 #202 default-tenant behavior preserved exactly.TENANT_ID_QUERY_PARAM_NAMEconstant pinned for the cross-stack drift-guard test.Frontend
web/src/hooks/useActiveTenant.ts(new) — URL → localStorage → default resolution; cross-tab + in-tab event-driven updates; fetches/api/tenants/liston mount + tab focus; exposesisAllTenants+isMultiTenant.web/src/components/TenantPicker.tsx(new) — sidebar-header dropdown; renders nothing whenavailableTenants.length < 2(A.6 degradation); aggregate "All tenants" pseudo-option.web/src/App.tsx— placed<TenantPicker />directly below the "Hermes Agent" sidebar branding.web/src/lib/api.ts—getCostState({tenantId}),listTenants(), plus?tenant_id=forwarding through every audit + promotion getter via sharedbuildAuditQueryString()helper.DashboardPage.tsxprojectCostFromSnapshot()— readssnap.cost_ladder_by_tenant[activeTenant]when non-default; borrowsmodel_defaultfrom the legacy block (v6 by-tenant block omits it — router-side, not per-tenant).CostStatePage.tsx— fetches/api/cost-state?tenant_id=<activeTenant>; surfaces "Viewing tenant: X" badge for non-default; explains All-tenants fallback inline.?tenant_id=. BE-side filter lands with CC#1 fix: add missing re.DOTALL flag to DeepSeek V3 tool call parser NousResearch/hermes-agent#444; FE wires the param + graceful fallback today (extra query params are no-ops under FastAPI).Wizard (B)
web/src/pages/WizardPage.tsx—sessionStorage[\"kora_wizard_state\"]persists{config: stripCreds(config), stepIdx, v: 1}on every change. On mount, ifstepIdx > 0, render Resume? card. Yes → restore + jump; No → clear + fresh. Cleared on wizard complete + skip.stripCreds()removesanthropicApiKey,substrateServiceRoleKey,slackBotTokenAND all three validation results — a "success" badge with no key to back it is misleading; force re-validation.v: 1in the persisted blob — refuse stale shapes on future config-shape change.Tests
tests/test_tenants_endpoint.py— endpoint contract + drift-guard pin assertingTENANT_ID_QUERY_PARAM_NAME == \"tenant_id\"AND grepping the FE source for the matching literal. Repo has no vitest inweb/, so the FE side gets pinned via Python-side source grep.Drift-guard pins
TENANT_ID_QUERY_PARAM_NAME(BE:kora_cli/web_server.py) ↔TENANT_ID_QUERY_PARAM(FE:web/src/hooks/useActiveTenant.ts) — both must equal\"tenant_id\".TENANT_PICKER_STORAGE_KEY = \"kora_active_tenant\"(FE) — asserted by Python-side source grep.DEFAULT_TENANT_ID = \"default\"mirrored both sides.WIZARD_RESUME_STORAGE_KEY = \"kora_wizard_state\"+ schemav: 1exported fromWizardPage.tsx.PM bucket note
The bucket spec says CostTelemetryPage consumes
snapshot.cost_ladder— in HEAD it doesn't (it reads/api/cost_telemetry, which is per-route, not per-tenant). The actual cost_ladder consumers areDashboardPageCostCardBody +CostStatePage. A.4 applied to the right consumers; CostTelemetryPage left untouched (its data isn't tenant-scoped at the telemetry-singleton layer today — a follow-on bucket if/when telemetry shards per tenant).Build
tsc -b && vite build✓ clean (one pre-existing chunk-size warning; not introduced by this bucket).python3 -m py_compile✓ fortests/test_tenants_endpoint.py+kora_cli/web_server.py. Pytest runtime not available in CC#2 worktree (no.venv); CI will execute on PR.Test plan
init_cost_holder(tenant_id=\"marvin\", ...)boot path, picker appears with [default, marvin] options/cost-state?tenant=marvindeep-link also works)cost_ladder_by_tenant.marvinon tenant switch?tenant_id=marvin(Network tab); pages don't crash if BE ignores the parampytest tests/test_tenants_endpoint.py— 5 tests pass (3 endpoint, 2 drift-guard pins)Recommendation for next CC#2 dispatch
Two natural follow-ons surfaced while implementing this bucket:
?tenant_id=BE-side filter on the audit endpoints, add a thin verification bucket: assert each FE audit page renders the correct subset when the BE actually filters. ~1 day.cost_ladder_by_tenantblock is the right cross-tenant surface. A small page rendering one card per tenant (tier badge + spend / pool / pct-used) closes the All-tenants UX gap. ~½ day.Lighter-weight option for the next CC#2 lane: per-tenant audit panel deep-link —
/probe-investigations?tenant=marvinshareable links rendered with the active-tenant badge in the page header. Tiny ergonomics pass; ~2 hours.🤖 Generated with Claude Code