Skip to content

AI in Stack Companion#1297

Open
aadesh18 wants to merge 2 commits intodevfrom
ai-in-stack-companion
Open

AI in Stack Companion#1297
aadesh18 wants to merge 2 commits intodevfrom
ai-in-stack-companion

Conversation

@aadesh18
Copy link
Copy Markdown
Contributor

@aadesh18 aadesh18 commented Mar 28, 2026

Summary by CodeRabbit

  • New Features

    • Added an "Ask AI" chat sidebar with streaming assistant responses and progressive word-by-word reveal
    • New chat UI primitives: inline/code blocks, smart links, and copy-to-clipboard controls for code and URLs
    • Tool-result cards that expand to show queries, results/errors, and copyable payloads
    • Keyboard shortcuts (Enter to send, Arrow keys to navigate) and improved auto-scroll during streaming
  • Bug Fixes

    • Prevented button/menu clicks inside list items from bubbling to parent row handlers

Copilot AI review requested due to automatic review settings March 28, 2026 00:35
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stack-auth-hosted-components Ready Ready Preview, Comment Mar 28, 2026 1:46am
stack-backend Ready Ready Preview, Comment Mar 28, 2026 1:46am
stack-dashboard Ready Ready Preview, Comment Mar 28, 2026 1:46am
stack-demo Ready Ready Preview, Comment Mar 28, 2026 1:46am
stack-docs Ready Ready Preview, Comment Mar 28, 2026 1:46am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 28, 2026

📝 Walkthrough

Walkthrough

Adds a shared AI chat UI module, refactors ask-ai to use it, and integrates a new AIChatWidget into the stack companion sidebar; also adjusts a list button event handler to stop propagation.

Changes

Cohort / File(s) Summary
AI Chat Shared Module
apps/dashboard/src/components/commands/ai-chat-shared.tsx
New shared module: authenticated streaming transport creator, UI primitives (CopyButton, InlineCode, CodeBlock, SmartLink), ToolInvocationCard, react-markdown overrides, message parsing helpers (countWords, getFirstNWords, getMessageContent, getToolInvocations), UserMessage/AssistantMessage renderers, and useWordStreaming hook.
Ask AI Refactoring
apps/dashboard/src/components/commands/ask-ai.tsx
Removed in-file chat rendering and streaming logic; now imports createAskAiTransport, AssistantMessage, UserMessage, getMessageContent, getToolInvocations, and useWordStreaming from ai-chat-shared and uses the shared transport.
AI Chat Widget & Integration
apps/dashboard/src/components/stack-companion/ai-chat-widget.tsx, apps/dashboard/src/components/stack-companion.tsx
Added AIChatWidget component (client-side, remount-on-key change, send/stream messages, word-by-word reveal, scroll management, error state) and integrated it into the stack companion sidebar as "Ask AI" with SparkleIcon and adjusted drawer layout for that view.
List Component Event Handling
apps/dashboard/src/components/design-components/list.tsx
Added e.stopPropagation() to ListItemButtons wrapper to prevent clicks inside buttons/menus from bubbling to parent row handlers.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant AIChatWidget
    participant useChat
    participant API as /api/latest/ai/query/stream
    participant useWordStreaming
    participant UI as MessageRenderer

    User->>AIChatWidget: submit message
    AIChatWidget->>useChat: sendMessage (includes system prompt, tools, auth)
    useChat->>API: open streaming request
    API-->>useChat: stream response chunks
    useChat->>AIChatWidget: emit assistant message parts (including tool invocations)
    AIChatWidget->>useWordStreaming: provide assistant content for progressive reveal
    useWordStreaming-->>UI: incremental display updates (word-by-word)
    UI->>AIChatWidget: render messages & tool cards, update scroll
    AIChatWidget->>User: display streamed content
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Cmd K #1037: Similar refactor/addition around dashboard AI chat UI, markdown/code/link helpers, and streaming chat wiring.

Suggested reviewers

  • N2D4
  • TheUntraceable

Poem

🐰✨ I hopped through code to share a chat,
Words unfurl softly, bit by bit, like that.
Buttons to copy, links trimmed neat,
A widget that listens, streams, and greets.
Hooray — new chat trails for us to meet!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description contains only a template comment with no actual content describing the changes, objectives, implementation details, or testing information. Provide a meaningful description that explains what AI features were added, why they were added, how they work together, and any relevant testing or migration notes.
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'AI in Stack Companion' clearly summarizes the main change: adding AI chat functionality to the Stack Companion component.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ai-in-stack-companion

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.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 28, 2026

Greptile Summary

This PR adds an "Ask AI" panel to the Stack Companion sidebar by introducing a new AIChatWidget component and extracting all shared AI-chat UI primitives (message bubbles, markdown rendering, copy buttons, word-streaming hook, etc.) from the existing ask-ai.tsx command-palette component into a new ai-chat-shared.tsx module. A minor fix to list.tsx also prevents button clicks inside list rows from bubbling to the parent row handler.

Key changes:

  • ai-chat-shared.tsx (new): shared UserMessage, AssistantMessage, ToolInvocationCard, CodeBlock, InlineCode, SmartLink, markdownComponents, and useWordStreaming — consumed by both the command-palette and sidebar chat.
  • ai-chat-widget.tsx (new): standalone sidebar widget with an idle/conversation two-phase UI; key-based remounting for "new conversation" resets chat state cleanly.
  • ask-ai.tsx: slimmed down to consume ai-chat-shared; note that targetWordCount is now an unused destructured variable (lint warning), and the DefaultChatTransport configuration is duplicated verbatim from the new widget.
  • stack-companion.tsx: "Ask AI" added as the first sidebar item; content area layout adjusted to remove padding/scroll for the full-height chat panel.
  • list.tsx: e.stopPropagation() added to the buttons container to prevent action-button clicks from triggering the parent row handler.

Confidence Score: 5/5

Safe to merge; all remaining findings are P2 style/cleanup suggestions with no correctness or data-integrity impact.

The feature is well-structured (shared module, keyed remounting for conversation resets, proper scroll handling). The only open items are: an unused variable that produces a lint warning but has no runtime impact, a project-convention suggestion around runAsynchronouslyWithAlert, and a code-duplication note about the DefaultChatTransport config. None of these affect correctness or safety.

apps/dashboard/src/components/commands/ask-ai.tsx (unused variable), apps/dashboard/src/components/stack-companion/ai-chat-widget.tsx (duplicated transport config)

Important Files Changed

Filename Overview
apps/dashboard/src/components/commands/ai-chat-shared.tsx New shared module extracting AI chat UI primitives (CopyButton, CodeBlock, markdown components, message components, word-streaming hook) from ask-ai.tsx; note the truncateUrl refactor drops the URL-constructor try/catch in favour of a regex approach
apps/dashboard/src/components/commands/ask-ai.tsx Stripped down to import from ai-chat-shared; introduces an unused targetWordCount variable and uses runAsynchronously for async button/keyboard handlers where runAsynchronouslyWithAlert is specified by project rules
apps/dashboard/src/components/stack-companion/ai-chat-widget.tsx New sidebar AI chat widget with two-phase UI (idle vs active conversation); duplicates the useChat DefaultChatTransport configuration from ask-ai.tsx verbatim and uses runAsynchronously instead of runAsynchronouslyWithAlert in async handlers
apps/dashboard/src/components/stack-companion.tsx Adds 'Ask AI' as the first sidebar item and conditionally removes padding/overflow for the AI panel; straightforward integration change
apps/dashboard/src/components/design-components/list.tsx Adds e.stopPropagation() on the ListItemButtons wrapper to prevent button clicks from bubbling to the parent list-item click handler; correct fix
pnpm-lock.yaml Lockfile bump: updates @babel/core peer from 7.26.0 to 7.29.0 for next, adds rolldown to the nitro snapshot, removes the now-deleted packages/private devDependency, and tightens eslint-import-resolver-typescript snapshot keys

Sequence Diagram

sequenceDiagram
    participant User
    participant AIChatWidget
    participant AIChatWidgetInner
    participant useChat
    participant Backend as /api/latest/ai/query/stream

    User->>AIChatWidget: types question, hits Enter
    AIChatWidget->>AIChatWidgetInner: setConversationStarted(true)
    AIChatWidgetInner->>useChat: sendMessage({ text })
    useChat->>Backend: POST (systemPrompt, tools, messages)
    Backend-->>useChat: streaming response (tokens)
    useChat-->>AIChatWidgetInner: messages[], status="streaming"
    AIChatWidgetInner->>AIChatWidgetInner: useWordStreaming reveals words
    AIChatWidgetInner-->>User: AssistantMessage rendered progressively

    User->>AIChatWidgetInner: types follow-up, hits Enter
    AIChatWidgetInner->>useChat: sendMessage({ text })
    useChat->>Backend: POST (full conversation history)
    Backend-->>useChat: streaming response
    useChat-->>AIChatWidgetInner: updated messages[]
    AIChatWidgetInner-->>User: new AssistantMessage rendered

    User->>AIChatWidgetInner: clicks "New conversation"
    AIChatWidgetInner->>AIChatWidget: onNewConversation()
    AIChatWidget->>AIChatWidget: setConversationKey(prev+1), reset state
    AIChatWidget->>AIChatWidgetInner: remount via key change
    AIChatWidgetInner-->>User: idle input screen shown
Loading

Comments Outside Diff (2)

  1. apps/dashboard/src/components/commands/ask-ai.tsx, line 95 (link)

    P2 Unused destructured variable targetWordCount

    targetWordCount is destructured from useWordStreaming but never referenced anywhere in the component after this line. This will produce an ESLint no-unused-vars warning. Consider removing it.

  2. apps/dashboard/src/components/commands/ask-ai.tsx, line 137 (link)

    P2 runAsynchronouslyWithAlert preferred for async handlers

    Per project conventions, runAsynchronouslyWithAlert should be used for async button-click and form-submission handlers instead of runAsynchronously. The same pattern applies in ai-chat-widget.tsx at lines 142 and 165.

    Note: if errors are intentionally suppressed here because they are already surfaced via the aiError state, add a brief comment explaining that decision so future readers understand the deviation.

    Rule Used: Use runAsynchronouslyWithAlert from `@stackframe... (source)

    Learnt From
    stack-auth/stack-auth#943

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Reviews (1): Last reviewed commit: "initial commit" | Re-trigger Greptile

Comment on lines +76 to +97
transport: new DefaultChatTransport({
api: `${backendBaseUrl}/api/latest/ai/query/stream`,
headers: () => buildStackAuthHeaders(currentUser),
prepareSendMessagesRequest: async ({ messages: uiMessages, headers }) => {
const modelMessages = await convertToModelMessages(uiMessages);
return {
body: {
systemPrompt: "command-center-ask-ai",
tools: ["docs", "sql-query"],
quality: "smart",
speed: "slow",
projectId,
messages: modelMessages.map(m => ({
role: m.role,
content: m.content,
})),
},
headers,
};
},
}),
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Duplicated DefaultChatTransport configuration

The entire useChat / DefaultChatTransport setup here (API URL, headers, prepareSendMessagesRequest body shape) is a verbatim copy of the same block in ask-ai.tsx (lines 58–79). Any future change to the system prompt key, tool list, or body schema must be applied in two places.

Consider extracting the transport factory into a shared utility in ai-chat-shared.tsx or a dedicated lib/ai-chat-transport.ts file, then importing it in both consumers.

Copy link
Copy Markdown
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.

🧹 Nitpick comments (2)
apps/dashboard/src/components/stack-companion.tsx (1)

432-436: Consider updating the Playground page to showcase the new AI chat widget.

The conditional layout change for ask-ai (no padding, flex column, overflow hidden) makes sense for a chat interface that manages its own scrolling. As per coding guidelines, any design components added to the dashboard should have corresponding updates in the Playground page.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/components/stack-companion.tsx` around lines 432 - 436,
Playground page is missing the updated layout and AIChatWidget rendering when
activeItem === 'ask-ai'; update the Playground component to apply the same
conditional className logic used here (using cn with "flex-1 overflow-x-hidden
no-drag cursor-auto" and the branch that sets "overflow-hidden p-0 flex
flex-col" for activeItem === 'ask-ai' vs "overflow-y-auto p-5") and render
<AIChatWidget isActive={true} /> when activeItem === 'ask-ai'; ensure
AIChatWidget is imported and that the Playground's activeItem state/value uses
the same 'ask-ai' key so the widget receives the correct isActive prop and the
container handles its scrolling.
apps/dashboard/src/components/commands/ai-chat-shared.tsx (1)

407-411: Type cast through unknown bypasses type safety.

The as unknown as ToolInvocationPart cast assumes the filtered parts match the ToolInvocationPart structure. While the startsWith("tool-") check provides some validation, this doesn't guarantee the full structure. Consider using a type guard or runtime validation for safer typing.

♻️ Suggested type guard approach
+function isToolInvocationPart(part: unknown): part is ToolInvocationPart {
+  if (typeof part !== 'object' || part === null) return false;
+  const p = part as Record<string, unknown>;
+  return (
+    typeof p.type === 'string' &&
+    p.type.startsWith('tool-') &&
+    typeof p.toolCallId === 'string' &&
+    typeof p.state === 'string'
+  );
+}
+
 // Helper to extract tool invocations from UIMessage parts
 export function getToolInvocations(message: UIMessage): ToolInvocationPart[] {
   return message.parts
-    .filter((part) => part.type.startsWith("tool-"))
-    .map((part) => part as unknown as ToolInvocationPart);
+    .filter(isToolInvocationPart);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/components/commands/ai-chat-shared.tsx` around lines 407 -
411, The cast in getToolInvocations blindly forces parts to ToolInvocationPart
via "as unknown as ToolInvocationPart"; replace that with a proper type guard or
runtime validation (e.g., implement isToolInvocationPart(part: any): part is
ToolInvocationPart that checks required fields like type, name, args/result
shapes) and use it in the filter (message.parts.filter(isToolInvocationPart)).
Ensure the guard references the ToolInvocationPart shape and return the typed
array without double-casting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/dashboard/src/components/commands/ai-chat-shared.tsx`:
- Around line 407-411: The cast in getToolInvocations blindly forces parts to
ToolInvocationPart via "as unknown as ToolInvocationPart"; replace that with a
proper type guard or runtime validation (e.g., implement
isToolInvocationPart(part: any): part is ToolInvocationPart that checks required
fields like type, name, args/result shapes) and use it in the filter
(message.parts.filter(isToolInvocationPart)). Ensure the guard references the
ToolInvocationPart shape and return the typed array without double-casting.

In `@apps/dashboard/src/components/stack-companion.tsx`:
- Around line 432-436: Playground page is missing the updated layout and
AIChatWidget rendering when activeItem === 'ask-ai'; update the Playground
component to apply the same conditional className logic used here (using cn with
"flex-1 overflow-x-hidden no-drag cursor-auto" and the branch that sets
"overflow-hidden p-0 flex flex-col" for activeItem === 'ask-ai' vs
"overflow-y-auto p-5") and render <AIChatWidget isActive={true} /> when
activeItem === 'ask-ai'; ensure AIChatWidget is imported and that the
Playground's activeItem state/value uses the same 'ask-ai' key so the widget
receives the correct isActive prop and the container handles its scrolling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3917f0c5-7955-49ce-9a5a-a843b3e2cf2e

📥 Commits

Reviewing files that changed from the base of the PR and between 4ddf6a5 and f3e27f8.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • apps/dashboard/src/components/commands/ai-chat-shared.tsx
  • apps/dashboard/src/components/commands/ask-ai.tsx
  • apps/dashboard/src/components/design-components/list.tsx
  • apps/dashboard/src/components/stack-companion.tsx
  • apps/dashboard/src/components/stack-companion/ai-chat-widget.tsx

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Introduces an “Ask AI” experience inside the Stack Companion UI, sharing chat rendering logic between the command palette preview and the new companion widget.

Changes:

  • Adds a new Stack Companion sidebar item and full-page chat widget for “Ask AI”.
  • Extracts shared AI chat rendering utilities/components into ai-chat-shared.tsx and wires ask-ai to use them.
  • Updates list-row button behavior to prevent row click handlers from firing when interacting with action buttons.

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pnpm-lock.yaml Updates lockfile for dependency graph changes introduced by the PR.
apps/dashboard/src/components/stack-companion/ai-chat-widget.tsx New Stack Companion AI chat widget implementation (streaming, scrolling, input handling).
apps/dashboard/src/components/stack-companion.tsx Adds “Ask AI” to the companion sidebar and renders the new widget when selected.
apps/dashboard/src/components/design-components/list.tsx Stops click propagation from row action buttons to the row onClick handler.
apps/dashboard/src/components/commands/ask-ai.tsx Refactors the command palette AI preview to use shared chat UI helpers.
apps/dashboard/src/components/commands/ai-chat-shared.tsx New shared components/utilities for AI chat rendering (markdown, tool invocations, streaming).
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

const pathname = usePathname();
const projectId = pathname.startsWith("/projects/") ? pathname.split("/")[2] : undefined;

const backendBaseUrl = getPublicEnvVar("NEXT_PUBLIC_BROWSER_STACK_API_URL") ?? getPublicEnvVar("NEXT_PUBLIC_STACK_API_URL") ?? throwErr("NEXT_PUBLIC_BROWSER_STACK_API_URL is not set");
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

The fallback error message is misleading: throwErr("NEXT_PUBLIC_BROWSER_STACK_API_URL is not set") is reached when both NEXT_PUBLIC_BROWSER_STACK_API_URL and NEXT_PUBLIC_STACK_API_URL are missing. Update the message to mention both variables (or include the actual resolved env var names) so configuration issues are easier to diagnose.

Suggested change
const backendBaseUrl = getPublicEnvVar("NEXT_PUBLIC_BROWSER_STACK_API_URL") ?? getPublicEnvVar("NEXT_PUBLIC_STACK_API_URL") ?? throwErr("NEXT_PUBLIC_BROWSER_STACK_API_URL is not set");
const backendBaseUrl = getPublicEnvVar("NEXT_PUBLIC_BROWSER_STACK_API_URL") ?? getPublicEnvVar("NEXT_PUBLIC_STACK_API_URL") ?? throwErr("Neither NEXT_PUBLIC_BROWSER_STACK_API_URL nor NEXT_PUBLIC_STACK_API_URL is set");

Copilot uses AI. Check for mistakes.
const aiLoading = status === "submitted" || status === "streaming";

// Word streaming for the last assistant message
const lastAssistantMessage = messages.findLast((m: UIMessage) => m.role === "assistant");
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

Array.prototype.findLast isn't polyfilled by the app's polyfills.tsx, and the dashboard TS target is es2015, so this can throw at runtime in older browsers. Consider replacing with a backwards-compatible loop (iterate from the end) or another supported approach instead of findLast.

Suggested change
const lastAssistantMessage = messages.findLast((m: UIMessage) => m.role === "assistant");
const lastAssistantMessage = (() => {
for (let i = messages.length - 1; i >= 0; i--) {
const m = messages[i] as UIMessage;
if (m.role === "assistant") {
return m;
}
}
return undefined;
})();

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
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

Caution

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

⚠️ Outside diff range comments (2)
apps/dashboard/src/components/commands/ask-ai.tsx (2)

127-127: ⚠️ Potential issue | 🟡 Minor

handleFollowUp() returns void, not a Promise.

handleFollowUp internally calls runAsynchronously(sendMessage(...)) and returns void. Wrapping runAsynchronously(handleFollowUp()) wraps undefined, not a promise. This is a no-op.

Either remove the wrapper or make handleFollowUp return the promise:

Proposed fix (remove redundant wrapper)
       if (e.key === "Enter" && !e.shiftKey) {
         e.preventDefault();
         e.stopPropagation();
-        runAsynchronously(handleFollowUp());
+        handleFollowUp();
       } else if (e.key === "ArrowLeft") {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/components/commands/ask-ai.tsx` at line 127, The call
runAsynchronously(handleFollowUp()) is a no-op because handleFollowUp() returns
void; either remove the wrapper and call handleFollowUp() directly, or change
handleFollowUp to return the promise from runAsynchronously/sendMessage (e.g.,
have handleFollowUp return runAsynchronously(sendMessage(...)) or be async and
return the awaited promise). Update the implementation of handleFollowUp (the
function that currently calls runAsynchronously(sendMessage(...))) to return
that promise if you need to keep runAsynchronously(...) here, otherwise call
handleFollowUp() without wrapping.

239-239: ⚠️ Potential issue | 🟡 Minor

Same issue: redundant runAsynchronously wrapper.

Same as above—handleFollowUp() returns void, so runAsynchronously(handleFollowUp()) wraps undefined.

Proposed fix
-          onClick={() => runAsynchronously(handleFollowUp())}
+          onClick={handleFollowUp}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/components/commands/ask-ai.tsx` at line 239, The onClick
currently invokes handleFollowUp immediately
(runAsynchronously(handleFollowUp())), passing undefined; change it to pass the
function reference instead or call the handler directly: either use onClick={()
=> runAsynchronously(handleFollowUp)} if you intend runAsynchronously to wrap
the handler, or simply use onClick={handleFollowUp} (or onClick={() =>
handleFollowUp()} if you prefer an arrow) so handleFollowUp is executed on click
rather than at render time.
🧹 Nitpick comments (5)
apps/dashboard/src/components/commands/ai-chat-shared.tsx (3)

214-216: Add comment explaining the type assertions for SDK types.

Per coding guidelines, when using as casts, leave a comment explaining why and how errors would still be flagged. The input/output shapes depend on the tool definition on the backend.

Proposed documentation
-  // Extract query from input
-  const input = invocation.input as { query?: string } | undefined;
-  const queryArg = input?.query;
-  const result = invocation.output as { success?: boolean, result?: unknown[], error?: string, rowCount?: number } | undefined;
+  // Extract query from input
+  // Type assertion: invocation.input/output are typed as `unknown` by the AI SDK.
+  // The actual shape depends on the tool definition (sql-query tool) on the backend.
+  // Using optional chaining ensures safe access even if the shape differs.
+  const input = invocation.input as { query?: string } | undefined;
+  const queryArg = input?.query;
+  const result = invocation.output as { success?: boolean, result?: unknown[], error?: string, rowCount?: number } | undefined;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/components/commands/ai-chat-shared.tsx` around lines 214 -
216, Add an inline comment explaining the use of `as` casts on
`invocation.input` and `invocation.output` near the `input`, `queryArg`, and
`result` declarations: state that these casts are narrowing the SDK-generic
types to the expected shapes produced by the backend tool definition, note that
runtime shape mismatches will still surface as errors, and mention where the
true canonical shape is defined (the backend tool/schema). Keep the comment
succinct and colocated with the `const input = invocation.input as { query?:
string } | undefined;` and `const result = invocation.output as { success?:
boolean, result?: unknown[], error?: string, rowCount?: number } | undefined;`
lines.

52-56: Clean up timeout on unmount to avoid state update on unmounted component.

If CopyButton unmounts before the 1500ms timeout fires, React will warn about setting state on an unmounted component.

Proposed fix using useEffect cleanup
 export const CopyButton = memo(function CopyButton({ text, className, size = "sm" }: {
   text: string,
   className?: string,
   size?: "sm" | "xs",
 }) {
   const [copied, setCopied] = useState(false);
+  const timeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
+
+  useEffect(() => {
+    return () => {
+      if (timeoutRef.current) clearTimeout(timeoutRef.current);
+    };
+  }, []);

   const handleCopy = useCallback(async () => {
     await navigator.clipboard.writeText(text);
     setCopied(true);
-    setTimeout(() => setCopied(false), 1500);
+    if (timeoutRef.current) clearTimeout(timeoutRef.current);
+    timeoutRef.current = setTimeout(() => setCopied(false), 1500);
   }, [text]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/components/commands/ai-chat-shared.tsx` around lines 52 -
56, handleCopy sets a timeout that calls setCopied after 1500ms but never clears
it, causing a potential state update on an unmounted component; add a ref (e.g.,
copiedTimeoutRef) to store the timeout id, clear any existing timeout before
creating a new one inside handleCopy, and add a useEffect with a cleanup that
clears copiedTimeoutRef.current on unmount to avoid setState on unmounted
component; reference the handleCopy function, setCopied state updater, and
useCallback so reviewers can find and update the logic.

516-526: Interval continues running after all words are revealed.

The interval keeps firing every 15ms even after displayedWordCount equals targetWordCount. Consider clearing the interval when reveal completes to reduce unnecessary state updates.

Proposed fix
     const intervalId = setInterval(() => {
       setDisplayedWordCount(prev => {
         if (prev < targetWordCountRef.current) {
           return prev + 1;
         }
+        clearInterval(intervalId);
         return prev;
       });
     }, 15);

     return () => clearInterval(intervalId);
-  }, [hasContent]);
+  }, [hasContent]);

Alternatively, use a ref to track the interval ID and clear it from within the state updater callback.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/components/commands/ai-chat-shared.tsx` around lines 516 -
526, The interval created in the effect keeps running even after
displayedWordCount reaches targetWordCountRef.current; update the logic so the
interval is cleared when the reveal completes: store the interval ID
(intervalId) in a ref or accessible variable, and inside the
setDisplayedWordCount updater check if prev < targetWordCountRef.current and if
the next value will reach or exceed the target, call clearInterval(intervalId)
before returning the final value; ensure the effect cleanup still clears the
interval to cover unmounts (references: setDisplayedWordCount,
targetWordCountRef, intervalId).
apps/dashboard/src/components/commands/ask-ai.tsx (1)

69-69: Consider using findLast instead of reverse().find().

findLast is more idiomatic and avoids creating an intermediate reversed array. It's supported in ES2023+ (TypeScript 5+, Node 18+, modern browsers).

Proposed refactor
-  const lastAssistantMessage = messages.slice(1).reverse().find((m: UIMessage) => m.role === "assistant");
+  const lastAssistantMessage = messages.slice(1).findLast((m: UIMessage) => m.role === "assistant");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/components/commands/ask-ai.tsx` at line 69, The current
computation of lastAssistantMessage uses messages.slice(1).reverse().find(...)
which allocates an intermediate reversed array; replace it with the ES2023
String.prototype-like array method by calling findLast on the sliced array
(e.g., messages.slice(1).findLast(m => m.role === "assistant")) to avoid
reversing and reduce allocation; ensure the runtime/TS target supports
Array.prototype.findLast or add a polyfill if needed and update the reference to
the const lastAssistantMessage accordingly in ask-ai.tsx.
apps/dashboard/src/components/stack-companion/ai-chat-widget.tsx (1)

77-77: Same as ask-ai.tsx: prefer findLast over reverse().find().

For consistency with the suggested refactor in ask-ai.tsx:

Proposed refactor
-  const lastAssistantMessage = messages.slice().reverse().find((m: UIMessage) => m.role === "assistant");
+  const lastAssistantMessage = messages.findLast((m: UIMessage) => m.role === "assistant");

Note: Unlike ask-ai.tsx, this doesn't skip the first message, so no .slice(1) needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/components/stack-companion/ai-chat-widget.tsx` at line 77,
Replace the messages.slice().reverse().find(...) pattern used to compute
lastAssistantMessage with Array.prototype.findLast for consistency and clarity:
locate the expression assigning lastAssistantMessage in ai-chat-widget.tsx (the
variable computed from messages and UIMessage) and change it to use
messages.findLast((m: UIMessage) => m.role === "assistant") so it directly
returns the last assistant message without creating a reversed copy.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/dashboard/src/components/stack-companion/ai-chat-widget.tsx`:
- Around line 19-24: The component AIChatWidget calls React hooks (useState)
before returning early when isActive is false, violating the Rules of Hooks; fix
by moving the early return to the top of AIChatWidget before any hooks or by
refactoring stateful logic into an inner component (e.g., create
AIChatWidgetInner that holds the useState calls and onNewConversation reset
logic) and have AIChatWidget conditionally render that inner component, or
alternatively remove the isActive prop entirely and let the parent unmount
AIChatWidget (stack-companion.tsx already conditionally renders it) so hooks are
only used when mounted.

---

Outside diff comments:
In `@apps/dashboard/src/components/commands/ask-ai.tsx`:
- Line 127: The call runAsynchronously(handleFollowUp()) is a no-op because
handleFollowUp() returns void; either remove the wrapper and call
handleFollowUp() directly, or change handleFollowUp to return the promise from
runAsynchronously/sendMessage (e.g., have handleFollowUp return
runAsynchronously(sendMessage(...)) or be async and return the awaited promise).
Update the implementation of handleFollowUp (the function that currently calls
runAsynchronously(sendMessage(...))) to return that promise if you need to keep
runAsynchronously(...) here, otherwise call handleFollowUp() without wrapping.
- Line 239: The onClick currently invokes handleFollowUp immediately
(runAsynchronously(handleFollowUp())), passing undefined; change it to pass the
function reference instead or call the handler directly: either use onClick={()
=> runAsynchronously(handleFollowUp)} if you intend runAsynchronously to wrap
the handler, or simply use onClick={handleFollowUp} (or onClick={() =>
handleFollowUp()} if you prefer an arrow) so handleFollowUp is executed on click
rather than at render time.

---

Nitpick comments:
In `@apps/dashboard/src/components/commands/ai-chat-shared.tsx`:
- Around line 214-216: Add an inline comment explaining the use of `as` casts on
`invocation.input` and `invocation.output` near the `input`, `queryArg`, and
`result` declarations: state that these casts are narrowing the SDK-generic
types to the expected shapes produced by the backend tool definition, note that
runtime shape mismatches will still surface as errors, and mention where the
true canonical shape is defined (the backend tool/schema). Keep the comment
succinct and colocated with the `const input = invocation.input as { query?:
string } | undefined;` and `const result = invocation.output as { success?:
boolean, result?: unknown[], error?: string, rowCount?: number } | undefined;`
lines.
- Around line 52-56: handleCopy sets a timeout that calls setCopied after 1500ms
but never clears it, causing a potential state update on an unmounted component;
add a ref (e.g., copiedTimeoutRef) to store the timeout id, clear any existing
timeout before creating a new one inside handleCopy, and add a useEffect with a
cleanup that clears copiedTimeoutRef.current on unmount to avoid setState on
unmounted component; reference the handleCopy function, setCopied state updater,
and useCallback so reviewers can find and update the logic.
- Around line 516-526: The interval created in the effect keeps running even
after displayedWordCount reaches targetWordCountRef.current; update the logic so
the interval is cleared when the reveal completes: store the interval ID
(intervalId) in a ref or accessible variable, and inside the
setDisplayedWordCount updater check if prev < targetWordCountRef.current and if
the next value will reach or exceed the target, call clearInterval(intervalId)
before returning the final value; ensure the effect cleanup still clears the
interval to cover unmounts (references: setDisplayedWordCount,
targetWordCountRef, intervalId).

In `@apps/dashboard/src/components/commands/ask-ai.tsx`:
- Line 69: The current computation of lastAssistantMessage uses
messages.slice(1).reverse().find(...) which allocates an intermediate reversed
array; replace it with the ES2023 String.prototype-like array method by calling
findLast on the sliced array (e.g., messages.slice(1).findLast(m => m.role ===
"assistant")) to avoid reversing and reduce allocation; ensure the runtime/TS
target supports Array.prototype.findLast or add a polyfill if needed and update
the reference to the const lastAssistantMessage accordingly in ask-ai.tsx.

In `@apps/dashboard/src/components/stack-companion/ai-chat-widget.tsx`:
- Line 77: Replace the messages.slice().reverse().find(...) pattern used to
compute lastAssistantMessage with Array.prototype.findLast for consistency and
clarity: locate the expression assigning lastAssistantMessage in
ai-chat-widget.tsx (the variable computed from messages and UIMessage) and
change it to use messages.findLast((m: UIMessage) => m.role === "assistant") so
it directly returns the last assistant message without creating a reversed copy.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6b013c2d-cf9b-4b12-bdf3-35a79bad649c

📥 Commits

Reviewing files that changed from the base of the PR and between f3e27f8 and 53ac1ff.

📒 Files selected for processing (3)
  • apps/dashboard/src/components/commands/ai-chat-shared.tsx
  • apps/dashboard/src/components/commands/ask-ai.tsx
  • apps/dashboard/src/components/stack-companion/ai-chat-widget.tsx

Comment on lines +19 to +24
export function AIChatWidget({ isActive }: { isActive: boolean }) {
const [input, setInput] = useState("");
const [conversationStarted, setConversationStarted] = useState(false);
const [conversationKey, setConversationKey] = useState(0);

if (!isActive) return null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

React hooks called before conditional return violates Rules of Hooks.

useState is called on lines 20-22, but line 24 returns null conditionally based on isActive. React requires hooks to be called in the same order on every render. This will cause runtime errors when isActive changes.

Move the early return before any hooks, or move state to the inner component:

Proposed fix: move state into AIChatWidgetInner
 export function AIChatWidget({ isActive }: { isActive: boolean }) {
-  const [input, setInput] = useState("");
-  const [conversationStarted, setConversationStarted] = useState(false);
-  const [conversationKey, setConversationKey] = useState(0);
-
   if (!isActive) return null;

   return (
     <AIChatWidgetInner
-      key={conversationKey}
-      input={input}
-      setInput={setInput}
-      conversationStarted={conversationStarted}
-      setConversationStarted={setConversationStarted}
-      onNewConversation={() => {
-        setConversationKey(prev => prev + 1);
-        setConversationStarted(false);
-        setInput("");
-      }}
+      key="ai-chat"
     />
   );
 }

 function AIChatWidgetInner({
-  input,
-  setInput,
-  conversationStarted,
-  setConversationStarted,
-  onNewConversation,
-}: {
-  input: string,
-  setInput: (v: string) => void,
-  conversationStarted: boolean,
-  setConversationStarted: (v: boolean) => void,
-  onNewConversation: () => void,
-}) {
+}: Record<string, never>) {
+  const [input, setInput] = useState("");
+  const [conversationStarted, setConversationStarted] = useState(false);
   const [followUpInput, setFollowUpInput] = useState("");

Then update onNewConversation to reset state locally. Alternatively, lift the conditional rendering to the parent (stack-companion.tsx already does {activeItem === 'ask-ai' && <AIChatWidget isActive={true} />}), so the component unmounts when inactive—making the isActive prop and early return unnecessary.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/components/stack-companion/ai-chat-widget.tsx` around
lines 19 - 24, The component AIChatWidget calls React hooks (useState) before
returning early when isActive is false, violating the Rules of Hooks; fix by
moving the early return to the top of AIChatWidget before any hooks or by
refactoring stateful logic into an inner component (e.g., create
AIChatWidgetInner that holds the useState calls and onNewConversation reset
logic) and have AIChatWidget conditionally render that inner component, or
alternatively remove the isActive prop entirely and let the parent unmount
AIChatWidget (stack-companion.tsx already conditionally renders it) so hooks are
only used when mounted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants