Ask AI: migrate to Kapa Agent SDK — tools, saved history, sign-in gate - #395
Ask AI: migrate to Kapa Agent SDK — tools, saved history, sign-in gate#395JakeSCahill wants to merge 26 commits into
Conversation
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>
✅ Deploy Preview for docs-ui ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughChangesThis PR migrates the Ask AI chat widget from the Kapa react-sdk with local persistence to the Kapa Agent SDK ( 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
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 winConsole 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 winSame modal-card overflow gap as header-bump.css.
Mirrors the missing
max-height/overflow-yissue flagged insrc/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
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (18)
gulp.d/tasks/bundle-react.jspackage.jsonsrc/css/chat-panel-bump.csssrc/css/chat-panel.csssrc/css/header-bump.csssrc/css/header.csssrc/js/19-chat-panel.jssrc/js/26-docs-account.jssrc/js/react/AskAI.jsxsrc/js/react/agentTools.jssrc/js/react/chatPersistence.jssrc/js/react/components/ChatInterface.jsxsrc/js/react/heap.jssrc/js/react/persistentApiService.jssrc/partials/chat-panel-bump.hbssrc/partials/chat-panel.hbssrc/partials/head-scripts.hbssrc/partials/header-content.hbs
💤 Files with no reviewable changes (2)
- src/js/react/persistentApiService.js
- src/js/react/chatPersistence.js
| /* ——— 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; | ||
| } |
There was a problem hiding this comment.
📐 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
doneRepository: 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.cssRepository: 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
doneRepository: 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('---')
PYRepository: 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.cssRepository: 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.jsxRepository: 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.jsxRepository: 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.jsxRepository: 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
doneRepository: 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.
| .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; | ||
| } |
There was a problem hiding this comment.
🎯 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.
| .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.
| 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() | ||
| } |
There was a problem hiding this comment.
🎯 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.
| 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.
|
|
||
| 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 */ }) |
There was a problem hiding this comment.
🔒 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
| 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` | ||
| } |
There was a problem hiding this comment.
📐 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
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>
…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>
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>
* 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>
|
Review scope: this is +6007/−887 (≈2.5k of that is Fix before mergeThe approval gating is documented backwards, in two places. Only 1 of the 8 registered tools is approval-gated:
Both summaries say the opposite. The PR body:
And the module's own header comment:
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 Suggestions1. 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 2. Split out the Verified
What works well
One questionGiven 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.
|
Summaries now match the shipped posture: the module header and PR body both state that only |
…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>
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>

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/sessionendpoint + 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
AskAI.jsx(AgentProvider+getSessionToken),ChatInterface.jsxrewritten ontouseAgentChat(flat messages, single streaming state, sources from tool-call blocks, approval cards). Old localStorage persistence removed in favor of server-side history.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. Onlysubmit_docs_feedbackis 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 itsneedsApprovalline.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).AgentThreadHistory, gated on the session'sauthenticatedflag.addFeedback); signed-in submissions attach email + thread id server-side.Testing
gulp lintclean; playground WASM tests pass; production bundle ~660 KB smaller than the old Chat SDK bundle.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.comADP console URL is used as a placeholder pending product confirmation.🤖 Generated with Claude Code