Skip to content

custom streamdown components for wave ai#2404

Merged
sawka merged 9 commits intomainfrom
sawka/streamdown
Oct 8, 2025
Merged

custom streamdown components for wave ai#2404
sawka merged 9 commits intomainfrom
sawka/streamdown

Conversation

@sawka
Copy link
Member

@sawka sawka commented Oct 8, 2025

No description provided.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 8, 2025

Walkthrough

Replaces Streamdown usage in frontend/app/aipanel/aimessage.tsx with a new WaveStreamdown component and supplies it a codeBlockMaxWidth atom from WaveAIModel. Adds frontend/app/element/streamdown.tsx implementing WaveStreamdown plus Code, CodeBlock, Collapsible, and async Shiki-based highlighting with copy/execute controls. Imports Streamdown stylesheet in frontend/tailwindsetup.css and adds shiki to package.json. Changes tsconfig.json moduleResolution to "bundler". Extends WaveAIModel with containerWidth and codeBlockMaxWidth atoms and registerScrollToBottom/scrollToBottom methods. AIPanel now tracks and persists panel width via ResizeObserver and wires model scroll-to-bottom handling (including a 100ms delayed scroll on load). Also updates Go prompts to prefer ~100-char lines for code/command blocks.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description Check ❓ Inconclusive No pull request description was provided, leaving the motivations and context of the changes unclear and effectively rendering the description too generic to assess the relation to the changeset. Please provide a meaningful description outlining the purpose and scope of the changes, including why custom Streamdown components were introduced and how they integrate with Wave AI.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The title clearly indicates the introduction of custom Streamdown-based components tailored for Wave AI, which reflects the primary change in the pull request by replacing and extending Streamdown functionality with new WaveStreamdown components and associated utilities.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sawka/streamdown

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dbe823c and 3e1dd37.

📒 Files selected for processing (1)
  • frontend/app/element/streamdown.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/app/element/streamdown.tsx (3)
frontend/util/util.ts (1)
  • useAtomValueSafe (425-425)
frontend/app/element/copybutton.tsx (1)
  • CopyButton (59-59)
frontend/app/element/iconbutton.tsx (1)
  • IconButton (12-34)
🪛 ast-grep (0.39.5)
frontend/app/element/streamdown.tsx

[warning] 102-102: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🪛 Biome (2.1.2)
frontend/app/element/streamdown.tsx

[error] 103-103: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)


[error] 216-216: Avoid passing children using a prop

The canonical way to pass children in React is to use JSX elements

(lint/correctness/noChildrenProp)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build for TestDriver.ai
  • GitHub Check: Analyze (go)
  • GitHub Check: Analyze (javascript-typescript)

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

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f9b3761 and fe4c940.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (5)
  • frontend/app/aipanel/aimessage.tsx (3 hunks)
  • frontend/app/element/streamdown.tsx (1 hunks)
  • frontend/tailwindsetup.css (1 hunks)
  • package.json (1 hunks)
  • tsconfig.json (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
frontend/app/element/streamdown.tsx (1)
frontend/app/element/iconbutton.tsx (1)
  • IconButton (12-34)
frontend/app/aipanel/aimessage.tsx (1)
frontend/app/element/streamdown.tsx (1)
  • WaveStreamdown (170-274)
🪛 ast-grep (0.39.5)
frontend/app/element/streamdown.tsx

[warning] 56-56: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🪛 Biome (2.1.2)
frontend/app/element/streamdown.tsx

[error] 57-57: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)


[error] 192-192: Avoid passing children using a prop

The canonical way to pass children in React is to use JSX elements

(lint/correctness/noChildrenProp)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Analyze (go)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Build for TestDriver.ai

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
frontend/app/element/streamdown.tsx (1)

223-225: Use JSX children instead of the children prop.

Passing {props.children} via the children prop violates our React lint rule and is unnecessary—wrap <CodeBlock> and place {props.children} between the tags instead.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fe4c940 and 95149a5.

📒 Files selected for processing (1)
  • frontend/app/element/streamdown.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/app/element/streamdown.tsx (3)
emain/updater.ts (1)
  • start (124-132)
frontend/app/element/copybutton.tsx (1)
  • CopyButton (59-59)
frontend/app/element/iconbutton.tsx (1)
  • IconButton (12-34)
🪛 ast-grep (0.39.5)
frontend/app/element/streamdown.tsx

[warning] 93-93: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🪛 Biome (2.1.2)
frontend/app/element/streamdown.tsx

[error] 94-94: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)


[error] 224-224: Avoid passing children using a prop

The canonical way to pass children in React is to use JSX elements

(lint/correctness/noChildrenProp)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build for TestDriver.ai
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (go)

Comment on lines +220 to +225
defaultOrigin="http://localhost"
components={{
code: Code,
pre: (props: React.HTMLAttributes<HTMLPreElement>) => (
<CodeBlock children={props.children} onClickExecute={onClickExecute} />
),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid forcing defaultOrigin to localhost.

Hard-coding defaultOrigin="http://localhost" rewrites every relative link in streamed markdown to point at localhost, so production users land on dead URLs. Drop the override (let Streamdown infer the current origin) or plumb the real host in from the caller.

🧰 Tools
🪛 Biome (2.1.2)

[error] 224-224: Avoid passing children using a prop

The canonical way to pass children in React is to use JSX elements

(lint/correctness/noChildrenProp)

🤖 Prompt for AI Agents
In frontend/app/element/streamdown.tsx around lines 220–225, the component is
forcing defaultOrigin="http://localhost" which rewrites relative links to
localhost; remove that hard-coded defaultOrigin prop so Streamdown uses the
current window origin, or change the call site to pass a real origin prop (e.g.,
read from window.location.origin or a forwarded prop from the server/env) and
pass it through instead of the literal "http://localhost".

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (4)
frontend/app/element/streamdown.tsx (4)

13-15: Fix getTextContent to handle all React node types.

This implementation only extracts text from bare strings. During streaming, markdown children frequently arrive as arrays, fragments, numbers, or nested React elements, causing getTextContent to return "" and leaving code blocks blank.

Based on past review feedback, replace with a recursive extractor:

-function getTextContent(node: React.ReactNode): string {
-    return typeof node === "string" ? node : "";
-}
+function extractText(node: React.ReactNode): string {
+    if (node == null) return "";
+    if (typeof node === "string" || typeof node === "number") return String(node);
+    if (Array.isArray(node)) return node.map(extractText).join("");
+    if (React.isValidElement(node)) return extractText(node.props?.children);
+    return "";
+}

Also update the import on line 7:

-import { useEffect, useRef, useState } from "react";
+import { isValidElement, useEffect, useRef, useState } from "react";

Then use extractText instead of getTextContent throughout the file (lines 111, 146, 152).


50-80: Extract text from children prop, not stale DOM.

After the first highlight pass, codeRef.current?.textContent returns the previously highlighted HTML markup instead of the raw source. On subsequent renders during streaming, this causes the effect to re-highlight stale content, so code blocks never update with new text.

Based on past review feedback, extract the raw text from the current children prop (using the recursive extractText function suggested above) instead of reading from the DOM:

     useEffect(() => {
         let disposed = false;
 
-        if (!text) {
+        const raw = extractText(text) || codeRef.current?.textContent || "";
+        if (!raw) {
             setHtml("");
             return;
         }
 
         (async () => {
             try {
-                const full = await codeToHtml(text, { lang, theme: ShikiTheme });
+                const full = await codeToHtml(raw, { lang, theme: ShikiTheme });
                 const start = full.indexOf("<code");
                 const open = full.indexOf(">", start);
                 const end = full.lastIndexOf("</code>");
                 const inner = start !== -1 && open !== -1 && end !== -1 ? full.slice(open + 1, end) : "";
                 if (!disposed) {
                     setHtml(inner);
                     setHasError(false);
                 }
             } catch (e) {
                 if (!disposed) {
                     setHasError(true);
                 }
                 console.warn(`Shiki highlight failed for ${lang}`, e);
             }
         })();
 
         return () => {
             disposed = true;
         };
-    }, [text, lang]);
+    }, [raw, lang]);

126-135: Remove duplicate getTextContent implementation.

CodeBlock reimplements getTextContent with a more complete recursive version, but the top-level helper (lines 13-15) is too simplistic. Once you replace the top-level version with the recursive extractText helper (per the earlier comment), remove this duplicate and use the shared implementation.

 const CodeBlock = ({ children, onClickExecute }: CodeBlockProps) => {
-    const getTextContent = (children: any): string => {
-        if (typeof children === "string") {
-            return children;
-        } else if (Array.isArray(children)) {
-            return children.map(getTextContent).join("");
-        } else if (children.props && children.props.children) {
-            return getTextContent(children.props.children);
-        }
-        return "";
-    };
-
     const getLanguage = (children: any): string => {
         if (children?.props?.className) {
             const match = children.props.className.match(/language-([\w+-]+)/i);
             if (match) return match[1];
         }
         return "text";
     };
 
     const handleCopy = async (e: React.MouseEvent) => {
-        let textToCopy = getTextContent(children);
+        let textToCopy = extractText(children);
         textToCopy = textToCopy.replace(/\n$/, "");
         await navigator.clipboard.writeText(textToCopy);
     };
 
     const handleExecute = (e: React.MouseEvent) => {
-        let textToCopy = getTextContent(children);
+        let textToCopy = extractText(children);
         textToCopy = textToCopy.replace(/\n$/, "");
         if (onClickExecute) {
             onClickExecute(textToCopy);
             return;
         }
     };

228-228: Remove hardcoded localhost origin.

defaultOrigin="http://localhost" forces all relative links in streamed markdown to point at localhost, breaking them for production users. Drop this prop so Streamdown uses the current window.location.origin, or pass the real origin from the environment.

Based on past review feedback:

-            defaultOrigin="http://localhost"
🧹 Nitpick comments (1)
frontend/app/element/streamdown.tsx (1)

102-102: Document XSS safety of dangerouslySetInnerHTML usage.

Static analysis flags dangerouslySetInnerHTML, but this usage is acceptable because the HTML comes from Shiki (a trusted syntax highlighter library) rather than user input. Consider adding a brief comment explaining the safety:

     return (
         <code
             ref={codeRef}
             className={cn("font-mono text-[12px]", className)}
+            // Safe: HTML generated by trusted Shiki highlighter
             dangerouslySetInnerHTML={{ __html: html }}
         />
     );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 95149a5 and 80978c6.

📒 Files selected for processing (4)
  • frontend/app/aipanel/aipanel.tsx (1 hunks)
  • frontend/app/aipanel/aipanelmessages.tsx (3 hunks)
  • frontend/app/aipanel/waveai-model.tsx (2 hunks)
  • frontend/app/element/streamdown.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
frontend/app/aipanel/aipanelmessages.tsx (1)
frontend/app/aipanel/waveai-model.tsx (2)
  • WaveAIModel (24-213)
  • scrollToBottom (148-150)
frontend/app/element/streamdown.tsx (2)
frontend/app/element/copybutton.tsx (1)
  • CopyButton (59-59)
frontend/app/element/iconbutton.tsx (1)
  • IconButton (12-34)
🪛 ast-grep (0.39.5)
frontend/app/element/streamdown.tsx

[warning] 101-101: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🪛 Biome (2.1.2)
frontend/app/element/streamdown.tsx

[error] 102-102: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)


[error] 232-232: Avoid passing children using a prop

The canonical way to pass children in React is to use JSX elements

(lint/correctness/noChildrenProp)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Analyze (go)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Build for TestDriver.ai
🔇 Additional comments (2)
frontend/app/aipanel/waveai-model.tsx (1)

27-27: LGTM! Clean callback pattern for scroll coordination.

The scrollToBottom callback mechanism is well-implemented. The nullable callback provides flexibility, and the simple synchronous methods are appropriate for coordinating scroll behavior across components.

Also applies to: 144-150

frontend/app/aipanel/aipanelmessages.tsx (1)

8-8: LGTM! Proper callback registration.

The integration with WaveAIModel is correctly implemented. The useEffect with model as a dependency ensures the scroll callback is properly wired whenever the model instance changes, and the scrollToBottom function reference is stable.

Also applies to: 29-29, 42-44

Comment on lines +107 to +109
setTimeout(() => {
model.scrollToBottom();
}, 100);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Clean up setTimeout on unmount.

If the component unmounts during the 100ms delay, the timeout will still fire and attempt to call model.scrollToBottom(). Store the timeout ID and clear it in the cleanup function:

     useEffect(() => {
         const loadMessages = async () => {
             const messages = await model.loadChat();
             setMessages(messages as any);
             setIsLoadingChat(false);
-            setTimeout(() => {
+            const timeoutId = setTimeout(() => {
                 model.scrollToBottom();
             }, 100);
+            return () => clearTimeout(timeoutId);
         };
         loadMessages();
     }, [model, setMessages]);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In frontend/app/aipanel/aipanel.tsx around lines 107 to 109, the setTimeout used
to call model.scrollToBottom() is not cleaned up on unmount; store the timeout
ID (e.g., in a ref or local variable captured by the effect) when calling
setTimeout and in the effect's cleanup call clearTimeout(timeoutId) so the
callback won't run after unmount; ensure the timeoutId type is compatible with
TS (number | NodeJS.Timeout depending on environment) and clear it in the
returned cleanup function.

…ll. have code block scroll instead. (keeps copy button / controls visible as well)
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (3)
frontend/app/element/streamdown.tsx (2)

1-16: Text extraction still needs fixing per previous reviews.

The getTextContent helper at lines 14-16 only handles bare strings. During streaming, markdown often arrives as fragments/arrays, causing the extractor to return "" and breaking both highlighting and toolbar actions (copy/execute). This issue was flagged in previous reviews and remains unresolved.

Per the previous review comment on lines 8-145: Replace this helper with a recursive extractor that handles strings, numbers, arrays, and React elements, then use it consistently in both Code and CodeBlock.


225-225: Hard-coded localhost origin breaks production links.

This issue was flagged in a previous review and remains unresolved. Setting defaultOrigin="http://localhost" rewrites all relative links to localhost, breaking them for production users.

Per the previous review comment on lines 225-234: Remove this hard-coded prop and let Streamdown infer the current origin, or pass window.location.origin from the caller.

frontend/app/aipanel/aipanel.tsx (1)

108-110: Timeout cleanup still missing.

The setTimeout here was flagged in a previous review but remains unfixed. If the component unmounts during the 100ms delay, the callback will still attempt to invoke model.scrollToBottom().

Per the previous review comment on lines 108-110: Store the timeout ID and clear it in the effect's cleanup function:

         const loadMessages = async () => {
             const messages = await model.loadChat();
             setMessages(messages as any);
             setIsLoadingChat(false);
-            setTimeout(() => {
+            const timeoutId = setTimeout(() => {
                 model.scrollToBottom();
             }, 100);
         };
         loadMessages();
+        return () => {
+            if (timeoutId !== undefined) clearTimeout(timeoutId);
+        };
🧹 Nitpick comments (1)
frontend/app/element/streamdown.tsx (1)

228-234: Avoid passing children as a prop.

React's canonical pattern uses JSX element children, not children={...} props. While functional, this violates React conventions and triggers linter warnings.

Refactor to use JSX element syntax:

-                pre: (props: React.HTMLAttributes<HTMLPreElement>) => (
-                    <CodeBlock
-                        children={props.children}
-                        onClickExecute={onClickExecute}
-                        codeBlockMaxWidthAtom={codeBlockMaxWidthAtom}
-                    />
-                ),
+                pre: (props: React.HTMLAttributes<HTMLPreElement>) => (
+                    <CodeBlock
+                        onClickExecute={onClickExecute}
+                        codeBlockMaxWidthAtom={codeBlockMaxWidthAtom}
+                    >
+                        {props.children}
+                    </CodeBlock>
+                ),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 80978c6 and cee6fd4.

📒 Files selected for processing (4)
  • frontend/app/aipanel/aimessage.tsx (3 hunks)
  • frontend/app/aipanel/aipanel.tsx (3 hunks)
  • frontend/app/aipanel/waveai-model.tsx (3 hunks)
  • frontend/app/element/streamdown.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/app/aipanel/aimessage.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/app/element/streamdown.tsx (3)
frontend/util/util.ts (1)
  • useAtomValueSafe (425-425)
frontend/app/element/copybutton.tsx (1)
  • CopyButton (59-59)
frontend/app/element/iconbutton.tsx (1)
  • IconButton (12-34)
🪛 ast-grep (0.39.5)
frontend/app/element/streamdown.tsx

[warning] 86-86: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🪛 Biome (2.1.2)
frontend/app/element/streamdown.tsx

[error] 87-87: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)


[error] 230-230: Avoid passing children using a prop

The canonical way to pass children in React is to use JSX elements

(lint/correctness/noChildrenProp)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build for TestDriver.ai
  • GitHub Check: Analyze (go)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (3)
frontend/app/element/streamdown.tsx (1)

30-90: Shiki HTML insertion is safe here.

The static analysis tools flag dangerouslySetInnerHTML at line 87, but this is a false positive. Shiki is a trusted syntax highlighter that generates sanitized HTML. The string manipulation (lines 46-49) extracts only the inner <code> content, which is already safe.

frontend/app/aipanel/aipanel.tsx (1)

115-132: LGTM! Width tracking with ResizeObserver.

The ResizeObserver implementation correctly tracks the panel's width, persists it to globalStore, and properly cleans up on unmount. This integrates well with the model's containerWidth atom for responsive code block sizing.

frontend/app/aipanel/waveai-model.tsx (1)

27-27: LGTM! Width management and scroll callback additions.

The new width-tracking atoms (containerWidth, codeBlockMaxWidth) and scroll callback registration cleanly extend the model's capabilities:

  • containerWidth is updated by the panel's ResizeObserver
  • codeBlockMaxWidth derives a responsive max-width (container width - 35px padding)
  • The callback pattern for scrollToBottom is simple and correct

These changes integrate well with the panel's width tracking and enable responsive code block rendering in the Streamdown component.

Also applies to: 34-35, 65-68, 151-157

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
frontend/app/element/streamdown.tsx (2)

8-8: Unify robust text extraction for streamed children (fix highlight/copy/execute).

Current getTextContent only handles bare strings; streamed nodes often arrive as arrays/fragments/elements, causing empty text in Code and drift with CodeBlock’s local extractor. Replace with a single recursive extractor and reuse it everywhere.

- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+ import { isValidElement, useCallback, useEffect, useMemo, useRef, useState } from "react";
@@
-function getTextContent(node: React.ReactNode): string {
-    return typeof node === "string" ? node : "";
-}
+function extractText(node: React.ReactNode): string {
+    if (node == null) return "";
+    if (typeof node === "string" || typeof node === "number") return String(node);
+    if (Array.isArray(node)) return node.map(extractText).join("");
+    if (isValidElement(node)) return extractText(node.props?.children);
+    return "";
+}
@@
-    const text = getTextContent(children);
+    const text = extractText(children);
@@
-    const getTextContent = (children: any): string => {
-        if (typeof children === "string") {
-            return children;
-        } else if (Array.isArray(children)) {
-            return children.map(getTextContent).join("");
-        } else if (children.props && children.props.children) {
-            return getTextContent(children.props.children);
-        }
-        return "";
-    };
+    // use shared extractText
@@
-        let textToCopy = getTextContent(children);
+        let textToCopy = extractText(children);
         textToCopy = textToCopy.replace(/\n$/, "");
         await navigator.clipboard.writeText(textToCopy);
@@
-        let textToCopy = getTextContent(children);
+        let textToCopy = extractText(children);
         textToCopy = textToCopy.replace(/\n$/, "");
         if (onClickExecute) {
             onClickExecute(textToCopy);
             return;
         }

Also applies to: 15-17, 103-114, 124-134, 144-147, 150-156


319-321: Remove hard-coded defaultOrigin=localhost.

This rewrites relative links to localhost in production. Let Streamdown infer origin or pass the real origin.

-            defaultOrigin="http://localhost"
🧹 Nitpick comments (2)
frontend/app/element/streamdown.tsx (2)

94-101: dangerouslySetInnerHTML: confirm Shiki escaping or sanitize.

Shiki typically escapes user content, but this bypasses React XSS guards. Sanitize or document why it’s safe (e.g., DOMPurify) to satisfy linters and harden.

Example:

-            dangerouslySetInnerHTML={{ __html: html }}
+            dangerouslySetInnerHTML={{ __html: html /* already sanitized by Shiki */ }}

Or sanitize before setHtml:

// import DOMPurify from "dompurify";
// setHtml(DOMPurify.sanitize(inner, { ALLOWED_ATTR: ["class"] }));

222-228: Pass children via JSX, not children prop.

Avoid anti-pattern flagged by linters; keeps React idiomatic.

-            pre: (props: React.HTMLAttributes<HTMLPreElement>) => (
-                <CodeBlock
-                    children={props.children}
-                    onClickExecute={onClickExecute}
-                    codeBlockMaxWidthAtom={codeBlockMaxWidthAtom}
-                />
-            ),
+            pre: ({ children }: React.HTMLAttributes<HTMLPreElement>) => (
+                <CodeBlock onClickExecute={onClickExecute} codeBlockMaxWidthAtom={codeBlockMaxWidthAtom}>
+                    {children}
+                </CodeBlock>
+            ),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cee6fd4 and dbe823c.

📒 Files selected for processing (2)
  • frontend/app/element/streamdown.tsx (1 hunks)
  • pkg/aiusechat/usechat.go (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/app/element/streamdown.tsx (3)
frontend/util/util.ts (1)
  • useAtomValueSafe (425-425)
frontend/app/element/copybutton.tsx (1)
  • CopyButton (59-59)
frontend/app/element/iconbutton.tsx (1)
  • IconButton (12-34)
🪛 ast-grep (0.39.5)
frontend/app/element/streamdown.tsx

[warning] 97-97: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🪛 Biome (2.1.2)
frontend/app/element/streamdown.tsx

[error] 98-98: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)


[error] 224-224: Avoid passing children using a prop

The canonical way to pass children in React is to use JSX elements

(lint/correctness/noChildrenProp)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build for TestDriver.ai
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (go)
🔇 Additional comments (1)
pkg/aiusechat/usechat.go (1)

71-71: Prompt tweak LGTM.

Clear, actionable guidance for code/command width. No functional impact.

@sawka sawka merged commit 2312752 into main Oct 8, 2025
7 of 8 checks passed
@sawka sawka deleted the sawka/streamdown branch October 8, 2025 20:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant