Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a shared AI chat utilities module, refactors ask-ai to use it, adds an Ask AI widget to the companion sidebar with streaming and word-by-word reveal, and prevents event bubbling from list button wrappers. (27 words) Changes
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 (systemPrompt, tools, projectId, auth)
useChat->>API: open streaming request
API-->>useChat: stream response chunks (assistant parts + tool invocations)
useChat->>AIChatWidget: emit assistant message parts
AIChatWidget->>useWordStreaming: supply assistant content for reveal
useWordStreaming-->>UI: incremental (word-by-word) display updates
UI->>AIChatWidget: render messages & tool cards, update scroll
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ❌ 3❌ Failed checks (1 warning, 2 inconclusive)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 |
Greptile SummaryThis PR adds an "Ask AI" panel to the Stack Companion sidebar by introducing a new Key changes:
Confidence Score: 5/5Safe 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
Sequence DiagramsequenceDiagram
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
|
There was a problem hiding this comment.
🧹 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 throughunknownbypasses type safety.The
as unknown as ToolInvocationPartcast assumes the filtered parts match theToolInvocationPartstructure. While thestartsWith("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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (5)
apps/dashboard/src/components/commands/ai-chat-shared.tsxapps/dashboard/src/components/commands/ask-ai.tsxapps/dashboard/src/components/design-components/list.tsxapps/dashboard/src/components/stack-companion.tsxapps/dashboard/src/components/stack-companion/ai-chat-widget.tsx
There was a problem hiding this comment.
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.tsxand wiresask-aito 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.
There was a problem hiding this comment.
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()returnsvoid, not aPromise.
handleFollowUpinternally callsrunAsynchronously(sendMessage(...))and returnsvoid. WrappingrunAsynchronously(handleFollowUp())wrapsundefined, not a promise. This is a no-op.Either remove the wrapper or make
handleFollowUpreturn 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 | 🟡 MinorSame issue: redundant
runAsynchronouslywrapper.Same as above—
handleFollowUp()returnsvoid, sorunAsynchronously(handleFollowUp())wrapsundefined.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
ascasts, 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
CopyButtonunmounts 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
displayedWordCountequalstargetWordCount. 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 usingfindLastinstead ofreverse().find().
findLastis 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 asask-ai.tsx: preferfindLastoverreverse().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
📒 Files selected for processing (3)
apps/dashboard/src/components/commands/ai-chat-shared.tsxapps/dashboard/src/components/commands/ask-ai.tsxapps/dashboard/src/components/stack-companion/ai-chat-widget.tsx
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
apps/dashboard/src/components/commands/ai-chat-shared.tsx (1)
20-20: Clarify the error message to reflect the actual fallback logic.The error message states "NEXT_PUBLIC_BROWSER_STACK_API_URL is not set" but the code actually checks both
NEXT_PUBLIC_BROWSER_STACK_API_URLandNEXT_PUBLIC_STACK_API_URLas fallbacks. This could mislead developers during debugging.💡 Suggested improvement
- 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");🤖 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` at line 20, The backendBaseUrl assignment uses getPublicEnvVar for NEXT_PUBLIC_BROWSER_STACK_API_URL then NEXT_PUBLIC_STACK_API_URL but the throwErr message only mentions NEXT_PUBLIC_BROWSER_STACK_API_URL; update the error text in the fallback throwErr call (used with backendBaseUrl) to accurately list both env vars (e.g., "NEXT_PUBLIC_BROWSER_STACK_API_URL or NEXT_PUBLIC_STACK_API_URL is not set") so logs reflect the actual fallback logic involving getPublicEnvVar and throwErr.
🤖 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/commands/ai-chat-shared.tsx`:
- Around line 214-216: The code unsafely casts external AI invocation data using
"as" for invocation.input and invocation.output (variables input, queryArg,
result), which can cause runtime errors if the shape differs; replace these
casts with defensive runtime validation or safe access: check invocation?.input
and invocation?.output exist, use optional chaining (e.g.,
invocation?.input?.query) and validate types (e.g., typeof query === 'string',
Array.isArray for result.result, typeof result.error === 'string', typeof
result.success === 'boolean') before using values, or call a small
validator/guard function to assert the shape; if you truly rely on a backend
contract, add a clear comment near invocation stating the trust boundary and why
the cast is safe.
---
Nitpick comments:
In `@apps/dashboard/src/components/commands/ai-chat-shared.tsx`:
- Line 20: The backendBaseUrl assignment uses getPublicEnvVar for
NEXT_PUBLIC_BROWSER_STACK_API_URL then NEXT_PUBLIC_STACK_API_URL but the
throwErr message only mentions NEXT_PUBLIC_BROWSER_STACK_API_URL; update the
error text in the fallback throwErr call (used with backendBaseUrl) to
accurately list both env vars (e.g., "NEXT_PUBLIC_BROWSER_STACK_API_URL or
NEXT_PUBLIC_STACK_API_URL is not set") so logs reflect the actual fallback logic
involving getPublicEnvVar and throwErr.
🪄 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: 6efb31bc-56a0-4668-bf1b-c804a7c71a38
📒 Files selected for processing (1)
apps/dashboard/src/components/commands/ai-chat-shared.tsx
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
apps/dashboard/src/components/commands/ai-chat-shared.tsx (1)
213-215:⚠️ Potential issue | 🟠 MajorStop casting AI tool payloads to the happy path.
invocation.inputandinvocation.outputare external tool payloads. A non-stringqueryor unexpectedoutputshape can break this card when React renders it or whenCopyButtontries to copy it.🛡️ Defensive extraction sketch
- 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; + const queryArg = + typeof invocation.input === "object" && + invocation.input !== null && + "query" in invocation.input && + typeof invocation.input.query === "string" + ? invocation.input.query + : undefined; + + const result = + typeof invocation.output === "object" && invocation.output !== null + ? invocation.output + : undefined;As per coding guidelines, "Do NOT use
as/any/type casts or anything else to bypass the type system unless you specifically asked the user about it."🤖 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 213 - 215, The code currently force-casts invocation.input and invocation.output to assumed shapes (variables input, queryArg, result) which can break rendering or copying when external tool payloads differ; replace these casts with defensive extraction: read invocation.input and invocation.output as unknown, check and validate types at runtime (e.g., ensure input is an object and typeof input.query === "string" before using it, and ensure output is an object and its fields (success, result, error, rowCount) have expected primitive/array types), provide safe fallbacks (empty string, [] or undefined) and only pass validated values to the UI/CopyButton; update the code around variables input/queryArg/result (and any components that consume them) to use these validated values instead of type assertions.
🧹 Nitpick comments (1)
apps/dashboard/src/components/commands/ai-chat-shared.tsx (1)
20-22: UseurlStringfor the transport endpoint.This still builds the API URL with raw interpolation. It relies on the env value already having the right slash shape and misses the repo’s standard URL helper.
As per coding guidelines, "Use
urlStringorencodeURIComponent()instead of normal string interpolation for URLs for consistency."🤖 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 20 - 22, The transport endpoint is built via raw string interpolation; replace it with the repo URL helper by passing the base env + path to urlString so slashes are normalized. Keep using getPublicEnvVar and the existing backendBaseUrl/throwErr logic, then set the DefaultChatTransport api value using urlString (e.g., urlString({ base: backendBaseUrl, path: '/api/latest/ai/query/stream' })) instead of `${backendBaseUrl}/api/latest/ai/query/stream` so the URL helper handles encoding and slash normalization.
🤖 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/commands/ai-chat-shared.tsx`:
- Around line 507-525: The interval in the useEffect (using hasContent,
setInterval, clearInterval) never stops because the effect only depends on
hasContent; update the effect to depend on content (or hasContent) and
displayedWordCount (and read targetWordCountRef.current inside), and inside the
interval callback or in the effect body clear the interval as soon as
displayedWordCount >= targetWordCountRef.current so it stops waking the thread;
also ensure that when content changes you reset displayedWordCount
(setDisplayedWordCount(0)) and set up a fresh interval tied to the new
content/targetWordCountRef so the reveal restarts cleanly for new assistant
messages.
In `@apps/dashboard/src/components/stack-companion/ai-chat-widget.tsx`:
- Around line 197-209: The send buttons render only an SVG (e.g., the button
that calls handleSubmit and contains PaperPlaneTiltIcon), so screen readers have
no label; add an accessible name by giving the button an aria-label (e.g.,
aria-label="Send message") or include visually-hidden text (e.g., a span with
class "sr-only" containing "Send message") inside the button; apply the same
change to the other icon-only send button instance (the second occurrence around
the other PaperPlaneTiltIcon) so both are accessible to assistive tech.
- Around line 123-130: handleInputKeyDown and handleFollowUpKeyDown currently
submit on Enter even during IME composition; add a guard that checks
e.nativeEvent.isComposing (or e.nativeEvent && e.nativeEvent.isComposing) and
return early if true before handling Enter so compositions (CJK) aren’t
submitted prematurely; update both functions (handleInputKeyDown and
handleFollowUpKeyDown) to perform this composition check at the top of the
handler and keep the existing Enter/shift logic unchanged.
---
Duplicate comments:
In `@apps/dashboard/src/components/commands/ai-chat-shared.tsx`:
- Around line 213-215: The code currently force-casts invocation.input and
invocation.output to assumed shapes (variables input, queryArg, result) which
can break rendering or copying when external tool payloads differ; replace these
casts with defensive extraction: read invocation.input and invocation.output as
unknown, check and validate types at runtime (e.g., ensure input is an object
and typeof input.query === "string" before using it, and ensure output is an
object and its fields (success, result, error, rowCount) have expected
primitive/array types), provide safe fallbacks (empty string, [] or undefined)
and only pass validated values to the UI/CopyButton; update the code around
variables input/queryArg/result (and any components that consume them) to use
these validated values instead of type assertions.
---
Nitpick comments:
In `@apps/dashboard/src/components/commands/ai-chat-shared.tsx`:
- Around line 20-22: The transport endpoint is built via raw string
interpolation; replace it with the repo URL helper by passing the base env +
path to urlString so slashes are normalized. Keep using getPublicEnvVar and the
existing backendBaseUrl/throwErr logic, then set the DefaultChatTransport api
value using urlString (e.g., urlString({ base: backendBaseUrl, path:
'/api/latest/ai/query/stream' })) instead of
`${backendBaseUrl}/api/latest/ai/query/stream` so the URL helper handles
encoding and slash normalization.
🪄 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: fc2e0636-d206-47cc-a428-ba1b304caea9
📒 Files selected for processing (3)
apps/dashboard/src/components/commands/ai-chat-shared.tsxapps/dashboard/src/components/stack-companion.tsxapps/dashboard/src/components/stack-companion/ai-chat-widget.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/dashboard/src/components/stack-companion.tsx
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
apps/dashboard/src/components/commands/ai-chat-shared.tsx (1)
213-215:⚠️ Potential issue | 🟡 MinorAvoid raw casts on streamed tool payloads.
invocation.inputandinvocation.outputare the trust boundary for streamed tool data. Theseascasts drop type safety right where the UI branches onquery,success, androwCount. Please narrow the shape or add a short comment documenting the backend contract if this payload is guaranteed.As per coding guidelines, "Do NOT use
as/any/type casts or anything else to bypass the type system unless you specifically asked the user about it."🤖 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 213 - 215, The code currently unsafely casts invocation.input and invocation.output with "as", which bypasses the type system; replace these raw casts by defining a narrow runtime-validated shape (e.g., ToolInvocationInput { query?: string } and ToolInvocationOutput { success?: boolean; result?: unknown[]; error?: string; rowCount?: number }) and either perform a small type-guard function (isToolInvocationInput/isToolInvocationOutput) that checks typeof/Array.isArray for the fields before using them or access fields defensively (optional chaining + typeof checks) when reading query, success, and rowCount; update uses of invocation.input, queryArg, invocation.output, and result to use the guard/validated value and add a short comment documenting the backend contract if the validation can be guaranteed.
🤖 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/commands/ai-chat-shared.tsx`:
- Around line 61-79: The icon-only copy button lacks an accessible name; update
the button element (the CopyButton usage in ai-chat-shared.tsx that calls
runAsynchronously(handleCopy()), uses the copied state, and title prop) to
include an explicit aria-label (e.g., aria-label={copied ? "Copied" : "Copy"})
so screen readers get a reliable label even when title is ignored; ensure the
aria-label mirrors the existing title logic and remains in sync with the copied
state.
In `@apps/dashboard/src/components/stack-companion/ai-chat-widget.tsx`:
- Around line 187-198: The input currently uses only placeholder text (the JSX
input with ref={inputRef}, value={input}, onChange and
onKeyDown={handleInputKeyDown}) which is not accessible; add an accessible name
by either adding a visible <label> tied to that input via htmlFor/id or by
adding an appropriate aria-label (e.g., aria-label="Initial prompt" or
aria-label="Follow-up question") for both the initial prompt input and the
follow-up input referenced in the same file (the other input around the block at
lines 298-309), ensuring unique id attributes if you use labels and using the
same inputRef/handleInputKeyDown handlers unchanged.
---
Duplicate comments:
In `@apps/dashboard/src/components/commands/ai-chat-shared.tsx`:
- Around line 213-215: The code currently unsafely casts invocation.input and
invocation.output with "as", which bypasses the type system; replace these raw
casts by defining a narrow runtime-validated shape (e.g., ToolInvocationInput {
query?: string } and ToolInvocationOutput { success?: boolean; result?:
unknown[]; error?: string; rowCount?: number }) and either perform a small
type-guard function (isToolInvocationInput/isToolInvocationOutput) that checks
typeof/Array.isArray for the fields before using them or access fields
defensively (optional chaining + typeof checks) when reading query, success, and
rowCount; update uses of invocation.input, queryArg, invocation.output, and
result to use the guard/validated value and add a short comment documenting the
backend contract if the validation can be guaranteed.
🪄 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: 04cb44af-9bfc-42b1-bc9a-6cf8cad32205
📒 Files selected for processing (3)
apps/dashboard/src/components/commands/ai-chat-shared.tsxapps/dashboard/src/components/commands/ask-ai.tsxapps/dashboard/src/components/stack-companion/ai-chat-widget.tsx
Merging with AI in stack companion <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Added AI Chat functionality with conversation management capabilities * New "Ask AI" sidebar option for quick access to AI assistance * Ability to create, browse, and delete AI conversations * Rich chat UI with support for code blocks, links, and tool invocations * Word-by-word message streaming for better readability * **Bug Fixes** * Fixed event propagation issue in list item action buttons to prevent unintended triggering <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…/stack-auth into ai-in-stack-companion
This PR puts the ask ai functionality into the ai stack companion, along with persistent history.
Summary by CodeRabbit
New Features
Bug Fixes
Refactor