Skip to content

Add: dotenv visual editor#6964

Merged
bijin-bruno merged 9 commits intousebruno:mainfrom
naman-bruno:feat/dotenv-ui
Feb 2, 2026
Merged

Add: dotenv visual editor#6964
bijin-bruno merged 9 commits intousebruno:mainfrom
naman-bruno:feat/dotenv-ui

Conversation

@naman-bruno
Copy link
Collaborator

@naman-bruno naman-bruno commented Jan 28, 2026

Description

JIRA

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

    • Full .env support across collections and workspaces: create, select, edit (table + raw), save, delete, and live sync with filesystem changes.
    • New collapsible Variables and .env Files sections, inline creation flows, view toggles, and improved empty-state/UI for dotenv files.
    • Confirmation modal and dedicated .env editor with validation and table/raw conversion.
  • Bug Fixes / UX

    • Guards against losing unsaved changes, keyboard shortcuts for create/cancel, and success/error toasts for save/delete actions.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 28, 2026

Walkthrough

Adds end-to-end .env support: UI components (editor, table/raw views, details, collapsible lists), parsing utils, Redux thunks/reducers for collections and workspaces, Electron IPC handlers, a dedicated dotenv watcher, and filesystem helpers; integrates watcher events into the renderer store. (34 words)

Changes

Cohort / File(s) Summary
DotEnv Editor & Details
packages/bruno-app/src/components/Environments/DotEnvFileEditor/*, packages/bruno-app/src/components/Environments/DotEnvFileDetails/*
Adds DotEnvFileEditor (table & raw views), DotEnvTableView, DotEnvRawView, DotEnvFileDetails, DotEnvEmptyState, DotEnvErrorMessage, parsing utils (variablesToRaw, rawToVariables), and multiple styled wrappers.
Collapsible UI & List refactor
packages/bruno-app/src/components/Environments/CollapsibleSection/*, packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/*, packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/...
Introduces CollapsibleSection and refactors EnvironmentList/WorkspaceEnvironments to split Variables and .env Files into collapsible sections with inline creation, selection, view switching, and .env lifecycle handlers. Updates EnvironmentList signature to accept workspace.
Styled components & UI polish
packages/bruno-app/src/components/Environments/.../StyledWrapper.js
Multiple new and updated styled wrappers for editors, details, delete modal, lists, action buttons, and empty states; layout and scroll behavior adjustments.
Redux: collections dotenv
packages/bruno-app/src/providers/ReduxStore/slices/collections/*
Adds thunks: saveDotEnvVariables, saveDotEnvRaw, createDotEnvFile, deleteDotEnvFile and reducer setDotEnvVariables to manage collection.dotEnvFiles / dotEnvVariables and expose IPC-driven updates.
Redux: workspaces dotenv
packages/bruno-app/src/providers/ReduxStore/slices/workspaces/*, packages/bruno-app/src/providers/App/useIpcEvents.js
Adds workspace-scoped thunks (saveWorkspaceDotEnvVariables, saveWorkspaceDotEnvRaw, createWorkspaceDotEnvFile, deleteWorkspaceDotEnvFile), reducer setWorkspaceDotEnvVariables, and wires main:dotenv-file-update into renderer store.
Electron IPC handlers (collections & workspaces)
packages/bruno-electron/src/ipc/collection.js, packages/bruno-electron/src/ipc/global-environments.js
New IPC handlers to save/create/delete .env files for collections and workspaces (raw & structured). Note: collection handlers were duplicated in-file (two identical declarations).
Dotenv watcher & integration (main)
packages/bruno-electron/src/app/dotenv-watcher.js, packages/bruno-electron/src/app/collection-watcher.js, packages/bruno-electron/src/app/workspace-watcher.js
Adds a dotenv-watcher singleton using chokidar to monitor .env / .env.* files and emit main:dotenv-file-update; collection/workspace watchers now delegate to this watcher and remove inline parsing.
Process env & filesystem utils
packages/bruno-electron/src/store/process-env.js, packages/bruno-electron/src/utils/filesystem.js
Adds clearDotEnvVars and isValidDotEnvFilename utilities and exports them; isValidDotEnvFilename used by new IPC paths.
Misc UI wiring
packages/bruno-app/src/components/GlobalEnvironmentSettings/index.js, packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/index.js, packages/bruno-app/src/components/Environments/EnvironmentSettings/DeleteDotEnvFile/*
Passes workspace prop into EnvironmentList, simplifies empty-state/tab logic, adds DeleteDotEnvFile modal and its styles, and other small hook/signature updates (useOnClickOutside enabled flag).

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Frontend as Frontend (React)
    participant Redux as Redux Store
    participant IPC as Electron IPC
    participant FS as File System
    participant Watcher as DotEnv Watcher

    User->>Frontend: Edit .env (table/raw) & Click Save
    Frontend->>Redux: dispatch saveDotEnvVariables / saveWorkspaceDotEnvVariables
    Redux->>IPC: invoke renderer:save-dotenv-variables / renderer:save-workspace-dotenv-variables
    IPC->>FS: write .env file
    FS-->>IPC: file written
    IPC-->>Redux: resolve promise
    Redux-->>Frontend: update state (save success)
    Watcher->>FS: chokidar detects file change
    Watcher->>Watcher: read & parse .env
    Watcher->>IPC: emit main:dotenv-file-update (collection/workspace)
    IPC-->>Redux: dispatch setDotEnvVariables / setWorkspaceDotEnvVariables
    Redux-->>Frontend: subscribe & refresh UI
Loading
sequenceDiagram
    participant External as External .env change
    participant Watcher as DotEnv Watcher
    participant IPC as Electron IPC
    participant Redux as Redux Store
    participant Frontend as Frontend (React)

    External->>Watcher: file add/change/unlink
    Watcher->>Watcher: parse and build variables + exists flag
    Watcher->>IPC: emit main:dotenv-file-update (payload)
    IPC-->>Redux: dispatch setDotEnvVariables / setWorkspaceDotEnvVariables
    Redux-->>Frontend: state updated
    Frontend-->>User: UI reflects updated .env files
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

pr-feature

Suggested reviewers

  • helloanoop
  • lohit-bruno
  • bijin-bruno

Poem

🌱 Small files watched across the night,
Watchers parse and bring changes to light,
Tables, raw, and Redux in a row,
IPC and FS make the updates flow — ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add: dotenv visual editor' clearly summarizes the main feature being introduced—a visual editor for .env files—which aligns with the substantial changeset across UI components, Redux state management, and Electron IPC handlers.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@packages/bruno-app/src/components/Environments/DotEnvFileEditor/index.js`:
- Around line 234-267: The handleRemoveVar callback closes over the whole formik
object but only lists formik.values in its dependency array, causing a stale
closure; update the dependency array to include formik (i.e., use [formik]) so
the hook rebinds when the formik instance changes, and continue to call
formik.setValues(...) inside handleRemoveVar (referencing handleRemoveVar,
formik, and formik.setValues to locate the code).

In
`@packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/index.js`:
- Around line 366-374: Protect against a null/undefined workspace before
accessing workspace.uid in handleSaveDotEnv and handleSaveDotEnvRaw: add a guard
that checks selectedDotEnvFile and workspace (e.g., if (!selectedDotEnvFile ||
!workspace) return Promise.reject(new Error('No file or workspace selected'))),
or use the optional chaining workspace?.uid and reject early if missing, then
call dispatch(saveWorkspaceDotEnvVariables(workspace.uid, ...)) and
dispatch(saveWorkspaceDotEnvRaw(workspace.uid, ...)) only when workspace is
present.

In `@packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js`:
- Around line 2709-2771: The four actions saveDotEnvVariables, saveDotEnvRaw,
createDotEnvFile, and deleteDotEnvFile currently call ipcRenderer but do not
define it; fix by importing/getting ipcRenderer from the global window object
(e.g. const { ipcRenderer } = window) in the same manner other actions in this
module do, ensuring that ipcRenderer is defined before any ipcRenderer.invoke
calls and using that local variable inside each action.

In `@packages/bruno-electron/src/app/dotenv-watcher.js`:
- Around line 79-95: The collection handleUnlink handler currently sends a
'main:dotenv-file-update' message but does not clear the in-memory cached vars;
update handleUnlink to call the equivalent cache-clearing function (e.g.,
clearDotEnvVars or setDotEnvVars with empty values) for the deleted file so the
process-wide cache is cleared (similar to clearWorkspaceDotEnvVars used in the
workspace handler). Import or add the clearDotEnvVars helper if it doesn't
exist, and invoke it with the collectionUid and filename (and any required
params) when isDotEnvFile(filename) is true and the file is deleted, ensuring
the in-memory state matches the sent { exists: false } message.

In `@packages/bruno-electron/src/ipc/collection.js`:
- Around line 569-572: The quoted-value escaping in the block that builds
`${v.name}="..."` fails to escape backslashes, so values like `test\end` become
ambiguous; update the escaping in the routine that computes `escapedValue` (the
variable handling `value` for `v.name`) to first replace backslashes with double
backslashes and then escape double quotes (i.e., perform value.replace(/\\/g,
'\\\\') before value.replace(/"/g, '\\"')) so backslashes are preserved when the
string is re-parsed.

In `@packages/bruno-electron/src/ipc/global-environments.js`:
- Around line 2-3: Validate and canonicalize the renderer-provided filename to
prevent path traversal: restrict accepted names to a basename matching
/^\.env(\.[a-zA-Z0-9_-]+)?$/ (allow only .env or .env.*), ensure you use
path.basename(filename) before any join/resolve, then resolve the resulting path
against the workspace root and verify the resolved path starts with the
workspace directory to prevent escapes; apply this check wherever the filename
variable is used (all functions that read/write/delete dotenv files), replace
direct path.join usage with this safe resolution, and add JSDoc comments to each
dotenv-related abstraction describing expected input (basename .env/.env.*),
validation behavior, and that the function returns a workspace-contained
absolute path.
🧹 Nitpick comments (19)
packages/bruno-app/src/components/Environments/EnvironmentSettings/DeleteDotEnvFile/StyledWrapper.js (1)

4-12: Use theme props instead of CSS variables for colors.

Per project standards, styled-components should use theme props for colors rather than CSS variables. The var(--color-background-danger) should be replaced with a theme-based value.

Suggested fix
 const Wrapper = styled.div`
   button.submit {
-    color: white;
-    background-color: var(--color-background-danger) !important;
+    color: ${(props) => props.theme.bg};
+    background-color: ${(props) => props.theme.colors.text.danger} !important;
     border: inherit !important;

     &:hover {
       border: inherit !important;
     }
   }
 `;

Based on learnings: "Use styled component's theme prop to manage CSS colors, not CSS variables, when in the context of a styled component."

packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/StyledWrapper.js (1)

115-133: Note: Duplicate .btn-action styling.

There's a .btn-action block in .sidebar-header (lines 48-66) with width: 24px; height: 24px and this root-level .btn-action with width: 22px; height: 22px. If intentional for different contexts, consider adding a comment. Otherwise, this could be consolidated.

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

47-62: Consider documenting !important overrides.

Multiple !important declarations in the thead styling. If these are needed to override CodeMirror or third-party styles, a brief comment would help future maintainers understand the necessity.

packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/StyledWrapper.js (1)

115-133: Duplicate .btn-action class with different sizing.

There's a .btn-action defined in .sidebar-header (24x24) at lines 48-66 and another standalone (22x22) here. If this is intentional for different contexts, consider renaming one for clarity (e.g., .btn-action-sm).

packages/bruno-app/src/components/Environments/DotEnvFileEditor/index.js (2)

211-232: ErrorMessage component recreated on every render.

This inner component is defined inside DotEnvFileEditor and recreated on each render, causing unnecessary re-renders of tooltip elements.

Move outside or memoize

Extract ErrorMessage outside the component and pass formik as a prop, or use React.memo with appropriate deps:

// Option 1: Move outside component
const ErrorMessage = ({ formik, name, index }) => {
  // ...existing logic using formik passed as prop
};

// Then in DotEnvFileEditor, pass formik:
<ErrorMessage formik={formik} name={`${index}.name`} index={index} />

269-283: setTimeout for deferred state update may cause race conditions.

Using setTimeout to add a new row after name change could lead to unexpected behavior if the user types quickly or if React batches updates differently.

Consider using a callback or effect
   const handleNameChange = (index, e) => {
     formik.handleChange(e);
     const isLastRow = index === formik.values.length - 1;

     if (isLastRow) {
       const newVariable = {
         uid: uuid(),
         name: '',
         value: ''
       };
-      setTimeout(() => {
-        formik.setFieldValue(formik.values.length, newVariable, false);
-      }, 0);
+      // Use setValues to append in the same update cycle
+      const updatedValues = [...formik.values];
+      updatedValues[index] = { ...updatedValues[index], name: e.target.value };
+      updatedValues.push(newVariable);
+      formik.setValues(updatedValues, false);
     }
   };

Note: This changes the flow slightly - you may need to adjust based on desired UX.

packages/bruno-electron/src/app/dotenv-watcher.js (1)

37-42: Inconsistent awaitWriteFinish settings between collection and workspace watchers.

Collection uses pollInterval: 100 while workspace uses pollInterval: 250. If intentional (e.g., workspaces update less frequently), a comment would help; otherwise, align them.

Also applies to: 126-131

packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js (1)

764-782: Redundant Promise wrapper and unused dispatch parameter.

ipcRenderer.invoke already returns a Promise, so the explicit new Promise() wrapper is unnecessary. Additionally, dispatch is destructured but never used. This pattern repeats in all four new action creators.

Consider refactoring to match the existing async/await pattern used elsewhere in this file (e.g., saveWorkspaceDocs):

♻️ Suggested refactor
-export const saveWorkspaceDotEnvVariables = (workspaceUid, variables, filename = '.env') => (dispatch, getState) => {
-  return new Promise((resolve, reject) => {
-    const state = getState();
-    const workspace = state.workspaces.workspaces.find((w) => w.uid === workspaceUid);
-
-    if (!workspace) {
-      return reject(new Error('Workspace not found'));
-    }
-
-    if (!workspace.pathname) {
-      return reject(new Error('Workspace path not found'));
-    }
-
-    ipcRenderer
-      .invoke('renderer:save-workspace-dotenv-variables', { workspacePath: workspace.pathname, variables, filename })
-      .then(resolve)
-      .catch(reject);
-  });
-};
+export const saveWorkspaceDotEnvVariables = (workspaceUid, variables, filename = '.env') => async (dispatch, getState) => {
+  const state = getState();
+  const workspace = state.workspaces.workspaces.find((w) => w.uid === workspaceUid);
+
+  if (!workspace) {
+    throw new Error('Workspace not found');
+  }
+
+  if (!workspace.pathname) {
+    throw new Error('Workspace path not found');
+  }
+
+  return ipcRenderer.invoke('renderer:save-workspace-dotenv-variables', { workspacePath: workspace.pathname, variables, filename });
+};

Apply the same refactor to saveWorkspaceDotEnvRaw, createWorkspaceDotEnvFile, and deleteWorkspaceDotEnvFile.

packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/DotEnvFileDetails/StyledWrapper.js (1)

1-93: Duplicate StyledWrapper exists.

This file is identical to packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/DotEnvFileDetails/StyledWrapper.js. Consider extracting a shared StyledWrapper under a common location (e.g., components/Environments/Common/) to avoid maintenance burden.

packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/DotEnvFileDetails/index.js (1)

1-70: Duplicate component exists.

This component is identical to packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/DotEnvFileDetails/index.js. Consider consolidating into a single shared component under components/Environments/Common/DotEnvFileDetails/ to reduce duplication.

packages/bruno-app/src/components/Environments/GlobalEnvironmentSettings/index.js (1)

6-11: Remove or use the workspace prop being passed to EnvironmentList.

The workspace prop is passed from WorkspaceEnvironments to EnvironmentList, but it's never destructured or used in the component. Either remove it from the prop chain or use it if needed.

packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/index.js (5)

85-98: Stale closure risk in click-outside handler.

The handleCancelDotEnvCreate function referenced inside the effect isn't included in the dependency array. If handleCancelDotEnvCreate changes (e.g., due to closures over other state), the effect would reference a stale version.

Consider using useCallback for handleCancelDotEnvCreate and adding it to deps, or inline the logic.


203-213: handleDotEnvClose is defined but never invoked.

This function appears to be dead code. If it's intended for future use, consider adding a TODO comment. Otherwise, remove it to avoid confusion.


438-440: Variable shadowing: error parameter shadows outer scope.

The catch block parameter error shadows the error variable from the validation check above (line 422). While this works, it can cause confusion during debugging.

Rename catch parameter
-      .catch((error) => {
-        toast.error(error.message || 'Failed to create .env file');
+      .catch((err) => {
+        toast.error(err.message || 'Failed to create .env file');
       });

462-467: Backspace prevention uses magic number for .env prefix length.

The check input.selectionStart <= 4 relies on knowing .env is 4 characters. Consider extracting this to a constant for clarity.

Extract constant
+const DOT_ENV_PREFIX = '.env';
+const DOT_ENV_PREFIX_LENGTH = DOT_ENV_PREFIX.length;

 const handleDotEnvNameKeyDown = (e) => {
   // ...
   } else if (e.key === 'Backspace') {
     const input = e.target;
-    if (input.selectionStart <= 4 && input.selectionEnd <= 4) {
+    if (input.selectionStart <= DOT_ENV_PREFIX_LENGTH && input.selectionEnd <= DOT_ENV_PREFIX_LENGTH) {
       e.preventDefault();
     }
   }
 };

488-490: Same variable shadowing issue with error in catch block.

Rename catch parameter
-      .catch((error) => {
-        toast.error(error.message || 'Failed to delete .env file');
+      .catch((err) => {
+        toast.error(err.message || 'Failed to delete .env file');
       });
packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/index.js (3)

203-213: handleDotEnvClose is defined but never invoked (same as collection-level component).

This appears to be dead code in both files. Consider removing or documenting its intended use.


428-441: Same variable shadowing issues with error in catch blocks.

Apply the same fix as suggested for the collection-level component.

Also applies to: 470-491


1-788: Extract shared logic from duplicated EnvironmentList components into custom hooks and utilities.

Both EnvironmentList components (collection and workspace) are 791/788 lines with identical handler and validator function signatures. Key candidates for extraction:

  1. useClickOutside hook: Three identical click-outside patterns with boilerplate event listener code can be consolidated into a reusable hook
  2. useDotEnvManagement hook: Duplicated state and handlers for dotenv file operations (creation, deletion, selection, validation)
  3. Environment/DotEnv validation: The validateEnvironmentName and validateDotEnvName functions are nearly identical and can be shared

Note: Extraction requires abstracting Redux action differences—collection uses collection-scoped actions while workspace uses global/workspace-scoped actions. Design the hooks to accept action creators as parameters.

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

🤖 Fix all issues with AI agents
In `@packages/bruno-electron/src/app/dotenv-watcher.js`:
- Around line 26-33: The watcher startup in addCollectionWatcher currently
reopens an existing watcher but does not clear any previously cached env values
for that collection; at the start of addCollectionWatcher, reset the
collection's dotenv cache (e.g., delete this.collectionEnvCache[collectionPath]
or set this.collectionEnvCache[collectionPath] = null) so stale .env values
cannot persist when the watcher is restarted; keep this step just before
constructing dotEnvPattern/creating chokidar.watch and use the same
collectionPath key used by collectionWatchers to identify the cache entry to
clear.
🧹 Nitpick comments (1)
packages/bruno-electron/src/utils/filesystem.js (1)

448-514: Add JSDoc to document the dotenv filename validation contract.

This helper is already solid; a brief contract note will help callers understand the allowed patterns and basename requirement.

📝 Suggested JSDoc
+/**
+ * Validates a dotenv filename (".env" or ".env.*") and rejects paths.
+ * `@param` {string} filename
+ * `@returns` {boolean}
+ */
 const isValidDotEnvFilename = (filename) => {
   if (!filename || typeof filename !== 'string') return false;
   const basename = path.basename(filename);
   if (basename !== filename) return false;
   return basename === '.env' || (basename.startsWith('.env.') && /^\.env\.[a-zA-Z0-9._-]+$/.test(basename));
 };

As per coding guidelines: Add JSDoc comments to abstractions for additional details.

Comment on lines +26 to +33
addCollectionWatcher(win, collectionPath, collectionUid) {
if (this.collectionWatchers[collectionPath]) {
this.collectionWatchers[collectionPath].close();
}

const dotEnvPattern = path.join(collectionPath, '.env*');

const watcher = chokidar.watch(dotEnvPattern, {
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

Reset collection dotenv cache when (re)starting a watcher.

If a collection was previously watched with a .env and the file is deleted while the watcher is inactive, the cache can retain stale values. Clearing at watcher startup avoids stale process env on reopen.

🧹 Suggested fix
  addCollectionWatcher(win, collectionPath, collectionUid) {
+    clearDotEnvVars(collectionUid);
     if (this.collectionWatchers[collectionPath]) {
       this.collectionWatchers[collectionPath].close();
     }
🤖 Prompt for AI Agents
In `@packages/bruno-electron/src/app/dotenv-watcher.js` around lines 26 - 33, The
watcher startup in addCollectionWatcher currently reopens an existing watcher
but does not clear any previously cached env values for that collection; at the
start of addCollectionWatcher, reset the collection's dotenv cache (e.g., delete
this.collectionEnvCache[collectionPath] or set
this.collectionEnvCache[collectionPath] = null) so stale .env values cannot
persist when the watcher is restarted; keep this step just before constructing
dotEnvPattern/creating chokidar.watch and use the same collectionPath key used
by collectionWatchers to identify the cache entry to clear.

@@ -0,0 +1,70 @@
import React, { useState } from 'react';
Copy link
Collaborator

Choose a reason for hiding this comment

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

@naman-bruno i believe there is an opportunity to extact the this file to components folder and use it everywhere like in packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList , packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList

expanded={environmentsExpanded}
onToggle={() => setEnvironmentsExpanded(!environmentsExpanded)}
actions={(
<>
Copy link
Collaborator

Choose a reason for hiding this comment

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

validationSchema: Yup.array().of(
Yup.object({
name: Yup.string()
.when('$isLastRow', {
Copy link
Collaborator

Choose a reason for hiding this comment

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

$isLastRow i don't see Yup context variable being passed, probably this is redundant, we can just keep the validate function

}
}, [formik.values, savedValuesJson, setIsModified, viewMode, rawValue, rawContent]);

const ErrorMessage = ({ name, index }) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

maybe we can move the component outside so that it is not re-created on every re-render

// Outside DotEnvFileEditor
const ErrorMessage = React.memo(({ formik, name, index }) => {
  const meta = formik.getFieldMeta(name);
  const id = `error-${name}-${index}`;

  const isLastRow = index === formik.values.length - 1;
  const variable = formik.values[index];
  const isEmptyRow = !variable?.name || variable.name.trim() === '';

  if ((isLastRow && isEmptyRow) || !meta.error || !meta.touched) {
    return null;
  }

  return (
    <span>
      <IconAlertCircle id={id} className="text-red-600 cursor-pointer" size={20} />
      <Tooltip className="tooltip-mod" anchorId={id} html={meta.error || ''} />
    </span>
  );
});

// Usage inside DotEnvFileEditor:
<ErrorMessage formik={formik} name={`${index}.name`} index={index} />

);
};

const handleRemoveVar = useCallback((id) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

if i am not wrong formik.values is a new object on every render, so this useCallback never memoizes effectively.

should we use ref?

const valuesRef = useRef(formik.values);
valuesRef.current = formik.values;

const handleRemoveVar = useCallback((id) => {
  const currentValues = valuesRef.current;

  if (!currentValues || currentValues.length === 0) return;

  const lastRow = currentValues[currentValues.length - 1];
  const isLastEmptyRow = lastRow?.uid === id && (!lastRow.name || lastRow.name.trim() === '');

  if (isLastEmptyRow) return;

  const filteredValues = currentValues.filter((v) => v.uid !== id);
  const hasEmptyLastRow = filteredValues.length > 0 &&
    (!filteredValues[filteredValues.length - 1].name ||
      filteredValues[filteredValues.length - 1].name.trim() === '');

  const newValues = hasEmptyLastRow
    ? filteredValues
    : [...filteredValues, { uid: uuid(), name: '', value: '' }];

  formik.setValues(newValues);
}, []);

name: '',
value: ''
};
setTimeout(() => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

setTimeout captures stale formik.values.length, The length is captured at closure time, not execution time, using setTimeout here feels a bit odd, do we really need this?

or should we do something like this?

const handleNameChange = (index, e) => {
  formik.handleChange(e);
  const isLastRow = index === formik.values.length - 1;

  if (isLastRow) {
    const newVariable = { uid: uuid(), name: '', value: '' };
    // Use functional approach via setValues instead
    setTimeout(() => {
      formik.setValues((prev) => {
        const lastRow = prev[prev.length - 1];
        // Only add if last row still has content
        if (lastRow?.name?.trim()) {
          return [...prev, newVariable];
        }
        return prev;
      });
    }, 0);
  }
};

another alternative

const handleNameChange = (index, e) => {
  formik.handleChange(e);
  const isLastRow = index === formik.values.length - 1;

  if (isLastRow) {
    const newVariable = { uid: uuid(), name: '', value: '' };
    setTimeout(() => {
      const currentLength = valuesRef.current.length;
      formik.setFieldValue(currentLength, newVariable, false);
    }, 0);
  }
};

}
};

const handleSave = () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

should we consider any loading state for saving things? users can click save multiple time while pending a save.

setDotEnvExpanded(true);
};

const handleDotEnvClose = () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

handleDotEnvClose is defined but never used

onToggle={() => setEnvironmentsExpanded(!environmentsExpanded)}
actions={(
<>
<button className="btn-action" onClick={() => handleCreateEnvClick()} title="Create environment">
Copy link
Collaborator

Choose a reason for hiding this comment

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

All action buttons missing type="button".

<button 
  type="button" 
  className="btn-action" 
  onClick={handleCreateEnvClick}
  title="Create environment"
  aria-label="Create environment"
>

<div
key={env.uid}
id={env.uid}
className={`environment-item ${activeView === 'environment' && selectedEnvironment?.uid === env.uid ? 'active' : ''} ${renamingEnvUid === env.uid ? 'renaming' : ''} ${activeEnvironmentUid === env.uid ? 'activated' : ''}`}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Long className strings

className={classNames('environment-item', {
  active: activeView === 'environment' && selectedEnvironment?.uid === env.uid,
  renaming: renamingEnvUid === env.uid,
  activated: activeEnvironmentUid === env.uid
})}


const selectedDotEnvData = dotEnvFiles.find((f) => f.filename === selectedDotEnvFile);

const renderContent = () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

renderContent could be memoized

const content = useMemo(() => {
  if (activeView === 'dotenv' && selectedDotEnvFile && selectedDotEnvData) {
    return (
      <DotEnvFileDetails
        title={selectedDotEnvFile}
        onDelete={() => handleDeleteDotEnvFile(selectedDotEnvFile)}
        dotEnvExists={selectedDotEnvData?.exists}
        viewMode={dotEnvViewMode}
        onViewModeChange={handleDotEnvViewModeChange}
      >
        <DotEnvFileEditor
          variables={selectedDotEnvData?.variables || []}
          onSave={handleSaveDotEnv}
          onSaveRaw={handleSaveDotEnvRaw}
          isModified={isDotEnvModified}
          setIsModified={setIsDotEnvModified}
          dotEnvExists={selectedDotEnvData?.exists}
          viewMode={dotEnvViewMode}
        />
      </DotEnvFileDetails>
    );
  }

  if (selectedEnvironment) {
    return (
      <EnvironmentDetails
        environment={selectedEnvironment}
        setIsModified={setIsModified}
        originalEnvironmentVariables={originalEnvironmentVariables}
        collection={collection}
      />
    );
  }

  return (
    <EmptyState
      onCreateClick={handleCreateEnvClick}
      onImportClick={handleImportClick}
    />
  );
}, [
  activeView,
  selectedDotEnvFile,
  selectedDotEnvData,
  dotEnvViewMode,
  isDotEnvModified,
  selectedEnvironment,
  originalEnvironmentVariables,
  collection
]);

// Then in JSX:
{content}

@@ -1,22 +1,40 @@
import React, { useEffect, useState, useRef } from 'react';
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can be done at a later stage, i guess there is merit in refactoring the filestructure when it comes to environmentList within collection and workspace

### **Recommended refactor structure:**

components/Environments/
├── EnvironmentList/
   ├── index.jsx                     // Shared base component
   ├── CollectionEnvironmentList.jsx // Thin wrapper with collection actions
   ├── WorkspaceEnvironmentList.jsx  // Thin wrapper with workspace actions
   ├── EnvironmentItem.jsx           // Single item component
   ├── DotEnvSection.jsx             // DotEnv files section
   ├── InlineCreateInput.jsx         // Shared inline input
   ├── EmptyState.jsx                // Empty state component
   └── hooks/
       ├── useEnvironmentSelection.js
       ├── useDotEnvFiles.js
       ├── useInlineEdit.js
       └── useClickOutside.js

@sanish-bruno
Copy link
Collaborator

@naman-bruno would be nice if we have playwright test cases verifying the intended behaviours like addition, deletion env files, env rows etc, if time permits

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/Environments/DotEnvFileEditor/index.js`:
- Around line 567-572: The Save and Reset buttons in DotEnvFileEditor are
missing the disabled prop, which allows double submissions; update the two
buttons that call handleSave and handleReset to include disabled={isSaving}
(consistent with the raw-mode and empty-state buttons) so they are disabled
while the isSaving flag is true; ensure you reference the same isSaving state
used elsewhere in the component and apply the change to the buttons rendered in
the table/normal mode.
- Line 83: The unescaping order in the DotEnvFileEditor (the line that assigns
to variable `value` using chained .replace calls) is wrong: it turns literal
`\\n` into a newline. Fix by unescaping backslashes last (or use a placeholder
strategy) so `\\\\` → `\\` is applied after `\\n` and `\\"`; update the replace
sequence on `value` (or implement a placeholder-replace/unreplace pass) to
ensure `\\n` remains a literal `\n` only when intended and that `\\\\` is
handled last.

In
`@packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/index.js`:
- Line 425: In handleSaveNewDotEnv, avoid unguarded access to workspace.uid
before dispatching createWorkspaceDotEnvFile; add a null/undefined guard (e.g.,
if (!workspace || !workspace.uid) return or disable the path) so you only call
dispatch(createWorkspaceDotEnvFile(workspace.uid, newDotEnvName)) when
workspace.uid exists, and surface a user-facing error or early return otherwise;
update references in that handler and any related save flow to use the guarded
check.
- Line 468: handleDeleteDotEnvFile calls
dispatch(deleteWorkspaceDotEnvFile(workspace.uid, filename)) without ensuring
workspace exists; add a guard to avoid accessing workspace.uid when workspace is
null/undefined. Update handleDeleteDotEnvFile to check for workspace (e.g., if
(!workspace || !workspace.uid) return or show an error/toast) before calling
deleteWorkspaceDotEnvFile, or use optional chaining and handle the undefined
case so dispatch is only invoked with a valid workspace.uid.
🧹 Nitpick comments (3)
packages/bruno-app/src/components/Environments/DotEnvFileEditor/index.js (1)

421-494: Code duplication between empty-state and normal table rendering.

The table rendering logic at lines 431-482 is nearly identical to lines 499-563, except the empty-state table only shows the Name column. Consider extracting a shared table component or consolidating the rendering paths.

packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/index.js (1)

50-65: Large component with many state variables.

This component manages 11+ new state variables for dotenv handling. Per coding standards, consider extracting dotenv-related state and logic into a custom hook (e.g., useDotEnvManagement) to improve maintainability and testability.

packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/index.js (1)

24-34: Significant code duplication with EnvironmentSettings/EnvironmentList/index.js.

This component shares ~90% of its logic with the collection-level EnvironmentList. Consider extracting shared logic into a common component or hook that accepts configuration for the data source (collection vs. workspace) and corresponding actions.

return;
}

dispatch(createWorkspaceDotEnvFile(workspace.uid, newDotEnvName))
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

Unguarded workspace.uid access in handleSaveNewDotEnv.

Same issue as the save handlers — workspace.uid is accessed without null check.

Proposed fix
 const handleSaveNewDotEnv = () => {
   const error = validateDotEnvName(newDotEnvName);
   if (error) {
     setDotEnvNameError(error);
     return;
   }

+  if (!workspace?.uid) {
+    toast.error('Workspace not available');
+    return;
+  }
+
   dispatch(createWorkspaceDotEnvFile(workspace.uid, newDotEnvName))
🤖 Prompt for AI Agents
In
`@packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/index.js`
at line 425, In handleSaveNewDotEnv, avoid unguarded access to workspace.uid
before dispatching createWorkspaceDotEnvFile; add a null/undefined guard (e.g.,
if (!workspace || !workspace.uid) return or disable the path) so you only call
dispatch(createWorkspaceDotEnvFile(workspace.uid, newDotEnvName)) when
workspace.uid exists, and surface a user-facing error or early return otherwise;
update references in that handler and any related save flow to use the guarded
check.

};

const handleDeleteDotEnvFile = (filename) => {
dispatch(deleteWorkspaceDotEnvFile(workspace.uid, filename))
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

Unguarded workspace.uid access in handleDeleteDotEnvFile.

Add a null check before accessing workspace.uid.

Proposed fix
 const handleDeleteDotEnvFile = (filename) => {
+  if (!workspace?.uid) {
+    toast.error('Workspace not available');
+    return;
+  }
   dispatch(deleteWorkspaceDotEnvFile(workspace.uid, filename))
🤖 Prompt for AI Agents
In
`@packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/index.js`
at line 468, handleDeleteDotEnvFile calls
dispatch(deleteWorkspaceDotEnvFile(workspace.uid, filename)) without ensuring
workspace exists; add a guard to avoid accessing workspace.uid when workspace is
null/undefined. Update handleDeleteDotEnvFile to check for workspace (e.g., if
(!workspace || !workspace.uid) return or show an error/toast) before calling
deleteWorkspaceDotEnvFile, or use optional chaining and handle the undefined
case so dispatch is only invoked with a valid workspace.uid.

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

🤖 Fix all issues with AI agents
In `@packages/bruno-app/src/components/Environments/DotEnvFileDetails/index.js`:
- Around line 51-53: The delete button is icon-only and needs an accessible
label; update the button element rendered in DotEnvFileDetails (the button with
onClick={handleDeleteClick} and className "action-btn delete-btn") to include an
aria-label (e.g., aria-label="Delete .env file") so screen readers announce its
purpose; keep the existing title and IconTrash but add the aria-label attribute
on that button.

In
`@packages/bruno-app/src/components/Environments/DotEnvFileEditor/DotEnvTableView.js`:
- Around line 8-10: Replace the unstable row test id that uses item.name with
the stable UID: in the TableRow component update the data-testid value from
`dotenv-var-row-${item.name}` to `dotenv-var-row-${item.uid}` (the component
already uses item.uid as the key), and update any tests/selectors that relied on
the name-based test id to use the uid-based id instead.

In `@packages/bruno-app/src/components/Environments/DotEnvFileEditor/index.js`:
- Around line 76-119: The dirty-tracking and reset logic treat raw baseline as
'' when rawContent is undefined which causes raw mode to always appear modified
and resets to empty; fix by deriving a single baselineRaw (e.g., const
baselineRaw = rawContent ?? variablesToRaw(variables || [])) and use that
baseline in both the view-mode switch (where you build newRawValue) and in the
modification check (replace rawContent || '' with baselineRaw) as well as in the
other similar block around lines 248-261; update uses of
rawContent/variablesToRaw/rawValue comparisons to reference baselineRaw and
ensure setRawValue and setIsModified use that same derived baseline.

In
`@packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/index.js`:
- Around line 575-585: These icon-only action buttons (the JSX buttons that call
handleCreateEnvClick, handleImportClick, and handleExportClick) lack aria-label
attributes; update each button to include a descriptive aria-label (e.g.,
"Create environment", "Import environment", "Export environment") so screen
readers can announce their purpose, and apply the same change to the other
icon-only buttons around the 705–711 block to ensure consistent accessibility
across EnvironmentList.
- Around line 72-82: When dotEnvFiles becomes empty the useEffect handling
selection should also clear dotenv-related UI state; inside the existing
useEffect (referencing dotEnvFiles, selectedDotEnvFile, setSelectedDotEnvFile)
add logic in the dotEnvFiles.length === 0 branch to call
setSelectedDotEnvFile(null) (already present) and also reset the dotenv view
state by calling setActiveView(...) to a non-"dotenv" view (e.g. null or the
default list view) and call setIsDotEnvModified(false) to clear the modified
flag so navigation is not blocked; update the effect to import/use setActiveView
and setIsDotEnvModified where needed.

In
`@packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/index.js`:
- Around line 572-581: The icon-only action buttons (the three buttons invoking
handleCreateEnvClick, handleImportClick, and handleExportClick in
EnvironmentList) lack accessible labels; add descriptive aria-label attributes
(e.g., aria-label="Create environment", aria-label="Import environment",
aria-label="Export environment") to each corresponding <button> so screen
readers can announce their purpose, and apply the same change to the other
icon-only group referenced around the later block (the buttons at the 702-708
region).
- Around line 72-82: The effect that resets dotenv selection doesn't run when
selectedDotEnvFile changes, so add selectedDotEnvFile to the dependency list and
ensure the early-empty check still sets setSelectedDotEnvFile(null) and returns;
update the useEffect signature in EnvironmentList to use [dotEnvFiles,
selectedDotEnvFile] so the logic that checks dotEnvFiles.length, computes
fileExists (dotEnvFiles.some(...)), and sets
setSelectedDotEnvFile(dotEnvFiles[0].filename) runs correctly on both list-sync
and selection changes.

In `@packages/bruno-electron/src/app/dotenv-watcher.js`:
- Around line 149-172: When adding a watcher in addWorkspaceWatcher, clear any
cached workspace dotenv state before (re)creating the chokidar watcher to avoid
stale vars after a deleted .env; call clearWorkspaceDotEnvVars(workspacePath)
(or the same clearEnvVars used in handlerOptions) right after closing an
existing watcher and before initiating chokidar.watch so the workspace dotenv
cache is reset prior to re-watching.
🧹 Nitpick comments (1)
packages/bruno-electron/src/app/dotenv-watcher.js (1)

7-18: Add JSDoc for the new abstractions.

Please add brief JSDoc for the helper utilities, handler factories, and the DotEnvWatcher class so parameter/return contracts are clear.

💡 Example JSDoc additions
+/**
+ * Creates a handler for dotenv file add/change events.
+ * `@param` {object} win
+ * `@param` {object} options
+ * `@returns` {Function}
+ */
 const createFileHandler = (win, options) => (pathname) => {
+/**
+ * Watches collection/workspace dotenv files and emits IPC updates.
+ */
 class DotEnvWatcher {
As per coding guidelines, "Add JSDoc comments to abstractions for additional details".

Also applies to: 27-90, 92-210

Comment on lines +51 to +53
<button type="button" onClick={handleDeleteClick} title="Delete .env file" className="action-btn delete-btn">
<IconTrash size={15} strokeWidth={1.5} />
</button>
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 an aria-label to the icon-only delete button.
Title alone isn’t reliably announced by screen readers.

Suggested fix
-              <button type="button" onClick={handleDeleteClick} title="Delete .env file" className="action-btn delete-btn">
+              <button type="button" onClick={handleDeleteClick} title="Delete .env file" aria-label="Delete .env file" className="action-btn delete-btn">
                 <IconTrash size={15} strokeWidth={1.5} />
               </button>
📝 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
<button type="button" onClick={handleDeleteClick} title="Delete .env file" className="action-btn delete-btn">
<IconTrash size={15} strokeWidth={1.5} />
</button>
<button type="button" onClick={handleDeleteClick} title="Delete .env file" aria-label="Delete .env file" className="action-btn delete-btn">
<IconTrash size={15} strokeWidth={1.5} />
</button>
🤖 Prompt for AI Agents
In `@packages/bruno-app/src/components/Environments/DotEnvFileDetails/index.js`
around lines 51 - 53, The delete button is icon-only and needs an accessible
label; update the button element rendered in DotEnvFileDetails (the button with
onClick={handleDeleteClick} and className "action-btn delete-btn") to include an
aria-label (e.g., aria-label="Delete .env file") so screen readers announce its
purpose; keep the existing title and IconTrash but add the aria-label attribute
on that button.

Comment on lines +8 to +10
const TableRow = React.memo(({ children, item }) => (
<tr key={item.uid} data-testid={`dotenv-var-row-${item.name}`}>{children}</tr>
), (prevProps, nextProps) => {
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 UID in row test id to avoid duplicates.
item.name can repeat or be empty, which makes selectors ambiguous. Prefer the stable UID.

✅ Suggested fix
-const TableRow = React.memo(({ children, item }) => (
-  <tr key={item.uid} data-testid={`dotenv-var-row-${item.name}`}>{children}</tr>
+const TableRow = React.memo(({ children, item }) => (
+  <tr key={item.uid} data-testid={`dotenv-var-row-${item.uid}`}>{children}</tr>
📝 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 TableRow = React.memo(({ children, item }) => (
<tr key={item.uid} data-testid={`dotenv-var-row-${item.name}`}>{children}</tr>
), (prevProps, nextProps) => {
const TableRow = React.memo(({ children, item }) => (
<tr key={item.uid} data-testid={`dotenv-var-row-${item.uid}`}>{children}</tr>
), (prevProps, nextProps) => {
🤖 Prompt for AI Agents
In
`@packages/bruno-app/src/components/Environments/DotEnvFileEditor/DotEnvTableView.js`
around lines 8 - 10, Replace the unstable row test id that uses item.name with
the stable UID: in the TableRow component update the data-testid value from
`dotenv-var-row-${item.name}` to `dotenv-var-row-${item.uid}` (the component
already uses item.uid as the key), and update any tests/selectors that relied on
the name-based test id to use the uid-based id instead.

Comment on lines +575 to +585
<>
<button type="button" className="btn-action" onClick={() => handleCreateEnvClick()} title="Create environment">
<IconPlus size={14} strokeWidth={1.5} />
</button>
<button type="button" className="btn-action" onClick={() => handleImportClick()} title="Import environment">
<IconDownload size={14} strokeWidth={1.5} />
</button>
<button type="button" className="btn-action" onClick={() => handleExportClick()} title="Export environment">
<IconUpload size={14} strokeWidth={1.5} />
</button>
</>
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 aria-labels to icon-only section actions.
These buttons are icon-only; add aria-label for accessibility.

Suggested fix
-                  <button type="button" className="btn-action" onClick={() => handleCreateEnvClick()} title="Create environment">
+                  <button type="button" className="btn-action" onClick={() => handleCreateEnvClick()} title="Create environment" aria-label="Create environment">
                     <IconPlus size={14} strokeWidth={1.5} />
                   </button>
-                  <button type="button" className="btn-action" onClick={() => handleImportClick()} title="Import environment">
+                  <button type="button" className="btn-action" onClick={() => handleImportClick()} title="Import environment" aria-label="Import environment">
                     <IconDownload size={14} strokeWidth={1.5} />
                   </button>
-                  <button type="button" className="btn-action" onClick={() => handleExportClick()} title="Export environment">
+                  <button type="button" className="btn-action" onClick={() => handleExportClick()} title="Export environment" aria-label="Export environment">
                     <IconUpload size={14} strokeWidth={1.5} />
                   </button>
-                <button
-                  className="btn-action"
-                  onClick={handleCreateDotEnvInlineClick}
-                  title="Create .env file"
-                >
+                <button
+                  className="btn-action"
+                  onClick={handleCreateDotEnvInlineClick}
+                  title="Create .env file"
+                  aria-label="Create .env file"
+                >
                   <IconPlus size={14} strokeWidth={1.5} />
                 </button>

Also applies to: 705-711

🤖 Prompt for AI Agents
In
`@packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/index.js`
around lines 575 - 585, These icon-only action buttons (the JSX buttons that
call handleCreateEnvClick, handleImportClick, and handleExportClick) lack
aria-label attributes; update each button to include a descriptive aria-label
(e.g., "Create environment", "Import environment", "Export environment") so
screen readers can announce their purpose, and apply the same change to the
other icon-only buttons around the 705–711 block to ensure consistent
accessibility across EnvironmentList.

Comment on lines +72 to +82
useEffect(() => {
if (dotEnvFiles.length === 0) {
setSelectedDotEnvFile(null);
return;
}

const fileExists = dotEnvFiles.some((f) => f.filename === selectedDotEnvFile);
if (!selectedDotEnvFile || !fileExists) {
setSelectedDotEnvFile(dotEnvFiles[0].filename);
}
}, [dotEnvFiles]);
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

Reset dotenv view state when the list becomes empty.
Same edge case as collection view—reset dotenv state when the list is cleared via sync.

Suggested fix
   useEffect(() => {
     if (dotEnvFiles.length === 0) {
       setSelectedDotEnvFile(null);
+      setIsDotEnvModified(false);
+      setActiveView('environment');
       return;
     }
🤖 Prompt for AI Agents
In
`@packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/index.js`
around lines 72 - 82, The effect that resets dotenv selection doesn't run when
selectedDotEnvFile changes, so add selectedDotEnvFile to the dependency list and
ensure the early-empty check still sets setSelectedDotEnvFile(null) and returns;
update the useEffect signature in EnvironmentList to use [dotEnvFiles,
selectedDotEnvFile] so the logic that checks dotEnvFiles.length, computes
fileExists (dotEnvFiles.some(...)), and sets
setSelectedDotEnvFile(dotEnvFiles[0].filename) runs correctly on both list-sync
and selection changes.

Comment on lines +572 to +581
<>
<button type="button" className="btn-action" onClick={() => handleCreateEnvClick()} title="Create environment">
<IconPlus size={14} strokeWidth={1.5} />
</button>
<button type="button" className="btn-action" onClick={() => handleImportClick()} title="Import environment">
<IconDownload size={14} strokeWidth={1.5} />
</button>
<button type="button" className="btn-action" onClick={() => handleExportClick()} title="Export environment">
<IconUpload size={14} strokeWidth={1.5} />
</button>
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 aria-labels to icon-only section actions.
Improves screen reader support.

Suggested fix
-                  <button type="button" className="btn-action" onClick={() => handleCreateEnvClick()} title="Create environment">
+                  <button type="button" className="btn-action" onClick={() => handleCreateEnvClick()} title="Create environment" aria-label="Create environment">
                     <IconPlus size={14} strokeWidth={1.5} />
                   </button>
-                  <button type="button" className="btn-action" onClick={() => handleImportClick()} title="Import environment">
+                  <button type="button" className="btn-action" onClick={() => handleImportClick()} title="Import environment" aria-label="Import environment">
                     <IconDownload size={14} strokeWidth={1.5} />
                   </button>
-                  <button type="button" className="btn-action" onClick={() => handleExportClick()} title="Export environment">
+                  <button type="button" className="btn-action" onClick={() => handleExportClick()} title="Export environment" aria-label="Export environment">
                     <IconUpload size={14} strokeWidth={1.5} />
                   </button>
-                <button
-                  className="btn-action"
-                  onClick={handleCreateDotEnvInlineClick}
-                  title="Create .env file"
-                >
+                <button
+                  className="btn-action"
+                  onClick={handleCreateDotEnvInlineClick}
+                  title="Create .env file"
+                  aria-label="Create .env file"
+                >
                   <IconPlus size={14} strokeWidth={1.5} />
                 </button>

Also applies to: 702-708

🤖 Prompt for AI Agents
In
`@packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/index.js`
around lines 572 - 581, The icon-only action buttons (the three buttons invoking
handleCreateEnvClick, handleImportClick, and handleExportClick in
EnvironmentList) lack accessible labels; add descriptive aria-label attributes
(e.g., aria-label="Create environment", aria-label="Import environment",
aria-label="Export environment") to each corresponding <button> so screen
readers can announce their purpose, and apply the same change to the other
icon-only group referenced around the later block (the buttons at the 702-708
region).

Comment on lines +149 to +172
addWorkspaceWatcher(win, workspacePath, workspaceUid) {
if (this.workspaceWatchers.has(workspacePath)) {
this.workspaceWatchers.get(workspacePath).close();
}

const dotEnvPattern = path.join(workspacePath, '.env*');

const watcher = chokidar.watch(dotEnvPattern, {
...DEFAULT_WATCHER_OPTIONS,
awaitWriteFinish: {
stabilityThreshold: 80,
pollInterval: 250
}
});

const handlerOptions = {
type: 'workspace',
uid: workspaceUid,
uidKey: 'workspaceUid',
pathKey: 'workspacePath',
basePath: workspacePath,
setEnvVars: (data) => setWorkspaceDotEnvVars(workspacePath, data),
clearEnvVars: () => clearWorkspaceDotEnvVars(workspacePath)
};
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

Reset workspace dotenv cache before re-watching.

If the watcher is restarted after .env is deleted, cached workspace vars can remain stale. Clearing on startup avoids that edge case.

🧹 Suggested fix
  addWorkspaceWatcher(win, workspacePath, workspaceUid) {
+    clearWorkspaceDotEnvVars(workspacePath);
     if (this.workspaceWatchers.has(workspacePath)) {
       this.workspaceWatchers.get(workspacePath).close();
     }
📝 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
addWorkspaceWatcher(win, workspacePath, workspaceUid) {
if (this.workspaceWatchers.has(workspacePath)) {
this.workspaceWatchers.get(workspacePath).close();
}
const dotEnvPattern = path.join(workspacePath, '.env*');
const watcher = chokidar.watch(dotEnvPattern, {
...DEFAULT_WATCHER_OPTIONS,
awaitWriteFinish: {
stabilityThreshold: 80,
pollInterval: 250
}
});
const handlerOptions = {
type: 'workspace',
uid: workspaceUid,
uidKey: 'workspaceUid',
pathKey: 'workspacePath',
basePath: workspacePath,
setEnvVars: (data) => setWorkspaceDotEnvVars(workspacePath, data),
clearEnvVars: () => clearWorkspaceDotEnvVars(workspacePath)
};
addWorkspaceWatcher(win, workspacePath, workspaceUid) {
clearWorkspaceDotEnvVars(workspacePath);
if (this.workspaceWatchers.has(workspacePath)) {
this.workspaceWatchers.get(workspacePath).close();
}
const dotEnvPattern = path.join(workspacePath, '.env*');
const watcher = chokidar.watch(dotEnvPattern, {
...DEFAULT_WATCHER_OPTIONS,
awaitWriteFinish: {
stabilityThreshold: 80,
pollInterval: 250
}
});
const handlerOptions = {
type: 'workspace',
uid: workspaceUid,
uidKey: 'workspaceUid',
pathKey: 'workspacePath',
basePath: workspacePath,
setEnvVars: (data) => setWorkspaceDotEnvVars(workspacePath, data),
clearEnvVars: () => clearWorkspaceDotEnvVars(workspacePath)
};
🤖 Prompt for AI Agents
In `@packages/bruno-electron/src/app/dotenv-watcher.js` around lines 149 - 172,
When adding a watcher in addWorkspaceWatcher, clear any cached workspace dotenv
state before (re)creating the chokidar watcher to avoid stale vars after a
deleted .env; call clearWorkspaceDotEnvVars(workspacePath) (or the same
clearEnvVars used in handlerOptions) right after closing an existing watcher and
before initiating chokidar.watch so the workspace dotenv cache is reset prior to
re-watching.

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/Environments/EnvironmentSettings/EnvironmentList/index.js`:
- Around line 86-101: The effect that attaches the click-outside handler
references handleCancelDotEnvCreate but omits it from the dependency array,
risking stale closures; fix by either adding handleCancelDotEnvCreate to the
effect dependencies ([isCreatingDotEnvInline, handleCancelDotEnvCreate]) or by
stabilizing it with useCallback (wrap handleCancelDotEnvCreate in useCallback so
its identity is stable) and then include it in the dependency array alongside
isCreatingDotEnvInline; ensure this change touches the useEffect that uses
isCreatingDotEnvInline, dotEnvCreateContainerRef, and handleCancelDotEnvCreate.

In
`@packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/index.js`:
- Around line 86-99: The useEffect that installs the click-outside handler
references handleCancelDotEnvCreate but doesn't list it in the dependency array;
update the code so the effect depends on a stable callback — either add
handleCancelDotEnvCreate to the dependency array of the useEffect or wrap
handleCancelDotEnvCreate in useCallback (so its identity is stable) and then
include that stable callback in the dependency list; keep references to
dotEnvCreateContainerRef and isCreatingDotEnvInline as they are when adjusting
dependencies.
🧹 Nitpick comments (2)
packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/StyledWrapper.js (1)

1-346: Consider extracting shared styles to reduce duplication.

This StyledWrapper is nearly identical to packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/StyledWrapper.js. Both define the same selectors (.sections-container, .environments-list, .btn-action, .environment-item, .empty-state, etc.) with identical styles.

Consider extracting common styles into a shared module to reduce maintenance burden and ensure consistency.

packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/index.js (1)

1-784: Significant duplication with EnvironmentSettings/EnvironmentList.

This component is ~90% identical to packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/index.js. The main differences are:

  • Workspace-scoped vs collection-scoped Redux actions
  • workspace prop vs collection prop for dotenv operations
  • Global environment actions vs collection environment actions

Consider extracting shared logic into a custom hook (e.g., useDotEnvManagement) or a higher-order component to reduce duplication. As per coding guidelines: "Prefer custom hooks for business logic, data fetching, and side-effects."

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

🤖 Fix all issues with AI agents
In
`@packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/index.js`:
- Around line 75-87: The useEffect that checks dotEnvFiles and
selectedDotEnvFile is missing selectedDotEnvFile from its dependency array,
which risks stale reads; update the effect dependencies to include
selectedDotEnvFile so the effect reruns when selection changes (i.e., the effect
that contains checks using dotEnvFiles.some((f) => f.filename ===
selectedDotEnvFile) should depend on both dotEnvFiles and selectedDotEnvFile),
leaving the existing state setters (setSelectedDotEnvFile, setActiveView,
setIsDotEnvModified) unchanged.

Comment on lines +75 to +87
useEffect(() => {
if (dotEnvFiles.length === 0) {
setSelectedDotEnvFile(null);
setActiveView('environment');
setIsDotEnvModified(false);
return;
}

const fileExists = dotEnvFiles.some((f) => f.filename === selectedDotEnvFile);
if (!selectedDotEnvFile || !fileExists) {
setSelectedDotEnvFile(dotEnvFiles[0].filename);
}
}, [dotEnvFiles]);
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 selectedDotEnvFile in useEffect dependency array.

The effect references selectedDotEnvFile on line 83-84 but omits it from the dependency array at line 87. This can lead to stale closure issues when checking if the currently selected file still exists.

Suggested fix
-  }, [dotEnvFiles]);
+  }, [dotEnvFiles, selectedDotEnvFile]);
🤖 Prompt for AI Agents
In
`@packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/index.js`
around lines 75 - 87, The useEffect that checks dotEnvFiles and
selectedDotEnvFile is missing selectedDotEnvFile from its dependency array,
which risks stale reads; update the effect dependencies to include
selectedDotEnvFile so the effect reruns when selection changes (i.e., the effect
that contains checks using dotEnvFiles.some((f) => f.filename ===
selectedDotEnvFile) should depend on both dotEnvFiles and selectedDotEnvFile),
leaving the existing state setters (setSelectedDotEnvFile, setActiveView,
setIsDotEnvModified) unchanged.

@bijin-bruno bijin-bruno merged commit 700e25a into usebruno:main Feb 2, 2026
7 of 8 checks passed
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.

3 participants