Skip to content

Feature/transient request#6878

Merged
sid-bruno merged 33 commits intousebruno:mainfrom
chirag-bruno:feature/temporary-request
Jan 29, 2026
Merged

Feature/transient request#6878
sid-bruno merged 33 commits intousebruno:mainfrom
chirag-bruno:feature/temporary-request

Conversation

@chirag-bruno
Copy link
Collaborator

@chirag-bruno chirag-bruno commented Jan 21, 2026

Description

This PR introduces Transient Requests, a new way to quickly create requests within a collection without requiring a name upfront. These requests are intended to be disposable and are not automatically saved to the collection. Users can choose to persist a transient request either when closing it or by explicitly saving it via the Save button or Ctrl/Cmd + S.

This change addresses the issue tracked in BRU-2114: https://usebruno.atlassian.net/browse/BRU-2114

Transient requests are stored in a temporary directory and are excluded from the collection directory. This improves the user experience by allowing users to create requests instantly using the + icon in the tab bar. Right-clicking the + icon opens a dropdown to select the request type, while a left-click creates a request using the collection’s preset request type (defaulting to HTTP if no preset is configured).

The PR also introduces an enhanced save flow that allows users to rename a request at the time of saving and choose a target directory, which defaults to the collection root. From the save modal, users can additionally create a new folder and immediately save the request within that newly created directory.

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.

Summary by CodeRabbit

  • New Features

    • Quick-create transient requests (HTTP, GraphQL, gRPC, WebSocket) via an inline control with left-click create and right-click type picker.
    • Save transient requests via a single-item modal or a multi-item container with folder browse/search and new-folder support.
    • Temporary collection mounting and transient directory handling for ephemeral workflows.
  • UX / Bug Fixes

    • Transient requests hidden from sidebars and searches; transient tabs open as previews and are cleaned up on close.
    • Collection removal dialog separates Saved vs Transient drafts and supports per-transient save/discard.
  • Tests

    • End-to-end tests added for transient request flows and updated collection removal handling.

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

…rocess

* add action to store transient directory paths in Redux state
* update IPC handler to create and return a temporary directory for collections
* modify collection mount action to dispatch transient directory addition
…uests

* implement CreateTransientRequest component to facilitate the creation of HTTP, GraphQL, gRPC, and WebSocket transient requests
* integrate the component into RequestTabs for user interaction
* update collection and request handling to differentiate between transient and non-transient requests
* enhance Redux actions to support transient request creation and management
* refactor Redux actions for HTTP, gRPC, and WebSocket requests to utilize a unified task queue for transient requests
* implement a new helper function to retrieve collection paths from temporary directory metadata
* add functionality to watch transient directories for file changes, excluding metadata.json
* integrate transient directory watcher into the collection mounting process
…ation

* update generateTransientRequestName function to accept tempDirectory parameter for improved request naming
* modify CreateTransientRequest component to utilize tempDirectory for transient request creation
* adjust middleware to check for transient status based on tempDirectory
* implement transient file and directory identification in collections slice for better state management
* implement SaveTransientRequest component to facilitate saving transient requests to selected folders
* create StyledWrapper for component styling
* introduce useCollectionFolderTree hook for managing folder navigation and state
* update Redux actions to handle saving requests from transient state
…ement

* add SaveTransientRequestContainer to manage multiple transient request modals
* refactor SaveTransientRequest component to utilize Redux for modal state management
* implement open and close actions for transient request modals in Redux slice
* update Bruno page to include SaveTransientRequestContainer for improved UI integration
… functionality

* add input for creating new folders within the SaveTransientRequest component
* implement validation for new folder names and filesystem names
* integrate folder creation logic with Redux actions for better state management
* update styling for new folder input elements in StyledWrapper
* improve modal behavior to reset state when opened
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 21, 2026

Walkthrough

Adds transient-request support: UI to create transient requests, save modal(s) and container, Redux state/actions/middleware for transient directories and modal queuing, a folder-tree hook, Electron IPC/watcher to manage temp dirs and persist moves, and Playwright tests and helpers for transient flows.

Changes

Cohort / File(s) Summary
Create Transient UI
packages/bruno-app/src/components/CreateTransientRequest/index.js
New inline component to create transient HTTP/GraphQL/gRPC/WebSocket requests; dispatches new*Request(..., isTransient: true), left-click default, right-click dropdown, naming/presets, dropdown close/outside-click handling, toast errors.
Save Transient UI
packages/bruno-app/src/components/SaveTransientRequest/index.js, packages/bruno-app/src/components/SaveTransientRequest/Container.js, packages/bruno-app/src/components/SaveTransientRequest/StyledWrapper.js
New save modal, container and styles: folder navigation/search/new-folder creation, filename validation, IPC save call (renderer:save-transient-request), close tabs/remove transient source, modal queueing and per-item save/discard.
Request Tabs / Main Integration
packages/bruno-app/src/components/RequestTabs/index.js, packages/bruno-app/src/pages/Bruno/index.js
Replaces modal trigger with inline CreateTransientRequest; injects transient modals renderer to choose single-modal vs container rendering based on queued modals.
Folder Navigation Hook
packages/bruno-app/src/hooks/useCollectionFolderTree/index.js
New hook building in-memory folder tree with navigation, breadcrumbs, selection and utilities used by save modal.
Redux: Collections slice & Actions
packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js, .../actions.js
Adds tempDirectories and saveTransientRequestModals state; reducers/actions addTransientDirectory, addSaveTransientRequestModal, removeSaveTransientRequestModal, clearAllSaveTransientRequestModals; newHttpRequest/newGrpcRequest/newWsRequest accept isTransient and create transient files, saveRequest enqueues modal for transient items.
Middlewares / Tabs behavior
packages/bruno-app/src/providers/ReduxStore/middlewares/tasks/middleware.js, packages/bruno-app/src/providers/ReduxStore/middlewares/autosave/middleware.js
Tab preview set based on isTransient; closeTabs triggers IPC deletion of transient files; autosave middleware skips transient items.
Sidebar, Search & Filters
packages/bruno-app/src/components/Sidebar/.../CollectionItem/index.js, .../Collection/index.js, packages/bruno-app/src/utils/collections/search.js, packages/bruno-app/src/utils/collections/index.js
Filters transient items out of folder/request listings and search helpers; adds isItemTransientRequest utility.
Confirm Close Drafts UI
packages/bruno-app/src/components/Sidebar/Collections/Collection/RemoveCollection/ConfirmCollectionCloseDrafts.js
Splits drafts into non-transient and transient, shows both with per-transient Save actions, blocks batch save if transient drafts exist, updates messaging and discard flow.
Electron: Watcher, IPC & Utils
packages/bruno-electron/src/app/collection-watcher.js, packages/bruno-electron/src/ipc/collection.js, packages/bruno-electron/src/utils/collection.js
Tracks tempDirectoryMap, adds temp-directory watcher lifecycle, mount-collection creates temp dir with metadata.json and returns temp path, adds renderer:save-transient-request IPC to move transient → permanent, and marks hydrated items as isTransient for temp-prefixed paths.
Tests & Playwright helpers
tests/transient-requests/transient-requests.spec.ts, tests/utils/page/actions.ts, tests/onboarding/sample-collection.spec.ts
E2E tests for transient flows, test helpers to create/fill transient requests, and robust modal handling for collection removal.
Other small edits
various files across app and middleware
Minor wiring, transient flag propagation, UI adjustments, and autosave/tab handling changes across multiple modules.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as CreateTransientRequest / SaveModal
    participant Redux
    participant Electron
    participant FS as FileSystem

    User->>UI: create transient request (click)
    UI->>Redux: dispatch new*Request(..., isTransient: true)
    Redux->>Electron: mount-collection (returns tempDirectoryPath)
    Electron->>FS: write file under /tmp/bruno-<id>/ and metadata.json
    Electron->>Redux: emit collectionAddFileEvent (mapped to real collection path)
    Redux->>UI: open tab (preview set based on isTransient)

    User->>UI: open Save modal and confirm
    UI->>Electron: renderer:save-transient-request(sourcePath, targetDir, filename, request, format)
    Electron->>FS: validate/write target file, remove transient source
    Electron->>Redux: notify saved, remove save-modal entry
    Redux->>UI: show success toast
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

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

Poem

A temp file blooms in /tmp's light,
Click to open, peek, and write.
Breadcrumbs hum a saving tune,
Move it home beneath the moon. ✨

🚥 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 'Feature/transient request' is vague and uses a generic naming pattern; it does not clearly convey the specific feature being introduced or its primary purpose. Consider revising to a more descriptive title such as 'Add transient request functionality' or 'Implement disposable request tabs for quick API testing' to better communicate the feature's scope.
✅ 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.

… request URLs

* Refactored CreateTransientRequest component to retrieve request URLs from collection presets.
* Enhanced request handling by dynamically setting request URLs based on the selected collection's configuration.
…ections actions

* Removed unused imports from actions.js to streamline the code.
* Updated the saveRequest function to reject the modal instead of resolving it when handling transient requests.
* Cleaned up comments in index.js for better clarity.
…tionality

* Removed success toast notifications from CreateTransientRequest component to simplify user feedback.
* Enhanced SaveTransientRequest component to handle transient requests more effectively, including improved filename resolution and validation.
* Added IPC handler for saving transient requests, ensuring proper file management and error handling.
* Updated Redux actions to check for duplicate transient request names within the temporary directory.
* Added logic to differentiate between transient and non-transient drafts, ensuring transient requests are saved individually before closing the collection.
* Improved user feedback by displaying unsaved changes for both regular and transient requests.
* Updated save and discard functionality to handle all drafts appropriately, enhancing overall user experience.
@chirag-bruno chirag-bruno changed the title Feature/temporary request Feature/transient request Jan 23, 2026
@chirag-bruno chirag-bruno marked this pull request as ready for review January 23, 2026 19:41
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: 8

Caution

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

⚠️ Outside diff range comments (4)
packages/bruno-electron/src/ipc/collection.js (1)

1674-1701: Add cleanup for temp directory and watcher on collection removal.

The temp directory created in renderer:mount-collection is never cleaned up. When renderer:remove-collection is called, only the collection watcher is removed—the temp directory watcher (stored under tempDirectoryPath key) and the temp directory itself remain. No cleanup occurs on app exit either.

Add a removeTempDirectoryWatcher method to the watcher module and call it when the collection is removed, then delete the temp directory.

packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js (1)

2574-2639: Propagate isTransient to request items from transient file events.

Line 2607 and Line 2623: isTransientFile is computed but only applied to folders. Request items added/updated from the transient directory won’t carry isTransient, which can break filtering/UI logic that expects that flag.

💡 Suggested patch
-            currentItem.pathname = file.meta.pathname;
+            currentItem.pathname = file.meta.pathname;
+            currentItem.isTransient = isTransientFile;
...
-            currentSubItems.push({
+            currentSubItems.push({
               uid: file.data.uid,
               name: file.data.name,
               type: file.data.type,
+              isTransient: isTransientFile,
               seq: file.data.seq,
               tags: file.data.tags,
               request: file.data.request,
packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js (2)

1257-1334: Guard against missing transient directory before using path.join.

Line 1257 (and similar in gRPC/WS paths): if isTransient is true but tempDirectory isn’t in state yet, path.join(tempDirectory, ...) can throw and break request creation. Add an early guard and fail gracefully.

💡 Suggested guard (apply to HTTP/GRPC/WS)
     const tempDirectory = isTransient ? state.collections.tempDirectories?.[collectionUid] : null;
+    if (isTransient && !tempDirectory) {
+      return reject(new Error('Transient directory is not available yet. Please retry.'));
+    }

Also applies to: 1426-1524, 1549-1649


138-158: Defer transient saves without rejecting the promise.

Lines 158-160: The transient save flow dispatches a modal but then rejects the promise with no error. Since most callers use bare dispatch(saveRequest(...)) without .catch(), this could surface as unhandled promise rejections. The transient flow is intentional (user must confirm via modal), not a failure.

Consider resolving with a sentinel value instead:

Suggested fix
      dispatch(openSaveTransientRequestModal({ modalId, item, collection, isOpen: true }));
-     return reject();
+     return resolve({ deferred: true });

This signals to callers that the save is deferred to the modal, avoiding unhandled rejections while preserving the intended behavior.

🤖 Fix all issues with AI agents
In `@packages/bruno-app/src/components/CreateTransientRequest/index.js`:
- Around line 88-175: The handlers handleCreateHttpRequest,
handleCreateGraphQLRequest, handleCreateWebSocketRequest and
handleCreateGrpcRequest capture tempDirectory (via generateTransientRequestName)
and collectionPresets.requestUrl but only list [dispatch, collection]; update
each useCallback dependency array to include tempDirectory and
collectionPresets.requestUrl so the closures refresh when those values change
(i.e., change the dependency arrays for handleCreateHttpRequest,
handleCreateGraphQLRequest, handleCreateWebSocketRequest,
handleCreateGrpcRequest to [dispatch, collection, tempDirectory,
collectionPresets.requestUrl]).

In `@packages/bruno-app/src/components/SaveTransientRequest/index.js`:
- Around line 91-114: In handleConfirm, guard against requestName collapsing to
an empty sanitized filename: after computing sanitizedFilename =
sanitizeName(requestName.trim()) (and before calling resolveRequestFilename or
any IPC save), check that sanitizedFilename is non-empty and handle the error
path (e.g., show validation/notification and return) to avoid constructing
targetFilename or attempting the IPC save; update the logic around sanitizeName,
resolveRequestFilename, and any use of targetFilename so they only run when
sanitizedFilename has content.

In
`@packages/bruno-app/src/components/Sidebar/Collections/Collection/RemoveCollection/ConfirmCollectionCloseDrafts.js`:
- Around line 25-43: Both currentDrafts and currentTransientDrafts call
item.pathname.startsWith(tempDirectory) which will throw if tempDirectory is
falsy; update the predicate in both React.useMemo blocks (the filter callbacks)
to guard tempDirectory (e.g. tempDirectory &&
item.pathname.startsWith(tempDirectory)) and for transient detection fall back
to item.isTransient when tempDirectory is not set. Keep the rest of the mapping
(including adding collectionUid) the same and reference symbols: currentDrafts,
currentTransientDrafts, flattenItems, filter, isItemARequest, hasRequestChanges,
item.pathname, item.isTransient, collectionUid.

In `@packages/bruno-app/src/providers/ReduxStore/middlewares/tasks/middleware.js`:
- Around line 25-39: The transient detection currently uses "tempDirectory &&
task.itemPathname.startsWith(tempDirectory)" which treats items as non‑transient
when tempDirectory is not set; change this so missing tempDirectory defaults to
treating items as transient (i.e., set isTransient = tempDirectory ?
task.itemPathname.startsWith(tempDirectory) : true) before calling
listenerApi.dispatch(addTab(...)) so preview remains false for uncertain cases;
update the isTransient variable in the block that uses findCollectionByUid,
findItemInCollectionByPathname, getDefaultRequestPaneTab and addTab.

In `@packages/bruno-electron/src/app/collection-watcher.js`:
- Around line 871-931: The temp watcher is stored under
this.watchers[tempDirectoryPath] in addTempDirectoryWatcher but removeWatcher
only closes this.watchers[collectionPath], causing temp watchers to leak; fix by
ensuring removeWatcher also closes the tempDirectory watcher: either (A) when
adding a temp watcher, also store a mapping from collectionUid/collectionPath to
its tempDirectoryPath (or push tempDirectoryPath into
this.watchers[collectionPath] metadata) and update removeWatcher to look up and
close this.watchers[tempDirectoryPath] and delete that key, or (B) change
removeWatcher signature to accept tempDirectoryPath (or accept collectionUid and
resolve tempDirectoryPath using getCollectionPathFromTempDirectory / reverse
lookup) and explicitly call this.watchers[tempDirectoryPath].close() and delete
the entry; update addTempDirectoryWatcher to ensure the tempDirectoryPath key is
created and documented so removeWatcher can find and cleanup it.

In `@packages/bruno-electron/src/ipc/collection.js`:
- Around line 388-429: The handler for 'renderer:save-transient-request'
currently constructs targetPathname and writes to it without verifying
collection boundaries; call validatePathIsInsideCollection(targetPathname) (or
validatePathIsInsideCollection(targetDirname, targetFilename) if that overload
exists) right after computing targetPathname and before checking for conflicts
or performing any writes, and throw or reject when validation fails so the
subsequent steps in this function (the conflict check,
writeFile(targetPathname), and removePath(sourcePathname)) only run for
validated in-collection paths.
- Around line 86-107: The transient-detection logic in
findCollectionPathByItemPath treats any path under os.tmpdir() as transient and
returns null, breaking real collections stored in temp; change the detection so
a path is only treated as transient if it is under os.tmpdir() AND contains a
'bruno-' segment. Concretely, in findCollectionPathByItemPath locate the path
parts and the index for parts.findIndex(p => p.startsWith('bruno-')), then only
run the transient-handling block if filePath.startsWith(tmpDir) && index !== -1;
if there is no 'bruno-' segment, do not return null and allow normal collection
resolution to continue; keep the existing metadata read/JSON parse and its
catch-return-null behavior inside the bruno-specific branch.

In `@packages/bruno-electron/src/utils/collection.js`:
- Around line 406-410: The current transient detection marks any path under
os.tmpdir() as transient; change the check so request.isTransient is set true
only when the pathname is inside the temp dir AND uses the Bruno transient
prefix (e.g., directory or filename starting with "bruno-"); update the block
that references pathname and os.tmpdir() to perform a more specific test (for
example verify path.relative(os.tmpdir(), pathname) does not escape and that the
first path segment or basename startsWith "bruno-") and set request.isTransient
accordingly.

Comment on lines +25 to +39
const tempDirectory = state.collections.tempDirectories?.[collectionUid];
const openRequestTasks = filter(state.app.taskQueue, { type: taskTypes.OPEN_REQUEST });
each(openRequestTasks, (task) => {
if (collectionUid === task.collectionUid) {
const collection = findCollectionByUid(state.collections.collections, collectionUid);
if (collection && collection.mountStatus === 'mounted' && !collection.isLoading) {
const item = findItemInCollectionByPathname(collection, task.itemPathname);
const isTransient = tempDirectory && task.itemPathname.startsWith(tempDirectory);
if (item) {
listenerApi.dispatch(
addTab({
uid: item.uid,
collectionUid: collection.uid,
requestPaneTab: getDefaultRequestPaneTab(item)
requestPaneTab: getDefaultRequestPaneTab(item),
preview: !isTransient
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 transient detection when tempDirectory isn’t available.
If the temp path isn’t populated yet, transient items can be treated as non‑transient and opened with preview. Consider a fallback flag to avoid misclassification.

🔧 Suggested fix
-          const isTransient = tempDirectory && task.itemPathname.startsWith(tempDirectory);
+          const isTransient = tempDirectory
+            ? task.itemPathname.startsWith(tempDirectory)
+            : item?.isTransient;
📝 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
const tempDirectory = state.collections.tempDirectories?.[collectionUid];
const openRequestTasks = filter(state.app.taskQueue, { type: taskTypes.OPEN_REQUEST });
each(openRequestTasks, (task) => {
if (collectionUid === task.collectionUid) {
const collection = findCollectionByUid(state.collections.collections, collectionUid);
if (collection && collection.mountStatus === 'mounted' && !collection.isLoading) {
const item = findItemInCollectionByPathname(collection, task.itemPathname);
const isTransient = tempDirectory && task.itemPathname.startsWith(tempDirectory);
if (item) {
listenerApi.dispatch(
addTab({
uid: item.uid,
collectionUid: collection.uid,
requestPaneTab: getDefaultRequestPaneTab(item)
requestPaneTab: getDefaultRequestPaneTab(item),
preview: !isTransient
const tempDirectory = state.collections.tempDirectories?.[collectionUid];
const openRequestTasks = filter(state.app.taskQueue, { type: taskTypes.OPEN_REQUEST });
each(openRequestTasks, (task) => {
if (collectionUid === task.collectionUid) {
const collection = findCollectionByUid(state.collections.collections, collectionUid);
if (collection && collection.mountStatus === 'mounted' && !collection.isLoading) {
const item = findItemInCollectionByPathname(collection, task.itemPathname);
const isTransient = tempDirectory
? task.itemPathname.startsWith(tempDirectory)
: item?.isTransient;
if (item) {
listenerApi.dispatch(
addTab({
uid: item.uid,
collectionUid: collection.uid,
requestPaneTab: getDefaultRequestPaneTab(item),
preview: !isTransient
🤖 Prompt for AI Agents
In `@packages/bruno-app/src/providers/ReduxStore/middlewares/tasks/middleware.js`
around lines 25 - 39, The transient detection currently uses "tempDirectory &&
task.itemPathname.startsWith(tempDirectory)" which treats items as non‑transient
when tempDirectory is not set; change this so missing tempDirectory defaults to
treating items as transient (i.e., set isTransient = tempDirectory ?
task.itemPathname.startsWith(tempDirectory) : true) before calling
listenerApi.dispatch(addTab(...)) so preview remains false for uncertain cases;
update the isTransient variable in the block that uses findCollectionByUid,
findItemInCollectionByPathname, getDefaultRequestPaneTab and addTab.

Comment on lines +871 to +931
// Add watcher for transient directory
addTempDirectoryWatcher(win, tempDirectoryPath, collectionUid, collectionPath) {
if (this.watchers[tempDirectoryPath]) {
this.watchers[tempDirectoryPath].close();
}

// Ignore metadata.json file
const ignored = (filepath) => {
const basename = path.basename(filepath);
return basename === 'metadata.json';
};

setTimeout(() => {
const watcher = chokidar.watch(tempDirectoryPath, {
ignoreInitial: true, // Don't process existing files
usePolling: isWSLPath(tempDirectoryPath) ? true : false,
ignored,
persistent: true,
ignorePermissionErrors: true,
awaitWriteFinish: {
stabilityThreshold: 80,
pollInterval: 10
},
depth: 1, // Only watch the temp directory itself, not subdirectories
disableGlobbing: true
});

// Wrapper function to handle temp directory files
const addTempFile = async (pathname) => {
// Skip metadata.json
if (path.basename(pathname) === 'metadata.json') {
return;
}

// Get the actual collection path from metadata
const actualCollectionPath = this.getCollectionPathFromTempDirectory(tempDirectoryPath);
if (!actualCollectionPath) {
console.error(`Could not determine collection path for temp directory: ${tempDirectoryPath}`);
return;
}

// Use the collection format from the actual collection
const format = getCollectionFormat(actualCollectionPath);

// Only process request files
if (hasRequestExtension(pathname, format)) {
// Call the regular add function with the actual collection path
// This will hydrate and send the file to the renderer
await add(win, pathname, collectionUid, actualCollectionPath, false, this);
}
};

watcher
.on('add', (pathname) => addTempFile(pathname))
.on('error', (error) => {
console.error(`An error occurred in the temp directory watcher for: ${tempDirectoryPath}`, error);
});

this.watchers[tempDirectoryPath] = watcher;
}, 100);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find where temp directory watchers are removed / temp dirs cleaned up
rg -n "addTempDirectoryWatcher|getCollectionPathFromTempDirectory|tempDirectoryPath" packages/bruno-electron/src -g'*.js'
rg -n "removeWatcher|removePath|mkdtempSync" packages/bruno-electron/src -g'*.js'

Repository: usebruno/bruno

Length of output: 4128


🏁 Script executed:

#!/bin/bash
# Get removeWatcher implementation and context around where addTempDirectoryWatcher is called
sed -n '820,860p' packages/bruno-electron/src/app/collection-watcher.js

# Check what happens around the addTempDirectoryWatcher call
sed -n '1690,1710p' packages/bruno-electron/src/ipc/collection.js

# Check what happens in the remove collection flow
sed -n '910,920p' packages/bruno-electron/src/ipc/collection.js

# Look for cleanup of temp directories (rm, unlink, rmSync patterns)
rg -n "rm\(|rmSync|unlink|unlinktempDirectory" packages/bruno-electron/src/ipc/collection.js -A 2 -B 2

Repository: usebruno/bruno

Length of output: 3165


🏁 Script executed:

#!/bin/bash
# Search for any code that might close temp watchers or clean up temp directories
rg -n "this\.watchers\[tempDirectoryPath\]" packages/bruno-electron/src -A 1 -B 1
rg -n "tempDirectoryPath.*close\|tempDirectoryPath.*unlink\|tempDirectoryPath.*rm" packages/bruno-electron/src -A 2 -B 2
rg -n "fs\.rm\(.*tempo\|fs\.rmSync\(.*tempo" packages/bruno-electron/src
rg -n "mainWindow.*closed\|app.*quit" packages/bruno-electron/src/app/collection-watcher.js -A 5 -B 2

Repository: usebruno/bruno

Length of output: 711


Fix temp directory watcher cleanup—watchers leak on collection removal.

addTempDirectoryWatcher stores watchers under tempDirectoryPath key, but removeWatcher (called during collection removal) closes watchers under collectionPath key. These keys don't match, so temp watchers are never closed and temp directories created via fs.mkdtempSync() are never deleted. This causes a file descriptor and disk space leak.

Either:

  1. Pass tempDirectoryPath to removeWatcher and close it there, or
  2. Track and clean up temp watchers separately when collections are removed.
🤖 Prompt for AI Agents
In `@packages/bruno-electron/src/app/collection-watcher.js` around lines 871 -
931, The temp watcher is stored under this.watchers[tempDirectoryPath] in
addTempDirectoryWatcher but removeWatcher only closes
this.watchers[collectionPath], causing temp watchers to leak; fix by ensuring
removeWatcher also closes the tempDirectory watcher: either (A) when adding a
temp watcher, also store a mapping from collectionUid/collectionPath to its
tempDirectoryPath (or push tempDirectoryPath into this.watchers[collectionPath]
metadata) and update removeWatcher to look up and close
this.watchers[tempDirectoryPath] and delete that key, or (B) change
removeWatcher signature to accept tempDirectoryPath (or accept collectionUid and
resolve tempDirectoryPath using getCollectionPathFromTempDirectory / reverse
lookup) and explicitly call this.watchers[tempDirectoryPath].close() and delete
the entry; update addTempDirectoryWatcher to ensure the tempDirectoryPath key is
created and documented so removeWatcher can find and cleanup it.

…ogic

* Updated CreateTransientRequest to utilize useMemo for improved performance and prevent unnecessary re-renders.
* Refactored generateTransientRequestName to focus solely on transient requests, removing tempDirectory dependency.
* Streamlined SaveTransientRequest by consolidating form reset logic and removing unused state variables.
* Improved ConfirmCollectionCloseDrafts to differentiate between transient and non-transient drafts more effectively.
* Cleaned up imports and optimized Redux actions for better maintainability.
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: 4

🤖 Fix all issues with AI agents
In `@packages/bruno-app/src/components/CreateTransientRequest/index.js`:
- Around line 58-60: The memoized "collection" computed with useMemo uses
collectionUid inside the find callback but only depends on collections; update
the dependency array of the useMemo that defines collection to include
collectionUid as well as collections so the memo recalculates when collectionUid
changes (i.e., in the useMemo surrounding the find call that sets collection).

In `@packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js`:
- Around line 144-166: saveRequest currently computes isTransient by calling
item.pathname.startsWith(tempDirectory) without first ensuring tempDirectory
exists, causing exceptions or misclassification when the temp directory isn't
ready; update the isTransient calculation in saveRequest to first check
tempDirectory is truthy before using startsWith and otherwise fall back to
item.isTransient (e.g., compute isTransient = tempDirectory ?
item.pathname.startsWith(tempDirectory) : !!item.isTransient), keeping
references to item, tempDirectory, saveRequest, findItemInCollection,
openSaveTransientRequestModal and uuid unchanged so the transient modal flow
still dispatches correctly when transient is detected.
- Around line 1000-1087: The pasteItem action uses an async function as the
Promise executor (in the returned new Promise(async (resolve, reject) => { ...
})), which is unsafe; change pasteItem to avoid an async Promise executor by
returning a normal Promise that runs an immediately-invoked async function
(IIFE) or by making the outer function async and awaiting the operations
directly: move the try/catch body into an async IIFE (or top-level async
function) and call it, then resolve/reject based on its result; update the same
pattern in importCollection (the other occurrence) and keep the internal logic
(findCollectionByUid, generateUniqueName, ipcRenderer.invoke,
itemSchema.validate, dispatch insertTaskIntoQueue, etc.) identical while
removing async from the Promise executor to ensure resolver callbacks are not
blocked.

In `@packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js`:
- Around line 3158-3163: The clear operation
collectionClearOauth2CredentialsByUrl currently filters
collectionOauth2Credentials by url and collectionUid only, while
collectionGetOauth2CredentialsByUrl still checks credentialsId; make these
behaviors consistent by either (A) updating
collectionClearOauth2CredentialsByUrl to also check creds.credentialsId ===
credentialsId when removing entries, or (B) changing
collectionGetOauth2CredentialsByUrl to ignore credentialsId (matching the
current clear behavior); if the asymmetry is intentional, add a clarifying
comment near collectionClearOauth2CredentialsByUrl and
collectionGetOauth2CredentialsByUrl explaining why one removes all creds for a
URL but the getter requires credentialsId.

Comment on lines +58 to +60
const collection = useMemo(() => {
return collections?.find((c) => c.uid === collectionUid);
}, [collections]);
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 collectionUid in useMemo dependency array.

The memo uses collectionUid in the find callback but only lists collections as a dependency. This can lead to stale closures where collection doesn't update when collectionUid changes.

🔧 Suggested fix
   const collection = useMemo(() => {
     return collections?.find((c) => c.uid === collectionUid);
-  }, [collections]);
+  }, [collections, collectionUid]);
🤖 Prompt for AI Agents
In `@packages/bruno-app/src/components/CreateTransientRequest/index.js` around
lines 58 - 60, The memoized "collection" computed with useMemo uses
collectionUid inside the find callback but only depends on collections; update
the dependency array of the useMemo that defines collection to include
collectionUid as well as collections so the memo recalculates when collectionUid
changes (i.e., in the useMemo surrounding the find call that sets collection).

Comment on lines +144 to +166
export const saveRequest
= (itemUid, collectionUid, silent = false) =>
(dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
const tempDirectory = state.collections.tempDirectories?.[collectionUid];
return new Promise((resolve, reject) => {
if (!collection) {
return reject(new Error('Collection not found'));
}

return new Promise((resolve, reject) => {
if (!collection) {
return reject(new Error('Collection not found'));
}
const collectionCopy = cloneDeep(collection);
const item = findItemInCollection(collectionCopy, itemUid);
if (!item) {
return reject(new Error('Not able to locate item'));
}

const collectionCopy = cloneDeep(collection);
const item = findItemInCollection(collectionCopy, itemUid);
if (!item) {
return reject(new Error('Not able to locate item'));
}
const isTransient = tempDirectory && item.pathname.startsWith(tempDirectory);
if (isTransient) {
const modalId = uuid();
dispatch(openSaveTransientRequestModal({ modalId, item, collection, isOpen: true }));
return reject();
}
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

Guard transient flows when the temp directory isn’t ready.

If isTransient is true but tempDirectory is missing, path.join will throw and startsWith can misclassify items. Add a guard and use item.isTransient as a fallback.

🛡️ Suggested guard
-        const isTransient = tempDirectory && item.pathname.startsWith(tempDirectory);
+        const isTransient = item.isTransient || (tempDirectory && item.pathname && item.pathname.startsWith(tempDirectory));
-    const tempDirectory = isTransient ? state.collections.tempDirectories?.[collectionUid] : null;
+    const tempDirectory = isTransient ? state.collections.tempDirectories?.[collectionUid] : null;
+    if (isTransient && !tempDirectory) {
+      return reject(new Error('Transient directory is not ready yet. Please retry.'));
+    }

Also applies to: 1324-1752

🤖 Prompt for AI Agents
In `@packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js`
around lines 144 - 166, saveRequest currently computes isTransient by calling
item.pathname.startsWith(tempDirectory) without first ensuring tempDirectory
exists, causing exceptions or misclassification when the temp directory isn't
ready; update the isTransient calculation in saveRequest to first check
tempDirectory is truthy before using startsWith and otherwise fall back to
item.isTransient (e.g., compute isTransient = tempDirectory ?
item.pathname.startsWith(tempDirectory) : !!item.isTransient), keeping
references to item, tempDirectory, saveRequest, findItemInCollection,
openSaveTransientRequestModal and uuid unchanged so the transient modal flow
still dispatches correctly when transient is detected.

Comment on lines +1000 to +1087
export const pasteItem
= (targetCollectionUid, targetItemUid = null) =>
(dispatch, getState) => {
const state = getState();

const clipboardResult = brunoClipboard.read();
const clipboardResult = brunoClipboard.read();

if (!clipboardResult.hasData) {
return Promise.reject(new Error('No item in clipboard'));
}
if (!clipboardResult.hasData) {
return Promise.reject(new Error('No item in clipboard'));
}

const targetCollection = findCollectionByUid(state.collections.collections, targetCollectionUid);
const targetCollection = findCollectionByUid(state.collections.collections, targetCollectionUid);

if (!targetCollection) {
return Promise.reject(new Error('Target collection not found'));
}
if (!targetCollection) {
return Promise.reject(new Error('Target collection not found'));
}

return new Promise(async (resolve, reject) => {
try {
for (const clipboardItem of clipboardResult.items) {
const copiedItem = cloneDeep(clipboardItem);

const targetCollectionCopy = cloneDeep(targetCollection);
let targetItem = null;
let targetParentPathname = targetCollection.pathname;

// If targetItemUid is provided, we're pasting into a folder
if (targetItemUid) {
targetItem = findItemInCollection(targetCollectionCopy, targetItemUid);
if (!targetItem) {
return reject(new Error('Target folder not found'));
}
if (!isItemAFolder(targetItem)) {
return reject(new Error('Target must be a folder or collection'));
}
targetParentPathname = targetItem.pathname;
}
return new Promise(async (resolve, reject) => {
try {
for (const clipboardItem of clipboardResult.items) {
const copiedItem = cloneDeep(clipboardItem);

const targetCollectionCopy = cloneDeep(targetCollection);
let targetItem = null;
let targetParentPathname = targetCollection.pathname;

// If targetItemUid is provided, we're pasting into a folder
if (targetItemUid) {
targetItem = findItemInCollection(targetCollectionCopy, targetItemUid);
if (!targetItem) {
return reject(new Error('Target folder not found'));
}
if (!isItemAFolder(targetItem)) {
return reject(new Error('Target must be a folder or collection'));
}
targetParentPathname = targetItem.pathname;
}

const existingItems = targetItem ? targetItem.items : targetCollection.items;
const existingItems = targetItem ? targetItem.items : targetCollection.items;

// Handle folder pasting
if (isItemAFolder(copiedItem)) {
// Generate unique name for folder
const { newName, newFilename } = generateUniqueName(copiedItem.name, existingItems, true);
// Handle folder pasting
if (isItemAFolder(copiedItem)) {
// Generate unique name for folder
const { newName, newFilename } = generateUniqueName(copiedItem.name, existingItems, true);

set(copiedItem, 'name', newName);
set(copiedItem, 'filename', newFilename);
set(copiedItem, 'root.meta.name', newName);
set(copiedItem, 'root.meta.seq', (existingItems?.length ?? 0) + 1);
set(copiedItem, 'name', newName);
set(copiedItem, 'filename', newFilename);
set(copiedItem, 'root.meta.name', newName);
set(copiedItem, 'root.meta.seq', (existingItems?.length ?? 0) + 1);

const fullPathname = path.join(targetParentPathname, newFilename);
const { ipcRenderer } = window;
const fullPathname = path.join(targetParentPathname, newFilename);
const { ipcRenderer } = window;

await ipcRenderer.invoke('renderer:clone-folder', copiedItem, fullPathname, targetCollection.pathname);
} else {
// Handle request pasting
// Generate unique name for request
const { newName, newFilename } = generateUniqueName(copiedItem.name, existingItems, false);
await ipcRenderer.invoke('renderer:clone-folder', copiedItem, fullPathname, targetCollection.pathname);
} else {
// Handle request pasting
// Generate unique name for request
const { newName, newFilename } = generateUniqueName(copiedItem.name, existingItems, false);

const filename = resolveRequestFilename(newFilename, targetCollection.format);
const itemToSave = refreshUidsInItem(transformRequestToSaveToFilesystem(copiedItem));
set(itemToSave, 'name', trim(newName));
set(itemToSave, 'filename', trim(filename));
const filename = resolveRequestFilename(newFilename, targetCollection.format);
const itemToSave = refreshUidsInItem(transformRequestToSaveToFilesystem(copiedItem));
set(itemToSave, 'name', trim(newName));
set(itemToSave, 'filename', trim(filename));

const fullPathname = path.join(targetParentPathname, filename);
const { ipcRenderer } = window;
const requestItems = filter(existingItems, (i) => i.type !== 'folder');
itemToSave.seq = requestItems ? requestItems.length + 1 : 1;
const fullPathname = path.join(targetParentPathname, filename);
const { ipcRenderer } = window;
const requestItems = filter(existingItems, (i) => i.type !== 'folder');
itemToSave.seq = requestItems ? requestItems.length + 1 : 1;

await itemSchema.validate(itemToSave);
await ipcRenderer.invoke('renderer:new-request', fullPathname, itemToSave, targetCollection.format);
await itemSchema.validate(itemToSave);
await ipcRenderer.invoke('renderer:new-request', fullPathname, itemToSave, targetCollection.format);

dispatch(insertTaskIntoQueue({
uid: uuid(),
type: 'OPEN_REQUEST',
collectionUid: targetCollectionUid,
itemPathname: fullPathname
}));
}
}
dispatch(
insertTaskIntoQueue({
uid: uuid(),
type: 'OPEN_REQUEST',
collectionUid: targetCollectionUid,
itemPathname: fullPathname
})
);
}
}

resolve();
} catch (error) {
reject(error);
}
});
};
resolve();
} catch (error) {
reject(error);
}
});
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:

# First, find the file
fd -type f -name "actions.js" | grep -E "collections"

Repository: usebruno/bruno

Length of output: 228


🏁 Script executed:

# Check the file length and get line counts around the mentioned ranges
wc -l packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js

Repository: usebruno/bruno

Length of output: 136


🏁 Script executed:

# Extract the code around lines 1000-1087 to verify the async executor pattern
sed -n '1000,1087p' packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js | head -100

Repository: usebruno/bruno

Length of output: 3650


🏁 Script executed:

# Extract the code around lines 2727-2761 to check the second instance
sed -n '2727,2761p' packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js

Repository: usebruno/bruno

Length of output: 1175


Wrap async work in an IIFE instead of using async Promise executor.

Both pasteItem (lines 1000–1087) and importCollection (lines 2727–2761) use async Promise executors, which blocks the resolver callbacks. Refactor to use an IIFE:

♻️ Refactor pattern
-return new Promise(async (resolve, reject) => {
+return new Promise((resolve, reject) => {
+  (async () => {
     try {
       // async work
-    } catch (error) {
-      reject(error);
-    }
-  });
+    } catch (error) {
+      reject(error);
+    }
+  })();
+});
📝 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
export const pasteItem
= (targetCollectionUid, targetItemUid = null) =>
(dispatch, getState) => {
const state = getState();
const clipboardResult = brunoClipboard.read();
const clipboardResult = brunoClipboard.read();
if (!clipboardResult.hasData) {
return Promise.reject(new Error('No item in clipboard'));
}
if (!clipboardResult.hasData) {
return Promise.reject(new Error('No item in clipboard'));
}
const targetCollection = findCollectionByUid(state.collections.collections, targetCollectionUid);
const targetCollection = findCollectionByUid(state.collections.collections, targetCollectionUid);
if (!targetCollection) {
return Promise.reject(new Error('Target collection not found'));
}
if (!targetCollection) {
return Promise.reject(new Error('Target collection not found'));
}
return new Promise(async (resolve, reject) => {
try {
for (const clipboardItem of clipboardResult.items) {
const copiedItem = cloneDeep(clipboardItem);
const targetCollectionCopy = cloneDeep(targetCollection);
let targetItem = null;
let targetParentPathname = targetCollection.pathname;
// If targetItemUid is provided, we're pasting into a folder
if (targetItemUid) {
targetItem = findItemInCollection(targetCollectionCopy, targetItemUid);
if (!targetItem) {
return reject(new Error('Target folder not found'));
}
if (!isItemAFolder(targetItem)) {
return reject(new Error('Target must be a folder or collection'));
}
targetParentPathname = targetItem.pathname;
}
return new Promise(async (resolve, reject) => {
try {
for (const clipboardItem of clipboardResult.items) {
const copiedItem = cloneDeep(clipboardItem);
const targetCollectionCopy = cloneDeep(targetCollection);
let targetItem = null;
let targetParentPathname = targetCollection.pathname;
// If targetItemUid is provided, we're pasting into a folder
if (targetItemUid) {
targetItem = findItemInCollection(targetCollectionCopy, targetItemUid);
if (!targetItem) {
return reject(new Error('Target folder not found'));
}
if (!isItemAFolder(targetItem)) {
return reject(new Error('Target must be a folder or collection'));
}
targetParentPathname = targetItem.pathname;
}
const existingItems = targetItem ? targetItem.items : targetCollection.items;
const existingItems = targetItem ? targetItem.items : targetCollection.items;
// Handle folder pasting
if (isItemAFolder(copiedItem)) {
// Generate unique name for folder
const { newName, newFilename } = generateUniqueName(copiedItem.name, existingItems, true);
// Handle folder pasting
if (isItemAFolder(copiedItem)) {
// Generate unique name for folder
const { newName, newFilename } = generateUniqueName(copiedItem.name, existingItems, true);
set(copiedItem, 'name', newName);
set(copiedItem, 'filename', newFilename);
set(copiedItem, 'root.meta.name', newName);
set(copiedItem, 'root.meta.seq', (existingItems?.length ?? 0) + 1);
set(copiedItem, 'name', newName);
set(copiedItem, 'filename', newFilename);
set(copiedItem, 'root.meta.name', newName);
set(copiedItem, 'root.meta.seq', (existingItems?.length ?? 0) + 1);
const fullPathname = path.join(targetParentPathname, newFilename);
const { ipcRenderer } = window;
const fullPathname = path.join(targetParentPathname, newFilename);
const { ipcRenderer } = window;
await ipcRenderer.invoke('renderer:clone-folder', copiedItem, fullPathname, targetCollection.pathname);
} else {
// Handle request pasting
// Generate unique name for request
const { newName, newFilename } = generateUniqueName(copiedItem.name, existingItems, false);
await ipcRenderer.invoke('renderer:clone-folder', copiedItem, fullPathname, targetCollection.pathname);
} else {
// Handle request pasting
// Generate unique name for request
const { newName, newFilename } = generateUniqueName(copiedItem.name, existingItems, false);
const filename = resolveRequestFilename(newFilename, targetCollection.format);
const itemToSave = refreshUidsInItem(transformRequestToSaveToFilesystem(copiedItem));
set(itemToSave, 'name', trim(newName));
set(itemToSave, 'filename', trim(filename));
const filename = resolveRequestFilename(newFilename, targetCollection.format);
const itemToSave = refreshUidsInItem(transformRequestToSaveToFilesystem(copiedItem));
set(itemToSave, 'name', trim(newName));
set(itemToSave, 'filename', trim(filename));
const fullPathname = path.join(targetParentPathname, filename);
const { ipcRenderer } = window;
const requestItems = filter(existingItems, (i) => i.type !== 'folder');
itemToSave.seq = requestItems ? requestItems.length + 1 : 1;
const fullPathname = path.join(targetParentPathname, filename);
const { ipcRenderer } = window;
const requestItems = filter(existingItems, (i) => i.type !== 'folder');
itemToSave.seq = requestItems ? requestItems.length + 1 : 1;
await itemSchema.validate(itemToSave);
await ipcRenderer.invoke('renderer:new-request', fullPathname, itemToSave, targetCollection.format);
await itemSchema.validate(itemToSave);
await ipcRenderer.invoke('renderer:new-request', fullPathname, itemToSave, targetCollection.format);
dispatch(insertTaskIntoQueue({
uid: uuid(),
type: 'OPEN_REQUEST',
collectionUid: targetCollectionUid,
itemPathname: fullPathname
}));
}
}
dispatch(
insertTaskIntoQueue({
uid: uuid(),
type: 'OPEN_REQUEST',
collectionUid: targetCollectionUid,
itemPathname: fullPathname
})
);
}
}
resolve();
} catch (error) {
reject(error);
}
});
};
resolve();
} catch (error) {
reject(error);
}
});
export const pasteItem
= (targetCollectionUid, targetItemUid = null) =>
(dispatch, getState) => {
const state = getState();
const clipboardResult = brunoClipboard.read();
if (!clipboardResult.hasData) {
return Promise.reject(new Error('No item in clipboard'));
}
const targetCollection = findCollectionByUid(state.collections.collections, targetCollectionUid);
if (!targetCollection) {
return Promise.reject(new Error('Target collection not found'));
}
return new Promise((resolve, reject) => {
(async () => {
try {
for (const clipboardItem of clipboardResult.items) {
const copiedItem = cloneDeep(clipboardItem);
const targetCollectionCopy = cloneDeep(targetCollection);
let targetItem = null;
let targetParentPathname = targetCollection.pathname;
// If targetItemUid is provided, we're pasting into a folder
if (targetItemUid) {
targetItem = findItemInCollection(targetCollectionCopy, targetItemUid);
if (!targetItem) {
return reject(new Error('Target folder not found'));
}
if (!isItemAFolder(targetItem)) {
return reject(new Error('Target must be a folder or collection'));
}
targetParentPathname = targetItem.pathname;
}
const existingItems = targetItem ? targetItem.items : targetCollection.items;
// Handle folder pasting
if (isItemAFolder(copiedItem)) {
// Generate unique name for folder
const { newName, newFilename } = generateUniqueName(copiedItem.name, existingItems, true);
set(copiedItem, 'name', newName);
set(copiedItem, 'filename', newFilename);
set(copiedItem, 'root.meta.name', newName);
set(copiedItem, 'root.meta.seq', (existingItems?.length ?? 0) + 1);
const fullPathname = path.join(targetParentPathname, newFilename);
const { ipcRenderer } = window;
await ipcRenderer.invoke('renderer:clone-folder', copiedItem, fullPathname, targetCollection.pathname);
} else {
// Handle request pasting
// Generate unique name for request
const { newName, newFilename } = generateUniqueName(copiedItem.name, existingItems, false);
const filename = resolveRequestFilename(newFilename, targetCollection.format);
const itemToSave = refreshUidsInItem(transformRequestToSaveToFilesystem(copiedItem));
set(itemToSave, 'name', trim(newName));
set(itemToSave, 'filename', trim(filename));
const fullPathname = path.join(targetParentPathname, filename);
const { ipcRenderer } = window;
const requestItems = filter(existingItems, (i) => i.type !== 'folder');
itemToSave.seq = requestItems ? requestItems.length + 1 : 1;
await itemSchema.validate(itemToSave);
await ipcRenderer.invoke('renderer:new-request', fullPathname, itemToSave, targetCollection.format);
dispatch(
insertTaskIntoQueue({
uid: uuid(),
type: 'OPEN_REQUEST',
collectionUid: targetCollectionUid,
itemPathname: fullPathname
})
);
}
}
resolve();
} catch (error) {
reject(error);
}
})();
});
};
🧰 Tools
🪛 Biome (2.1.2)

[error] 1017-1087: Promise executor functions should not be async.

(lint/suspicious/noAsyncPromiseExecutor)

🤖 Prompt for AI Agents
In `@packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js`
around lines 1000 - 1087, The pasteItem action uses an async function as the
Promise executor (in the returned new Promise(async (resolve, reject) => { ...
})), which is unsafe; change pasteItem to avoid an async Promise executor by
returning a normal Promise that runs an immediately-invoked async function
(IIFE) or by making the outer function async and awaiting the operations
directly: move the try/catch body into an async IIFE (or top-level async
function) and call it, then resolve/reject based on its result; update the same
pattern in importCollection (the other occurrence) and keep the internal logic
(findCollectionByUid, generateUniqueName, ipcRenderer.invoke,
itemSchema.validate, dispatch insertTaskIntoQueue, etc.) identical while
removing async from the Promise executor to ensure resolver callbacks are not
blocked.

Comment on lines 3158 to 3163
const filteredOauth2Credentials = filter(
collectionOauth2Credentials,
(creds) =>
!(creds.url === url && creds.collectionUid === collectionUid)
(creds) => !(creds.url === url && creds.collectionUid === collectionUid)
);
collection.oauth2Credentials = filteredOauth2Credentials;
}
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

Inconsistent filter condition between clear and get operations.

collectionClearOauth2CredentialsByUrl filters without credentialsId, but collectionGetOauth2CredentialsByUrl at Line 3171 still uses credentialsId in its lookup. This asymmetry means clearing credentials will remove all credentials for a URL regardless of credentialsId, while getting still requires it.

If intentional, consider adding a comment explaining the design choice.

🤖 Prompt for AI Agents
In `@packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js`
around lines 3158 - 3163, The clear operation
collectionClearOauth2CredentialsByUrl currently filters
collectionOauth2Credentials by url and collectionUid only, while
collectionGetOauth2CredentialsByUrl still checks credentialsId; make these
behaviors consistent by either (A) updating
collectionClearOauth2CredentialsByUrl to also check creds.credentialsId ===
credentialsId when removing entries, or (B) changing
collectionGetOauth2CredentialsByUrl to ignore credentialsId (matching the
current clear behavior); if the asymmetry is intentional, add a clarifying
comment near collectionClearOauth2CredentialsByUrl and
collectionGetOauth2CredentialsByUrl explaining why one removes all creds for a
URL but the getter requires credentialsId.

* Refactored SaveTransientRequest and its container to improve modal handling for unsaved transient requests.
* Introduced state management for opening specific modals and added functionality to discard all unsaved requests.
* Updated Redux actions to manage transient request modals more effectively, ensuring no duplicates are added.
* Enhanced user interface to display a list of unsaved requests with options to save or discard them.
* Added useEffect to reset openItemUid when the corresponding modal is no longer present.
* Implemented functionality to close all tabs associated with transient requests and show a success message upon discarding them.
* Removed unnecessary modal close handler and streamlined modal opening logic for better clarity and performance.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js (1)

1323-1414: Fail fast when the transient temp directory is missing.

path.join(tempDirectory, ...) will throw if the temp directory isn’t hydrated yet, which can happen before mount completes. Add a guard in all three creators.

🛡️ Suggested guard (apply in newHttpRequest/newGrpcRequest/newWsRequest)
-    const tempDirectory = isTransient ? state.collections.tempDirectories?.[collectionUid] : null;
+    const tempDirectory = isTransient ? state.collections.tempDirectories?.[collectionUid] : null;
+    if (isTransient && !tempDirectory) {
+      return reject(new Error('Transient directory is not ready yet. Please retry.'));
+    }

Also applies to: 1500-1577, 1638-1715

🤖 Fix all issues with AI agents
In `@packages/bruno-electron/src/app/collection-watcher.js`:
- Around line 866-877: The watcher can crash on invalid/stale metadata because
getCollectionFormat (and subsequent add/unlink logic) may throw; wrap the format
resolution and any downstream operations that act on the temp directory (calls
to getCollectionFormat and the add/unlink handlers referenced in the watcher
flow) in a try/catch inside the async handler so exceptions are caught, logged
(including the tempDirectoryPath and error), and the handler returns/continues
without rethrowing; apply the same defensive try/catch pattern around the
related block that processes temp directories (the code range handling format
resolution and add/unlink between the getCollectionPathFromTempDirectory call
and the end of that handler).
🧹 Nitpick comments (1)
packages/bruno-app/src/components/SaveTransientRequest/index.js (1)

109-113: Preserve the user-facing name; only sanitize the filename.

Right now the saved request name becomes the sanitized filename, which can strip spaces/symbols from what the user typed. Consider using the trimmed name for itemToSave.name and keep sanitizedFilename just for the file path.

♻️ Suggested tweak
-      const itemToSave = latestItem.draft ? { ...latestItem, ...latestItem.draft } : { ...latestItem };
-      itemToSave.name = sanitizedFilename;
+      const itemToSave = latestItem.draft ? { ...latestItem, ...latestItem.draft } : { ...latestItem };
+      itemToSave.name = trimmedName;

Comment on lines +866 to +877
// Helper function to get collection path from temp directory metadata
getCollectionPathFromTempDirectory(tempDirectoryPath) {
const metadataPath = path.join(tempDirectoryPath, 'metadata.json');
try {
const metadataContent = fs.readFileSync(metadataPath, 'utf8');
const metadata = JSON.parse(metadataContent);
return metadata.collectionPath;
} catch (error) {
console.error(`Error reading metadata from temp directory ${tempDirectoryPath}:`, error);
return null;
}
}
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

Guard temp file processing against format/metadata failures.

getCollectionFormat(actualCollectionPath) can throw (missing/invalid config or stale metadata), which will reject the async handler and can surface as unhandled rejections in the watcher. Wrapping format resolution (and downstream add/unlink) in a try/catch keeps the watcher resilient.

🛠️ Suggested hardening
       const addTempFile = async (pathname) => {
         // Skip metadata.json
         if (path.basename(pathname) === 'metadata.json') {
           return;
         }

         // Get the actual collection path from metadata
         const actualCollectionPath = this.getCollectionPathFromTempDirectory(tempDirectoryPath);
         if (!actualCollectionPath) {
           console.error(`Could not determine collection path for temp directory: ${tempDirectoryPath}`);
           return;
         }

-        // Use the collection format from the actual collection
-        const format = getCollectionFormat(actualCollectionPath);
+        // Use the collection format from the actual collection
+        let format;
+        try {
+          format = getCollectionFormat(actualCollectionPath);
+        } catch (error) {
+          console.error('Could not determine collection format for', actualCollectionPath, error);
+          return;
+        }

         // Only process request files
         if (hasRequestExtension(pathname, format)) {
           // Call the regular add function with the actual collection path
           // This will hydrate and send the file to the renderer
           await add(win, pathname, collectionUid, actualCollectionPath, false, this);
         }
       };
       const unlinkTempFile = async (pathname) => {
         // Skip metadata.json
         if (path.basename(pathname) === 'metadata.json') {
           return;
         }

         // Get the actual collection path from metadata
         const actualCollectionPath = this.getCollectionPathFromTempDirectory(tempDirectoryPath);
         if (!actualCollectionPath) {
           console.error(`Could not determine collection path for temp directory: ${tempDirectoryPath}`);
           return;
         }

-        // Use the collection format from the actual collection
-        const format = getCollectionFormat(actualCollectionPath);
+        // Use the collection format from the actual collection
+        let format;
+        try {
+          format = getCollectionFormat(actualCollectionPath);
+        } catch (error) {
+          console.error('Could not determine collection format for', actualCollectionPath, error);
+          return;
+        }

         // Only process request files
         if (hasRequestExtension(pathname, format)) {
           // Call the regular unlink function with the actual collection path
           await unlink(win, pathname, collectionUid, actualCollectionPath);
         }
       };

Also applies to: 879-966

🤖 Prompt for AI Agents
In `@packages/bruno-electron/src/app/collection-watcher.js` around lines 866 -
877, The watcher can crash on invalid/stale metadata because getCollectionFormat
(and subsequent add/unlink logic) may throw; wrap the format resolution and any
downstream operations that act on the temp directory (calls to
getCollectionFormat and the add/unlink handlers referenced in the watcher flow)
in a try/catch inside the async handler so exceptions are caught, logged
(including the tempDirectoryPath and error), and the handler returns/continues
without rethrowing; apply the same defensive try/catch pattern around the
related block that processes temp directories (the code range handling format
resolution and add/unlink between the getCollectionPathFromTempDirectory call
and the end of that handler).

const handleSaveAll = () => {
// If there are transient drafts, we can't proceed with batch save
if (currentTransientDrafts.length > 0) {
toast.error('Please save or discard transient requests first');
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not sure about this functionality, I'm guessing this was discussed with product but I remember saying we save all into the root if they select save

Comment on lines +143 to +151
{saveTransientRequestModals.length === 1 ? (
<SaveTransientRequest
item={saveTransientRequestModals[0].item}
collection={saveTransientRequestModals[0].collection}
isOpen={true}
/>
) : saveTransientRequestModals.length > 1 ? (
<SaveTransientRequestContainer />
) : null}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Wrap into a sub component with a cleaner if else condition, this will become unreadable quickly

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

return basename === 'metadata.json';
};

setTimeout(() => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Don't see the point of the setTimeout but if the delay is irrelevant (considering 100 was passed), just use a setImmediate instead to push this into the timer thread

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Removing the timeout entirely. The caller of addTempDirectoryWatcher creates the directory and the metadata file before calling addTempDirectoryWatcher. Adding timeout or immediate is of no use and creates an unnecessary async call. Removing it.

Copy link
Collaborator

@sid-bruno sid-bruno left a comment

Choose a reason for hiding this comment

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

General review, there's a lot of whitespace changes that will need to be either considered during sync or reset and re-added into the diffs.

I've cc'd @bijin-bruno for references in files that conflict during sync because of this, please take @chirag-bruno into it when doing the sync to be safe.

There's some minor comments for additional calls and work being done, which should be fixed.

There's 0 tests in the PR, please add in basic request tests for all 3 types (http, grpc, ws) and make sure they work

You can add these tests directly in their respective tests files with the modification being the creation of these transient requests

…ction actions

* Consolidated multiple lines of code into single lines for better readability in ConfirmCollectionCloseDrafts and actions.js.
* Enhanced consistency in the formatting of function parameters and return statements across the collections slice.
* Removed unnecessary line breaks and improved the structure of the code for easier maintenance.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@packages/bruno-app/src/components/Sidebar/Collections/Collection/RemoveCollection/ConfirmCollectionCloseDrafts.js`:
- Around line 60-72: The current catch for
dispatch(saveMultipleRequests(currentDrafts)) duplicates an existing toast from
saveMultipleRequests; keep the catch to handle rejections but remove the
redundant toast.error call so it doesn't show the same message twice.
Specifically, inside the block that calls
dispatch(saveMultipleRequests(currentDrafts)) → .catch(...), delete the
toast.error('Failed to save requests!') but still ensure the promise is handled
(e.g., leave an empty catch or rethrow/log silently) and preserve the subsequent
flow that dispatches removeCollection(collectionUid), toast.success and onClose
in the success path.

In `@packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js`:
- Around line 2643-2655: The catch handler in openMultipleCollections drops the
original error by calling reject() with no argument; update the catch to call
reject(err) so the original error is propagated to callers (or alternatively
return ipcRenderer.invoke(...) directly from openMultipleCollections to
naturally propagate the promise rejection); locate the function
openMultipleCollections in the Redux store collections actions and change the
.catch((err) => { reject(); }) to .catch((err) => { reject(err); }) (or remove
the manual Promise wrapper and return
ipcRenderer.invoke('renderer:open-multiple-collections', collectionPaths,
options)).

…tions

* Consolidated multiple lines of code into single lines for better readability in middleware.js and actions.js.
* Enhanced consistency in formatting function parameters and return statements across the collections slice.
* Removed unnecessary line breaks and improved the structure of the code for easier maintenance.
* Streamlined dispatch calls for better clarity and performance.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js (1)

1246-1348: Guard transient creation when the temp directory isn’t ready.

When isTransient is true but tempDirectory is missing, path.join will throw and duplicate checks become unreliable. Add an early guard before using the temp path (apply the same pattern in newHttpRequest, newGrpcRequest, and newWsRequest).

🛠️ Suggested fix (apply in each transient branch)
-    const tempDirectory = isTransient ? state.collections.tempDirectories?.[collectionUid] : null;
+    const tempDirectory = isTransient ? state.collections.tempDirectories?.[collectionUid] : null;
+    if (isTransient && !tempDirectory) {
+      return reject(new Error('Transient directory is not ready yet. Please retry.'));
+    }

Also applies to: 1424-1537, 1551-1664

🤖 Fix all issues with AI agents
In `@packages/bruno-app/src/providers/ReduxStore/middlewares/tasks/middleware.js`:
- Around line 6-8: Remove the unused imports by editing the import statements:
delete closeAllCollectionTabs from the import from
'providers/ReduxStore/slices/tabs' and delete flattenItems from the import from
'utils/collections/index' so only actually used symbols (e.g., addTab,
closeTabs, collectionAddFileEvent, collectionChangeFileEvent,
findCollectionByUid, findItemInCollectionByPathname, getDefaultRequestPaneTab,
findItemInCollectionByItemUid, findItemInCollection) remain.

…actions

* Improved formatting and structure in middleware.js for dispatch calls.
* Streamlined comments and indentation in actions.js for better clarity.
* Consolidated multiple lines into single lines where appropriate to enhance readability.
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: 3

🤖 Fix all issues with AI agents
In `@packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js`:
- Around line 1552-1554: The code computes tempDirectory with const
tempDirectory = isTransient ? state.collections.tempDirectories?.[collectionUid]
: null but doesn't guard for undefined before using it in the surrounding
action; update the same action (the function that handles creating requests in
packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js — the
blocks that mirror newHttpRequest and newGrpcRequest) to early-return or handle
the case when tempDirectory is undefined (e.g., if isTransient && !tempDirectory
then abort/throw/log and skip further processing), referencing the same symbols:
tempDirectory, isTransient, state.collections.tempDirectories, and collectionUid
so the behavior matches newHttpRequest/newGrpcRequest; apply the same guard
changes to the other occurrence noted (~lines 1596-1628).
- Around line 1258-1260: The code assumes tempDirectory is a string when
isTransient is true but it can be undefined; update the logic around the
tempDirectory lookup (state.collections.tempDirectories?.[collectionUid]) and
all uses (the startsWith check and path.join calls) to guard for a non-empty
string before using string methods or path.join: check that tempDirectory is
truthy and of type string (e.g., tempDirectory && typeof tempDirectory ===
'string') before calling tempDirectory.startsWith(...) or
path.join(tempDirectory, filename), and if it's missing, take the safe
alternative (skip the transient-path branch or return/null fallback) so no
"undefined" is coerced into paths or comparisons.
- Around line 1424-1427: The code sets const tempDirectory = isTransient ?
state.collections.tempDirectories?.[collectionUid] : null but doesn't guard
against tempDirectory being undefined before it's used, which can produce
malformed paths; update the logic in the affected functions (e.g., where
tempDirectory is read for the current HTTP request handling — referenced as
newHttpRequest and the block around lines 1469-1501) to treat undefined the same
as null: check that tempDirectory is truthy before constructing paths or call
early-return/throw when isTransient && !tempDirectory, or explicitly set
tempDirectory = isTransient ?
(state.collections.tempDirectories?.[collectionUid] ?? null) : null so
downstream path-building only runs when tempDirectory is non-null. Ensure the
same guard is applied both at the newHttpRequest site and the separate block
covering 1469-1501.

Comment on lines +1258 to +1260
// Get temp directory if isTransient is true
const tempDirectory = isTransient ? state.collections.tempDirectories?.[collectionUid] : null;

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

Guard against undefined tempDirectory before using it.

If isTransient is true but the temp directory hasn't been created yet (collection not fully mounted), tempDirectory will be undefined. This causes:

  1. Line 1321: startsWith(undefined) coerces to "undefined" string — incorrect match logic
  2. Line 1328: path.join(undefined, filename) produces "undefined/filename.bru"
🛠️ Suggested fix
     // Get temp directory if isTransient is true
     const tempDirectory = isTransient ? state.collections.tempDirectories?.[collectionUid] : null;
+
+    if (isTransient && !tempDirectory) {
+      return reject(new Error('Transient directory is not ready. Please try again.'));
+    }

     const parts = splitOnFirst(requestUrl, '?');

Also applies to: 1315-1348

🤖 Prompt for AI Agents
In `@packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js`
around lines 1258 - 1260, The code assumes tempDirectory is a string when
isTransient is true but it can be undefined; update the logic around the
tempDirectory lookup (state.collections.tempDirectories?.[collectionUid]) and
all uses (the startsWith check and path.join calls) to guard for a non-empty
string before using string methods or path.join: check that tempDirectory is
truthy and of type string (e.g., tempDirectory && typeof tempDirectory ===
'string') before calling tempDirectory.startsWith(...) or
path.join(tempDirectory, filename), and if it's missing, take the safe
alternative (skip the transient-path branch or return/null fallback) so no
"undefined" is coerced into paths or comparisons.

Comment on lines +1424 to +1427

// Get temp directory if isTransient is true
const tempDirectory = isTransient ? state.collections.tempDirectories?.[collectionUid] : null;

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

Same tempDirectory guard needed here.

Identical to newHttpRequest: if the transient directory isn't ready, tempDirectory is undefined, leading to malformed paths.

🛠️ Suggested fix
     // Get temp directory if isTransient is true
     const tempDirectory = isTransient ? state.collections.tempDirectories?.[collectionUid] : null;
+
+    if (isTransient && !tempDirectory) {
+      return reject(new Error('Transient directory is not ready. Please try again.'));
+    }

     // do we need to handle query, path params for grpc requests?

Also applies to: 1469-1501

🤖 Prompt for AI Agents
In `@packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js`
around lines 1424 - 1427, The code sets const tempDirectory = isTransient ?
state.collections.tempDirectories?.[collectionUid] : null but doesn't guard
against tempDirectory being undefined before it's used, which can produce
malformed paths; update the logic in the affected functions (e.g., where
tempDirectory is read for the current HTTP request handling — referenced as
newHttpRequest and the block around lines 1469-1501) to treat undefined the same
as null: check that tempDirectory is truthy before constructing paths or call
early-return/throw when isTransient && !tempDirectory, or explicitly set
tempDirectory = isTransient ?
(state.collections.tempDirectories?.[collectionUid] ?? null) : null so
downstream path-building only runs when tempDirectory is non-null. Ensure the
same guard is applied both at the newHttpRequest site and the separate block
covering 1469-1501.

Comment on lines +1552 to +1554
// Get temp directory if isTransient is true
const tempDirectory = isTransient ? state.collections.tempDirectories?.[collectionUid] : null;

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

Same tempDirectory guard needed here.

Consistent with newHttpRequest and newGrpcRequest, add a guard for undefined tempDirectory.

🛠️ Suggested fix
     // Get temp directory if isTransient is true
     const tempDirectory = isTransient ? state.collections.tempDirectories?.[collectionUid] : null;
+
+    if (isTransient && !tempDirectory) {
+      return reject(new Error('Transient directory is not ready. Please try again.'));
+    }

     const item = {

Also applies to: 1596-1628

🤖 Prompt for AI Agents
In `@packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js`
around lines 1552 - 1554, The code computes tempDirectory with const
tempDirectory = isTransient ? state.collections.tempDirectories?.[collectionUid]
: null but doesn't guard for undefined before using it in the surrounding
action; update the same action (the function that handles creating requests in
packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js — the
blocks that mirror newHttpRequest and newGrpcRequest) to early-return or handle
the case when tempDirectory is undefined (e.g., if isTransient && !tempDirectory
then abort/throw/log and skip further processing), referencing the same symbols:
tempDirectory, isTransient, state.collections.tempDirectories, and collectionUid
so the behavior matches newHttpRequest/newGrpcRequest; apply the same guard
changes to the other occurrence noted (~lines 1596-1628).

* Improved the modal handling logic for removing collections to differentiate between regular and drafts confirmation modals.
* Added new tests for creating and saving transient requests (HTTP, GraphQL, gRPC, WebSocket) ensuring they do not appear in the sidebar until saved.
* Introduced utility functions for creating transient requests and filling request URLs, improving code reusability and clarity.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@tests/transient-requests/transient-requests.spec.ts`:
- Around line 124-128: The test uses page.keyboard.press('Meta+s') which only
works on macOS; update the keyboard press to use the cross-platform modifier
'ControlOrMeta+s' in the test step that triggers the save action (the block
labeled "Trigger save action using keyboard shortcut" that calls
page.keyboard.press) and make the same change in the other test step where
page.keyboard.press('Meta+s') is used so both save shortcuts use
'ControlOrMeta+s'.

In `@tests/utils/page/actions.ts`:
- Around line 159-161: Update the inconsistent WebSocket casing: change the
union member 'Websocket' to 'WebSocket' in the CreateTransientRequestOptions
type so it matches CreateUntitledRequestOptions and UI/protocol naming, and also
update any tests that assert/create transient requests (the test referenced as
transient-requests.spec.ts uses 'Websocket') to use 'WebSocket' instead.
🧹 Nitpick comments (5)
tests/onboarding/sample-collection.spec.ts (1)

95-108: Consider extracting duplicated modal handling logic.

This modal-handling pattern (checking for "Discard All and Remove" vs regular submit) is duplicated in tests/utils/page/actions.ts (closeAllCollections and removeCollection). Extract this to a shared helper function to improve maintainability and reduce duplication.

💡 Example helper extraction
// In tests/utils/page/actions.ts
const handleRemoveCollectionModal = async (page: Page) => {
  const removeModal = page.locator('.bruno-modal').filter({ hasText: 'Remove Collection' });
  await removeModal.waitFor({ state: 'visible', timeout: 5000 });

  const hasDiscardButton = await page.getByRole('button', { name: 'Discard All and Remove' }).isVisible().catch(() => false);

  if (hasDiscardButton) {
    await page.getByRole('button', { name: 'Discard All and Remove' }).click();
  } else {
    await page.locator('.bruno-modal-footer .submit').click();
  }

  await removeModal.waitFor({ state: 'hidden', timeout: 5000 });
};
tests/transient-requests/transient-requests.spec.ts (3)

3-3: Remove unused import.

buildWebsocketCommonLocators is imported but never used in this file.

🧹 Proposed fix
-import { buildCommonLocators, buildWebsocketCommonLocators } from '../utils/page/locators';
+import { buildCommonLocators } from '../utils/page/locators';

143-145: Replace fixed timeout with proper wait.

Instead of page.waitForTimeout(1000), wait for an observable state change such as the modal closing or a success toast appearing.

⏱️ Proposed fix
       await saveModal.getByRole('button', { name: 'Save' }).click();

-      // Wait for success toast
-      await page.waitForTimeout(1000);
+      // Wait for modal to close
+      await saveModal.waitFor({ state: 'hidden', timeout: 5000 });

Apply similar fix at line 187.


41-50: Replace timeout with assertion-based wait.

The waitForTimeout(300) can be eliminated since expect(collectionItems).toHaveCount(0) already has built-in auto-waiting.

⏱️ Proposed fix
     await test.step('Verify request is NOT in sidebar', async () => {
       // Click on the collection to ensure it's expanded
       await locators.sidebar.collection('transient-requests-test').click();
-      await page.waitForTimeout(300);

       // Check that there are no requests in the collection
       // Transient requests should not appear in the sidebar
       const collectionItems = page.locator('.collection-item-name');
       await expect(collectionItems).toHaveCount(0);
     });
tests/utils/page/actions.ts (1)

380-396: Duplicated modal handling logic with closeAllCollections.

This is the same pattern as lines 20-37. Extract to a shared helper function (e.g., handleRemoveCollectionModal) to reduce duplication and simplify maintenance.

Comment on lines +124 to +128
await test.step('Trigger save action using keyboard shortcut', async () => {
// Try to save using Cmd+S (Mac) or Ctrl+S (other platforms)
await page.keyboard.press('Meta+s');
await page.waitForTimeout(500);
});
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

Use cross-platform keyboard shortcut.

Meta+s only works on macOS. For cross-platform compatibility, use the ControlOrMeta modifier.

🔧 Proposed fix
-      await page.keyboard.press('Meta+s');
+      await page.keyboard.press('ControlOrMeta+s');

Apply the same fix at line 174.

📝 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
await test.step('Trigger save action using keyboard shortcut', async () => {
// Try to save using Cmd+S (Mac) or Ctrl+S (other platforms)
await page.keyboard.press('Meta+s');
await page.waitForTimeout(500);
});
await test.step('Trigger save action using keyboard shortcut', async () => {
// Try to save using Cmd+S (Mac) or Ctrl+S (other platforms)
await page.keyboard.press('ControlOrMeta+s');
await page.waitForTimeout(500);
});
🤖 Prompt for AI Agents
In `@tests/transient-requests/transient-requests.spec.ts` around lines 124 - 128,
The test uses page.keyboard.press('Meta+s') which only works on macOS; update
the keyboard press to use the cross-platform modifier 'ControlOrMeta+s' in the
test step that triggers the save action (the block labeled "Trigger save action
using keyboard shortcut" that calls page.keyboard.press) and make the same
change in the other test step where page.keyboard.press('Meta+s') is used so
both save shortcuts use 'ControlOrMeta+s'.

…ection watcher logic

* Introduced a new TransientRequestModalsRenderer component to streamline modal rendering based on the number of transient requests.
* Refactored the collection watcher logic to enhance readability by removing unnecessary setTimeout and consolidating file handling functions.
* Improved error handling and logging for the temp directory watcher.
…and tests

* Updated the spelling of 'Websocket' to 'WebSocket' in CreateTransientRequest component, transient requests test, and action type definitions for consistency and accuracy.
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.

2 participants