Skip to content

Ask AI: migrate to Kapa Agent SDK — tools, saved history, sign-in gate - #395

Open
JakeSCahill wants to merge 26 commits into
mainfrom
feature/ask-ai-agent-sdk
Open

Ask AI: migrate to Kapa Agent SDK — tools, saved history, sign-in gate#395
JakeSCahill wants to merge 26 commits into
mainfrom
feature/ask-ai-agent-sdk

Conversation

@JakeSCahill

@JakeSCahill JakeSCahill commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Migrates the Ask AI widget from Kapa's Chat SDK to the Agent SDK (@kapaai/agent-react) and builds the signed-in "AI agent" experience on top of it.

Pairs with docs-site redpanda-data/docs-site#190 (the /kapa/session endpoint + docs login). This bundle talks to that backend by URL at runtime, so there's no code dependency — it can merge independently and is simply inert until the backend ships (anonymous users still get today's stock Kapa widget; the agent panel shows a sign-in prompt). Prerequisite before it does anything live: KAPA_PROJECT_ID + Agent integration ID provisioned (already wired in the partials).

Highlights

  • Agent SDK migrationAskAI.jsx (AgentProvider + getSessionToken), ChatInterface.jsx rewritten onto useAgentChat (flat messages, single streaming state, sources from tool-call blocks, approval cards). Old localStorage persistence removed in favor of server-side history.
  • Client tools (agentTools.js) — navigate to page, switch product, run_bloblang (executes the agent's mapping in the playground WASM and feeds errors back so it self-corrects before presenting), open playground, config property lookup, latest version, open Console, submit feedback. Only submit_docs_feedback is approval-gated — it is the one tool with a side effect beyond the user's own browsing (it sends content to the docs team). Navigation and product-switch are deliberately ungated (same-site, reversible), and new-tab tools degrade to a plain link when the popup is blocked; each tool records its reasoning on its needsApproval line.
  • Custom instructions (server-side system prompt, never shown to users) — deployment clarifying questions (asks Cloud vs Self-Managed first, drills into version only when it changes the answer), Bloblang verify-before-present, feedback consent/PII rules. Structured per Kapa's best-practices guidance.
  • Two-tier gating — the agent panel is signed-in only and doubles as the discovery/upsell surface; anonymous Ask AI falls back to the stock Kapa widget. Header account control (26-docs-account.js): Sign in opens a feature modal, signed-in shows an avatar menu with a component-scoped console link (Cloud/Data Platform → Cloud Console, ADP → ADP Console).
  • HistoryAgentThreadHistory, gated on the session's authenticated flag.
  • Feedback — Heap-only (Agent SDK dropped addFeedback); signed-in submissions attach email + thread id server-side.
  • Sources — de-duped and disambiguated (same title across versions → "(Streaming 25.2)"; anchors → section name).
  • Theming/UX — unified indigo palette + WCAG-AA contrast across drawer, header modal, and login page in light and dark; single-scroll panel with sticky input; full-screen toggle; textarea input (Enter sends, Shift+Enter newline).

Testing

  • gulp lint clean; playground WASM tests pass; production bundle ~660 KB smaller than the old Chat SDK bundle.
  • Verified end-to-end on a local docs-site (netlify dev, integration Auth0 tenant): sign-in gate → login → agent chat with the feedback-tool approval card → verified Bloblang mapping → saved history resume → clarifying questions → sign-out. Both light and dark.

Note

The standalone Bump.sh widget shares this bundle, so it gets the same behavior; the ai.redpanda.com ADP console URL is used as a placeholder pending product confirmation.

🤖 Generated with Claude Code

Replaces the Kapa Chat SDK (@kapaai/react-sdk) with the Agent SDK
(@kapaai/agent-react) and builds the signed-in "AI agent" experience on
top of it. Requires the docs-site backend (kapa-session endpoint + docs
login) — inert until that ships.

- Agent SDK migration: AskAI.jsx (AgentProvider + getSessionToken), and
  ChatInterface.jsx rewritten onto useAgentChat (flat messages, isStreaming,
  sources from tool-call blocks, tool-call approval cards). Drops the old
  localStorage persistence (chatPersistence/persistentApiService) in favor of
  server-side history.
- Client tools (agentTools.js): navigate_to_page, switch_product,
  run_bloblang (verifies mappings in the playground WASM before the agent
  presents them), open_bloblang_playground, submit_docs_feedback,
  ask_community. All approval-gated except the sandboxed run_bloblang.
- Custom instructions (system prompt, not shown to users): deployment
  clarifying questions (Cloud vs Self-Managed first, version only when it
  matters), Bloblang verify-before-present, feedback consent/PII rules.
- Two-tier gating: agent panel is signed-in only; the panel's sign-in screen
  is the discovery surface. Header account control (26-docs-account.js) —
  Sign in opens a feature modal; signed-in shows an avatar menu. Component-
  scoped console link (Cloud/Data Platform -> Cloud Console; ADP -> ADP
  Console). conversation history UI (AgentThreadHistory).
- Feedback is Heap-only (Agent SDK dropped addFeedback); signed-in feedback
  attaches email + thread id server-side.
- Sources list de-dupes and disambiguates repeated titles by version/section.
- Theming/UX: unified indigo palette + WCAG-AA contrast across drawer, header
  modal and login page in both themes; single-scroll panel with sticky input;
  full-screen toggle; textarea input (Enter sends, Shift+Enter newline).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@netlify

netlify Bot commented Jul 6, 2026

Copy link
Copy Markdown

Deploy Preview for docs-ui ready!

Name Link
🔨 Latest commit 35284fa
🔍 Latest deploy log https://app.netlify.com/projects/docs-ui/deploys/6a73610e98436000087d99de
😎 Deploy Preview https://deploy-preview-395--docs-ui.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 28 (no change from production)
Accessibility: 89 (no change from production)
Best Practices: 92 (no change from production)
SEO: 89 (no change from production)
PWA: -
View the detailed breakdown and full score reports
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 407d4068-cd99-4719-a9e8-c0903bff293c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

This PR migrates the Ask AI chat widget from the Kapa react-sdk with local persistence to the Kapa Agent SDK (@kapaai/agent-react), introducing session-token-based authentication, a browser-side agentTools module (navigation, Bloblang execution, product switching, playground link, feedback submission, community link), conversation history UI, and a fullscreen chat panel mode. It also adds a header "Docs account" widget with a sign-in modal, cookie-based auth hinting, and account dropdown, alongside extensive CSS reworks for chat layout/scrolling and new sign-in/account styles. Build config and dependencies are updated accordingly, and prior local conversation-persistence code is removed.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ChatInterface
  participant AskAI as AskAI (AgentProvider)
  participant Backend
  participant agentTools

  User->>ChatInterface: open chat
  ChatInterface->>AskAI: probeSession via cookie hint
  AskAI->>Backend: POST session endpoint
  Backend-->>AskAI: token or 401
  AskAI-->>ChatInterface: kapa-session event (authenticated/loginUrl)
  alt authenticated
    User->>ChatInterface: submit question
    ChatInterface->>AskAI: sendMessage
    AskAI->>agentTools: invoke tool if needed
    agentTools-->>AskAI: tool result
    AskAI-->>ChatInterface: streamed answer + sources
  else not authenticated
    ChatInterface-->>User: show sign-in gate / quick ask
  end
Loading

Suggested reviewers: micheleRP

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main migration to the Kapa Agent SDK and its key additions.
Description check ✅ Passed The description directly explains the Agent SDK migration, tools, history, sign-in gate, and related user experience changes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/ask-ai-agent-sdk

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/partials/header-content.hbs (1)

63-82: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Console link URL/markup duplicated 3× — high risk given placeholder ADP URL.

The Cloud/ADP console href, icon SVG, and label text are repeated across desktop nav (Lines 63-82), mobile overflow menu (Lines 97-102), and the account dropdown (Lines 143-148). Per the PR objectives, the ADP console URL is a placeholder pending confirmation — when it's finalized, all three (soon four, once mobile+desktop+dropdown are counted) occurrences must be updated in lockstep, or the UI will show inconsistent URLs.

Consider extracting this into a Handlebars partial (e.g., {{> console-link mode="nav"}}) parameterized by placement, so the URL/label only needs to change once.

Also applies to: 97-102, 143-148

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/partials/header-content.hbs` around lines 63 - 82, The console link
markup is duplicated across multiple branches in header-content.hbs, so the
Cloud/ADP URL, icon SVG, and label can drift when the placeholder ADP URL
changes. Refactor the repeated link blocks in the page.component.name
conditionals into a shared Handlebars partial or helper (for example, a
console-link partial) and pass in the placement-specific label/class while
keeping the href defined in one place. Ensure every existing console-link usage
in the header template reads from the same source so future URL updates only
need to happen once.
♻️ Duplicate comments (1)
src/css/header.css (1)

1252-1262: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Same modal-card overflow gap as header-bump.css.

Mirrors the missing max-height/overflow-y issue flagged in src/css/header-bump.css (Lines 979-989); fix both files consistently since they are kept in sync.

🛠️ Proposed fix
 .tb-signin-modal-card {
   position: relative;
   width: 100%;
   max-width: 440px;
+  max-height: 90vh;
+  overflow-y: auto;
   background: `#fff`;
   color: `#181818`;
   border-radius: 14px;
   padding: 28px 28px 24px;
   box-shadow: 0 20px 60px -12px rgba(15, 23, 42, 0.4);
   text-align: left;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/css/header.css` around lines 1252 - 1262, The .tb-signin-modal-card style
is missing the same vertical overflow constraints as the matching modal card in
header-bump.css, so the sign-in modal can grow beyond the viewport. Update the
.tb-signin-modal-card rule to stay in sync with the corresponding modal-card
styles by adding the same max-height and overflow-y handling used elsewhere in
the header styles. Keep the change consistent across both header.css and
header-bump.css using the shared modal-card selectors/classes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/css/chat-panel.css`:
- Around line 1167-1184: The .scroll-down-button in chat-panel mode is
absolutely positioned but its .chat-footer-wrapper container is still static, so
it anchors to .chat-scroll and can be clipped by overflow handling. Update the
panel-scoped .chat-footer-wrapper in chat-panel.css to be the positioned
containing block for the button, and mirror the same positioning change in
chat-panel-bump.css so the scroll-down button is consistently placed in both
panel styles.

In `@src/css/header-bump.css`:
- Around line 979-989: The sign-in modal card can exceed short viewport height
and has no internal scroll fallback. Update .tb-signin-modal-card to constrain
its height with a viewport-based max-height and enable vertical scrolling (or
otherwise make the modal content scrollable) so the CTA and close controls
remain reachable on small/landscape screens. Use the .tb-signin-modal and
.tb-signin-modal-card selectors to locate the modal styling and keep the layout
visually intact while adding the overflow handling.

In `@src/js/26-docs-account.js`:
- Around line 96-107: The signed-in user email is being stored in sessionStorage
in cleartext via the /auth/me flow and the kapa-session handling, which should
be removed or reduced. Update the docs account logic in showUser and the related
session update path to avoid caching the raw email; instead store only non-PII
state needed for the UI (for example, an avatar initial) or re-fetch the user
when needed. Keep the UI behavior in sync by using the existing showUser
rendering path without persisting sensitive data.
- Around line 36-48: The sign-in modal in openModal/closeModal only toggles
visibility and Escape handling, but it does not manage keyboard focus. Update
openModal to move focus into the modal when it opens, update closeModal to
restore focus to the element that triggered it, and add a focus trap so tabbing
stays within the modal while it is open. Use the existing openModal, closeModal,
and onModalKey hooks to keep the behavior centralized.

In `@src/js/react/components/ChatInterface.jsx`:
- Around line 113-207: The new helper and component declarations in
ChatInterface.jsx are missing the required space before function parentheses,
which will trip the project’s space-before-function-paren lint rule. Update the
declarations for extractSources, baseTitle, humanize, sourceQualifier,
AnswerSources, and autosizeTextarea to match the repo’s existing style used in
AskAI.jsx, keeping the same behavior while fixing only the function signature
spacing.

---

Outside diff comments:
In `@src/partials/header-content.hbs`:
- Around line 63-82: The console link markup is duplicated across multiple
branches in header-content.hbs, so the Cloud/ADP URL, icon SVG, and label can
drift when the placeholder ADP URL changes. Refactor the repeated link blocks in
the page.component.name conditionals into a shared Handlebars partial or helper
(for example, a console-link partial) and pass in the placement-specific
label/class while keeping the href defined in one place. Ensure every existing
console-link usage in the header template reads from the same source so future
URL updates only need to happen once.

---

Duplicate comments:
In `@src/css/header.css`:
- Around line 1252-1262: The .tb-signin-modal-card style is missing the same
vertical overflow constraints as the matching modal card in header-bump.css, so
the sign-in modal can grow beyond the viewport. Update the .tb-signin-modal-card
rule to stay in sync with the corresponding modal-card styles by adding the same
max-height and overflow-y handling used elsewhere in the header styles. Keep the
change consistent across both header.css and header-bump.css using the shared
modal-card selectors/classes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f4edf8f5-4ee6-4ea0-90fa-f4cc660b04c7

📥 Commits

Reviewing files that changed from the base of the PR and between 42657e2 and 43c5be6.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (18)
  • gulp.d/tasks/bundle-react.js
  • package.json
  • src/css/chat-panel-bump.css
  • src/css/chat-panel.css
  • src/css/header-bump.css
  • src/css/header.css
  • src/js/19-chat-panel.js
  • src/js/26-docs-account.js
  • src/js/react/AskAI.jsx
  • src/js/react/agentTools.js
  • src/js/react/chatPersistence.js
  • src/js/react/components/ChatInterface.jsx
  • src/js/react/heap.js
  • src/js/react/persistentApiService.js
  • src/partials/chat-panel-bump.hbs
  • src/partials/chat-panel.hbs
  • src/partials/head-scripts.hbs
  • src/partials/header-content.hbs
💤 Files with no reviewable changes (2)
  • src/js/react/persistentApiService.js
  • src/js/react/chatPersistence.js

Comment thread src/css/chat-panel.css
Comment on lines +1167 to +1184
/* ——— Scroll-to-input button (panel-scoped; home.css only styles it under .home) ——— */
#chat-panel-kapa-root .scroll-down-button {
position: absolute;
top: -3rem;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
justify-content: center;
background: var(--kapa-surface, #fff);
color: var(--kapa-text, #181818);
border: 1px solid var(--kapa-border, #e5e5e5);
border-radius: 50%;
padding: 8px;
z-index: 10;
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.18);
cursor: pointer;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Files ==\n'
git ls-files 'src/css/*panel*.css' 'src/css/*home*.css' | sed 's#^`#-` #'

printf '\n== Outline chat-panel.css ==\n'
ast-grep outline src/css/chat-panel.css --view expanded || true

printf '\n== Relevant line ranges ==\n'
for f in src/css/chat-panel.css src/css/chat-panel-bump.css; do
  echo "--- $f"
  wc -l "$f"
  rg -n "chat-footer-wrapper|chat-scroll|scroll-down-button|position:" "$f" -n -C 3
done

Repository: redpanda-data/docs-ui

Length of output: 5184


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== chat-panel.css: footer / scroll button context ==\n'
sed -n '330,380p' src/css/chat-panel.css
printf '\n---\n'
sed -n '1158,1195p' src/css/chat-panel.css

printf '\n== chat-panel-bump.css: matching area ==\n'
sed -n '330,380p' src/css/chat-panel-bump.css
printf '\n---\n'
sed -n '1158,1195p' src/css/chat-panel-bump.css

Repository: redpanda-data/docs-ui

Length of output: 4247


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in src/css/chat-panel.css src/css/chat-panel-bump.css; do
  echo "===== $f ====="
  sed -n '340,380p' "$f"
  echo
  sed -n '1160,1190p' "$f"
  echo
done

Repository: redpanda-data/docs-ui

Length of output: 3530


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

for path in [Path('src/css/chat-panel.css'), Path('src/css/chat-panel-bump.css')]:
    text = path.read_text()
    for needle in ['.chat-footer-wrapper', '.chat-scroll', '.scroll-down-button']:
        print(f'[{path}] {needle}')
        idx = text.find(needle)
        if idx == -1:
            print('  not found')
            continue
        start = max(0, text.rfind('\n', 0, idx-1) - 200)
        end = min(len(text), text.find('\n}', idx) + 2)
        print(text[start:end])
        print('---')
PY

Repository: redpanda-data/docs-ui

Length of output: 3874


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== file list ==='
git ls-files 'src/css/*panel*.css' 'src/css/*home*.css' | sed 's#^`#-` #'

for f in src/css/chat-panel.css src/css/chat-panel-bump.css; do
  echo
  echo "=== $f (context around relevant selectors) ==="
  rg -n "chat-footer-wrapper|chat-scroll|scroll-down-button|position:" "$f" -n -C 4
done

echo
echo '=== chat-panel.css excerpt ==='
sed -n '340,380p' src/css/chat-panel.css
echo '---'
sed -n '1160,1190p' src/css/chat-panel.css

echo
echo '=== chat-panel-bump.css excerpt ==='
sed -n '340,380p' src/css/chat-panel-bump.css
echo '---'
sed -n '1160,1190p' src/css/chat-panel-bump.css

Repository: redpanda-data/docs-ui

Length of output: 9529


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' --glob '!coverage' "scroll-down-button|chat-footer-wrapper|chat-scroll" .

Repository: redpanda-data/docs-ui

Length of output: 2291


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '740,790p' src/js/react/components/ChatInterface.jsx

Repository: redpanda-data/docs-ui

Length of output: 1903


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== chat-panel.hbs ==\n'
sed -n '1,120p' src/partials/chat-panel.hbs

printf '\n== chat-panel-bump.hbs ==\n'
sed -n '1,120p' src/partials/chat-panel-bump.hbs

printf '\n== ChatInterface.jsx around scroll/footer ==\n'
sed -n '660,790p' src/js/react/components/ChatInterface.jsx

Repository: redpanda-data/docs-ui

Length of output: 24573


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "`#chat-panel-kapa-root`|chat-scroll" src/css src/js/react/components/ChatInterface.jsx

Repository: redpanda-data/docs-ui

Length of output: 26944


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in src/css/chat-panel.css src/css/chat-panel-bump.css; do
  echo "===== $f ====="
  sed -n '118,145p' "$f"
  echo
done

Repository: redpanda-data/docs-ui

Length of output: 1578


.scroll-down-button needs a positioned footer wrapper in panel mode.
#chat-panel-kapa-root .chat-footer-wrapper is position: static, so the absolute button falls back to .chat-scroll and gets clipped by its overflow: hidden. Make the footer wrapper the containing block, and apply the same change in chat-panel-bump.css.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/css/chat-panel.css` around lines 1167 - 1184, The .scroll-down-button in
chat-panel mode is absolutely positioned but its .chat-footer-wrapper container
is still static, so it anchors to .chat-scroll and can be clipped by overflow
handling. Update the panel-scoped .chat-footer-wrapper in chat-panel.css to be
the positioned containing block for the button, and mirror the same positioning
change in chat-panel-bump.css so the scroll-down button is consistently placed
in both panel styles.

Comment thread src/css/header-bump.css
Comment on lines +979 to +989
.tb-signin-modal-card {
position: relative;
width: 100%;
max-width: 440px;
background: #fff;
color: #181818;
border-radius: 14px;
padding: 28px 28px 24px;
box-shadow: 0 20px 60px -12px rgba(15, 23, 42, 0.4);
text-align: left;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Sign-in modal card can overflow on short viewports with no scroll affordance.

.tb-signin-modal-card has no max-height/overflow-y. With a title, subtitle, 4-item feature list, CTA, and footer, the card can exceed the viewport height on short/landscape mobile screens, making the CTA or close button unreachable since .tb-signin-modal (Lines 963-971) doesn't scroll either.

🛠️ Proposed fix
 .tb-signin-modal-card {
   position: relative;
   width: 100%;
   max-width: 440px;
+  max-height: 90vh;
+  overflow-y: auto;
   background: `#fff`;
   color: `#181818`;
   border-radius: 14px;
   padding: 28px 28px 24px;
   box-shadow: 0 20px 60px -12px rgba(15, 23, 42, 0.4);
   text-align: left;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.tb-signin-modal-card {
position: relative;
width: 100%;
max-width: 440px;
background: #fff;
color: #181818;
border-radius: 14px;
padding: 28px 28px 24px;
box-shadow: 0 20px 60px -12px rgba(15, 23, 42, 0.4);
text-align: left;
}
.tb-signin-modal-card {
position: relative;
width: 100%;
max-width: 440px;
max-height: 90vh;
overflow-y: auto;
background: `#fff`;
color: `#181818`;
border-radius: 14px;
padding: 28px 28px 24px;
box-shadow: 0 20px 60px -12px rgba(15, 23, 42, 0.4);
text-align: left;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/css/header-bump.css` around lines 979 - 989, The sign-in modal card can
exceed short viewport height and has no internal scroll fallback. Update
.tb-signin-modal-card to constrain its height with a viewport-based max-height
and enable vertical scrolling (or otherwise make the modal content scrollable)
so the CTA and close controls remain reachable on small/landscape screens. Use
the .tb-signin-modal and .tb-signin-modal-card selectors to locate the modal
styling and keep the layout visually intact while adding the overflow handling.

Comment thread src/js/26-docs-account.js
Comment on lines +36 to +48
function openModal () {
if (!modal) return
modal.hidden = false
document.addEventListener('keydown', onModalKey)
}
function closeModal () {
if (!modal) return
modal.hidden = true
document.removeEventListener('keydown', onModalKey)
}
function onModalKey (e) {
if (e.key === 'Escape') closeModal()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Sign-in modal lacks focus management.

openModal/closeModal toggle hidden and only wire Escape-to-close, but never move focus into the modal on open or restore it to the trigger on close, and there's no focus trap. Keyboard users can tab past the visible modal into background content behind the overlay.

♿ Suggested focus handling
   function openModal () {
     if (!modal) return
     modal.hidden = false
+    var focusable = modal.querySelector('[data-signin-modal-continue], [data-signin-modal-close]')
+    if (focusable) focusable.focus()
     document.addEventListener('keydown', onModalKey)
   }
   function closeModal () {
     if (!modal) return
     modal.hidden = true
     document.removeEventListener('keydown', onModalKey)
+    signinLink.focus()
   }
   function onModalKey (e) {
     if (e.key === 'Escape') closeModal()
+    if (e.key === 'Tab') {
+      var f = modal.querySelectorAll('a[href], button:not([disabled])')
+      if (!f.length) return
+      var first = f[0], last = f[f.length - 1]
+      if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus() }
+      else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus() }
+    }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function openModal () {
if (!modal) return
modal.hidden = false
document.addEventListener('keydown', onModalKey)
}
function closeModal () {
if (!modal) return
modal.hidden = true
document.removeEventListener('keydown', onModalKey)
}
function onModalKey (e) {
if (e.key === 'Escape') closeModal()
}
function openModal () {
if (!modal) return
modal.hidden = false
var focusable = modal.querySelector('[data-signin-modal-continue], [data-signin-modal-close]')
if (focusable) focusable.focus()
document.addEventListener('keydown', onModalKey)
}
function closeModal () {
if (!modal) return
modal.hidden = true
document.removeEventListener('keydown', onModalKey)
signinLink.focus()
}
function onModalKey (e) {
if (e.key === 'Escape') closeModal()
if (e.key === 'Tab') {
var f = modal.querySelectorAll('a[href], button:not([disabled])')
if (!f.length) return
var first = f[0], last = f[f.length - 1]
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus() }
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus() }
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/js/26-docs-account.js` around lines 36 - 48, The sign-in modal in
openModal/closeModal only toggles visibility and Escape handling, but it does
not manage keyboard focus. Update openModal to move focus into the modal when it
opens, update closeModal to restore focus to the element that triggered it, and
add a focus trap so tabbing stays within the modal while it is open. Use the
existing openModal, closeModal, and onModalKey hooks to keep the behavior
centralized.

Comment thread src/js/26-docs-account.js
Comment on lines +96 to +107

showUser(null)
fetch('/auth/me', { credentials: 'include' })
.then(function (res) { return res.ok ? res.json() : null })
.then(function (me) {
if (!me) return
showUser(me)
try {
sessionStorage.setItem(CACHE_KEY, JSON.stringify({ email: me.email || null }))
} catch (e) { /* private browsing */ }
})
.catch(function () { /* header still shows generic signed-in state */ })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

PII (email) cached in sessionStorage.

The verified email from /auth/me and from the kapa-session event is cached in sessionStorage as plaintext. Static analysis flags this as cleartext storage of sensitive data (CWE-312) — any XSS on the page can read it, and it persists for the tab's lifetime. Since the email is already rendered in the DOM for the signed-in UI, the marginal exposure is real but limited; still, consider avoiding raw email storage (e.g., cache only the initial for the avatar, or re-fetch each session) to reduce PII footprint.

🔒 Example: avoid caching the raw email
-        try {
-          sessionStorage.setItem(CACHE_KEY, JSON.stringify({ email: me.email || null }))
-        } catch (e) { /* private browsing */ }
+        try {
+          sessionStorage.setItem(CACHE_KEY, JSON.stringify({ initial: (me.email || '').charAt(0).toUpperCase() || null }))
+        } catch (e) { /* private browsing */ }

Also applies to: 122-130

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 103-103: Do not store sensitive data (credentials, tokens, PII) in localStorage or sessionStorage; it is readable by any script and persists on the device.
Context: sessionStorage.setItem(CACHE_KEY, JSON.stringify({ email: me.email || null }))
Note: [CWE-312] Cleartext Storage of Sensitive Information.

(local-storage-sensitive-data)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/js/26-docs-account.js` around lines 96 - 107, The signed-in user email is
being stored in sessionStorage in cleartext via the /auth/me flow and the
kapa-session handling, which should be removed or reduced. Update the docs
account logic in showUser and the related session update path to avoid caching
the raw email; instead store only non-PII state needed for the UI (for example,
an avatar initial) or re-fetch the user when needed. Keep the UI behavior in
sync by using the existing showUser rendering path without persisting sensitive
data.

Source: Linters/SAST tools

Comment on lines +113 to +207
function extractSources(blocks) {
const seen = new Set()
const sources = []
for (const block of blocks || []) {
if (block.type !== 'tool_calls') continue
for (const call of block.toolCalls || []) {
for (const source of call.sources || []) {
if (!source?.sourceUrl || seen.has(source.sourceUrl)) continue
// Only render web URLs — React doesn't block javascript: hrefs
if (!/^https?:\/\//i.test(source.sourceUrl)) continue
seen.add(source.sourceUrl)
sources.push(source)
}
}
}
return sources
}

// Kapa titles can arrive pipe-joined ("Page title|Page title") or empty
function baseTitle(source) {
const title = (source.title || '').split('|').map((p) => p.trim()).filter(Boolean)[0]
if (title) return title
try {
const segs = new URL(source.sourceUrl).pathname.split('/').filter(Boolean)
const last = segs[segs.length - 1] || ''
return humanize(last) || source.sourceUrl
} catch {
return source.sourceUrl
}
}

function humanize(slug) {
return decodeURIComponent(slug).replace(/[-_]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()).trim()
}

// Friendly names for the docs product/section path segments
const PRODUCT_LABELS = {
'redpanda-cloud': 'Cloud',
'cloud-data-platform': 'Cloud',
'self-managed': 'Self-Managed',
'data-platform': 'Data Platform',
streaming: 'Streaming',
connect: 'Connect',
'redpanda-connect': 'Connect',
}

// A short qualifier to disambiguate same-titled sources: the section (for
// same-page anchors) or the product/version (for the same page across versions).
function sourceQualifier(url) {
try {
const u = new URL(url)
if (u.hash && u.hash.length > 1) return humanize(u.hash.slice(1))
const segs = u.pathname.split('/').filter(Boolean)
const version = segs.find((s) => /^\d+\.\d+$/.test(s) || s === 'current')
const product = PRODUCT_LABELS[segs[0]]
const ver = version === 'current' ? 'latest' : version
if (product && ver) return `${product} ${ver}`
return ver || product || ''
} catch {
return ''
}
}

function AnswerSources({ blocks }) {
const sources = extractSources(blocks)
if (sources.length === 0) return null
// Only qualify titles that repeat, so unique sources stay clean
const titles = sources.map(baseTitle)
const counts = titles.reduce((acc, t) => ({ ...acc, [t]: (acc[t] || 0) + 1 }), {})
return (
<div className="feedback-container">
<div className="feedback-group">
<button
className="feedback-button"
type="button"
onClick={() => handleFeedback('upvote')}
title="This was helpful"
>
<ThumbsUp className="feedback-icon" />
</button>
<button
className="feedback-button"
type="button"
onClick={() => handleFeedback('downvote')}
title="This wasn't helpful"
>
<ThumbsDown className="feedback-icon" />
</button>
</div>
<div className="answer-sources">
<span className="answer-sources-label">Sources</span>
<ul>
{sources.map((s, i) => {
const title = titles[i]
const qualifier = counts[title] > 1 ? sourceQualifier(s.sourceUrl) : ''
return (
<li key={s.sourceUrl}>
<a href={s.sourceUrl} target="_blank" rel="noopener noreferrer">
{title}{qualifier ? ` (${qualifier})` : ''}
</a>
</li>
)
})}
</ul>
</div>
)
}

// Grow the input with its content, capped by the CSS max-height
function autosizeTextarea(el) {
if (!el) return
el.style.height = 'auto'
el.style.height = `${el.scrollHeight}px`
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Missing space before function parentheses (fails project lint).

The new helpers (extractSources, baseTitle, humanize, sourceQualifier, AnswerSources, autosizeTextarea) are declared without a space before (, which violates the style the repo enforces elsewhere — AskAI.jsx in this same PR uses function announceSession (…), getSessionToken (), etc. Static analysis flags each of these lines with space-before-function-paren, so lint/CI will fail.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 190-192: A list component should have a key to prevent re-rendering
Context:
{title}{qualifier ? (${qualifier}) : ''}

Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(list-component-needs-key)

🪛 ESLint

[error] 113-113: Missing space before function parentheses.

(space-before-function-paren)


[error] 132-132: Missing space before function parentheses.

(space-before-function-paren)


[error] 144-144: Missing space before function parentheses.

(space-before-function-paren)


[error] 161-161: Missing space before function parentheses.

(space-before-function-paren)


[error] 176-176: 'AnswerSources' is defined but never used.

(no-unused-vars)


[error] 176-176: Missing space before function parentheses.

(space-before-function-paren)


[error] 203-203: Missing space before function parentheses.

(space-before-function-paren)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/js/react/components/ChatInterface.jsx` around lines 113 - 207, The new
helper and component declarations in ChatInterface.jsx are missing the required
space before function parentheses, which will trip the project’s
space-before-function-paren lint rule. Update the declarations for
extractSources, baseTitle, humanize, sourceQualifier, AnswerSources, and
autosizeTextarea to match the repo’s existing style used in AskAI.jsx, keeping
the same behavior while fixing only the function signature spacing.

Source: Linters/SAST tools

JakeSCahill and others added 9 commits July 7, 2026 10:04
Kapa is an approved processor for this PII, so set AgentProvider `user={email}`
(from the session probe) — conversations are attributed to the person in Kapa's
dashboard. external_owner_id stays the salted-sub hash; the SDK reads `user`
reactively so setting it after the probe resolves is fine.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Logged-out users now get the same drawer UI powered by the Chat SDK
(@kapaai/react-sdk) instead of being routed to the stock Kapa widget.
Signed-in users keep the Agent SDK (tools, history, email attribution).

- AskAI.jsx App() picks the provider by session tier: agent (true),
  Chat SDK (false), brief loading (null). Probe now falls to the anonymous
  tier on any non-authenticated outcome (401, missing backend, network
  error) so the drawer never hangs.
- ChatSdkInterface.jsx — anonymous drawer on useChat: welcome, chips,
  QA rendering, textarea input, Heap analytics, a slim sign-in upsell
  banner. No session backend needed (Chat SDK uses its own bot protection).
- chatShared.jsx — extracted marked/Answer/Toast so both tiers render
  identically; ChatInterface imports them.
- KAPA_CHAT_INTEGRATION_ID global (partials) for the Chat SDK integration.
- Bundle +~72KB gzipped (both SDKs) — expected cost of the two-tier UX.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The signed-in welcome screen now shows capability-demonstrating actions
instead of generic doc questions, so the agent's value is legible on first
open. Bloblang leads — it auto-runs (no approval) and visibly does something.

- ChatInterface.jsx: AGENT_EXAMPLES (run_bloblang, navigate, feedback,
  community) rendered as icon'd action cards; description updated.
- CSS: .suggestion-card-action flex layout with accent icon.
- Anonymous tier keeps the generic AI_SUGGESTIONS questions, reinforcing the
  tier difference.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reworks the sign-in modal (header) and the in-drawer sign-in screen so
they sell the agent rather than just list features. Both surfaces now share:

- A "Free with Redpanda Cloud" badge that answers the cost objection up front.
- Benefit-led title and copy ("Sign in to unlock the docs AI agent").
- Four capabilities as icon tiles (History, Compass, Braces, MessageSquare),
  each a benefit label plus a supporting line, instead of plain bullets.
- Dark-mode variants for the badge, icon tiles, and text.

Also folds in two smaller tweaks from the same pass:
- Modal CTA "Continue with Redpanda Cloud" -> "Continue" (the login page
  after it already says the full phrase, so it was redundant).
- Bumped sign-in intro/bullet font sizes for readability.

Copy is intentionally free of em dashes.

Changed:
- src/partials/header-content.hbs - modal markup
- src/js/react/components/ChatInterface.jsx - drawer sign-in screen + icons
- src/css/{header,header-bump,chat-panel,chat-panel-bump}.css - styles + dark mode

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
.tb-account has `display: inline-flex`, an author rule that overrides the
`hidden` attribute's UA `display:none`. So the account container (and its
"Sign in" link) was visible during page parse, before the bundled site.js
ran 26-docs-account.js to attach the modal click handler.

An early click in that window hit the raw <a href="/login"> and navigated
straight to the interstitial — the feature modal never opened, and the full
page load read as "the modal took a while". Clicks after JS ran opened the
modal instantly, hence the intermittency.

Honor `hidden` on the container itself (.tb-account[hidden]) so it stays
hidden until 26-docs-account.js reveals it — by which point the handler is
attached. Mirrors the existing .tb-signin-modal[hidden] guard. Applied to
header.css and header-bump.css (standalone widget).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…hele feedback)

Agent instructions (AskAI.jsx CUSTOM_INSTRUCTIONS):
- Products reframed as Cloud / Self-Managed (Streaming or Connect) / ADP, with
  ADP first-class and Connect added under Self-Managed.
- Clarify-first-from-context: the agent now infers the product from the current
  page and conversation before asking; only asks when context is unclear, and
  then asks which product (Cloud / Self-Managed / ADP; if Self-Managed, Streaming
  or Connect). Current page + Antora component are injected via currentPageContext().
- Added a Writing style section (no em dashes, avoid "please"/"once", neutral
  non-first-person confirmations) so agent-generated text — including feedback
  confirmations — follows Redpanda docs style. Removed the em dashes that were in
  the instructions themselves.

Copy (ChatInterface.jsx), per Michele's review:
- Welcome line: dropped the em dash, replaced vague "open the right page" with
  "navigate you to the right doc", and removed first-person "I can".
- Quickstart example is no longer "Redpanda quickstart" (which read as
  Self-Managed); now "the right quickstart for my setup" so the agent routes by
  product instead of presuming one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
/login is a Netlify function that cold-starts at ~6.6s (then ~0.3s warm), so
clicking sign-in felt unresponsive and invited re-clicks (the "sticky, many
clicks" and part of the INC-2905 loop). Add immediate progress feedback and
block re-clicks on every sign-in trigger:

- Header modal "Continue" (26-docs-account.js): swaps to "Signing in…" + is-loading.
- Drawer upsell banner (ChatSdkInterface.jsx): "Signing you in…" + is-signing-in.
- Agent-panel sign-in button (ChatInterface.jsx): "Signing in…" + is-signing-in.
- CSS (header{,-bump}.css, chat-panel{,-bump}.css): dim + pointer-events:none while loading.

This masks the latency and prevents duplicate sign-in navigations; the actual
cold start is a separate server follow-up (warm the function / lighten init).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-out feedback

Sign-in/out land on cold Netlify functions plus a scale-to-zero Neon DB,
so a cold click stalled ~6s with no feedback beyond the tab spinner.

- Prewarm on intent: opening the sign-in modal or account menu fetches
  /auth/warm and /.well-known/jwks.json, warming both login functions and
  resuming Neon before the user clicks Continue. The chat panel requests
  the same warm-up when explicitly opened signed-out (not on page-load
  restore, to avoid warming on every pageview).
- Sign out shows "Signing out…" and blocks double clicks, same treatment
  as the existing sign-in CTA.
- Privacy note added to the sign-in modal and the agent panel's sign-in
  screen; those surfaces link /login?disclosed=1 so the backend skips its
  interstitial - one consent click instead of two (docs-site pair).
- The chat panel's slim upsell opens the header modal (single pitch +
  disclosure surface) instead of navigating; falls back to plain /login
  where no modal markup exists.
- Fix: re-parent the modal to <body>. The navbar is a z-index 5 stacking
  context and the chat drawer sits at 120, so the modal rendered under an
  open panel despite its own z-index.

Pairs with docs-site (disclosed=1 handling, /auth/warm, edge /logout);
inert until that backend ships, same as the rest of the login flow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
JakeSCahill added a commit to redpanda-data/docs-site that referenced this pull request Jul 28, 2026
…gout

Cold, the docs-login function stalls ~6s (Lambda cold start + Neon
scale-to-zero resume on the /login INSERT), and logout paid the same
cold start for what is just cookie clearing.

- /auth/warm: 204 after a best-effort store ping() (SELECT 1 on Neon,
  no-op on Blobs). The docs-ui header fetches it on sign-in/account-menu
  intent so the function and DB are warm when /login is clicked.
- /login?disclosed=1 302s straight to Auth0: the referring docs-ui
  surface already showed the privacy note, so the interstitial would be
  a redundant second consent click. Bare /login keeps the interstitial.
- Interstitial Continue button shows "Redirecting…" and blocks re-clicks
  (benefits the MCP flow too).
- /logout moved to an edge function (near-zero cold start): cookie
  clearing, the INC-2905 rp_docs_reauth flag, and the safeReturnTo guard
  ported byte-identically from docs-login.mjs / lib/docs-session.mjs.

Pairs with the docs-ui prewarm/disclosed=1 changes in
redpanda-data/docs-ui#395.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
JakeSCahill and others added 9 commits July 29, 2026 19:03
WCAG AA fixes surfaced reviewing the login/Ask AI release:
- Darken light --kapa-text-muted #79797d→#6a6a6e (4.34→5.39:1 on white);
  used by sign-in privacy note, quick-ask, feature descriptions, source labels.
- Define --kapa-accent-soft in both themes (previously undefined, so every
  var(--kapa-accent-soft, #eef0fe) silently fell back to the light tint).
- Error answer text #ef4444 (failed AA both themes) → #d92d20 light +
  #f87171 dark override.
- Sign-in modal focus management (aria-modal dialog): move focus into the
  dialog on open, trap Tab within it, and restore focus to the trigger on
  close (previously focus stayed on <body> and Tab reached the page behind).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New chat: a button in the drawer toolbar (next to History) that resets the
  conversation via the Agent SDK's resetConversation (through the existing
  handleReset). Shows once there's a conversation to clear; hides on the empty
  welcome screen and while browsing history. Toolbar row gains a gap so the two
  buttons don't sit flush.
- Remove the ask_community tool (opened the community Slack) — it wasn't working
  reliably. Also drops its welcome-screen suggestion and the now-unused Users
  icon import. Agent now exposes 5 tools: navigate_to_page, switch_product,
  run_bloblang, open_bloblang_playground, submit_docs_feedback.

gulp lint + bundle clean; New chat verified end-to-end (submit → button appears
→ click clears the thread and returns to the welcome screen).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- lookup_config_property: returns a Redpanda config property's type/default/
  scope/restart/cloud-support/description from redpanda-properties.json (the
  reference already shipped in the bundle). Fetched from the site root like
  blobl.wasm (static_file), with a preview fallback; caches per session.
- get_latest_version: latest Redpanda version from the page meta tag, plus the
  current page's product/version.
- open_console: opens curated, id-independent Redpanda Cloud pages (home,
  clusters, create_cluster, sign_up) in a new tab; approval-gated. Cannot deep-
  link to specific resources (no access to the user's IDs). ADP deferred until
  its console URLs are confirmed.
- copy_to_clipboard: copies a command/snippet locally (no approval); degrades
  by returning the text if the clipboard API is unavailable.

Agent now exposes 9 tools. Plumbing verified live: property fetch resolves to
/redpanda-properties.json and returns real data; version meta reads 26.2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The release build repeatedly stalled the full 20-min timeout on `npm ci`:
puppeteer (a devDep used only by the playground tests, not the bundle) downloads
~120 MB of Chromium on install. Because each hung job was cancelled, the old
node_modules cache never saved, so every run re-downloaded from cold.

- PUPPETEER_SKIP_DOWNLOAD=true so install skips the browser download.
- Replace the fragile node_modules actions/cache (never saved on timeout) with
  setup-node's cache: npm (~/.npm, saved as a post-step), and always npm ci
  --no-audit --no-fund --prefer-offline.

The next v* tag will build via CI again instead of needing a local publish.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
switch_product was querying [data-product-menu] [data-product-id] — an
attribute that doesn't exist, so it matched nothing and always returned
"product switcher not available" (even where the switcher is present, incl.
the home page). Rewrite against the real markup: .sb-product-opt with
data-product-url and .sb-product-opt-name (partials/product-switcher.hbs).
Verified live: 6 products resolve, "agentic-data-plane" → /agentic-data-plane/
home/, current product detected. Keeps an actionable fallback (use
navigate_to_page) if a page ever lacks the switcher.

Reduce approval friction: navigate_to_page and switch_product now run without
approval — both are reversible, same-tab, same-site navigations. Approval stays
on the tools that leave the site or send data: open_console and
open_bloblang_playground (open a new tab — the approval click is also the user
gesture that keeps the popup from being blocked) and submit_docs_feedback
(sends the user's email + content).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Only submit_docs_feedback stays gated now (it sends the user's email + feedback
text to the docs team — real consent). The two tab-openers no longer prompt;
when the browser blocks the popup they degrade to a link the agent renders, so
it's a link-click instead of an approve-click, not extra friction.

Final gating: 8/9 tools run without approval; only submit_docs_feedback prompts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Remove copy_to_clipboard: an agent-invoked clipboard write needs a user
  gesture, so it never actually copied. Down to 7 tools.
- Per-component agent example prompts. New get-agent-suggestions helper (mirrors
  get-ai-suggestions) exposes window.AGENT_SUGGESTIONS with tool-showcasing
  prompts keyed by component (author override via agent-suggestion-N attrs).
  ChatInterface welcome cards now read AGENT_SUGGESTIONS (was a hardcoded list),
  so signed-in examples fit the product being read (Bloblang on Connect, config
  lookup on Self-Managed, open-Cloud on Cloud, etc.).
- System prompt: Redpanda Connect runs on both Cloud and Self-Managed with
  different setup, so the agent now clarifies which one for Connect questions
  (it was framed as Self-Managed-only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
redpanda-properties.json (what lookup_config_property reads) is Self-Managed
only — Cloud abstracts that config away — so a 'default value of X' example
prompt is misleading there. Drop the property prompt from the cloud-data-platform,
data-platform, generic, and Bump default sets (keep it on self-managed and
streaming). Also note in the tool description that its reference is Self-Managed
and must not be used to answer Cloud configuration questions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Welcome screen now leads with the original per-component question prompts
(window.AI_SUGGESTIONS) and surfaces a small, distinct row of agentic actions
below under 'Or get the agent to help:' — so the agent tools are discoverable
without overwhelming. (Was showing only the tool prompts.)

Also fix terminology: it's the latest Redpanda STREAMING version (the core
streaming product, versioned 25.2/26.2), not a generic 'Redpanda version' —
updated the example prompts, the get_latest_version tool description, and its
return field. Softened the row label from 'have the agent do it' to 'get the
agent to help' so it doesn't overstate what the tools do.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
JakeSCahill added a commit to redpanda-data/docs-site that referenced this pull request Aug 3, 2026
* DOC-XXXX: Add docs browser login + Kapa Agent session endpoint

Builds on the OAuth 2.1 AS from #181 to add "phase 2": a human browser
login for the docs site, plus the backend the Ask AI Agent SDK widget
(docs-ui) needs.

- docs-login.mjs — /login, /logout, /auth/me. /login reuses the existing
  Auth0 federation leg (same public client + allow-listed /mcp/callback);
  auth requests are tagged flow:'docs-login'.
- mcp-oauth.mjs — the shared /callback now branches on flow:'docs-login' to
  set a browser session cookie and redirect back, instead of minting an
  OAuth code.
- lib/docs-session.mjs — stateless RS256-JWT session (same signing key as MCP
  access tokens, audience-separated `${origin}/docs-session`), HttpOnly
  rp_docs_session cookie + JS-readable rp_docs_auth hint; open-redirect guard.
- kapa-session.mjs — mints short-lived Kapa Agent SDK session tokens; holds
  the Kapa API key server-side, derives external_owner_id from the docs
  session (salted sub), returns { session_token, expires_at, authenticated,
  user }. 401 for anonymous visitors (agent tier is signed-in only).
- api-feedback form + mcp.mjs — add `source` and `thread-id` fields so
  widget vs MCP submissions are distinguishable and carry conversation context.
- pages.mjs — interstitial takes heading/lead params, switched to the indigo
  AI-surface palette, added dark mode (follows the docs `theme` key).

New env: KAPA_PROJECT_ID (+ KAPA_OWNER_SALT for salting), optional
DOCS_LOGIN_URL. Tests: tests/docs-session.test.ts (8) + existing suites pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Document KAPA_OWNER_SALT as a do-not-rotate secret

Changing it re-keys every external_owner_id and orphans all saved Kapa
conversation history. Set once, back up, never rotate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Preview: use WIP Ask AI beta bundle (docs-ui v3.2.0-beta.1)

Point the preview playbook's UI bundle at the docs-ui prerelease
v3.2.0-beta.1 (PR #395: Agent SDK widget + sign-in redesign) so this
PR's deploy preview exercises the full login + agent flow.

Temporary: revert to releases/latest before merge. Production
antora-playbook.yml is unchanged and still tracks /latest/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Preview: bump WIP Ask AI bundle to v3.2.0-beta.2

beta.2 is docs-ui #395 merged with current main (#396/#397/#398), so the
deploy preview now carries the latest main fixes plus the Agent SDK widget
and sign-in redesign.

Still temporary: revert to releases/latest before merge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix docs-login crash on Neon backend (null client_id)

docs-login reuses the shared auth_requests table but, unlike the MCP OAuth
flow, has no downstream OAuth client: it never sets client_id or
client_redirect_uri, and instead carries flow ('docs-login') + return_to
through the upstream leg for the shared /callback to branch on.

The Neon backend only had the MCP columns (client_id/client_redirect_uri
NOT NULL, no flow/return_to), so docs-login worked on the Blobs backend
(stores the whole object) but crashed on Neon with:
  null value in column "client_id" ... violates not-null constraint

- Migration: relax client_id/client_redirect_uri to nullable, add
  flow + return_to columns (idempotent, backward compatible with MCP).
- neon.mjs: persist and return flow + return_to; tolerate null client fields.
- test: apply all migrations (not just the first) before the DB-backed run.

NOTE: the migration must be applied to the Neon database before this takes
effect (no auto-runner). ADD COLUMN IF NOT EXISTS / DROP NOT NULL are safe
to run against the live table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Preview: bump WIP Ask AI bundle to v3.2.0-beta.3

beta.3 adds the sign-in modal early-click fix (docs-ui #395) on top of the
beta.2 content. Still temporary: revert to releases/latest before merge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix docs logout: force re-prompt so it can't silently SSO re-login (INC-2905)

Logging out only cleared our session cookies while the shared Cloud Auth0 SSO
session stayed live, so the next sign-in silently re-authenticated. To the user
logout looked broken, and a retry loop bounced the browser through the Cloud
authorize flow repeatedly — which made the Cloud console fire and cancel read
RPCs, spiking the Public API error rate enough to page SEV-1 (INC-2905, judged a
false positive, no customer impact).

Fix (local logout, no cross-product side effect):
- /logout sets a short-lived rp_docs_reauth flag alongside clearing the session.
- The next /login sees the flag and requests Auth0 prompt=login (no silent SSO
  re-login), then consumes the flag so ordinary later sign-ins stay one-click.
- buildAuthorizeUrl gains an optional `prompt` param.

This deliberately does NOT do a federated Auth0 logout (which would also sign the
user out of Redpanda Cloud + needs an Auth0 Allowed Logout URLs entry). If we
want true SSO logout, that's a follow-up decision with Cloud/identity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs login interstitial: clearer 'navigate you to the right page' copy (Michele feedback)

Replaces the vague 'open pages for you' in the sign-in interstitial lead, matching the drawer/modal wording. No em dashes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Add /mcp/session: exchange docs login cookie for a short-lived MCP bearer

The Ask AI widget's API reference tools call the docs MCP server from the
browser. mcp.mjs authenticates with Authorization: Bearer only (audience
${origin}/mcp) and never reads cookies, so once REQUIRE_AUTH is enforced
the widget needs a token source. This endpoint verifies the rp_docs_session
cookie (readDocsSession) and re-mints a 15-minute access token with the
same signing key and claims shape the OAuth AS produces, preserving the
docs-session/mcp audience separation.

Modeled on kapa-session.mjs: same CORS allowlist, rate limiting, 401
shape, and KAPA_TEST_OWNER_ID dev escape hatch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Cut docs login/logout latency: /auth/warm, disclosed=1 skip, edge /logout

Cold, the docs-login function stalls ~6s (Lambda cold start + Neon
scale-to-zero resume on the /login INSERT), and logout paid the same
cold start for what is just cookie clearing.

- /auth/warm: 204 after a best-effort store ping() (SELECT 1 on Neon,
  no-op on Blobs). The docs-ui header fetches it on sign-in/account-menu
  intent so the function and DB are warm when /login is clicked.
- /login?disclosed=1 302s straight to Auth0: the referring docs-ui
  surface already showed the privacy note, so the interstitial would be
  a redundant second consent click. Bare /login keeps the interstitial.
- Interstitial Continue button shows "Redirecting…" and blocks re-clicks
  (benefits the MCP flow too).
- /logout moved to an edge function (near-zero cold start): cookie
  clearing, the INC-2905 rp_docs_reauth flag, and the safeReturnTo guard
  ported byte-identically from docs-login.mjs / lib/docs-session.mjs.

Pairs with the docs-ui prewarm/disclosed=1 changes in
redpanda-data/docs-ui#395.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* preview: bump pinned UI bundle to v3.2.0-beta.4 (login latency fixes)

Picks up the docs-ui prewarm/disclosed=1/sign-out-feedback frontend so
the full login flow can be tested end-to-end on this deploy preview.
Still TEMPORARY - revert to /latest/ before merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* preview: bump pinned UI bundle to v3.2.0-beta.5 (a11y fixes)

beta.5 = #395 HEAD + the Ask AI a11y fixes (light-mode muted-text contrast,
sign-in modal focus trap, error-text contrast, --kapa-accent-soft). Preview-only
pin; antora-playbook.yml (prod) still uses releases/latest. Revert before merge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Harden docs login: login-CSRF binding, tighter CORS, rate limits

Security fixes from the login/MCP release review:

- Login-CSRF / forced login: bind the browser that starts /login to the one
  that finishes /mcp/callback. /login mints a per-request nonce (HttpOnly
  cookie) and stores its SHA-256 hash on the auth request; the shared callback
  requires the cookie to hash back before minting a session (docs-session.mjs,
  docs-login.mjs, mcp-oauth.mjs, neon.mjs + migration). Scoped to
  flow=docs-login so the MCP flow is untouched. Verified: a replayed callback
  URL in a cookie-less browser is rejected (state_mismatch), legit login works.

- CORS: narrow the credentialed allowlist from all *.netlify.app to this site's
  own previews (*--redpanda-documentation.netlify.app) + localhost, via a shared
  lib/cors-origin.mjs used by kapa-session, mcp-session and /auth/me (also adds
  CORS to /auth/me). Removes the copy-pasted per-file allowlists.

- Rate limit /login and /auth/warm (unauthenticated DB-touching endpoints) with
  the Blobs-backed limiter (new allowLogin).

- Hardening: warn when KAPA_OWNER_SALT is unset; guard /logout against
  subresource (<img>) triggers via Sec-Fetch-Dest.

Tests: +10 (nonce round-trip/fail-closed, login-flow nonce binding, CORS
allowlist incl. suffix-smuggling). Suite 87 passing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* a11y: fix login interstitial button contrast + focus/title

- Scope the dark button-label override to real dark mode so a light-mode first
  visit (no stored theme) keeps white-on-indigo (6.1:1) instead of the ~2.9:1
  near-black-on-indigo it rendered before (WCAG AA fail).
- Add :focus-visible styles for the button and links.
- Dynamic <title> per flow (was hardcoded 'Redpanda Docs MCP' for docs login).
- Add noindex meta.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* preview: bump pinned UI bundle to v3.2.0-beta.7

Adds the New chat button + 4 grounded agent tools (lookup_config_property,
get_latest_version, open_console (Cloud), copy_to_clipboard) and drops the
broken ask_community tool, on top of beta.5's a11y fixes. Preview-only pin;
antora-playbook.yml (prod) still uses releases/latest. Revert before merge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* preview: bump pinned UI bundle to v3.2.0-beta.9

Fixed switch_product (correct .sb-product-opt selector) and reduced approval
friction (8/9 tools ungated; only submit_docs_feedback still prompts). CI-built.
Preview-only pin; prod still uses releases/latest. Revert before merge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* preview: bump pinned UI bundle to v3.2.0-beta.10

Removed the copy_to_clipboard tool, added per-component agent example prompts
(get-agent-suggestions helper + window.AGENT_SUGGESTIONS), and a Redpanda
Connect Cloud-vs-Self-Managed clarifying question. CI-built. Revert to
/latest/ before merge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* preview: bump pinned UI bundle to v3.2.0-beta.11

Drops config-property example prompts on Cloud docs (the property reference is
Self-Managed only). CI-built. Revert to /latest/ before merge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* preview: bump pinned UI bundle to v3.2.0-beta.12

Welcome screen keeps the original question prompts and adds a small
'get the agent to help' row; fixes 'Redpanda Streaming version' wording.
CI-built. Revert to /latest/ before merge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Harden docs login against DoS + unhandled errors (adversarial review)

Fixes from adversarial testing of the login/session surface:

- Rate limiter no longer fails OPEN. On a Blobs error allow() returned
  allowed:true, silently removing all limiting (live: 200+ /login, no 429).
  Now enforces an in-memory per-instance cap first, so a store outage degrades
  to per-instance limiting. clientIp trusts only x-nf-client-connection-ip (no
  spoofable x-forwarded-for/UA fallback). Adds allowSession (per-subject).
- Blobs OAuth state now gets GC. Abandoned /login auth requests + /register
  clients + rate-limit buckets accumulated forever (cleanup was Neon-only).
  Adds blobs.cleanupExpired + cleanupRateLimits, wired into the daily job; warns
  when STORE_BACKEND=blobs in production (no atomic CAS for reuse detection).
- Unhandled 502: a malformed  at /mcp/callback reliably 502'd
  (no outer try/catch). Wrapped the handler -> generic 500. /auth/me wraps the
  session read so a config/infra fault returns anonymous, not 500.
- Session endpoints (/kapa/session, /mcp/session) use the shared Blobs-backed
  allowSession keyed on sub (was in-memory per-Lambda), bounding one account's
  Kapa-quota amplification across instances.
- Login-nonce robustness: reuse the browser's existing nonce across concurrent
  /login (double-click, prefetch, second tab) instead of overwriting a single
  cookie (which dead-ended legit users with state_mismatch); TTL now exceeds the
  auth-request TTL. CSRF guarantee unchanged.
- Credentialed CORS to localhost is now dev-only (NETLIFY_DEV), not reflected
  from deployed environments.

Tests: +3 (fail-closed limiter, nonce reuse); suite 91 passed, 3 skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* login: per-IP edge rate limit on /login (+/auth/warm,/auth/me)

Adds Netlify's platform rate limit (100 req/60s/IP, aggregateBy ip) to the
docs-login function. This runs at the edge BEFORE the function executes, so a
single-source flood is rejected with 429 before it can insert an auth request /
touch the DB — the reliable outer bound the in-function Blobs limiter can't
guarantee on serverless (no atomic CAS + scale-to-zero cold starts).

Notes: Netlify rate limits per function, so the limit is shared across /login,
/auth/warm and /auth/me (generous enough — /auth/me is cached client-side and
/auth/warm is client-throttled). Does NOT stop a distributed IP-rotating flood
(needs a WAF/DDoS layer). /oauth/register is deliberately NOT edge-limited: it
shares mcp-oauth.mjs with /token and /callback, so a function-level limit would
throttle legitimate OAuth — it keeps its in-function allowRegister limiter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
New signed-in-only pageview beacon. For users with the rp_docs_auth hint
cookie, reports one pageview per navigation (path, Antora component, and the
search term on /search) to the docs-site /docs-activity endpoint, which
records it server-side against the account. Replaces anonymous third-party
analytics (Plausible/Heap/Algolia) for lead purposes.

Anonymous visitors send nothing. Same-origin (session cookie rides along).
Deduped once-per-path-per-tab-session to keep write volume to real
navigations. Fire-and-forget; never blocks or throws into the page.

Capture only — the endpoint stores to our own user store; nothing is
delivered to sales.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@micheleRP

Copy link
Copy Markdown
Contributor

Review scope: this is +6007/−887 (≈2.5k of that is package-lock.json, so ~3.4k lines of app code). I reviewed the risk surfaces rather than every line: the client tools and their approval gating, the session-token path, secret handling, HTML sanitization, and whether removing the old persistence layer left anything dangling. I did not line-by-line review ChatInterface.jsx, ChatSdkInterface.jsx, or the ~1.4k lines of CSS. Flagging that so the depth here isn't mistaken for a full audit.

Fix before merge

The approval gating is documented backwards, in two places.

Only 1 of the 8 registered tools is approval-gated:

Tool needsApproval
submit_docs_feedback true
navigate_to_page false
switch_product false
run_bloblang false
open_bloblang_playground false
lookup_config_property false
get_latest_version false
open_console false

Both summaries say the opposite. The PR body:

All approval-gated except the sandboxed run_bloblang.

And the module's own header comment:

Tools that move the user (navigation, product switch) or contact the docs team require explicit approval — the chat UI renders approve/reject buttons via the SDK's ToolCallCard.

navigate_to_page and switch_product are named there specifically, and neither is gated.

To be clear: I think the per-tool inline rationales are defensible — "same-tab, same-site navigation — reversible, no gate" is a reasonable call, and resolveDocsUrl constrains where navigation can go. The problem is that a reviewer reading either summary approves a security posture that isn't the one being shipped. Fix the header and the body to describe the actual gating (feedback submission gated; navigation and read-only tools not), and the reasoning stands on its own.

Suggestions

1. switch_product matches loosely, then navigates without a gate.

const match = options.find((o) => {
  const label = norm(o.label)
  return label === wanted || label.includes(wanted) || wanted.includes(label) || norm(o.url).includes(wanted)
})
...
window.location.assign(match.url)

Bidirectional includes is generous — a short wanted can substring-match an unintended label, and wanted.includes(label) matches when the label is the shorter string. Combined with no approval step, a fuzzy match navigates the user away with no confirmation. Same-site so the harm is bounded, but ordering the candidates by match quality (exact, then prefix, then substring) and returning available on an ambiguous match would be more predictable than first-wins over DOM order.

2. Split out the publish-release.yml change. The PUPPETEER_SKIP_DOWNLOAD + cache: npm fix is a genuinely good change — the old actions/cache on node_modules gated by if: cache-hit != 'true' is a known anti-pattern, and it's exactly why a timeout left the cache permanently empty. But it has nothing to do with the Agent SDK, and it's currently trapped behind review of a 6.9k-line feature PR. As its own PR it merges today and starts paying off immediately.

Verified

  • No hardcoded secrets. Every token/key/secret hit in the diff is package-lock.json noise. The session token is fetched at runtime from /kapa/session (overridable via window.KAPA_SESSION_ENDPOINT), not embedded.
  • Navigation is origin-constrained. resolveDocsUrl resolves against window.location.origin and accepts only same-origin or https://docs.redpanda.com, returning null otherwise — so navigate_to_page can't be steered off-site. openTab passes noopener.
  • Markdown rendering is sanitized. The one innerHTML assignment in the diff is DOMPurify.sanitize(marked.parse(md || '')), with a textContent fallback on throw. That's the correct pattern for agent-authored content.
  • The removed persistence layer is fully unwired. chatPersistence.js and persistentApiService.js are deleted (−78/−55) and nothing in the diff references either module.
  • run_bloblang ungated is justified. It runs in page-local WASM with no network, DOM, or navigation effects, and the "verify before presenting" instruction in its description is a genuinely good use of a tool contract — the agent self-corrects mappings instead of publishing broken ones.

What works well

  • The two-tier design — stock Kapa widget for anonymous users, agent panel behind sign-in — means this can merge inert and ship live later. That's a much safer rollout than flipping everything at once, and the body is explicit that it's a no-op until the docs-site#190 backend lands.
  • Tool return values are shaped for the model to narrate, and every popup-opening tool returns { opened, url, note } so a blocked popup degrades to a link instead of silently failing. That's a real failure mode, handled deliberately.
  • submit_docs_feedback waits for the thread ID rather than submitting without conversation context, caps feedback at 5000 chars, and deliberately sends only email + thread reference instead of the transcript, with the reasoning about pasted configs and PII in a comment. Good instincts on the one tool that leaves the browser.
  • loadBloblWasm reuses the mini playground's engine and load paths, falls back from instantiateStreaming to instantiate on a cloned response, and nulls the promise on failure so a retry is possible. That last detail is the kind of thing usually missed.

One question

Given the size and that it's inert until docs-site#190 ships — is there a smaller cut worth landing first? The CI fix and the deleted persistence layer are both independently safe. If the agent surface has to land as one unit that's fine, but it'd be worth saying so in the body, because right now the size is the main obstacle to it getting a careful review.

Review finding: the header claimed navigation and product-switch tools
require approval. Only submit_docs_feedback is gated, and each tool
documents its reasoning on its needsApproval line. The header now
matches what ships so nobody approves a posture that isn't there.
@JakeSCahill

Copy link
Copy Markdown
Contributor Author

Summaries now match the shipped posture: the module header and PR body both state that only submit_docs_feedback is approval-gated (the one tool with a side effect beyond the user's own browsing) and why the other seven are deliberately ungated, with per-tool reasoning on each needsApproval line. Left unapproved for the extra pair of eyes on the chat components per your note.

JakeSCahill and others added 2 commits August 4, 2026 09:35
…anner

The OAuth callback bounces a failed sign-in back to the page with
?login_error=<code> (upstream_failed / work_email_required / state_mismatch),
but nothing displayed it — the user just saw a bare query param. Show a
dismissible role="alert" banner with a friendly message per code, then strip the
param from the URL so a refresh/re-click doesn't repeat it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the full-width top banner with a bottom-right auto-dismissing toast
(role=alert), so it doesn't cover the header. On a failed-login page, also point
the sign-in retry at ?reauth=1 so the FIRST retry click forces a fresh Auth0
login screen (docs-login.mjs adds prompt=login) instead of silently reusing the
SSO session that just failed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
JakeSCahill and others added 3 commits August 4, 2026 11:42
Clarify for anyone new that the user={{ email }} prop is EMAIL ATTRIBUTION (what
appears in Kapa / sales searches by), separate from the opaque hashed
external_owner_id history key minted server-side (docs-site lib/kapa-owner.mjs).
Comment-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Docs pages are full reloads (static site), so the Agent SDK drawer re-mounted
and useAgentChat() started a fresh thread every navigation — the chat appeared
to reset even though history was saved server-side. Persist the active threadId
in sessionStorage (per-tab) and resume it on mount via resumeThread(); 'New chat'
clears the pointer, a missing/deleted thread clears it on failure, and a new tab
still starts clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The anonymous drawer upsell (ChatSdkInterface) and the header sign-in button
(26-docs-account.js) showed whenever the user wasn't signed in, falling back to
/login — which 404s if this UI ships before the auth backend. Gate both on the
same signal ChatInterface already uses: loginUrl / window.__KAPA_LOGIN_URL, set
by the /kapa/session probe only when the backend answers. No backend -> no
sign-in entry (chat still works anonymously); backend present -> sign-in appears
as before. Header re-renders on the kapa-session event once the probe resolves.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants