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 4 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 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>

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, submit feedback, open community Slack. All approval-gated except the sandboxedrun_bloblang.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 tool approvals → 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