10BC0 feat: ai chatbot by pooja-bruno · Pull Request #6907 · usebruno/bruno · GitHub
[go: up one dir, main page]

Skip to content

Conversation

@pooja-bruno
Copy link
Collaborator
@pooja-bruno pooja-bruno commented Jan 23, 2026

Description

Contribution Checklist:

  • I've used AI significantly to create this pull request
  • The pull request only addresses one issue or adds one feature.
  • The pull request does not introduce any breaking changes
  • I have added screenshots or gifs to help explain the change if applicable.
  • I have read the contribution guidelines.
  • Create an issue and link to the pull request.

Note: Keeping the PR small and focused helps make it easier to review and merge. If you have multiple changes you want to make, please consider submitting them as separate pull requests.

Publishing to New Package Managers

Please see here for more information.

Summary by CodeRabbit

  • New Features
    • Added AI Assistant Panel for generating and improving request scripts, response handlers, and tests
    • Multi-file diff viewer for reviewing and selectively applying AI-suggested changes
    • Multiple AI interaction modes: ask, auto-accept, and ask-before-edit
    • File selector for managing multi-file AI operations with syntax highlighting
    • Keyboard shortcut to toggle AI panel visibility

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor
coderabbitai bot commented Jan 23, 2026

Walkthrough

This PR introduces a comprehensive AI Assistant panel to Bruno, featuring UI components for chat-based code generation, file selection, multi-file diff review, streaming responses, and Redux-backed state management. It includes Electron backend integration with OpenAI API support via the AI SDK, IPC handlers for streaming AI responses, and hotkey support for panel toggling.

Changes

Cohort / File(s) Summary
AI Assistant Panel - Components
packages/bruno-app/src/components/AIAssistantPanel/StyledWrapper.js, packages/bruno-app/src/components/AIAssistantPanel/index.js
Main AI panel container with extensive theming, resizable sidebar, header, scrollable content area, chat messages, loading states, and input section with file attachments and send controls.
AI Assistant Panel - Chat
packages/bruno-app/src/components/AIAssistantPanel/ChatMessage/StyledWrapper.js, packages/bruno-app/src/components/AIAssistantPanel/ChatMessage/index.js
Chat message rendering with markdown support, sanitization via DOMPurify, and styling for user, assistant, error, success, info, and warning message variants.
AI Assistant Panel - Code Block
packages/bruno-app/src/components/AIAssistantPanel/CodeBlock/StyledWrapper.js, packages/bruno-app/src/components/AIAssistantPanel/CodeBlock/index.js
Syntax-highlighted code rendering with custom JavaScript tokenizer, line numbers, copy-to-clipboard with visual feedback.
AI Assistant Panel - Diff Viewing
packages/bruno-app/src/components/AIAssistantPanel/DiffView/StyledWrapper.js, packages/bruno-app/src/components/AIAssistantPanel/DiffView/index.js, packages/bruno-app/src/components/AIAssistantPanel/MultiFileDiffView/StyledWrapper.js, packages/bruno-app/src/components/AIAssistantPanel/MultiFileDiffView/index.js
Line-by-line and multi-file diff visualization with per-file status tracking, accept/reject controls, and completion messaging.
AI Assistant Panel - File & Mode Selection
packages/bruno-app/src/components/AIAssistantPanel/FileSelector/StyledWrapper.js, packages/bruno-app/src/components/AIAssistantPanel/FileSelector/index.js, packages/bruno-app/src/components/AIAssistantPanel/ModeSelector/StyledWrapper.js, packages/bruno-app/src/components/AIAssistantPanel/ModeSelector/index.js
File multi-select with debounced search and request filtering; AI mode dropdown (ask, auto-accept, ask-before-edit).
Redux State Management
packages/bruno-app/src/providers/ReduxStore/slices/app.js
New AI panel state (showAIPanel, aiPanelContext, aiPanelWidth, aiMode) and multi-file selection state; 13 new reducer actions for panel lifecycle and file selection.
Component Integration
packages/bruno-app/src/components/RequestPane/Script/index.js, packages/bruno-app/src/components/RequestTabPanel/index.js, packages/bruno-app/src/components/ResponsePane/ResponseLayoutToggle/index.js
AI button in Script tab, context sync on tab changes; layout logic to conditionally hide response pane when AI panel is open; disable response layout toggle when AI panel active.
Page & Layout
packages/bruno-app/src/pages/Bruno/index.js, packages/bruno-app/src/pages/Bruno/StyledWrapper.js
Render AIAssistantPanel at top level; adjust wrapper min-width constraint and add z-index/height styling.
Hotkeys
packages/bruno-app/src/providers/Hotkeys/index.js, packages/bruno-app/src/providers/Hotkeys/keyMappings.js
Add AI panel toggle hotkey binding (Cmd+Shift+A / Ctrl+Shift+A).
Dependencies
packages/bruno-app/package.json
Add diff@^5.2.0 for line-by-line diffing.
Electron Backend - Dependencies
packages/bruno-electron/package.json
Add @ai-sdk/openai@^1.0.0 and ai@^4.0.0 for AI SDK integration.
Electron Backend - IPC & AI
packages/bruno-electron/src/index.js, packages/bruno-electron/src/ipc/ai.js
Register AI IPC handler; implement ai:generate request handling with streaming response via ai:stream-chunk and ai:stream-end events.
Electron Backend - Providers & Prompts
packages/bruno-electron/src/ai/providers/index.js, packages/bruno-electron/src/ai/prompts/index.js
AI provider factory (OpenAI); four prompt builders for test generation, script improvement, chat, and multi-file chat with Bruno API context.

Sequence Diagram

sequenceDiagram
    participant User
    participant Renderer as Renderer Process<br/>(AIAssistantPanel)
    participant IPC as IPC Bridge
    participant Backend as Main Process<br/>(AI Handler)
    participant AI as OpenAI API
    
    User->>Renderer: Types message & sends
    Renderer->>Renderer: Build prompt with context
    Renderer->>IPC: ai:generate (action, context)
    IPC->>Backend: Receive request
    Backend->>Backend: Validate & prepare prompt
    Backend->>AI: streamText() with prompt
    AI-->>Backend: Stream chunk
    Backend->>IPC: ai:stream-chunk (content)
    IPC->>Renderer: Receive chunk
    Renderer->>Renderer: Accumulate & display
    AI-->>Backend: Stream end
    Backend->>Backend: Infer responseType (text/code/multi-file)
    Backend->>IPC: ai:stream-end (type, full content)
    IPC->>Renderer: Receive end event
    Renderer->>Renderer: Parse & prepare diff or message
    Renderer->>User: Display result (chat/diff/review)
    
    alt Multi-file mode
        Renderer->>User: Show per-file accept/reject UI
        User->>Renderer: Accept/reject files
        Renderer->>Renderer: Apply changes via Redux
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • PR #6786: Modifies Redux app slice preferences/state management alongside this PR's AI state additions.
  • PR #6388: Changes RequestTabPanel and ResponsePane layout logic that this PR also modifies for AI panel integration.
  • PR #6382: Adds ModeSelector component that depends on MenuDropdown component which may have concurrent API changes.

Suggested reviewers

  • lohit-bruno
  • naman-bruno
  • bijin-bruno

🤖 ✨ The AI assistant arrives with streaming flair,
Diffs in hand and prompts laid bare,
Code generation flows through IPC wire,
Multi-file reviews lift spirits higher! 🚀

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ 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%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'feat: ai chatbot' is vague and does not clearly convey the specific primary change. The PR actually implements a comprehensive AI assistant panel with multi-file diffing, file selection, code execution modes, and IPC streaming—far broader than just a chatbot. Consider a more descriptive title such as 'feat: add AI assistant panel with multi-file diff review' or 'feat: integrate AI-powered code generation with streaming support' to better reflect the extensive scope and primary functionality.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

@pooja-bruno pooja-bruno marked this pull request as draft January 23, 2026 09:39
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: 15

🤖 Fix all issues with AI agents
In `@packages/bruno-app/src/components/AIAssistantPanel/ChatMessage/index.js`:
- Around line 23-33: The current regex line.match(/^(`{3,})(\w*)?$/) in the
code-fence detection only captures word characters for the info string and fails
for languages like "c++" or "objective-c"; update the match to allow any
non-empty info string (e.g. use a second capture like (.*) or (\S.*)?), then
assign codeBlockLang = (captureGroup2 || '').trim() so you accept and trim
arbitrary info strings; ensure the logic that compares closing backticks still
uses openingBackticks unchanged and that codeBlockLang handling in the
surrounding code (variables tripleBacktickMatch, inCodeBlock, openingBackticks,
codeBlockContent) uses the trimmed value.
- Around line 129-131: Add a targeted Biome ignore comment immediately above the
div using dangerouslySetInnerHTML to suppress the specific lint rule that flags
direct HTML insertion (while keeping the existing DOMPurify sanitization).
Locate the JSX element with className "markdown-content" that sets
dangerouslySetInnerHTML with sanitizedHtml and insert a single-line or block
Biome ignore for the specific lint rule (e.g., the react/no-danger-equivalent
rule) so only this occurrence is suppressed rather than disabling linting
broadly.

In `@packages/bruno-app/src/components/AIAssistantPanel/CodeBlock/index.js`:
- Around line 148-162: The CodeBlock component currently calls code.split('\n')
which will throw if code is null/undefined; update the component to guard the
input by defaulting code to an empty string (e.g., change the prop default or
create a local safeCode = code ?? ''), use safeCode for splitting and for
navigator.clipboard.writeText in handleCopy, and ensure any references (lines,
handleCopy) use that safe value so runtime errors are avoided.

In `@packages/bruno-app/src/components/AIAssistantPanel/FileSelector/index.js`:
- Around line 112-120: The IconX clear control uses onKeyDown handling that only
triggers on Enter; update the keyboard handler for the IconX instance(s) (the
one using onClick={handleClearSearch} and the similar control at the second
occurrence) to also respond to Spacebar by checking for e.key === 'Enter' ||
e.key === ' ' || e.key === 'Spacebar' (and call e.preventDefault() before
invoking handleClearSearch), ensuring the role="button" + tabIndex remains;
apply the same change to the other occurrence (lines near the second IconX block
referenced) so both keyboard users can toggle via Space as well as Enter.
- Around line 1-47: The debouncedSetSearch created with useMemo can fire after
clearing or after unmount; call debouncedSetSearch.cancel() inside
handleClearSearch to avoid re-applying the old search text, and add a useEffect
cleanup that calls debouncedSetSearch.cancel() on unmount to prevent state
updates after unmount. Update references to debouncedSetSearch (from useMemo)
and handleClearSearch accordingly so the debounce is cancelled both when
clearing and when the component unmounts.

In
`@packages/bruno-app/src/components/AIAssistantPanel/FileSelector/StyledWrapper.js`:
- Around line 86-97: The checkmark color in StyledWrapper.js is hard-coded to
'white' inside the .checked &::after rule; replace that literal with the theme
token (e.g., use (props) => props.theme.textWhite or your theme's equivalent
token) so the border color uses the theme's white text color instead of the
literal 'white' (update the border: solid white to border: solid ${(props) =>
props.theme.<textWhiteToken>} in the .checked ::after rule).

In `@packages/bruno-app/src/components/AIAssistantPanel/index.js`:
- Around line 181-198: The useEffect that conditionally clears and adds the
current item reads selectedFilesForAI but doesn't include it in the dependency
array, causing stale-checks; update the effect's dependencies to include
selectedFilesForAI (along with item, collection, dispatch) so the
isAlreadySelected check uses fresh state, ensuring clearAIFileSelection() and
addFileToAISelection(...) logic runs correctly when selectedFilesForAI changes.
- Around line 651-656: handleStopGeneration currently only clears frontend state
(currentStreamId, status, messages, resetStreamState) but doesn't abort the
backend stream; modify handleStopGeneration to also send an IPC "ai:cancel"
message with the active currentStreamId (and guard if null) before clearing
state, and update its dependency array to include currentStreamId and the ipc
sender (e.g., ipcRenderer). On the backend, implement an "ai:cancel" IPC handler
that looks up the stored AbortController for the given stream id used by the
"ai:generate" handler and calls controller.abort(), and ensure "ai:generate"
stores its AbortController keyed by the stream id so cancellation will stop the
server-side streaming/token consumption.

In `@packages/bruno-app/src/components/AIAssistantPanel/ModeSelector/index.js`:
- Around line 27-48: The dropdown's selectedItemId uses the possibly-invalid
currentMode while the displayed label falls back to AI_MODES[2]; update
ModeSelector to use the resolved id from currentModeConfig so the highlighted
item matches the fallback label—i.e., compute currentModeConfig (as already
done) and pass currentModeConfig.id (or the fallback id) to the MenuDropdown
selectedItemId prop instead of currentMode.

In
`@packages/bruno-app/src/components/AIAssistantPanel/MultiFileDiffView/index.js`:
- Around line 1-16: MultiFileDiffView can leave activeIndex out of bounds when
the changes array shrinks; add a useEffect that watches the changes array (the
changes reference itself) and clamps or resets activeIndex via setActiveIndex:
if the current activeIndex >= changes.length or the active change is no longer
valid, set activeIndex to the index of the first pending change (from
pendingChanges) or 0; ensure the effect references activeIndex, changes and
pendingChanges and only updates state when a change is needed to avoid extra
renders.

In `@packages/bruno-app/src/components/RequestPane/Script/index.js`:
- Around line 102-112: The effect that updates AI panel context (useEffect)
references activeTab and calls getCurrentScript(activeTab) but does not include
activeTab in the dependency array; update the dependency array for that
useEffect to include activeTab so the effect re-runs when activeTab changes
(retain showAIPanel, item, collection, dispatch and any other referenced
variables like getCurrentScript if it's not stable).

In `@packages/bruno-app/src/components/RequestTabPanel/index.js`:
- Around line 56-61: The hooks (useSelector calls for forceVerticalLayoutForAI,
isConsoleOpen, showAIPanel, responsePaneHiddenByAI and the computed
isVerticalLayout) are being invoked after an early return guarded by if (typeof
window === 'undefined'), violating Rules of Hooks; fix by extracting the
hook-using logic into a new child component (e.g., InnerRequestTabPanel) which
contains all useSelector calls and the isVerticalLayout computation, and have
the original component conditionally render that child (or null) based on typeof
window === 'undefined' so hooks always run at the top level of the child
component.
- Around line 360-375: When shouldHideResponsePane is true the request pane
keeps split dimensions from requestPaneStyle and leaves empty space; update the
layout logic that computes/assigns requestPaneStyle so that when
shouldHideResponsePane === true you remove/override the fixed split size and let
the request pane flex to fill available area (e.g. set style to
flex-grow/height:100% in vertical or width:100% in horizontal). Locate where
requestPaneStyle is computed/used in RequestTabPanel (and keep existing symbols
like shouldHideResponsePane, requestPaneStyle, resetPaneBoundaries,
handleDragbarMouseDown, renderResponsePane) and apply the override only when the
response pane is hidden so the dragbar/drag handlers still work when visible.

In `@packages/bruno-electron/src/ai/prompts/index.js`:
- Around line 145-155: Add a redaction helper and apply it before interpolating
request/response data into prompts: implement a function (e.g., redactForAi)
that clones the passed object and replaces sensitive header fields
(authorization, cookie, set-cookie, x-api-key, etc.) and any obvious PII in
bodies with a placeholder like "[REDACTED]"; then call redactForAi(request) and
redactForAi(response) inside generateTestsPrompt, chatPrompt, and
multiFileChatPrompt and use the sanitized objects when building the template
strings so headers/bodies sent to the external AI provider never include raw
credentials or cookies.

In `@packages/bruno-electron/src/ipc/ai.js`:
- Around line 34-90: The handler for ipcMain.handle('ai:generate') can receive
an undefined payload and currently uses context directly, causing TypeError;
ensure the second argument defaults to an object and that context defaults to {}
before use (either by changing the handler signature to default the param to {}
and/or adding an early line like: if (!context) context = {}), so all downstream
uses (generateTestsPrompt, improveScriptPrompt, chatPrompt, multiFileChatPrompt
and any context.* checks) operate on a safe object and return a descriptive
error when required fields are missing.
🧹 Nitpick comments (7)
packages/bruno-app/src/components/AIAssistantPanel/MultiFileDiffView/StyledWrapper.js (1)

77-95: Replace hard-coded colors with theme tokens.

Use theme-provided colors for white text and error states to keep styling consistent across themes.

♻️ Suggested adjustment
-      color: white;
+      color: ${(props) => props.theme.colors.text.primary};
...
-      &.rejected { color: ${(props) => props.theme.colors.text.red || '#f85149'}; }
+      &.rejected { color: ${(props) => props.theme.colors.text.danger}; }
...
-        color: white;
+        color: ${(props) => props.theme.colors.text.primary};

Note: For rejected states, prefer theme.colors.text.danger (design system standard for errors) over theme.colors.text.red, which aligns with the error-handling patterns used elsewhere in AIAssistantPanel.

Also applies to: 121-126

packages/bruno-app/src/pages/Bruno/index.js (1)

66-68: Unused selector: aiPanelWidth.

aiPanelWidth is selected on line 68 but never used in this component. The AIAssistantPanel reads panelWidth directly from Redux internally.

🔧 Suggested fix
  const showAIPanel = useSelector((state) => state.app.showAIPanel);
  const aiPanelContext = useSelector((state) => state.app.aiPanelContext);
-  const aiPanelWidth = useSelector((state) => state.app.aiPanelWidth);
packages/bruno-app/src/components/RequestPane/Script/index.js (1)

161-193: Duplicate Tabs component with same value and onValueChange.

There are two <Tabs> components (lines 132 and 161) sharing the same state. This works but is unconventional. Consider wrapping both TabsList and TabsContent elements under a single <Tabs> parent for clearer structure.

packages/bruno-app/src/components/AIAssistantPanel/StyledWrapper.js (1)

415-436: Hardcoded white color should use theme.

Per coding guidelines, colors should use the theme prop rather than hardcoded values. The color: white on line 418 should reference a theme token.

Based on learnings, use styled component's theme prop for colors.

♻️ Suggested fix
          .send-btn {
            display: flex;
            align-items: center;
            justify-content: center;
            background: ${(props) => props.theme.brand};
            border: none;
            cursor: pointer;
-            color: white;
+            color: ${(props) => props.theme.colors.text.white};
            padding: 5px;
packages/bruno-app/src/components/AIAssistantPanel/index.js (2)

266-444: Large dependency array causes frequent IPC listener re-subscription.

The effect includes selectedFilesForAI and collections in deps. Every file selection change or collection update triggers listener teardown/setup. Consider moving the data lookups outside the handlers or using refs for values that change frequently but shouldn't trigger re-subscription.

♻️ Suggested approach

Use refs for frequently changing values that are only needed inside handlers:

const selectedFilesRef = useRef(selectedFilesForAI);
const collectionsRef = useRef(collections);

useEffect(() => {
  selectedFilesRef.current = selectedFilesForAI;
}, [selectedFilesForAI]);

useEffect(() => {
  collectionsRef.current = collections;
}, [collections]);

Then reference selectedFilesRef.current and collectionsRef.current inside the IPC handlers, removing them from the effect's dependency array.


734-741: Using array index as key for messages.

Array indices work for append-only lists, but if messages could be removed or reordered, this would cause React reconciliation issues. If the list is strictly append-only, this is acceptable.

packages/bruno-electron/src/ai/prompts/index.js (1)

7-128: Consider externalizing the Bruno API reference string.

Keeping this large reference in a separate markdown/JSON module would make updates easier and reduce inline template noise.

< 10BC0 /form>
Comment on lines +23 to +33
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const tripleBacktickMatch = line.match(/^(`{3,})(\w*)?$/);

if (tripleBacktickMatch && !inCodeBlock) {
// Starting a code block
inCodeBlock = true;
openingBackticks = tripleBacktickMatch[1];
codeBlockLang = tripleBacktickMatch[2] || '';
codeBlockContent = [];
} else if (inCodeBlock && line.trim() === openingBackticks) {
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

Code-fence detection breaks for languages like c++ or objective-c.

(\w*) only matches word chars, so fences with non‑word info strings aren’t recognized and the parser can desync. Consider allowing any info string and trimming it.

🛠️ Suggested fix
-    const tripleBacktickMatch = line.match(/^(`{3,})(\w*)?$/);
+    const tripleBacktickMatch = line.match(/^(`{3,})\s*(.*)?$/);
@@
-      codeBlockLang = tripleBacktickMatch[2] || '';
+      codeBlockLang = (tripleBacktickMatch[2] || '').trim();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const tripleBacktickMatch = line.match(/^(`{3,})(\w*)?$/);
if (tripleBacktickMatch && !inCodeBlock) {
// Starting a code block
inCodeBlock = true;
openingBackticks = tripleBacktickMatch[1];
codeBlockLang = tripleBacktickMatch[2] || '';
codeBlockContent = [];
} else if (inCodeBlock && line.trim() === openingBackticks) {
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const tripleBacktickMatch = line.match(/^(`{3,})\s*(.*)?$/);
if (tripleBacktickMatch && !inCodeBlock) {
// Starting a code block
inCodeBlock = true;
openingBackticks = tripleBacktickMatch[1];
codeBlockLang = (tripleBacktickMatch[2] || '').trim();
codeBlockContent = [];
} else if (inCodeBlock && line.trim() === openingBackticks) {
🤖 Prompt for AI Agents
In `@packages/bruno-app/src/components/AIAssistantPanel/ChatMessage/index.js`
around lines 23 - 33, The current regex line.match(/^(`{3,})(\w*)?$/) in the
code-fence detection only captures word characters for the info string and fails
for languages like "c++" or "objective-c"; update the match to allow any
non-empty info string (e.g. use a second capture like (.*) or (\S.*)?), then
assign codeBlockLang = (captureGroup2 || '').trim() so you accept and trim
arbitrary info strings; ensure the logic that compares closing backticks still
uses openingBackticks unchanged and that codeBlockLang handling in the
surrounding code (variables tripleBacktickMatch, inCodeBlock, openingBackticks,
codeBlockContent) uses the trimmed value.

Comment on lines +129 to +131
<div
className="markdown-content"
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
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

🧩 Analysis chain

🏁 Script executed:

# Find and examine the specific file
cat -n packages/bruno-app/src/components/AIAssistantPanel/ChatMessage/index.js | sed -n '120,140p'

Repository: usebruno/bruno

Length of output: 725


🏁 Script executed:

# Search for DOMPurify usage and sanitization in the file
rg -A 5 -B 5 "dangerouslySetInnerHTML|sanitizedHtml|DOMPurify" packages/bruno-app/src/components/AIAssistantPanel/ChatMessage/index.js

Repository: usebruno/bruno

Length of output: 760


🏁 Script executed:

# Check for any existing biome-ignore comments near lines 129-131
rg "biome-ignore" packages/bruno-app/src/components/AIAssistantPanel/ChatMessage/index.js

Repository: usebruno/bruno

Length of output: 40


🏁 Script executed:

# Check for biome configuration files to understand the linting setup
fd -t f "biome.json|biome.jsonc" .

Repository: usebruno/bruno

Length of output: 70


🏁 Script executed:

# Search the codebase for any existing dangerouslySetInnerHTML usages with or without suppressions
rg "dangerouslySetInnerHTML" --type jsx --type tsx -B 2 -A 2

Repository: usebruno/bruno

Length of output: 85


🌐 Web query:

Biome lint rule dangerouslySetInnerHTML security rule name and syntax

💡 Result:

Biome’s React/XSS-related lint rule for dangerouslySetInnerHTML is:

  • Rule name (grouped): security/noDangerouslySetInnerHtml (biomejs.dev)
  • Diagnostic category (as shown in errors): lint/security/noDangerouslySetInnerHtml (biomejs.dev)

Biome config syntax (biome.json):

{
  "linter": {
    "rules": {
      "security": {
        "noDangerouslySetInnerHtml": "error"
      }
    }
  }
}

(biomejs.dev)

Related rule (when both children and dangerouslySetInnerHTML are used):

  • security/noDangerouslySetInnerHtmlWithChildren (config key: noDangerouslySetInnerHtmlWithChildren) (biomejs.dev)

Citations:


🏁 Script executed:

# Check the biome.json configuration
cat biome.json

Repository: usebruno/bruno

Length of output: 1832


🏁 Script executed:

# Search for dangerouslySetInnerHTML usages with proper file types
rg "dangerouslySetInnerHTML" -B 2 -A 2 --type js --type ts

Repository: usebruno/bruno

Length of output: 1401


Add a biome-ignore comment to suppress the lint rule.

The dangerouslySetInnerHTML usage is guarded by DOMPurify sanitization, but Biome's recommended rules will flag it without an explicit suppression. Add a targeted ignore comment:

Suggested fix
+      {/* biome-ignore lint/security/noDangerouslySetInnerHtml: sanitized via DOMPurify */}
       <div
         className="markdown-content"
         dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div
className="markdown-content"
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: sanitized via DOMPurify */}
<div
className="markdown-content"
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
🧰 Tools
🪛 Biome (2.1.2)

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

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

(lint/security/noDangerouslySetInnerHtml)

🤖 Prompt for AI Agents
In `@packages/bruno-app/src/components/AIAssistantPanel/ChatMessage/index.js`
around lines 129 - 131, Add a targeted Biome ignore comment immediately above
the div using dangerouslySetInnerHTML to suppress the specific lint rule that
flags direct HTML insertion (while keeping the existing DOMPurify sanitization).
Locate the JSX element with className "markdown-content" that sets
dangerouslySetInnerHTML with sanitizedHtml and insert a single-line or block
Biome ignore for the specific lint rule (e.g., the react/no-danger-equivalent
rule) so only this occurrence is suppressed rather than disabling linting
broadly.

Comment on lines +148 to +162
const CodeBlock = ({ code, language = 'javascript' }) => {
const [copied, setCopied] = useState(false);

const handleCopy = async () => {
try {
await navigator.clipboard.writeText(code);
setCopied(true);
toast.success('Copied to clipboard');
setTimeout(() => setCopied(false), 2000);
} catch (err) {
toast.error('Failed to copy');
}
};

const lines = code.split('\n');
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

Guard against undefined code inputs.

code.split('\n') will throw if code is undefined or null. Defaulting to an empty string avoids runtime errors.

🐛 Proposed fix
-const CodeBlock = ({ code, language = 'javascript' }) => {
+const CodeBlock = ({ code = '', language = 'javascript' }) => {
🤖 Prompt for AI Agents
In `@packages/bruno-app/src/components/AIAssistantPanel/CodeBlock/index.js` around
lines 148 - 162, The CodeBlock component currently calls code.split('\n') which
will throw if code is null/undefined; update the component to guard the input by
defaulting code to an empty string (e.g., change the prop default or create a
local safeCode = code ?? ''), use safeCode for splitting and for
navigator.clipboard.writeText in handleCopy, and ensure any references (lines,
handleCopy) use that safe value so runtime errors are avoided.

Comment on lines +1 to +47
import React, { useState, useMemo, useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { IconSearch, IconX } from '@tabler/icons';
import { addFileToAISelection, removeFileFromAISelection, clearAIFileSelection } from 'providers/ReduxStore/slices/app';
import { isItemARequest, isItemAFolder } from 'utils/tabs';
import { debounce } from 'lodash';
import StyledWrapper from './StyledWrapper';

// Flatten collection items recursively, including path info
const flattenItems = (items, parentPath = '') => {
const result = [];
for (const item of items || []) {
const currentPath = parentPath ? `${parentPath}/${item.name}` : item.name;

if (isItemARequest(item)) {
result.push({
...item,
path: parentPath
});
} else if (isI CE3F temAFolder(item) && item.items) {
result.push(...flattenItems(item.items, currentPath));
}
}
return result;
};

const FileSelector = ({ collectionUid }) => {
const dispatch = useDispatch();
const [searchText, setSearchText] = useState('');
const [searchInput, setSearchInput] = useState('');

// Debounced search to avoid filtering on every keystroke
const debouncedSetSearch = useMemo(
() => debounce((value) => setSearchText(value), 150),
[]
);

const handleSearchChange = useCallback((e) => {
const value = e.target.value;
setSearchInput(value);
debouncedSetSearch(value);
}, [debouncedSetSearch]);

const handleClearSearch = useCallback(() => {
setSearchInput('');
setSearchText('');
}, []);
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

🧩 Analysis chain

🏁 Script executed:

fd "FileSelector" --type f --extension js --extension jsx

Repository: usebruno/bruno

Length of output: 172


🌐 Web query:

lodash debounce cancel method

💡 Result:

In Lodash, _.debounce(func, wait, options) returns a debounced function that has a .cancel() method.

  • debounced.cancel(): cancels any pending (scheduled) invocation of func (i.e., clears the internal timer), so the trailing call won’t run. It’s commonly used for cleanup (e.g., when unmounting a component or navigating away). [1][2]

Related:

  • debounced.flush(): immediately invokes any pending invocation instead of waiting. [1][2]

Example

const debouncedSave = _.debounce(save, 500);

// schedule
debouncedSave();

// prevent the scheduled trailing call from running
debouncedSave.cancel();

Sources: Lodash debounce docs. [1][2]


Cancel pending debounced updates on clear and unmount.

A pending debounce can re-apply the old search text after the user clicks "Clear", and can attempt to set state after component unmount. Cancel pending debounced calls on clear and add cleanup on unmount.

Suggested fix
-import React, { useState, useMemo, useCallback } from 'react';
+import React, { useState, useMemo, useCallback, useEffect } from 'react';
@@
   const debouncedSetSearch = useMemo(
     () => debounce((value) => setSearchText(value), 150),
     []
   );
 
+  useEffect(() => {
+    return () => {
+      debouncedSetSearch.cancel();
+    };
+  }, [debouncedSetSearch]);
+
   const handleClearSearch = useCallback(() => {
+    debouncedSetSearch.cancel();
     setSearchInput('');
     setSearchText('');
-  }, []);
+  }, [debouncedSetSearch]);
🤖 Prompt for AI Agents
In `@packages/bruno-app/src/components/AIAssistantPanel/FileSelector/index.js`
around lines 1 - 47, The debouncedSetSearch created with useMemo can fire after
clearing or after unmount; call debouncedSetSearch.cancel() inside
handleClearSearch to avoid re-applying the old search text, and add a useEffect
cleanup that calls debouncedSetSearch.cancel() on unmount to prevent state
updates after unmount. Update references to debouncedSetSearch (from useMemo)
and handleClearSearch accordingly so the debounce is cancelled both when
clearing and when the component unmounts.

Comment on lines +112 to +120
<IconX
size={14}
style={{ cursor: 'pointer' }}
onClick={handleClearSearch}
role="button"
aria-label="Clear search"
tabIndex={0}
onKeyDown={(e) => e.key === 'Enter' && handleClearSearch()}
/>
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

Add Space key support for button/checkbox roles.

Keyboard users expect Space to toggle. Currently only Enter works.

🛠️ Proposed fix
-            onKeyDown={(e) => e.key === 'Enter' && handleClearSearch()}
+            onKeyDown={(e) => {
+              if (e.key === 'Enter' || e.key === ' ') {
+                e.preventDefault();
+                handleClearSearch();
+              }
+            }}
@@
-                onKeyDown={(e) => e.key === 'Enter' && handleToggleItem(item)}
+                onKeyDown={(e) => {
+                  if (e.key === 'Enter' || e.key === ' ') {
+                    e.preventDefault();
+                    handleToggleItem(item);
+                  }
+                }}

Also applies to: 144-152

🤖 Prompt for AI Agents
In `@packages/bruno-app/src/components/AIAssistantPanel/FileSelector/index.js`
around lines 112 - 120, The IconX clear control uses onKeyDown handling that
only triggers on Enter; update the keyboard handler for the IconX instance(s)
(the one using onClick={handleClearSearch} and the similar control at the second
occurrence) to also respond to Spacebar by checking for e.key === 'Enter' ||
e.key === ' ' || e.key === 'Spacebar' (and call e.preventDefault() before
invoking handleClearSearch), ensuring the role="button" + tabIndex remains;
apply the same change to the other occurrence (lines near the second IconX block
referenced) so both keyboard users can toggle via Space as well as Enter.

Comment on lines +102 to +112
// Update AI panel context when panel opens (e.g., via Cmd+L)
useEffect(() => {
if (showAIPanel) {
dispatch(updateAIPanelContext({
scriptType: activeTab,
currentScript: getCurrentScript(activeTab),
item, 3D81
collection
}));
}
}, [showAIPanel, item, collection, dispatch]);
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

Missing activeTab in dependency array.

getCurrentScript(activeTab) is called inside the effect, but activeTab isn't listed in the deps. This could lead to stale context being dispatched if activeTab changes while other dependencies remain stable.

🐛 Suggested fix
  useEffect(() => {
    if (showAIPanel) {
      dispatch(updateAIPanelContext({
        scriptType: activeTab,
        currentScript: getCurrentScript(activeTab),
        item,
        collection
      }));
    }
-  }, [showAIPanel, item, collection, dispatch]);
+  }, [showAIPanel, activeTab, item, collection, dispatch]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CE3F
// Update AI panel context when panel opens (e.g., via Cmd+L)
useEffect(() => {
if (showAIPanel) {
dispatch(updateAIPanelContext({
scriptType: activeTab,
currentScript: getCurrentScript(activeTab),
item,
collection
}));
}
}, [showAIPanel, item, collection, dispatch]);
// Update AI panel context when panel opens (e.g., via Cmd+L)
useEffect(() => {
if (showAIPanel) {
dispatch(updateAIPanelContext({
scriptType: activeTab,
currentScript: getCurrentScript(activeTab),
item,
collection
}));
}
}, [showAIPanel, activeTab, item, collection, dispatch]);
🤖 Prompt for AI Agents
In `@packages/bruno-app/src/components/RequestPane/Script/index.js` around lines
102 - 112, The effect that updates AI panel context (useEffect) references
activeTab and calls getCurrentScript(activeTab) but does not include activeTab
in the dependency array; update the dependency array for that useEffect to
include activeTab so the effect re-runs when activeTab changes (retain
showAIPanel, item, collection, dispatch and any other referenced variables like
getCurrentScript if it's not stable).

Comment on lines +56 to +61
const forceVerticalLayoutForAI = useSelector((state) => state.app.forceVerticalLayoutForAI);
// Force vertical layout when AI panel is open, otherwise use user preference
const isVerticalLayout = forceVerticalLayoutForAI ? true : preferences?.layout?.responsePaneOrientation === 'vertical';
const isConsoleOpen = useSelector((state) => state.logs.isConsoleOpen);
const showAIPanel = useSelector((state) => state.app.showAIPanel);
const responsePaneHiddenByAI = useSelector((state) => state.app.responsePaneHiddenByAI);
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

fd "RequestTabPanel" --type f

Repository: usebruno/bruno

Length of output: 40


🏁 Script executed:

cat -n packages/bruno-app/src/components/RequestTabPanel/index.js | head -100

Repository: usebruno/bruno

Length of output: 6085


Hooks are called after an early return, violating React's Rules of Hooks.

Lines 49–61 invoke useSelector hooks after the if (typeof window === 'undefined') guard on lines 46–48. This conditional execution of hooks breaks the requirement that hooks run unconditionally on every render. Extract the inner logic into a separate component to ensure all hooks execute at the top level.

Suggested fix: wrap with component guard
-const RequestTabPanel = () => {
-  if (typeof window === 'undefined') {
-    return <div></div>;
-  }
+const RequestTabPanelInner = () => {
   const dispatch = useDispatch();
   const tabs = useSelector((state) => state.tabs.tabs);
   ...
-};
-
-export default RequestTabPanel;
+};
+
+const RequestTabPanel = () => {
+  if (typeof window === 'undefined') {
+    return <div />;
+  }
+  return <RequestTabPanelInner />;
+};
+
+export default RequestTabPanel;
🧰 Tools
🪛 Biome (2.1.2)

[error] 56-56: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)


[error] 59-59: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)


[error] 60-60: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)


[error] 61-61: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

🤖 Prompt for AI Agents
In `@packages/bruno-app/src/components/RequestTabPanel/index.js` around lines 56 -
61, The hooks (useSelector calls for forceVerticalLayoutForAI, isConsoleOpen,
showAIPanel, responsePaneHiddenByAI and the computed isVerticalLayout) are being
invoked after an early return guarded by if (typeof window === 'undefined'),
violating Rules of Hooks; fix by extracting the hook-using logic into a new
child component (e.g., InnerRequestTabPanel) which contains all useSelector
calls and the isVerticalLayout computation, and have the original component
conditionally render that child (or null) based on typeof window === 'undefined'
so hooks always run at the top level of the child component.

Comment on lines +360 to +375
{!shouldHideResponsePane && (
<>
<div
className="dragbar-wrapper"
onDoubleClick={(e) => {
e.preventDefault();
resetPaneBoundaries();
}}
onMouseDown={handleDragbarMouseDown}
>
<div className="dragbar-handle" />
</div>

<section className="response-pane flex-grow overflow-x-auto">
{renderResponsePane()}
</section>
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

🧩 Analysis chain

🏁 Script executed:

cd packages/bruno-app && find . -name "RequestTabPanel" -o -name "index.js" | grep -i requesttab | head -5

Repository: usebruno/bruno

Length of output: 172


🏁 Script executed:

find packages/bruno-app/src/components -name "*RequestTab*" -type d

Repository: usebruno/bruno

Length of output: 210


🏁 Script executed:

ls -la packages/bruno-app/src/components/ | grep -i request

Repository: usebruno/bruno

Length of output: 319


🏁 Script executed:

wc -l packages/bruno-app/src/components/RequestTabPanel/index.js

Repository: usebruno/bruno

Length of output: 120


🏁 Script executed:

cat -n packages/bruno-app/src/components/RequestTabPanel/index.js | head -100

Repository: usebruno/bruno

Length of output: 6085


🏁 Script executed:

cat -n packages/bruno-app/src/components/RequestTabPanel/index.js | sed -n '350,385p'

Repository: usebruno/bruno

Length of output: 1575


🏁 Script executed:

cat -n packages/bruno-app/src/components/RequestTabPanel/index.js | sed -n '150,250p'

Repository: usebruno/bruno

Length of output: 3930


🏁 Script executed:

grep -n "requestPaneStyle" packages/bruno-app/src/components/RequestTabPanel/index.js

Repository: usebruno/bruno

Length of output: 146


🏁 Script executed:

cat -n packages/bruno-
3D81
app/src/components/RequestTabPanel/index.js | sed -n '320,345p'

Repository: usebruno/bruno

Length of output: 1125


🏁 Script executed:

grep -n "request-pane\|requestPane" packages/bruno-app/src/components/RequestTabPanel/index.js

Repository: usebruno/bruno

Length of output: 193


🏁 Script executed:

cat packages/bruno-app/src/components/RequestTabPanel/StyledWrapper.js

Repository: usebruno/bruno

Length of output: 4660


🏁 Script executed:

cat -n packages/bruno-app/src/components/RequestTabPanel/index.js | sed -n '348,365p'

Repository: usebruno/bruno

Length of output: 758


Request pane should expand when response pane is hidden.

When shouldHideResponsePane is true, the response pane is removed but requestPaneStyle retains split dimensions (fixed height in vertical layout, fixed width in horizontal layout), leaving unused space. The request pane should fill the available space when the response pane is hidden.

🛠️ Suggested adjustment
-  const requestPaneStyle = isVerticalLayout
-    ? {
-        height: `${Math.max(topPaneHeight, MIN_TOP_PANE_HEIGHT)}px`,
-        minHeight: `${MIN_TOP_PANE_HEIGHT}px`,
-        width: '100%'
-      }
-    : {
-        width: `${Math.max(leftPaneWidth, MIN_LEFT_PANE_WIDTH)}px`
-      };
+  const requestPaneStyle = shouldHideResponsePane
+    ? {
+        width: '100%',
+        height: '100%'
+      }
+    : isVerticalLayout
+      ? {
+          height: `${Math.max(topPaneHeight, MIN_TOP_PANE_HEIGHT)}px`,
+          minHeight: `${MIN_TOP_PANE_HEIGHT}px`,
+          width: '100%'
+        }
+      : {
+          width: `${Math.max(leftPaneWidth, MIN_LEFT_PANE_WIDTH)}px`
+        };
🤖 Prompt for AI Agents
In `@packages/bruno-app/src/components/RequestTabPanel/index.js` around lines 360
- 375, When shouldHideResponsePane is true the request pane keeps split
dimensions from requestPaneStyle and leaves empty space; update the layout logic
that computes/assigns requestPaneStyle so that when shouldHideResponsePane ===
true you remove/override the fixed split size and let the request pane flex to
fill available area (e.g. set style to flex-grow/height:100% in vertical or
width:100% in horizontal). Locate where requestPaneStyle is computed/used in
RequestTabPanel (and keep existing symbols like shouldHideResponsePane,
requestPaneStyle, resetPaneBoundaries, handleDragbarMouseDown,
renderResponsePane) and apply the override only when the response pane is hidden
so the dragbar/drag handlers still work when visible.

Comment on lines +145 to +155
CE3F
- Method: ${request.method || 'GET'}
- URL: ${request.url || ''}
- Headers: ${JSON.stringify(request.headers || {}, null, 2)}
- Body: ${JSON.stringify(request.body || null, null, 2)}

## Response Details
- Status: ${response.status || 'N/A'}
- Status Text: ${response.statusText || ''}
- Headers: ${JSON.stringify(response.headers || {}, null, 2)}
- Body: ${JSON.stringify(response.data || response.body || null, null, 2)}
- Response Time: ${response.responseTime || 'N/A'}ms
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

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and get its size
wc -l packages/bruno-electron/src/ai/prompts/index.js 2>/dev/null || echo "File not found or error checking"

Repository: usebruno/bruno

Length of output: 109


🏁 Script executed:

# Read the entire file to understand context
cat -n packages/bruno-electron/src/ai/prompts/index.js

Repository: usebruno/bruno

Length of output: 22336


🏁 Script executed:

# Search for functions that call these prompt generators
rg -n "generateTestsPrompt|improveScriptPrompt|chatPrompt|multiFileChatPrompt" packages/bruno-electron/src --type js --type ts -A 2

Repository: usebruno/bruno

Length of output: 3158


🏁 Script executed:

# Look for AI service calls or API endpoints
rg -n "fetch|axios|http|api" packages/bruno-electron/src/ai --type js --type ts | head -50

Repository: usebruno/bruno

Length of output: 822


🏁 Script executed:

# Read the AI service integration file
cat -n packages/bruno-electron/src/ipc/ai.js

Repository: usebruno/bruno

Length of output: 7245


🏁 Script executed:

# Search for any existing redaction or consent mechanisms
rg -n "redact|sanitize|consent|privacy|secret" packages/bruno-electron/src/ai --type js --type ts

Repository: usebruno/bruno

Length of output: 40


🏁 Script executed:

# Check if there are any guard rails or warnings in the UI/frontend
find packages -name "*.jsx" -o -name "*.tsx" | xargs rg -l "ai:generate|generateTests|improveScript" | head -5

Repository: usebruno/bruno

Length of output: 40


🏁 Script executed:

# Find frontend files that handle AI generation
find packages/bruno-web packages/bruno-electron -name "*.jsx" -o -name "*.tsx" -o -name "*.vue" 2>/dev/null | xargs grep -l "ai:generate\|AI\|generateTests" 2>/dev/null | head -10

Repository: usebruno/bruno

Length of output: 40


🏁 Script executed:

# Check for any privacy disclaimers or consent flows in the codebase
rg -n "privacy\|disclaimer\|warning\|PII\|sensitive\|secret" packages/bruno-electron/src --type js --type ts | grep -i "ai\|prompt\|external" | head -20

Repository: usebruno/bruno

Length of output: 40


🏁 Script executed:

# Find all files that reference the AI IPC handler
rg "ai:generate" packages --type js --type ts --type jsx --type tsx -l

Repository: usebruno/bruno

Length of output: 85


🏁 Script executed:

# Look for any environment checks or configuration related to AI
rg -n "OPENAI_API_KEY\|AI_CONFIG\|ai:" packages/bruno-electron --type js --type ts -B 2 -A 2 | head -80

Repository: usebruno/bruno

Length of output: 40


🏁 Script executed:

# Verify the exact lines where secrets are exposed in multiFileChatPrompt
sed -n '343,352p' packages/bruno-electron/src/ai/prompts/index.js

Repository: usebruno/bruno

Length of output: 786


🏁 Script executed:

# Check if there's any documentation about AI privacy/security
find packages -name "*.md" | xargs grep -l "AI\|OpenAI\|privacy\|security" 2>/dev/null | head -5

Repository: usebruno/bruno

Length of output: 232


🏁 Script executed:

# Verify the exact sensitive data in chatPrompt function
sed -n '248,263p' packages/bruno-electron/src/ai/prompts/index.js

Repository: usebruno/bruno

Length of output: 1538


🏁 Script executed:

# Check improveScriptPrompt for any sensitive data exposure
sed -n '182,217p' packages/bruno-electron/src/ai/prompts/index.js

Repository: usebruno/bruno

Length of output: 1383


Redact secrets/PII before sending prompts to the external AI provider.

Headers, bodies, and scripts can contain credentials or sensitive data. Sending them verbatim to OpenAI is a privacy and compliance risk. Sanitize sensitive fields (authorization headers, API keys, cookies, etc.) before building these prompts.

Implement a redaction helper and apply it consistently across generateTestsPrompt, chatPrompt, and multiFileChatPrompt:

Suggested implementation
const redactForAi = (data = {}) => {
  const safe = { ...data };
  if (safe.headers && typeof safe.headers === 'object') {
    const headers = { ...safe.headers };
    const sensitive = ['authorization', 'cookie', 'set-cookie', 'x-api-key'];
    Object.keys(headers).forEach((header) => {
      if (sensitive.includes(header.toLowerCase())) {
        headers[header] = '[REDACTED]';
      }
    });
    safe.headers = headers;
  }
  return safe;
};

Then apply to request/response objects before interpolating into prompts.

🤖 Prompt for AI Agents
In `@packages/bruno-electron/src/ai/prompts/index.js` around lines 145 - 155, Add
a redaction helper and apply it before interpolating request/response data into
prompts: implement a function (e.g., redactForAi) that clones the passed object
and replaces sensitive header fields (authorization, cookie, set-cookie,
x-api-key, etc.) and any obvious PII in bodies with a placeholder like
"[REDACTED]"; then call redactForAi(request) and redactForAi(response) inside
generateTestsPrompt, chatPrompt, and multiFileChatPrompt and use the sanitized
objects when building the template strings so headers/bodies sent to the
external AI provider never include raw credentials or cookies.

Comment on lines +34 to +90
3D81
ipcMain.handle('ai:generate', async (event, { action, context }) => {
// Get API key from environment variable
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) {
throw new Error('OPENAI_API_KEY environment variable is not set. Please add it to your .env file in bruno-electron.');
}

// Create provider and get model
const provider = createProvider('openai', apiKey);
const modelId = AI_CONFIG.model;

// Build prompt based on action
let prompt;
switch (action) {
case 'generate-tests':
if (!context.response) {
throw new Error('No response data available. Please run the request first.');
}
prompt = generateTestsPrompt(
context.request || {},
context.response || {},
context.currentScript || ''
);
break;

case 'improve-script':
if (!context.script || !context.script.trim()) {
throw new Error('No script to improve. Please write some code first.');
}
prompt = improveScriptPrompt(
context.script,
context.scriptType || 'post-response',
context.request || {}
);
break;

case 'chat':
prompt = chatPrompt(
context.userMessage,
context.script || '',
context.scriptType || 'post-response',
context.request || {},
context.response || null,
context.mode || 'ask-before-edit',
context.testsScript || ''
);
break;

case 'multi-file-chat':
if (!context.files || context.files.length === 0) {
throw new Error('No files selected. Please select at least one file.');
}
prompt = multiFileChatPrompt(
context.userMessage,
context.files,
context.mode || 'ask-before-edit'
);
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

Guard against missing context to avoid TypeError.

If the renderer sends an invalid payload (or future callers omit context), the handler will throw before a useful error message. Default to {} and use that object consistently.

🔧 Proposed fix
 ipcMain.handle('ai:generate', async (event, { action, context }) => {
+  const ctx = context || {};
   // Get API key from environment variable
   const apiKey = process.env.OPENAI_API_KEY;
   if (!apiKey) {
     throw new Error('OPENAI_API_KEY environment variable is not set. Please add it to your .env file in bruno-electron.');
   }
@@
     switch (action) {
       case 'generate-tests':
-        if (!context.response) {
+        if (!ctx.response) {
           throw new Error('No response data available. Please run the request first.');
         }
         prompt = generateTestsPrompt(
-          context.request || {},
-          context.response || {},
-          context.currentScript || ''
+          ctx.request || {},
+          ctx.response || {},
+          ctx.currentScript || ''
         );
         break;

       case 'improve-script':
-        if (!context.script || !context.script.trim()) {
+        if (!ctx.script || !ctx.script.trim()) {
           throw new Error('No script to improve. Please write some code first.');
         }
         prompt = improveScriptPrompt(
-          context.script,
-          context.scriptType || 'post-response',
-          context.request || {}
+          ctx.script,
+          ctx.scriptType || 'post-response',
+          ctx.request || {}
         );
         break;

       case 'chat':
         prompt = chatPrompt(
-          context.userMessage,
-          context.script || '',
-          context.scriptType || 'post-response',
-          context.request || {},
-          context.response || null,
-          context.mode || 'ask-before-edit',
-          context.testsScript || ''
+          ctx.userMessage,
+          ctx.script || '',
+          ctx.scriptType || 'post-response',
+          ctx.request || {},
+          ctx.response || null,
+          ctx.mode || 'ask-before-edit',
+          ctx.testsScript || ''
         );
         break;

       case 'multi-file-chat':
-        if (!context.files || context.files.length === 0) {
+        if (!ctx.files || ctx.files.length === 0) {
           throw new Error('No files selected. Please select at least one file.');
         }
         prompt = multiFileChatPrompt(
-          context.userMessage,
-          context.files,
-          context.mode || 'ask-before-edit'
+          ctx.userMessage,
+          ctx.files,
+          ctx.mode || 'ask-before-edit'
         );
         break;
🤖 Prompt for AI Agents
In `@packages/bruno-electron/src/ipc/ai.js` around lines 34 - 90, The handler for
ipcMain.handle('ai:generate') can receive an undefined payload and currently
uses context directly, causing TypeError; ensure the second argument defaults to
an object and that context defaults to {} before use (either by changing the
handler signature to default the param to {} and/or adding an early line like:
if (!context) context = {}), so all downstream uses (generateTestsPrompt,
improveScriptPrompt, chatPrompt, multiFileChatPrompt and any context.* checks)
operate on a safe object and return a descriptive error when required fields are
missing.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

0