Compare commits

..

4 Commits

2702 changed files with 38251 additions and 280554 deletions

View File

@@ -1,79 +0,0 @@
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
language: 'en-US'
early_access: false
tone_instructions: 'You are an expert code reviewer in TypeScript, JavaScript, NodeJS, and ElectronJS. You work in an enterprise software developer team, providing concise and clear code review advice. You only elaborate or provide detailed explanations when requested.'
knowledge_base:
opt_out: false
code_guidelines:
enabled: true
filePatterns:
- '**/CODING_STANDARDS.md'
reviews:
profile: 'chill'
request_changes_workflow: false
high_level_summary: true
poem: true
review_status: true
collapse_walkthrough: false
auto_review:
enabled: true
drafts: false
base_branches: ['main', 'release/*']
path_instructions:
- path: '**/*'
instructions: |
Bruno is a cross-platform Electron desktop app that runs on macOS, Windows, and Linux. Ensure that all code is OS-agnostic:
- File paths must use `path.join()` or `path.resolve()` instead of hardcoded `/` or `\\` separators
- Never assume case-sensitive or case-insensitive filesystems
- Use `os.homedir()`, `app.getPath()`, or environment-appropriate APIs instead of hardcoded paths like `/home/`, `C:\\Users\\`, or `~/`
- Line endings should be handled consistently (be aware of CRLF vs LF issues)
- Use `path.sep` or `path.posix`/`path.win32` when platform-specific separators are needed
- Shell commands or child_process calls must account for platform differences (e.g., `which` vs `where`, `/bin/sh` vs `cmd.exe`)
- File permissions (e.g., `fs.chmod`, `fs.access`) should account for Windows not supporting Unix-style permission bits
- Avoid relying on Unix-only signals (e.g., `SIGKILL`) without Windows fallbacks
- Use `os.tmpdir()` instead of hardcoding `/tmp`
- Environment variable access should handle platform differences (e.g., `HOME` vs `USERPROFILE`)
- path: 'tests/**/**.*'
instructions: |
Review the following e2e test code written using the Playwright test library. Ensure that:
- Follow best practices for Playwright code and e2e automation
- Try to reduce usage of `page.waitForTimeout();` in code unless absolutely necessary and the locator cannot be found using existing `expect()` playwright calls
- Avoid using `page.pause()` in code
- Use locator variables for locators
- Avoid using test.only
- Use multiple assertions
- Promote the use of `test.step` as much as possible so the generated reports are easier to read
- Ensure that the `fixtures` like the collections are nested inside the `fixtures` folder
**Fixture Example***: Here's an example of possible fixture and test pair
```
.
├── fixtures
│ └── collection
│ ├── base.bru
│ ├── bruno.json
│ ├── collection.bru
│ ├── ws-test-request-with-headers.bru
│ ├── ws-test-request-with-subproto.bru
│ └── ws-test-request.bru
├── connection.spec.ts # <- Depends on the collection in ./fixtures/collection
├── headers.spec.ts
├── persistence.spec.ts
├── variable-interpolation
│ ├── fixtures
│ │ └── collection
│ │ ├── environments
│ │ ├── bruno.json
│ │ └── ws-interpolation-test.bru
│ ├── init-user-data
│ └── variable-interpolation.spec.ts # <- Depends on the collection in ./variable-interpolation/fixtures/collection
└── subproto.spec.ts
```
chat:
auto_reply: true

2
.gitattributes vendored
View File

@@ -1,2 +0,0 @@
# Force LF line endings for all text files
* text=auto eol=lf

2
.github/CODEOWNERS vendored
View File

@@ -1 +1 @@
* @helloanoop @bijin-bruno @lohit-bruno @naman-bruno @sid-bruno @vijayh-bruno @utkarsh-bruno @sanish-bruno
* @helloanoop @maintainer-bruno @bijin-bruno @lohit-bruno @naman-bruno

View File

@@ -49,21 +49,14 @@ body:
- type: textarea
attributes:
label: Describe the bug
description: A clear and concise description of the bug and how it's affecting your work
description: A clear and concise description of the bug and how it's effecting your work along with steps to reproduce.
validations:
required: true
- type: textarea
attributes:
label: Steps to reproduce
description: The exact steps that can be performed to reproduce the issue
validations:
required: true
- type: textarea
attributes:
label: Collection to reproduce
description: If possible, please attach the collection where the bug is present
label: .bru file to reproduce the bug
description: Attach your .bru file here that can reproduce the problem.
validations:
required: false

View File

@@ -1,4 +1,4 @@
blank_issues_enabled: false
blank_issues_enabled: true
contact_links:
- name: Discussions
url: https://github.com/usebruno/bruno/discussions

View File

@@ -1,10 +1,9 @@
### Description
# Description
<!-- Explain here the changes your PR introduces and text to help us understand the context of this change. -->
#### Contribution Checklist:
### 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.**
@@ -13,6 +12,6 @@
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
### Publishing to New Package Managers
Please see [here](../publishing.md) for more information.

View File

@@ -1,19 +0,0 @@
name: 'Run Auth E2E Tests - Linux'
description: 'Run Auth E2E tests on Linux'
runs:
using: 'composite'
steps:
- name: Run Auth E2E tests
shell: bash
run: |
set -euo pipefail
xvfb-run npm run test:e2e:auth
- name: Upload Playwright Report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-report-auth-linux
path: playwright-report/
retention-days: 30

View File

@@ -1,30 +0,0 @@
name: 'Run OAuth1 CLI Tests - Linux'
description: 'Run OAuth1 CLI tests on Linux'
runs:
using: 'composite'
steps:
- name: Run BRU format CLI tests
shell: bash
run: |
set -euo pipefail
BRU_CLI="../../../../../../packages/bruno-cli/bin/bru.js"
# navigate to BRU test collection directory
cd tests/auth/oauth1/fixtures/collections/bru
echo "=== BRU Format Collection Run ==="
node $BRU_CLI run --env Local --output junit-bru.xml --format junit
- name: Run YML format CLI tests
shell: bash
run: |
set -euo pipefail
BRU_CLI="../../../../../../packages/bruno-cli/bin/bru.js"
# navigate to YML test collection directory
cd tests/auth/oauth1/fixtures/collections/yml
echo "=== YML Format Collection Run ==="
node $BRU_CLI run --env Local --output junit-yml.xml --format junit

View File

@@ -1,18 +0,0 @@
name: 'Setup Auth Feature Dependencies - Linux'
description: 'Setup feature-specific dependencies for auth tests on Linux'
runs:
using: 'composite'
steps:
- name: Install additional OS dependencies for auth tests
shell: bash
run: |
sudo apt-get update
sudo apt-get --no-install-recommends install -y \
libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \
xvfb
CHROME_SANDBOX="${GITHUB_WORKSPACE}/node_modules/electron/dist/chrome-sandbox"
if [[ -f "$CHROME_SANDBOX" ]]; then
sudo chown root "$CHROME_SANDBOX"
sudo chmod 4755 "$CHROME_SANDBOX"
fi

View File

@@ -1,16 +0,0 @@
name: 'Start Test Server - Linux'
description: 'Start the bruno-tests mock server for OAuth1 CLI tests on Linux'
runs:
using: 'composite'
steps:
- name: Start test server
shell: bash
run: |
set -euo pipefail
cd packages/bruno-tests
echo "starting test server in background"
node src/index.js &
echo "server started with PID: $!"

View File

@@ -1,17 +0,0 @@
name: 'Run Auth E2E Tests - macOS'
description: 'Run Auth E2E tests on macOS'
runs:
using: 'composite'
steps:
- name: Run Auth E2E tests
shell: bash
run: |
npm run test:e2e:auth
- name: Upload Playwright Report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-report-auth-macos
path: playwright-report/
retention-days: 30

View File

@@ -1,30 +0,0 @@
name: 'Run OAuth1 CLI Tests - macOS'
description: 'Run OAuth1 CLI tests on macOS'
runs:
using: 'composite'
steps:
- name: Run BRU format CLI tests
shell: bash
run: |
set -euo pipefail
BRU_CLI="../../../../../../packages/bruno-cli/bin/bru.js"
# navigate to BRU test collection directory
cd tests/auth/oauth1/fixtures/collections/bru
echo "=== BRU Format Collection Run ==="
node $BRU_CLI run --env Local --output junit-bru.xml --format junit
- name: Run YML format CLI tests
shell: bash
run: |
set -euo pipefail
BRU_CLI="../../../../../../packages/bruno-cli/bin/bru.js"
# navigate to YML test collection directory
cd tests/auth/oauth1/fixtures/collections/yml
echo "=== YML Format Collection Run ==="
node $BRU_CLI run --env Local --output junit-yml.xml --format junit

View File

@@ -1,16 +0,0 @@
name: 'Start Test Server - macOS'
description: 'Start the bruno-tests mock server for OAuth1 CLI tests on macOS'
runs:
using: 'composite'
steps:
- name: Start test server
shell: bash
run: |
set -euo pipefail
cd packages/bruno-tests
echo "starting test server in background"
node src/index.js &
echo "server started with PID: $!"

View File

@@ -1,17 +0,0 @@
name: 'Run Auth E2E Tests - Windows'
description: 'Run Auth E2E tests on Windows'
runs:
using: 'composite'
steps:
- name: Run Auth E2E tests
shell: pwsh
run: |
npm run test:e2e:auth
- name: Upload Playwright Report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-report-auth-windows
path: playwright-report/
retention-days: 30

View File

@@ -1,34 +0,0 @@
name: 'Run OAuth1 CLI Tests - Windows'
description: 'Run OAuth1 CLI tests on Windows'
runs:
using: 'composite'
steps:
- name: Run BRU format CLI tests
shell: pwsh
run: |
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$BRU_CLI = "..\..\..\..\..\..\packages\bruno-cli\bin\bru.js"
# navigate to BRU test collection directory
Set-Location tests\auth\oauth1\fixtures\collections\bru
Write-Host "=== BRU Format Collection Run ==="
$process = Start-Process -FilePath "node" -ArgumentList "$BRU_CLI run --env Local --output junit-bru.xml --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul"
if ($process.ExitCode -ne 0) { exit 1 }
- name: Run YML format CLI tests
shell: pwsh
run: |
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$BRU_CLI = "..\..\..\..\..\..\packages\bruno-cli\bin\bru.js"
# navigate to YML test collection directory
Set-Location tests\auth\oauth1\fixtures\collections\yml
Write-Host "=== YML Format Collection Run ==="
$process = Start-Process -FilePath "node" -ArgumentList "$BRU_CLI run --env Local --output junit-yml.xml --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul"
if ($process.ExitCode -ne 0) { exit 1 }

View File

@@ -1,14 +0,0 @@
name: 'Start Test Server - Windows'
description: 'Start the bruno-tests mock server for OAuth1 CLI tests on Windows'
runs:
using: 'composite'
steps:
- name: Start test server
shell: pwsh
run: |
Set-StrictMode -Version Latest
Set-Location packages\bruno-tests
Write-Host "starting test server in background"
Start-Process -FilePath "node" -ArgumentList "src\index.js" -PassThru -WindowStyle Hidden

View File

@@ -1,14 +1,5 @@
name: 'Setup Node Dependencies'
description: 'Install Node.js and npm dependencies'
inputs:
skip-build:
description: 'Skip building libraries'
required: false
default: 'false'
shell:
description: 'Shell to use (bash, pwsh)'
required: false
default: 'bash'
runs:
using: 'composite'
steps:
@@ -18,14 +9,13 @@ runs:
node-version: v22.17.0
cache: 'npm'
cache-dependency-path: './package-lock.json'
- name: Install node dependencies
shell: ${{ inputs.shell }}
shell: bash
run: npm ci --legacy-peer-deps
- name: Build libraries
if: inputs.skip-build != 'true'
shell: ${{ inputs.shell }}
shell: bash
run: |
npm run build:graphql-docs
npm run build:bruno-query
@@ -33,5 +23,4 @@ runs:
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
npm run build:bruno-converters
npm run build:bruno-requests
npm run build:schema-types
npm run build:bruno-filestore

View File

@@ -7,13 +7,13 @@ runs:
shell: bash
run: |
set -euo pipefail
xvfb-run npm run test:e2e:ssl
- name: Upload Playwright Report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-report-linux-ssl
name: playwright-report-linux
path: playwright-report/
retention-days: 30

View File

@@ -11,8 +11,5 @@ runs:
libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \
xvfb libxml2-utils
CHROME_SANDBOX="${GITHUB_WORKSPACE}/node_modules/electron/dist/chrome-sandbox"
if [[ -f "$CHROME_SANDBOX" ]]; then
sudo chown root "$CHROME_SANDBOX"
sudo chmod 4755 "$CHROME_SANDBOX"
fi
sudo chown root /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
sudo chmod 4755 /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox

View File

@@ -12,6 +12,6 @@ runs:
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-report-macos-ssl
name: playwright-report-macos
path: playwright-report/
retention-days: 30

View File

@@ -12,6 +12,6 @@ runs:
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-report-windows-ssl
name: playwright-report-windows
path: playwright-report/
retention-days: 30

View File

@@ -1,38 +0,0 @@
name: 'Run Benchmark Tests'
description: 'Run Playwright benchmark tests and compare against baseline'
inputs:
os:
description: 'Operating system (ubuntu, macos, windows)'
default: 'ubuntu'
update-baseline:
description: 'Update baseline instead of comparing'
default: 'false'
runs:
using: 'composite'
steps:
- name: Run Benchmark Tests (Ubuntu)
if: inputs.os == 'ubuntu'
shell: bash
run: xvfb-run npm run test:benchmark
- name: Run Benchmark Tests
if: inputs.os != 'ubuntu'
shell: bash
run: npm run test:benchmark
- name: Update Baseline
if: inputs.update-baseline == 'true'
shell: bash
run: >-
node tests/benchmarks/utils/compare.js
--results tests/benchmarks/results/mounting.json
--baseline tests/benchmarks/mounting/baseline.${{ inputs.os }}.json
--update-baseline
- name: Compare Against Baseline
if: inputs.update-baseline != 'true'
shell: bash
run: >-
node tests/benchmarks/utils/compare.js
--results tests/benchmarks/results/mounting.json
--baseline tests/benchmarks/mounting/baseline.${{ inputs.os }}.json

View File

@@ -1,41 +0,0 @@
name: 'Run CLI Tests'
description: 'Setup dependencies, start local testbench and run CLI tests'
inputs:
shell:
description: 'Shell to use (bash, pwsh)'
required: false
default: 'bash'
runs:
using: 'composite'
steps:
- name: Install Test Collection Dependencies
shell: ${{ inputs.shell }}
run: npm ci --prefix packages/bruno-tests/collection
- name: Run Local Testbench and CLI Tests
if: inputs.shell != 'pwsh'
shell: ${{ inputs.shell }}
run: |
npm start --workspace=packages/bruno-tests &
sleep 5
cd packages/bruno-tests/collection
node ../../bruno-cli/bin/bru.js run --env Prod --output junit.xml --format junit --sandbox developer
- name: Run Local Testbench and CLI Tests - Windows
if: inputs.shell == 'pwsh'
shell: pwsh
run: |
$process = Start-Process "npm.cmd" `
-ArgumentList "start","--workspace=packages/bruno-tests" `
-NoNewWindow `
-PassThru
Start-Sleep -Seconds 5
if ($process.HasExited) {
Write-Error "Server exited early"
exit 1
}
cd packages/bruno-tests/collection
node ../../bruno-cli/bin/bru.js run --env Prod --output junit.xml --format junit --sandbox developer

View File

@@ -1,26 +0,0 @@
name: 'Run E2E Tests'
description: 'Setup dependencies, configure environment, and run Playwright E2E tests'
inputs:
os:
description: 'Operating system (ubuntu, macos, windows)'
default: 'ubuntu'
shell:
description: 'Shell to use (bash, pwsh)'
required: false
default: 'bash'
runs:
using: 'composite'
steps:
- name: Install Test Collection Dependencies
shell: ${{ inputs.shell }}
run: npm ci --prefix packages/bruno-tests/collection
- name: Run Playwright Tests (Ubuntu)
if: inputs.os == 'ubuntu'
shell: bash
run: xvfb-run dbus-run-session -- npm run test:e2e
- name: Run Playwright Tests
if: inputs.os != 'ubuntu'
shell: ${{ inputs.shell }}
run: npm run test:e2e

View File

@@ -1,53 +0,0 @@
name: 'Run Unit Tests'
description: 'Setup dependencies and run unit tests for all packages'
inputs:
shell:
description: 'Shell to use (bash, pwsh)'
required: false
default: 'bash'
runs:
using: 'composite'
steps:
- name: Test Package bruno-js
shell: ${{ inputs.shell }}
run: npm run test:ci --workspace=packages/bruno-js
- name: Test Package bruno-cli
shell: ${{ inputs.shell }}
run: npm run test:ci --workspace=packages/bruno-cli
- name: Test Package bruno-query
shell: ${{ inputs.shell }}
run: npm run test --workspace=packages/bruno-query
- name: Test Package bruno-lang
shell: ${{ inputs.shell }}
run: npm run test --workspace=packages/bruno-lang
- name: Test Package bruno-schema
shell: ${{ inputs.shell }}
run: npm run test --workspace=packages/bruno-schema
- name: Test Package bruno-app
shell: ${{ inputs.shell }}
run: npm run test --workspace=packages/bruno-app
- name: Test Package bruno-common
shell: ${{ inputs.shell }}
run: npm run test --workspace=packages/bruno-common
- name: Test Package bruno-converters
shell: ${{ inputs.shell }}
run: npm run test:ci --workspace=packages/bruno-converters
- name: Test Package bruno-electron
shell: ${{ inputs.shell }}
run: npm run test:ci --workspace=packages/bruno-electron
- name: Test Package bruno-requests
shell: ${{ inputs.shell }}
run: npm run test --workspace=packages/bruno-requests
- name: Test Package bruno-filestore
shell: ${{ inputs.shell }}
run: npm run test --workspace=packages/bruno-filestore

View File

@@ -1,70 +0,0 @@
const fs = require('fs');
const { execSync } = require('child_process');
// Check if flaky-tests.json exists
if (!fs.existsSync('flaky-tests.json')) {
console.log('No flaky-tests.json found');
process.exit(0);
}
// Get changed files in PR
let changedFiles = [];
try {
changedFiles = execSync('git diff --name-only origin/main...HEAD')
.toString()
.split('\n')
.filter(f => f.endsWith('.spec.ts'));
} catch (error) {
console.log('Could not determine changed files:', error.message);
process.exit(0);
}
if (changedFiles.length === 0) {
console.log('No test files were modified in this PR');
process.exit(0);
}
// Read flaky tests
const flakyTests = JSON.parse(fs.readFileSync('flaky-tests.json', 'utf8'));
if (flakyTests.length === 0) {
console.log('No flaky/failed tests found');
process.exit(0);
}
// Find modified flaky tests
const modifiedFlakyTests = flakyTests.filter(test =>
changedFiles.some(file => test.file.includes(file))
);
if (modifiedFlakyTests.length === 0) {
console.log('No modified test files are flaky');
process.exit(0);
}
// Generate comment markdown
let comment = '## ⚠️ Warning: You modified flaky/failed test files\n\n';
comment += 'The following test files you modified have reliability issues:\n\n';
modifiedFlakyTests.forEach(test => {
const testType = test.status === 'failed' ? '❌ Failed' : '⚠️ Flaky';
comment += `### ${testType}: \`${test.file}\`\n`;
comment += `**Test:** ${test.testTitle}\n`;
comment += `**Status:** ${test.status}\n`;
if (test.retryAttempt > 0) {
comment += `**Retry Attempt:** ${test.retryAttempt}\n`;
}
comment += '\n**To debug locally, run:**\n';
comment += '```bash\n';
comment += `npx playwright test ${test.file} --repeat-each=5 --workers=1\n`;
comment += '```\n\n';
});
comment += '---\n';
comment += '**Note:** Flaky tests passed after retrying, failed tests did not pass. ';
comment += 'Please investigate and fix the root cause before merging.\n';
// Save comment to file for GitHub Action to post
fs.writeFileSync('pr-comment.md', comment);
console.log(`Found ${modifiedFlakyTests.length} modified flaky tests`);

View File

@@ -1,78 +0,0 @@
const fs = require('fs');
// Read Playwright JSON report
const resultsPath = 'playwright-report/results.json';
if (!fs.existsSync(resultsPath)) {
console.log('No Playwright results found at', resultsPath);
process.exit(0);
}
const results = JSON.parse(fs.readFileSync(resultsPath, 'utf8'));
// Extract flaky tests
// A test is flaky if: status === "passed" AND retry > 0
// A test is failed if: status === "failed"
// This means it failed initially but passed on retry OR failed completely
const flakyTests = [];
function traverseSuites(suites) {
for (const suite of suites) {
// Process specs in this suite
for (const spec of suite.specs || []) {
for (const test of spec.tests || []) {
// Check each test result
for (const result of test.results || []) {
// Track two types of problematic tests:
// 1. Flaky: passed on a retry attempt (retry > 0)
// 2. Failed: failed on all attempts
if ((result.status === 'passed' && result.retry > 0) || result.status === 'failed') {
flakyTests.push({
file: spec.file,
title: spec.title,
testTitle: spec.title,
line: spec.line,
status: result.status,
retryAttempt: result.retry
});
break; // Only record once per test
}
}
}
}
// Recursively process nested suites
if (suite.suites && suite.suites.length > 0) {
traverseSuites(suite.suites);
}
}
}
traverseSuites(results.suites || []);
// Save flaky tests to JSON
fs.writeFileSync('flaky-tests.json', JSON.stringify(flakyTests, null, 2));
// Generate markdown report
let markdown = '## ⚠️ Flaky/Failed Tests Detected\n\n';
markdown += 'The following tests are problematic:\n\n';
flakyTests.forEach(test => {
const testType = test.status === 'failed' ? '❌ Failed' : '⚠️ Flaky';
markdown += `### ${testType}: \`${test.file}\`\n`;
markdown += `- **Test:** ${test.testTitle}\n`;
markdown += `- **Status:** ${test.status}\n`;
if (test.retryAttempt > 0) {
markdown += `- **Retry Attempt:** ${test.retryAttempt}\n`;
}
markdown += `- **Debug command:**\n`;
markdown += '```bash\n';
markdown += `npx playwright test ${test.file} --repeat-each=5 --workers=1\n`;
markdown += '```\n\n';
});
fs.writeFileSync('flaky-report.md', markdown);
console.log(`Found ${flakyTests.length} flaky/failed tests`);
process.exit(flakyTests.length > 0 ? 1 : 0);

View File

@@ -1,91 +0,0 @@
name: Benchmarks
on:
workflow_dispatch:
inputs:
update-baseline:
description: 'Update baseline with current results instead of comparing'
type: boolean
default: false
pull_request:
branches: [main, 'release/v*']
jobs:
benchmark:
name: Performance Benchmarks (${{ matrix.os }})
timeout-minutes: 60
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, macos-latest, windows-latest]
include:
- os: ubuntu-24.04
os-name: ubuntu
- os: macos-latest
os-name: macos
- os: windows-latest
os-name: windows
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v6
- name: Install System Dependencies (Ubuntu)
if: matrix.os-name == 'ubuntu'
run: |
sudo apt-get update
sudo apt-get --no-install-recommends install -y \
libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \
xvfb
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
- name: Configure Chrome Sandbox
if: matrix.os-name == 'ubuntu'
run: |
CHROME_SANDBOX="${GITHUB_WORKSPACE}/node_modules/electron/dist/chrome-sandbox"
if [[ -f "$CHROME_SANDBOX" ]]; then
sudo chown root "$CHROME_SANDBOX"
sudo chmod 4755 "$CHROME_SANDBOX"
fi
- name: Run Benchmark Tests
uses: ./.github/actions/tests/run-benchmark-tests
with:
os: ${{ matrix.os-name }}
update-baseline: ${{ github.event.inputs.update-baseline || 'false' }}
- name: Upload Benchmark Results
uses: actions/upload-artifact@v6
if: ${{ !cancelled() }}
with:
name: benchmark-results-${{ matrix.os-name }}
path: |
tests/benchmarks/results/
benchmark-report/
retention-days: 30
- name: Commit Updated Baseline
if: github.event.inputs.update-baseline == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add tests/benchmarks/mounting/baseline.${{ matrix.os-name }}.json
git diff --staged --quiet || git commit -m "chore: update ${{ matrix.os-name }} benchmark baseline" && git push
- name: Comment Benchmark Results on PR
if: github.event_name == 'pull_request' && !cancelled()
continue-on-error: true
uses: actions/github-script@v7
with:
script: |
const run = require('./tests/benchmarks/utils/pr-comment.js');
await run({
github,
context,
resultsPath: 'tests/benchmarks/results/mounting.json',
baselinePath: 'tests/benchmarks/mounting/baseline.${{ matrix.os-name }}.json',
title: 'Benchmark Results — Collection Mount (${{ matrix.os-name }})'
});

View File

@@ -1,123 +0,0 @@
name: Flaky Test Detector
on:
pull_request:
branches: [main]
paths:
- 'tests/**/*.spec.ts'
permissions:
contents: read
pull-requests: write
issues: write
checks: write
jobs:
detect-flaky-tests:
name: Detect Flaky Tests
runs-on: ubuntu-24.04
timeout-minutes: 60
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0 # Need full history to compare with main
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get --no-install-recommends install -y \
libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 \
libcups2 libgtk-3-0 libasound2t64 xvfb
- name: Install npm dependencies
run: |
npm ci --legacy-peer-deps
CHROME_SANDBOX="${GITHUB_WORKSPACE}/node_modules/electron/dist/chrome-sandbox"
if [[ -f "$CHROME_SANDBOX" ]]; then
sudo chown root "$CHROME_SANDBOX"
sudo chmod 4755 "$CHROME_SANDBOX"
fi
- name: Install test collection dependencies
run: npm ci --prefix packages/bruno-tests/collection
- name: Build libraries
run: |
npm run build:graphql-docs
npm run build:bruno-query
npm run build:bruno-common
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
npm run build:bruno-converters
npm run build:bruno-requests
npm run build:schema-types
npm run build:bruno-filestore
- name: Run Playwright tests
run: xvfb-run npm run test:e2e
continue-on-error: true # Continue even if tests fail
- name: Detect flaky tests
id: detect
run: node .github/scripts/detect-flaky-tests.js
continue-on-error: true # Don't fail workflow if flaky tests found
- name: Check modified flaky tests
id: check-modified
run: node .github/scripts/comment-on-flaky-tests.js
continue-on-error: true
- name: Post PR comment
if: hashFiles('pr-comment.md') != ''
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const comment = fs.readFileSync('pr-comment.md', 'utf8');
// Check if we already commented
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
const botComment = comments.find(c =>
c.user.type === 'Bot' && c.body.includes('Warning: You modified flaky/failed test files')
);
if (botComment) {
// Update existing comment
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: comment
});
} else {
// Create new comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});
}
- name: Upload flaky test artifacts
if: always()
uses: actions/upload-artifact@v6
with:
name: flaky-test-results
path: |
flaky-tests.json
flaky-report.md
playwright-report/
retention-days: 30

View File

@@ -1,26 +0,0 @@
name: Lint Checks
on:
workflow_dispatch:
push:
branches: [main, 'release/v*']
pull_request:
branches: [main, 'release/v*']
jobs:
lint:
name: Lint Check
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
with:
skip-build: 'true'
- name: Lint Check
run: npm run lint
env:
ESLINT_PLUGIN_DIFF_COMMIT: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || 'main' }}

View File

@@ -25,7 +25,7 @@ jobs:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version-file: '.nvmrc'
@@ -40,10 +40,10 @@ jobs:
run: |
cd packages/bruno-tests/collection
npm install
bru run --env Prod --output junit.xml --format junit --sandbox developer
bru run --env Prod --output junit.xml --format junit
- name: Publish Test Report
uses: dorny/test-reporter@v3
uses: dorny/test-reporter@v2
if: success() || failure()
with:
name: Test Report

91
.github/workflows/ssl-tests.yml vendored Normal file
View File

@@ -0,0 +1,91 @@
name: SSL Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
tests-for-linux:
name: SSL Tests - Linux
timeout-minutes: 60
runs-on: ubuntu-latest
permissions:
checks: write
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v5
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
- name: Setup Feature Dependencies
uses: ./.github/actions/ssl/linux/setup-feature-specific-deps
- name: Setup CA Certificates
uses: ./.github/actions/ssl/linux/setup-ca-certs
- name: Run Basic SSL CLI Tests
uses: ./.github/actions/ssl/linux/run-basic-ssl-cli-tests
- name: Run Custom CA Certs CLI Tests
uses: ./.github/actions/ssl/linux/run-custom-ca-certs-cli-tests
- name: Run Custom CA Certs E2E Tests
uses: ./.github/actions/ssl/linux/run-ssl-e2e-tests
tests-for-macos:
name: SSL Tests - macOS
timeout-minutes: 60
runs-on: macos-latest
permissions:
checks: write
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v5
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
- name: Setup Feature Dependencies
uses: ./.github/actions/ssl/macos/setup-feature-specific-deps
- name: Setup CA Certificates
uses: ./.github/actions/ssl/macos/setup-ca-certs
- name: Run Basic SSL CLI Tests
uses: ./.github/actions/ssl/macos/run-basic-ssl-cli-tests
- name: Run Custom CA Certs CLI Tests
uses: ./.github/actions/ssl/macos/run-custom-ca-certs-cli-tests
- name: Run Custom CA Certs E2E Tests
uses: ./.github/actions/ssl/macos/run-ssl-e2e-tests
tests-for-windows:
name: SSL Tests - Windows
timeout-minutes: 60
runs-on: windows-latest
permissions:
checks: write
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v5
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
- name: Setup CA Certificates
uses: ./.github/actions/ssl/windows/setup-ca-certs
- name: Run Basic SSL CLI Tests
uses: ./.github/actions/ssl/windows/run-basic-ssl-cli-tests
- name: Run Custom CA Certs CLI Tests
uses: ./.github/actions/ssl/windows/run-custom-ca-certs-cli-tests
- name: Run Custom CA Certs E2E Tests
uses: ./.github/actions/ssl/windows/run-ssl-e2e-tests

View File

@@ -1,146 +0,0 @@
name: Linux Tests
on:
workflow_dispatch:
push:
branches: [main, 'release/v*']
pull_request:
branches: [main, 'release/v*']
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
unit-test:
name: Unit Tests (Linux)
timeout-minutes: 60
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
- name: Run Unit Tests
uses: ./.github/actions/tests/run-unit-tests
cli-test:
name: CLI Tests (Linux)
runs-on: ubuntu-latest
permissions:
checks: write
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
- name: Run CLI Tests
uses: ./.github/actions/tests/run-cli-tests
- name: Publish Test Report
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
check_name: CLI Test Results (Linux)
files: packages/bruno-tests/collection/junit.xml
comment_mode: always
check_run: false
e2e-test:
name: Playwright E2E Tests (Linux)
timeout-minutes: 240
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- name: Install System Dependencies (Ubuntu)
run: |
sudo apt-get update
sudo apt-get --no-install-recommends install -y \
libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \
xvfb \
gsettings-desktop-schemas dbus-x11
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
- name: Configure Chrome Sandbox
run: |
CHROME_SANDBOX="${GITHUB_WORKSPACE}/node_modules/electron/dist/chrome-sandbox"
if [[ -f "$CHROME_SANDBOX" ]]; then
sudo chown root "$CHROME_SANDBOX"
sudo chmod 4755 "$CHROME_SANDBOX"
fi
- name: Run E2E Tests
uses: ./.github/actions/tests/run-e2e-tests
with:
os: ubuntu
- name: Upload Playwright Report
uses: actions/upload-artifact@v6
if: ${{ !cancelled() }}
with:
name: playwright-report-linux
path: playwright-report/
retention-days: 30
ssl-test:
name: SSL Tests (Linux)
timeout-minutes: 60
runs-on: ubuntu-latest
permissions:
checks: write
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
- name: Setup Feature Dependencies
uses: ./.github/actions/ssl/linux/setup-feature-specific-deps
- name: Setup CA Certificates
uses: ./.github/actions/ssl/linux/setup-ca-certs
- name: Run Basic SSL CLI Tests
uses: ./.github/actions/ssl/linux/run-basic-ssl-cli-tests
- name: Run Custom CA Certs CLI Tests
uses: ./.github/actions/ssl/linux/run-custom-ca-certs-cli-tests
- name: Run Custom CA Certs E2E Tests
uses: ./.github/actions/ssl/linux/run-ssl-e2e-tests
oauth1-tests:
name: OAuth 1.0 Auth Tests (Linux)
timeout-minutes: 60
runs-on: ubuntu-latest
permissions:
checks: write
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
- name: Setup Feature Dependencies
uses: ./.github/actions/auth/oauth1/linux/setup-feature-specific-deps
- name: Run Auth E2E Tests
uses: ./.github/actions/auth/oauth1/linux/run-auth-e2e-tests
- name: Start Test Server
uses: ./.github/actions/auth/oauth1/linux/start-test-server
- name: Run OAuth1 CLI Tests
uses: ./.github/actions/auth/oauth1/linux/run-oauth1-cli-tests

View File

@@ -1,127 +0,0 @@
name: macOS Tests
on:
workflow_dispatch:
push:
branches: [main, 'release/v*']
pull_request:
branches: [main, 'release/v*']
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
unit-test:
name: Unit Tests (macOS)
timeout-minutes: 60
runs-on: macos-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
- name: Run Unit Tests
uses: ./.github/actions/tests/run-unit-tests
cli-test:
name: CLI Tests (macOS)
runs-on: macos-latest
permissions:
checks: write
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
- name: Run CLI Tests
uses: ./.github/actions/tests/run-cli-tests
- name: Publish Test Report
uses: EnricoMi/publish-unit-test-result-action/macos@v2
if: always()
with:
check_name: CLI Test Results (macOS)
files: packages/bruno-tests/collection/junit.xml
comment_mode: off
check_run: false
e2e-test:
name: Playwright E2E Tests (macOS)
timeout-minutes: 240
runs-on: macos-latest
steps:
- uses: actions/checkout@v6
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
- name: Run E2E Tests
uses: ./.github/actions/tests/run-e2e-tests
with:
os: macos
- name: Upload Playwright Report
uses: actions/upload-artifact@v6
if: ${{ !cancelled() }}
with:
name: playwright-report-macos
path: playwright-report/
retention-days: 30
ssl-test:
name: SSL Tests (macOS)
timeout-minutes: 60
runs-on: macos-latest
permissions:
checks: write
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
- name: Setup Feature Dependencies
uses: ./.github/actions/ssl/macos/setup-feature-specific-deps
- name: Setup CA Certificates
uses: ./.github/actions/ssl/macos/setup-ca-certs
- name: Run Basic SSL CLI Tests
uses: ./.github/actions/ssl/macos/run-basic-ssl-cli-tests
- name: Run Custom CA Certs CLI Tests
uses: ./.github/actions/ssl/macos/run-custom-ca-certs-cli-tests
- name: Run Custom CA Certs E2E Tests
uses: ./.github/actions/ssl/macos/run-ssl-e2e-tests
oauth1-tests:
name: OAuth 1.0 Auth Tests (macOS)
timeout-minutes: 60
runs-on: macos-latest
permissions:
checks: write
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
- name: Run Auth E2E Tests
uses: ./.github/actions/auth/oauth1/macos/run-auth-e2e-tests
- name: Start Test Server
uses: ./.github/actions/auth/oauth1/macos/start-test-server
- name: Run OAuth1 CLI Tests
uses: ./.github/actions/auth/oauth1/macos/run-oauth1-cli-tests

View File

@@ -1,138 +0,0 @@
name: Windows Tests
on:
workflow_dispatch:
push:
branches: [main, 'release/v*']
pull_request:
branches: [main, 'release/v*']
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
unit-test:
name: Unit Tests (Windows)
if: false # @TODO: Temporarily disabled. Remove this once the tests are fixed.
timeout-minutes: 60
runs-on: windows-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
with:
shell: pwsh
- name: Run Unit Tests
uses: ./.github/actions/tests/run-unit-tests
with:
shell: pwsh
cli-test:
name: CLI Tests (Windows)
runs-on: windows-latest
permissions:
checks: write
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
with:
shell: pwsh
- name: Run CLI Tests
uses: ./.github/actions/tests/run-cli-tests
with:
shell: pwsh
- name: Publish Test Report
uses: EnricoMi/publish-unit-test-result-action/windows@v2
if: always()
with:
check_name: CLI Test Results (Windows)
files: packages/bruno-tests/collection/junit.xml
comment_mode: off
check_run: false
e2e-test:
name: Playwright E2E Tests (Windows)
timeout-minutes: 240
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
with:
shell: pwsh
- name: Run E2E Tests
uses: ./.github/actions/tests/run-e2e-tests
with:
os: windows
shell: pwsh
- name: Upload Playwright Report
uses: actions/upload-artifact@v6
if: ${{ !cancelled() }}
with:
name: playwright-report-windows
path: playwright-report/
retention-days: 30
ssl-test:
name: SSL Tests (Windows)
timeout-minutes: 60
runs-on: windows-latest
permissions:
checks: write
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
with:
shell: pwsh
- name: Setup CA Certificates
uses: ./.github/actions/ssl/windows/setup-ca-certs
- name: Run Basic SSL CLI Tests
uses: ./.github/actions/ssl/windows/run-basic-ssl-cli-tests
- name: Run Custom CA Certs CLI Tests
uses: ./.github/actions/ssl/windows/run-custom-ca-certs-cli-tests
- name: Run Custom CA Certs E2E Tests
uses: ./.github/actions/ssl/windows/run-ssl-e2e-tests
oauth1-tests:
name: OAuth 1.0 Auth Tests (Windows)
timeout-minutes: 60
runs-on: windows-latest
permissions:
checks: write
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
- name: Run Auth E2E Tests
uses: ./.github/actions/auth/oauth1/windows/run-auth-e2e-tests
- name: Start Test Server
uses: ./.github/actions/auth/oauth1/windows/start-test-server
- name: Run OAuth1 CLI Tests
uses: ./.github/actions/auth/oauth1/windows/run-oauth1-cli-tests

147
.github/workflows/tests.yml vendored Normal file
View File

@@ -0,0 +1,147 @@
name: Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
unit-test:
name: Unit Tests
timeout-minutes: 60
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache-dependency-path: './package-lock.json'
- name: Install dependencies
run: npm ci --legacy-peer-deps
# build libraries
- name: Build libraries
run: |
npm run build --workspace=packages/bruno-common
npm run build --workspace=packages/bruno-query
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
npm run build --workspace=packages/bruno-converters
npm run build --workspace=packages/bruno-requests
npm run build --workspace=packages/bruno-filestore
- name: Lint Check
run: npm run lint
env:
ESLINT_PLUGIN_DIFF_COMMIT: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || 'main' }}
# tests
- name: Test Package bruno-js
run: npm run test --workspace=packages/bruno-js
- name: Test Package bruno-cli
run: npm run test --workspace=packages/bruno-cli
- name: Test Package bruno-query
run: npm run test --workspace=packages/bruno-query
- name: Test Package bruno-lang
run: npm run test --workspace=packages/bruno-lang
- name: Test Package bruno-schema
run: npm run test --workspace=packages/bruno-schema
- name: Test Package bruno-app
run: npm run test --workspace=packages/bruno-app
- name: Test Package bruno-common
run: npm run test --workspace=packages/bruno-common
- name: Test Package bruno-converters
run: npm run test --workspace=packages/bruno-converters
- name: Test Package bruno-electron
run: npm run test --workspace=packages/bruno-electron
cli-test:
name: CLI Tests
runs-on: ubuntu-latest
permissions:
checks: write
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache-dependency-path: './package-lock.json'
- name: Install dependencies
run: npm ci --legacy-peer-deps
- name: Build Libraries
run: |
npm run build --workspace=packages/bruno-query
npm run build --workspace=packages/bruno-common
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
npm run build --workspace=packages/bruno-converters
npm run build --workspace=packages/bruno-requests
npm run build --workspace=packages/bruno-filestore
- name: Run Local Testbench
run: |
npm start --workspace=packages/bruno-tests &
sleep 5
- name: Run tests
run: |
cd packages/bruno-tests/collection
npm install
node ../../bruno-cli/bin/bru.js run --env Prod --output junit.xml --format junit
- name: Publish Test Report
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
check_name: CLI Test Results
files: packages/bruno-tests/collection/junit.xml
comment_mode: always
e2e-test:
name: Playwright E2E Tests
timeout-minutes: 60
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: v22.11.x
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get --no-install-recommends install -y \
libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \
xvfb
npm ci --legacy-peer-deps
sudo chown root /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
sudo chmod 4755 /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
- name: Install dependencies for test collection environment
run: |
npm ci --prefix packages/bruno-tests/collection
- name: Build libraries
run: |
npm run build:graphql-docs
npm run build:bruno-query
npm run build:bruno-common
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
npm run build:bruno-converters
npm run build:bruno-requests
npm run build:bruno-filestore
- name: Run Playwright tests
run: |
xvfb-run npm run test:e2e
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30

21
.gitignore vendored
View File

@@ -48,27 +48,6 @@ yarn-error.log*
bruno.iml
.idea
.vscode
.cursor
.claude
.codex
.agents
.agent
skills-lock.json
# Playwright
/blob-report/
# Benchmark results (generated at runtime)
tests/benchmarks/results/
/benchmark-report/
# Development plan files
CLAUDE.md
AGENTS.md
*.plan.md
# packages dist
packages/bruno-filestore/dist
packages/bruno-requests/dist
packages/bruno-schema-types/dist
packages/bruno-converters/dist

1
.npmrc
View File

@@ -1 +0,0 @@
min-release-age=10

2
.nvmrc
View File

@@ -1 +1 @@
v22.12.0
v22.11.0

7
.prettierrc.json Normal file
View File

@@ -0,0 +1,7 @@
{
"trailingComma": "none",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"printWidth": 120
}

View File

@@ -1,130 +0,0 @@
# Bruno Coding Standards
- No diffs unless an actual change is made, the code changes need to be as minimal as possible, avoid making un-necessary whitespace diffs. This is already handled by eslint but make sure you check your code changes before commiting and raising a PR.
## General Style Rules
- Use 2 spaces for indentation. No tabs, just spaces keeps everything neat and uniform.
- Stick to single quotes for strings. For JSX/TSX attributes, use double quotes (e.g., <svg xmlns="..." viewBox="...">) to follow React conventions.
- Always add semicolons at the end of statements. It's like putting a period at the end of a sentence clarity matters.
- JSX is enabled, so feel free to use it where it makes sense.
## Punctuation and Spacing
- No trailing commas. Keep it clean, no extra commas hanging around.
- Always use parentheses around parameters in arrow functions. Even for single params consistency is key.
- For multiline constructs, put opening braces on the same line, and ensure consistency. Minimum 2 elements for multiline.
- No newlines inside function parentheses. Keep 'em tight.
- Space before and after the arrow in arrow functions. `() => {}` is good.
- No space between function name and parentheses. `func()` not `func ()`.
- Semicolons go at the end of the line, not on a new line.
- No strict max length write readable code, not cramped lines.
- Multiple expressions per line in JSX are fine flexibility is nice.
Remember, these rules are here to make our codebase harmonious. If something doesn't fit perfectly, let's chat about it. Happy coding! 🚀
## Tests
- Add tests for any new functionality or meaningful changes. If code is added, removed, or significantly modified, corresponding tests should be updated or created.
- Prioritise high-value tests over maximum coverage. Focus on testing behaviour that is critical, complex, or likely to break—dont chase coverage numbers for their own sake.
- Write behaviour-driven tests, not implementation-driven ones. Tests should validate real expected output and observable behaviour, not internal details or mocked-out logic unless absolutely necessary.
- Minimise mocking unless it meaningfully increases clarity or isolates external dependencies. Prefer real flows where practical; only mock external services, slow systems, or non-deterministic behaviour.
- Keep tests readable and maintainable. Optimise for clarity over cleverness. Name tests descriptively, keep setup minimal, and avoid unnecessary abstraction.
- Aim for tests that fail usefully. When a test fails, it should clearly indicate what behaviour broke and why.
- Cover both the “happy path” and the realistically problematic paths. Validate expected success behaviour, but also validate error handling, edge cases, and degraded-mode behaviour when appropriate.
- Ensure tests are deterministic and reproducible. No randomness, timing dependencies, or environment-specific assumptions without explicit control.
- Avoid overfitting tests to current behaviour if future flexibility matters. Only assert what needs to be true, not incidental details.
- Use consistent patterns and helper utilities where they improve clarity. Prefer shared test utilities over copy-pasted setup code, but only when it actually reduces complexity.
- Tests should be fast enough to run continuously. Avoid long-running operations unless absolutely necessary; prefer lightweight fixtures and isolated units.
### E2E Tests
When reviewing Electron-specific Playwright tests, treat `<project-root>/tests/**` as the canonical location for specs, typically matching `<project-root>/tests/**/*.spec.{ts,js}`. For broader Playwright workflow guidance, also refer to `docs/playwright-testing-guide.md`.
Goal: rewrite or critique the tests so they are genuinely behavioural, maintainable, and safely parallelizable.
Rules:
1. Tests must verify user-visible behaviour, not implementation details.
- Prefer assertions on UI state, persisted data, windows, dialogs, filesystem effects, and app-level outcomes.
- Avoid hardcoded waits, brittle selectors, fake internal state checks, and “click then expect mock called” tests unless the user behaviour is the point.
2. Tests must be Electron-aware.
- Use Electron app launch patterns correctly.
- Handle main window, secondary windows, dialogs, menus, native prompts, clipboard, file pickers, and IPC-driven UI behaviour through observable outcomes.
- Do not reach into app internals unless absolutely necessary for setup or controlled test fixtures.
3. Tests must be parallel-safe.
- No shared user data directories.
- No shared ports, files, DBs, caches, clipboard assumptions, or global app state.
- Each test gets isolated temp paths, unique workspace/project names, and deterministic cleanup.
- Avoid test ordering assumptions.
4. No hardcoded mess.
- Replace magic timeouts with event-driven waits.
- Replace brittle text/index selectors with role, label, test id, or stable user-facing selectors.
- Replace duplicated setup with fixtures.
- Replace hardcoded absolute paths with temp dirs.
- Replace random sleeps with waiting for actual app signals.
5. Every test should follow this shape:
- Arrange: create isolated fixture state.
- Act: perform real user actions.
- Assert: verify observable behavioural outcome.
- Cleanup: remove isolated resources.
For each test file:
- Identify behavioural vs non-behavioural tests.
- Flag brittle selectors, hardcoded waits, shared state, serial dependencies, and fake assertions.
- Rewrite the tests using Playwright best practices for Electron.
- Make them parallel-ready.
- Explain briefly why each rewrite is better.
## UI Specific instructions
### React
- Use styled component's theme prop to manage CSS colors and not CSS variables when in the context of a styled component or any react component using the styled component
- Styled Components are used as wrappers to define both self and children components style, tailwind classes are used specifically for layout based styles.
- Styled Component CSS might also change layout but tailwind classes shouldn't define colors.
- MUST: Prefer custom hooks for business logic, data fetching, and side-effects.
- MUST: Avoid `useEffect` unless absolutely needed. Prefer derived state, event handlers.
- SHOULD: Memoize only when necessary (`useMemo`/`useCallback`), and prefer moving logic into hooks first.
- MUST: Do not use namespace access for hooks in app code (e.g., `React.useCallback`, `React.useMemo`, `React.useState`). Import hooks directly.
- Correct: `import { useCallback, useMemo, useState } from "react";`
- Avoid: `import * as React from "react";` then `React.useCallback(...)`
- Add `data-testid` to testable elements for Playwright
- Co-locate utilities that are truly component-specific next to the component, otherwise place shared items under a common folder
- Avoid mixed controlled and uncontrolled state in React components. A component is either controlled or uncontrolled. State needs a single source of truth instead of being computed by props and then recomputed internally.
- SHOULD: Use derived state variables instead of adding unneeded `React.useState` / `useState` hooks.
## Readability and Abstractions
- Avoid abstractions unless the exact same code is being used in more than 3 places.
- Names for functions need to be concise and descriptive.
- Add in JSDoc comments to add more details to the abstractions if needed.
- Follow functional programming but just enough to be readable, we don't need to go as deep as ADTs and Monads, we want to keep the code pipeline obvious and easy for everyone to read and contribute to.
- Avoid single line abstractions where all that's being done is increasing the call stack with one additional function.
- Add in meaningful comments instead of obvious ones where complex code flow is explained properly.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 346 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 347 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 584 KiB

After

Width:  |  Height:  |  Size: 813 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 615 KiB

After

Width:  |  Height:  |  Size: 153 KiB

View File

@@ -16,7 +16,6 @@
| [日本語](docs/contributing/contributing_ja.md)
| [हिंदी](docs/contributing/contributing_hi.md)
| [Dutch](docs/contributing/contributing_nl.md)
| [فارسی](docs/contributing/contributing_fa.md)
## Let's make Bruno better, together!!
@@ -70,13 +69,11 @@ npm run build:bruno-query
npm run build:bruno-common
npm run build:bruno-converters
npm run build:bruno-requests
npm run build:schema-types
npm run build:bruno-filestore
# bundle js sandbox libraries
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
```
##### Option 2
```bash
@@ -97,22 +94,18 @@ npm run dev:electron
```
##### Option 2
```bash
# run electron and react app concurrently
npm run dev
```
#### Customize Electron `userData` path
If `ELECTRON_USER_DATA_PATH` env-variable is present and its development mode, then `userData` path is modified accordingly.
e.g.
```sh
ELECTRON_USER_DATA_PATH=$(realpath ~/Desktop/bruno-test) npm run dev:electron
```
This will create a `bruno-test` folder on your Desktop and use it as the `userData` path.
### Troubleshooting

View File

@@ -1,92 +0,0 @@
[English](../../contributing.md)
## با هم، Bruno را بهتر می‌کنیم!
خوشحالم که قصد دارید Bruno را بهبود ببخشید. در ادامه قوانین و راهنماها برای راه‌اندازی Bruno روی سیستم شما آورده شده است.
### فناوری‌های استفاده‌شده
به فارسی برونو Bruno با استفاده از Next.js و React ساخته شده است. همچنین از Electron برای بسته‌بندی نسخه دسکتاپ (که امکان مجموعه‌های محلی را فراهم می‌کند) استفاده می‌کنیم.
کتابخانه‌هایی که استفاده می‌کنیم:
- CSS - Tailwind استایل
- Codemirror - ویرایشگر کد
- Redux - مدیریت وضعیت
- Tabler Icons - آیکون‌ها
- formik - فرم‌ها
- Yup اعتبارسنجی اسکیمـا
- axios - کلاینت درخواست
- chokidar - پایش‌گر سیستم فایل
### پیش‌نیازها
شما به [نود v20.x یا اخرین نسخه پایدار](https://nodejs.org/en/) و npm 8.x نیاز دارید. در این پروژه از فضای کاری npm (npm workspaces) استفاده می‌کنیم.
### شروع به کدنویسی
برای راه‌اندازی محیط توسعه محلی به فایل [مستندات توسعه](docs/development_fa.md) مراجعه کنید:
### ارسال Pull Request
1 - لطفاً Pull Requestها (PR) را کوتاه و متمرکز نگه دارید و تنها یک هدف مشخص را دنبال کنند. </br>
2 - لطفاً از فرمت نام‌گذاری شاخه‌ها استفاده کنید:
- feature/[name]: این شاخه باید شامل یک قابلیت مشخص باشد.
- feature/dark-mode : مثال
- bugfix/[name]: این شاخه باید تنها شامل رفع یک باگ مشخص باشد.
- bugfix/bug-1 : مثال
## توسعه
به فارسی برونو یا Bruno به‌صورت یک اپلیکیشن «سنگین» توسعه داده می‌شود. برای اجرا باید ابتدا Next.js را در یک پنجره ترمینال اجرا کنید و سپس اپلیکیشن Electron را در پنجره ترمینال دیگری راه‌اندازی نمایید.
### نیازمندی توسعه
- NodeJS v18
### اجرای محلی
```bash
# از ورژن NodeJS 18 استفاده کنید
nvm use
# نصب وابستگی‌ها
npm i --legacy-peer-deps
# ساخت مستندات GraphQL
npm run build:graphql-docs
# ساخت bruno-query
npm run build:bruno-query
# اجرای اپ Next (ترمینال 1)
npm run dev:web
# اجرای اپ Electron (ترمینال 2)
npm run dev:electron
```
### عیب‌یابی
ممکن است هنگام اجرای `npm install` خطای `Unsupported platform` ببینید. برای رفع این مشکل، پوشه `node_modules` و فایل `package-lock.json` را حذف کرده و سپس دوباره `npm install` را اجرا کنید. این کار معمولاً همه پکیج‌های لازم را نصب می‌کند.
```shell
# حذف پوشه node_modules در زیردایرکتوری‌ها
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
rm -rf "$dir"
done
# حذف فایل package-lock.json در زیردایرکتوری‌ها
find . -type f -name "package-lock.json" -delete
```
### تست‌ها
```bash
# اجرای تست‌های schema مربوط به bruno
npm test --workspace=packages/bruno-schema
# اجرای تست‌ها در همه فضاهای کاری (در صورت وجود)
npm test --workspaces --if-present
```

View File

@@ -324,7 +324,7 @@ test('should create and execute HTTP request', async ({ page, createTmpDir }) =>
await page.getByRole('button', { name: 'Create' }).click();
// Execute request
await page.getByTestId('send-arrow-icon').click();
await page.locator('#send-request').getByRole('img').nth(2).click();
// Verify response
await expect(page.getByRole('main')).toContainText('200 OK');

View File

@@ -1,8 +0,0 @@
[English](../../publishing.md)
### انتشار Bruno در یک پکیج منیجر جدید
اگرچه کد ما متن‌باز است و همه می‌توانند از آن استفاده کنند، لطفاً قبل از انتشار Bruno در مدیر بسته‌های جدید با ما تماس بگیرید. به عنوان سازنده Bruno، علامت تجاری `Bruno` را برای این پروژه دارم و مایلم توزیع آن را مدیریت کنم. اگر دوست دارید Bruno را در یک مدیر بسته جدید ببینید، لطفاً یک issue در گیت‌هاب ثبت کنید.
اگرچه بیشتر قابلیت‌های ما رایگان و متن‌باز هستند (شامل REST و GraphQL Apis)،
ما تلاش می‌کنیم بین اصول متن‌باز و توسعه پایدار تعادل مناسبی برقرار کنیم - https://github.com/usebruno/bruno/discussions/269

View File

@@ -3,7 +3,7 @@
### برونو - بيئة تطوير مفتوحة المصدر لاستكشاف واختبار واجهات برمجة التطبيقات (APIs).
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)

View File

@@ -3,7 +3,7 @@
### ব্রুনো - API অন্বেষণ এবং পরীক্ষা করার জন্য ওপেনসোর্স IDE।
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)

View File

@@ -3,7 +3,7 @@
### Bruno - 开源 IDE用于探索和测试 API。
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)

View File

@@ -3,7 +3,7 @@
### Bruno - Opensource IDE zum Erkunden und Testen von APIs.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)

View File

@@ -3,7 +3,7 @@
### Bruno - IDE de código abierto para explorar y probar APIs.
[![Versión en Github](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)
[![Versión en Github](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Actividad de Commits](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)

View File

@@ -1,143 +0,0 @@
<br />
<img src="../../assets/images/logo-transparent.png" width="80"/>
### برونو یا Bruno - محیط توسعه متن باز برای تست و توسعه API ها
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
[English](../../readme.md)
| [Українська](./readme_ua.md)
| [Русский](./readme_ru.md)
| [Türkçe](./readme_tr.md)
| [Deutsch](./readme_de.md)
| [Français](./readme_fr.md)
| [Português (BR)](./readme_pt_br.md)
| [한국어](./readme_kr.md)
| [বাংলা](./readme_bn.md)
| [Español](./readme_es.md)
| **فارسی**
| [Română](./readme_ro.md)
| [Polski](./readme_pl.md)
| [简体中文](./readme_cn.md)
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
برونو یک کلاینت API جدید و نوآورانه است که هدفش تغییر وضعیت فعلی ابزارهایی مانند Postman و سایر ابزارهای مشابه است.
برونو مجموعه‌های شما را مستقیماً در یک پوشه روی فایل‌سیستم شما ذخیره می‌کند. ما از یک زبان نشانه‌گذاری ساده به نام Bru برای ذخیره اطلاعات درخواست‌های API استفاده می‌کنیم.
شما می‌توانید برای همکاری روی مجموعه‌های API خود، از Git یا هر سیستم کنترل نسخه دلخواهتان استفاده کنید.
برونو فقط به صورت آفلاین کار می‌کند. هیچ برنامه‌ای برای اضافه کردن همگام‌سازی ابری به برونو در آینده وجود ندارد. ما به حریم خصوصی داده‌های شما اهمیت می‌دهیم و معتقدیم که باید روی دستگاه خودتان باقی بمانند. می‌توانید چشم‌انداز بلندمدت ما را مطالعه کنید. [اینجا (به انگلیسی)](https://github.com/usebruno/bruno/discussions/269)
📢 جدیدترین ارائه ما را در کنفرانس India FOSS 3.0 تماشا کنید.
[اینجا](https://www.youtube.com/watch?v=7bSMFpbcPiY)
![bruno](/assets/images/landing-2.png) <br /><br />
### نصب
برونو به صورت یک فایل باینری برای دانلود در دسترس است. [بر روی وبسایت ما](https://www.usebruno.com/downloads) برای مک لینکوس و ویندوز.
همچنین می‌توانید برونو را از طریق مدیر بسته‌هایی مانند Homebrew، Chocolatey، Snap و Apt نصب کنید.
```sh
# بر روی مک از طریق brew
brew install bruno
# بر روی ویندوز از طریق Chocolatey
choco install bruno
# بر روی لینوکس از طریق Snap
snap install bruno
# بر روی لینوکس از طریق Apt
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg curl
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
| gpg --dearmor \
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \
| sudo tee /etc/apt/sources.list.d/bruno.list
sudo apt update && sudo apt install bruno
```
### روی پلتفرم‌های مختلف کار می‌کند 🖥️
![bruno](/assets/images/run-anywhere.png) <br /><br />
### همکاری از طریق گیت 👩‍💻🧑‍💻
یا هر سیستم کنترل نسخه‌ای که ترجیح می‌دهید
![bruno](/assets/images/version-control.png) <br /><br />
### لینک‌های مهم 📌
- [آخرین نسخه پایدار ما](https://github.com/usebruno/bruno/discussions/269)
- [نقشه راه](https://github.com/usebruno/bruno/discussions/384)
- [مستندات](https://docs.usebruno.com)
- [وبسایت](https://www.usebruno.com)
- [اشتراک ها](https://www.usebruno.com/pricing)
- [دانلود](https://www.usebruno.com/downloads)
### ویدیوها 🎥
- [تجربه ها](https://github.com/usebruno/bruno/discussions/343)
- [مرکز دانش](https://github.com/usebruno/bruno/discussions/386)
- [اسکریپ مانیا](https://github.com/usebruno/bruno/discussions/385)
### حمایت ❤️
جوون! اگر این پروژه را دوست دارید، روی دکمه ⭐ کلیک کنید!
### تجربه‌های به اشتراک گذاشته‌شده 📣
اگر برونو به شما یا تیمتان کمک کرده است، لطفاً فراموش نکنید تجربه‌های خود را به اشتراک بگذارید. [تجربه‌های خود را در بحث گیت‌هاب ما به اشتراک بگذارید](https://github.com/usebruno/bruno/discussions/343).
### انتشار برونو در یک پکیچ منیجر جدید
لطفا چک بکنید [اینجارو](../../publishing.md) برای اطلاعات بیشتر.
### مشارکت 👩‍💻🧑‍💻
خوشحالم که می‌خواهید برونو را بهتر کنید. لطفا [راهنمای مشارکت را بررسی کنید](../contributing/contributing_fa.md).
حتی اگر نمی‌توانید از طریق کدنویسی مشارکت کنید، در گزارش باگ‌ها و درخواست قابلیت‌های جدید که به حل نیازهای شما کمک می‌کند تردید نکنید.
### نویسنده ها
<div align="center">
<a href="https://github.com/usebruno/bruno/graphs/contributors">
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
</a>
</div>
### در ارتباط باشید 🌐
[𝕏 (تویتر)](https://twitter.com/use_bruno) <br />
[وبسایت](https://www.usebruno.com) <br />
[دیسکورد](https://discord.com/invite/KgcZUncpjq) <br />
[لینکدین](https://www.linkedin.com/company/usebruno)
### برند
**نام**
به فارسی برونو - `Bruno` یک علامت تجاری ثبت‌شده متعلق به [Anoop M D](https://www.helloanoop.com/)
**لوگو**
لوگو توسط [OpenMoji](https://openmoji.org/library/emoji-1F436/) ساخته شده است. مجوز: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
### مجوز 📄
[MIT](../../license.md)

View File

@@ -3,7 +3,7 @@
### Bruno - IDE Opensource pour explorer et tester des APIs.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)

View File

@@ -3,7 +3,7 @@
### Bruno - Opensource IDE per esplorare e testare gli APIs.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)

View File

@@ -3,7 +3,7 @@
### Bruno - API の検証・動作テストのためのオープンソース IDE.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)

View File

@@ -3,7 +3,7 @@
### ბრუნო - ღია წყაროების IDE API-ების შესწავლისა და ტესტირებისათვის.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)

View File

@@ -3,7 +3,7 @@
### Bruno - API 탐색 및 테스트를 위한 오픈소스 IDE.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)

View File

@@ -3,7 +3,7 @@
### Bruno - Open source IDE voor het verkennen en testen van API's.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)

View File

@@ -3,7 +3,7 @@
### Bruno - Otwartoźródłowe IDE do eksploracji i testów APIs.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)

View File

@@ -3,7 +3,7 @@
### Bruno - IDE de código aberto para explorar e testar APIs.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)

View File

@@ -3,7 +3,7 @@
### Bruno - Mediu integrat de dezvoltare cu sursă deschisă pentru explorarea și testarea API-urilor.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)

View File

@@ -3,7 +3,7 @@
### Bruno - IDE с открытым исходным кодом для изучения и тестирования API.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)

View File

@@ -3,7 +3,7 @@
### Bruno - API'leri keşfetmek ve test etmek için açık kaynaklı IDE.
[![GitHub sürümü](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)
[![GitHub sürümü](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)

View File

@@ -3,7 +3,7 @@
### Bruno - IDE із відкритим кодом для тестування та дослідження API
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)

View File

@@ -3,7 +3,7 @@
### Bruno - 探索和測試 API 的開源 IDE 工具
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)

View File

@@ -1,6 +1,6 @@
// eslint.config.js
const { defineConfig } = require('eslint/config');
const globals = require('globals');
const { defineConfig } = require("eslint/config");
const globals = require("globals");
const { fixupPluginRules } = require('@eslint/compat');
const eslintPluginDiff = require('eslint-plugin-diff');
@@ -11,18 +11,6 @@ const runESMImports = async () => {
};
module.exports = runESMImports().then(() => defineConfig([
// Global ignores - must be a standalone object with ONLY ignores
{
ignores: [
'**/node_modules/**/*',
'**/dist/**/*',
'**/*.bru',
'packages/bruno-js/src/sandbox/bundle-browser-rollup.js',
'packages/bruno-app/public/static/**/*',
'packages/bruno-app/.next/**/*',
'packages/bruno-electron/web/**/*'
]
},
{
plugins: {
'diff': fixupPluginRules(eslintPluginDiff),
@@ -46,13 +34,13 @@ module.exports = runESMImports().then(() => defineConfig([
'packages/bruno-converters/**/*.js',
'packages/bruno-electron/**/*.js',
'packages/bruno-filestore/**/*.ts',
'packages/bruno-schema-types/**/*.ts',
'packages/bruno-js/**/*.js',
'packages/bruno-lang/**/*.js',
'packages/bruno-requests/**/*.ts',
'packages/bruno-requests/**/*.js',
'packages/bruno-tests/**/*.{js,ts}'
],
processor: 'diff/diff',
rules: {
...stylistic.configs.customize({
indent: 2,
@@ -68,7 +56,7 @@ module.exports = runESMImports().then(() => defineConfig([
minElements: 2,
consistent: true
}],
'@stylistic/function-paren-newline': ['off'],
'@stylistic/function-paren-newline': ['error', 'never'],
'@stylistic/array-bracket-spacing': ['error', 'never'],
'@stylistic/arrow-spacing': ['error', { before: true, after: true }],
'@stylistic/function-call-spacing': ['error', 'never'],
@@ -76,14 +64,12 @@ module.exports = runESMImports().then(() => defineConfig([
'@stylistic/padding-line-between-statements': ['off'],
'@stylistic/semi-style': ['error', 'last'],
'@stylistic/max-len': ['off'],
'@stylistic/jsx-one-expression-per-line': ['off'],
'@stylistic/max-statements-per-line': ['off'],
'@stylistic/no-mixed-operators': ['off']
'@stylistic/jsx-one-expression-per-line': ['off']
}
},
{
files: ['packages/bruno-app/**/*.{js,jsx,ts}'],
ignores: ['**/*.config.js', '**/public/**/*'],
files: ["packages/bruno-app/**/*.{js,jsx,ts}"],
ignores: ["**/*.config.js", "**/public/**/*"],
languageOptions: {
globals: {
...globals.browser,
@@ -96,127 +82,114 @@ module.exports = runESMImports().then(() => defineConfig([
},
parserOptions: {
ecmaFeatures: {
jsx: true
}
}
jsx: true,
},
},
},
rules: {
'no-undef': 'error'
}
"no-undef": "error",
},
},
{
// It prevents lint errors when using CommonJS exports (module.exports) in Jest mocks.
files: ['packages/bruno-app/src/test-utils/mocks/codemirror.js'],
files: ["packages/bruno-app/src/test-utils/mocks/codemirror.js"],
languageOptions: {
globals: {
...globals.node,
...globals.jest
}
...globals.jest,
},
},
rules: {
'no-undef': 'error'
}
},
{
// Storybook config files use CommonJS with __dirname and module.exports
files: ['packages/bruno-app/storybook/**/*.js'],
languageOptions: {
globals: {
...globals.node
}
"no-undef": "error",
},
rules: {
'no-undef': 'error'
}
},
{
files: ['packages/bruno-cli/**/*.js'],
ignores: ['**/*.config.js'],
files: ["packages/bruno-cli/**/*.js"],
ignores: ["**/*.config.js"],
languageOptions: {
globals: {
...globals.node,
...globals.jest
...globals.jest,
},
parserOptions: {
ecmaVersion: 'latest'
}
},
rules: {
'no-undef': 'error'
}
},
{
files: ['packages/bruno-common/**/*.ts'],
ignores: ['**/*.config.js', '**/dist/**/*'],
languageOptions: {
globals: {
...globals.node,
...globals.jest
ecmaVersion: "latest"
},
parser: require('@typescript-eslint/parser'),
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: './packages/bruno-common/tsconfig.json'
}
},
rules: {
'no-undef': 'error'
}
"no-undef": "error",
},
},
{
files: ['packages/bruno-converters/**/*.js'],
ignores: ['**/*.config.js', '**/dist/**/*'],
files: ["packages/bruno-common/**/*.ts"],
ignores: ["**/*.config.js", "**/dist/**/*"],
languageOptions: {
globals: {
...globals.node,
...globals.jest
...globals.jest,
},
parser: require("@typescript-eslint/parser"),
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
project: "./packages/bruno-common/tsconfig.json",
},
},
rules: {
"no-undef": "error",
},
},
{
files: ["packages/bruno-converters/**/*.js"],
ignores: ["**/*.config.js", "**/dist/**/*"],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
}
},
rules: {
'no-undef': 'error',
'no-case-declarations': 'error'
}
},
{
files: ['packages/bruno-electron/**/*.js'],
ignores: ['**/*.config.js', '**/web/**/*'],
languageOptions: {
globals: {
...globals.node,
...globals.jest
}
},
rules: {
'no-undef': 'error'
}
},
{
files: ['packages/bruno-filestore/**/*.ts'],
ignores: ['**/*.config.js', '**/dist/**/*'],
languageOptions: {
globals: {
...globals.node,
...globals.jest
ecmaVersion: "latest",
sourceType: "module",
},
parser: require('@typescript-eslint/parser'),
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: './packages/bruno-filestore/tsconfig.json'
}
},
rules: {
'no-undef': 'error'
}
"no-undef": "error",
},
},
{
files: ['packages/bruno-js/**/*.js'],
ignores: ['**/*.config.js', '**/dist/**/*'],
files: ["packages/bruno-electron/**/*.js"],
ignores: ["**/*.config.js", "**/web/**/*"],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
},
rules: {
"no-undef": "error",
},
},
{
files: ["packages/bruno-filestore/**/*.ts"],
ignores: ["**/*.config.js", "**/dist/**/*"],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
parser: require("@typescript-eslint/parser"),
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
project: "./packages/bruno-filestore/tsconfig.json",
},
},
rules: {
"no-undef": "error",
},
},
{
files: ["packages/bruno-js/**/*.js"],
ignores: ["**/*.config.js", "**/dist/**/*"],
languageOptions: {
globals: {
...globals.node,
@@ -227,65 +200,65 @@ module.exports = runESMImports().then(() => defineConfig([
typeDetectGlobalObject: false
},
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
}
ecmaVersion: "latest",
sourceType: "module",
},
},
rules: {
'no-undef': 'error'
}
"no-undef": "error",
},
},
{
files: ['packages/bruno-lang/**/*.js'],
ignores: ['**/*.config.js', '**/dist/**/*'],
files: ["packages/bruno-lang/**/*.js"],
ignores: ["**/*.config.js", "**/dist/**/*"],
languageOptions: {
globals: {
...globals.node,
...globals.jest
...globals.jest,
},
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
}
ecmaVersion: "latest",
sourceType: "module",
},
},
rules: {
'no-undef': 'error'
}
"no-undef": "error",
},
},
{
files: ['packages/bruno-requests/**/*.ts'],
ignores: ['**/*.config.js', '**/dist/**/*'],
files: ["packages/bruno-requests/**/*.ts"],
ignores: ["**/*.config.js", "**/dist/**/*"],
languageOptions: {
globals: {
...globals.node,
...globals.jest
...globals.jest,
},
parser: require('@typescript-eslint/parser'),
parser: require("@typescript-eslint/parser"),
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: './packages/bruno-requests/tsconfig.json'
}
ecmaVersion: "latest",
sourceType: "module",
project: "./packages/bruno-requests/tsconfig.json",
},
},
rules: {
'no-undef': 'error'
}
"no-undef": "error",
},
},
{
files: ['packages/bruno-requests/**/*.js'],
ignores: ['**/*.config.js', '**/dist/**/*'],
files: ["packages/bruno-requests/**/*.js"],
ignores: ["**/*.config.js", "**/dist/**/*"],
languageOptions: {
globals: {
...globals.node,
...globals.jest
...globals.jest,
},
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
}
ecmaVersion: "latest",
sourceType: "module",
},
},
rules: {
'no-undef': 'error'
}
}
"no-undef": "error",
},
},
]));

16183
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,6 @@
"packages/bruno-common",
"packages/bruno-converters",
"packages/bruno-schema",
"packages/bruno-schema-types",
"packages/bruno-query",
"packages/bruno-js",
"packages/bruno-lang",
@@ -25,53 +24,43 @@
"@jest/globals": "^29.2.0",
"@playwright/test": "^1.51.1",
"@rollup/plugin-json": "^6.1.0",
"@storybook/addon-webpack5-compiler-babel": "^4.0.0",
"@storybook/builder-webpack5": "^10.1.10",
"@storybook/react": "^10.1.10",
"@storybook/react-webpack5": "^10.1.10",
"@stylistic/eslint-plugin": "^5.3.1",
"@types/adm-zip": "^0.5.8",
"@types/jest": "^29.5.11",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.14.1",
"@typescript-eslint/parser": "^8.39.0",
"adm-zip": "^0.5.17",
"concurrently": "^8.2.2",
"cross-env": "10.1.0",
"eslint": "^9.39.4",
"eslint": "^9.26.0",
"eslint-plugin-diff": "^2.0.3",
"fs-extra": "^11.1.1",
"globals": "^16.1.0",
"husky": "^9.1.7",
"jest": "^29.2.0",
"lodash-es": "^4.17.23",
"lodash-es": "^4.17.21",
"nano-staged": "^0.8.0",
"playwright": "^1.51.1",
"pretty-quick": "^3.1.3",
"randomstring": "^1.2.2",
"rimraf": "^6.0.1",
"storybook": "^10.1.10",
"ts-jest": "^29.2.6"
},
"scripts": {
"setup": "node ./scripts/setup.js",
"watch:converters": "npm run watch --workspace=packages/bruno-converters",
"dev": "node ./scripts/dev.js",
"dev": "concurrently --kill-others \"npm run dev:web\" \"npm run dev:electron\"",
"watch": "npm run dev:watch",
"dev:watch": "node ./scripts/dev-hot-reload.js",
"dev:web": "npm run dev --workspace=packages/bruno-app",
"build:web": "npm run build --workspace=packages/bruno-app",
"prettier:web": "npm run prettier --workspace=packages/bruno-app",
"dev:electron": "npm run dev --workspace=packages/bruno-electron",
"dev:electron:debug": "npm run debug --workspace=packages/bruno-electron",
"storybook": "npm run storybook --workspace=packages/bruno-app",
"build:bruno-common": "npm run build --workspace=packages/bruno-common",
"build:bruno-requests": "npm run build --workspace=packages/bruno-requests",
"build:bruno-filestore": "npm run build --workspace=packages/bruno-filestore",
"build:bruno-converters": "npm run build --workspace=packages/bruno-converters",
"build:bruno-query": "npm run build --workspace=packages/bruno-query",
"build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs",
"build:schema-types": "npm run build --workspace=packages/bruno-schema-types",
"build:electron": "node ./scripts/build-electron.js",
"build:electron:mac": "./scripts/build-electron.sh mac",
"build:electron:win": "./scripts/build-electron.sh win",
@@ -80,15 +69,12 @@
"build:electron:rpm": "./scripts/build-electron.sh rpm",
"build:electron:snap": "./scripts/build-electron.sh snap",
"watch:common": "npm run watch --workspace=packages/bruno-common",
"watch:requests": "npm run watch --workspace=packages/bruno-requests",
"test:codegen": "node playwright/codegen.ts",
"test:e2e": "playwright test --project=default --project=system-pac",
"test:e2e": "playwright test --project=default",
"test:e2e:ssl": "playwright test --project=ssl",
"test:e2e:auth": "playwright test --project=auth",
"test:e2e:sanity": "playwright test --project=default --project=system-pac --grep @sanity",
"test:benchmark": "playwright test --config=playwright.benchmark.config.ts",
"lint": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" npx eslint",
"lint:fix": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" npx eslint --fix",
"test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app",
"lint": "node --max_old_space_size=4096 $(npx which eslint)",
"lint:fix": "node --max_old_space_size=4096 $(npx which eslint) --fix",
"prepare": "husky"
},
"nano-staged": {
@@ -97,17 +83,11 @@
]
},
"overrides": {
"axios": "1.16.0",
"rollup": "3.30.0",
"pbkdf2": "3.1.5",
"rollup": "3.29.5",
"electron-store": {
"conf": {
"json-schema-typed": "8.0.1"
}
}
},
"dependencies": {
"ajv": "^8.17.1",
"git-url-parse": "^14.1.0"
}
}

View File

@@ -1,4 +1,4 @@
{
"presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"],
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": [["styled-components", { "ssr": true }]]
}

View File

@@ -22,7 +22,6 @@ build
npm-debug.log*
yarn-debug.log*
yarn-error.log*
*.log
# local env files
.env.local
@@ -34,5 +33,4 @@ yarn-error.log*
.next/
dist/
.env
storybook-static/
.env

View File

@@ -0,0 +1,7 @@
{
"trailingComma": "none",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"printWidth": 120
}

View File

@@ -6,4 +6,4 @@ module.exports = {
}]
],
plugins: ['babel-plugin-styled-components']
};
};

View File

@@ -1,10 +1,10 @@
module.exports = {
rootDir: '.',
transform: {
'^.+\\.[jt]sx?$': 'babel-jest'
'^.+\\.[jt]sx?$': 'babel-jest',
},
transformIgnorePatterns: [
'/node_modules/(?!strip-json-comments|nanoid|xml-formatter)/'
"/node_modules/(?!strip-json-comments|nanoid|xml-formatter)/",
],
moduleNameMapper: {
'^assets/(.*)$': '<rootDir>/src/assets/$1',
@@ -22,9 +22,9 @@ module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['@testing-library/jest-dom'],
setupFiles: [
'<rootDir>/jest.setup.js'
'<rootDir>/jest.setup.js',
],
testMatch: [
'<rootDir>/src/**/*.spec.[jt]s?(x)'
]
};
};

View File

@@ -1,19 +1,3 @@
global.ResizeObserver = class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
};
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
addEventListener: jest.fn(),
removeEventListener: jest.fn()
}))
});
jest.mock('nanoid', () => {
return {
nanoid: () => {}

View File

@@ -8,8 +8,8 @@
"build": "rsbuild build -m production",
"preview": "rsbuild preview",
"test": "jest",
"storybook": "storybook dev -p 6006 --config-dir storybook",
"build-storybook": "storybook build --config-dir storybook"
"test:prettier": "prettier --check \"./src/**/*.{js,jsx,json,ts,tsx}\"",
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
},
"dependencies": {
"@fontsource/inter": "^5.0.15",
@@ -21,14 +21,10 @@
"@usebruno/common": "0.1.0",
"@usebruno/graphql-docs": "0.1.0",
"@usebruno/schema": "0.7.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"classnames": "^2.3.1",
"codemirror": "5.65.2",
"codemirror-graphql": "2.1.1",
"cookie": "0.7.1",
"diff": "^5.2.0",
"diff2html": "^3.4.47",
"dompurify": "^3.2.4",
"escape-html": "^1.0.3",
"fast-fuzzy": "^1.12.0",
@@ -40,25 +36,20 @@
"github-markdown-css": "^5.2.0",
"graphiql": "3.7.1",
"graphql": "^16.6.0",
"graphql-request": "4.2.0",
"hexy": "^0.3.5",
"graphql-request": "^3.7.0",
"httpsnippet": "^3.0.9",
"i18next": "24.1.2",
"idb": "^7.0.0",
"immer": "^9.0.15",
"js-yaml": "^4.1.0",
"jsesc": "^3.0.2",
"jshint": "^2.13.6",
"json5": "^2.2.3",
"jsonc-parser": "^3.2.1",
"jsonpath-plus": "^10.3.0",
"jsonschema": "^1.5.0",
"know-your-http-well": "^0.5.0",
"linkify-it": "^5.0.0",
"lodash": "4.18.1",
"lodash": "^4.17.21",
"markdown-it": "^13.0.2",
"markdown-it-replace-link": "^1.2.0",
"mime-types": "^3.0.2",
"moment": "^2.30.1",
"moment-timezone": "^0.5.47",
"mousetrap": "^1.6.5",
@@ -66,10 +57,9 @@
"path": "^0.12.7",
"pdfjs-dist": "4.4.168",
"platform": "^1.3.6",
"polished": "^4.3.1",
"posthog-node": "4.2.1",
"prettier": "^2.7.1",
"qs": "^6.14.1",
"qs": "^6.11.0",
"query-string": "^7.0.1",
"react": "19.0.0",
"react-copy-to-clipboard": "^5.1.0",
@@ -84,27 +74,23 @@
"react-player": "^2.16.0",
"react-redux": "^7.2.9",
"react-tooltip": "^5.5.2",
"react-virtuoso": "^4.18.1",
"sass": "^1.46.0",
"semver": "^7.7.1",
"shell-quote": "^1.8.4",
"shell-quote": "^1.8.3",
"strip-json-comments": "^5.0.1",
"styled-components": "^5.3.3",
"swagger-ui-react": "^5.31.0",
"system": "^2.0.1",
"url": "^0.11.3",
"xml-formatter": "^3.5.0",
"xml2js": "^0.6.2",
"yup": "^0.32.11"
},
"devDependencies": {
"@babel/core": "^7.27.1",
"@babel/preset-env": "^7.27.2",
"@babel/preset-react": "^7.27.1",
"@babel/preset-typescript": "^7.22.0",
"@rsbuild/core": "^1.1.2",
"@rsbuild/plugin-babel": "^1.0.3",
"@rsbuild/plugin-node-polyfill": "1.2.0",
"@rsbuild/plugin-node-polyfill": "^1.2.0",
"@rsbuild/plugin-react": "^1.0.7",
"@rsbuild/plugin-sass": "^1.1.0",
"@rsbuild/plugin-styled-components": "1.1.0",

File diff suppressed because it is too large Load Diff

View File

@@ -1,713 +0,0 @@
:host,
:root {
--d2h-bg-color: #fff;
--d2h-border-color: #ddd;
--d2h-dim-color: rgba(0, 0, 0, 0.3);
--d2h-line-border-color: #eee;
--d2h-file-header-bg-color: #f7f7f7;
--d2h-file-header-border-color: #d8d8d8;
--d2h-empty-placeholder-bg-color: #f1f1f1;
--d2h-empty-placeholder-border-color: #e1e1e1;
--d2h-selected-color: #c8e1ff;
--d2h-ins-bg-color: #dfd;
--d2h-ins-border-color: #b4e2b4;
--d2h-ins-highlight-bg-color: #97f295;
--d2h-ins-label-color: #399839;
--d2h-del-bg-color: #fee8e9;
--d2h-del-border-color: #e9aeae;
--d2h-del-highlight-bg-color: #ffb6ba;
--d2h-del-label-color: #c33;
--d2h-change-del-color: #fdf2d0;
--d2h-change-ins-color: #ded;
--d2h-info-bg-color: #f8fafd;
--d2h-info-border-color: #d5e4f2;
--d2h-change-label-color: #d0b44c;
--d2h-moved-label-color: #3572b0;
--d2h-dark-color: #e6edf3;
--d2h-dark-bg-color: #0d1117;
--d2h-dark-border-color: #30363d;
--d2h-dark-dim-color: #6e7681;
--d2h-dark-line-border-color: #21262d;
--d2h-dark-file-header-bg-color: #161b22;
--d2h-dark-file-header-border-color: #30363d;
--d2h-dark-empty-placeholder-bg-color: hsla(215, 8%, 47%, 0.1);
--d2h-dark-empty-placeholder-border-color: #30363d;
--d2h-dark-selected-color: rgba(56, 139, 253, 0.1);
--d2h-dark-ins-bg-color: rgba(46, 160, 67, 0.15);
--d2h-dark-ins-border-color: rgba(46, 160, 67, 0.4);
--d2h-dark-ins-highlight-bg-color: rgba(46, 160, 67, 0.4);
--d2h-dark-ins-label-color: #3fb950;
--d2h-dark-del-bg-color: rgba(248, 81, 73, 0.1);
--d2h-dark-del-border-color: rgba(248, 81, 73, 0.4);
--d2h-dark-del-highlight-bg-color: rgba(248, 81, 73, 0.4);
--d2h-dark-del-label-color: #f85149;
--d2h-dark-change-del-color: rgba(210, 153, 34, 0.2);
--d2h-dark-change-ins-color: rgba(46, 160, 67, 0.25);
--d2h-dark-info-bg-color: rgba(56, 139, 253, 0.1);
--d2h-dark-info-border-color: rgba(56, 139, 253, 0.4);
--d2h-dark-change-label-color: #d29922;
--d2h-dark-moved-label-color: #3572b0;
}
.d2h-wrapper {
text-align: left;
}
.d2h-file-header {
background-color: #f7f7f7;
background-color: var(--d2h-file-header-bg-color);
border-bottom: 1px solid #d8d8d8;
border-bottom: 1px solid var(--d2h-file-header-border-color);
display: -webkit-box;
display: -ms-flexbox;
display: flex;
font-family: Source Sans Pro, Helvetica Neue, Helvetica, Arial, sans-serif;
height: 35px;
padding: 5px 10px;
}
.d2h-file-header.d2h-sticky-header {
position: sticky;
top: 0;
z-index: 1;
}
.d2h-file-stats {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
font-size: 14px;
margin-left: auto;
}
.d2h-lines-added {
border: 1px solid #b4e2b4;
border: 1px solid var(--d2h-ins-border-color);
border-radius: 5px 0 0 5px;
color: #399839;
color: var(--d2h-ins-label-color);
padding: 2px;
text-align: right;
vertical-align: middle;
}
.d2h-lines-deleted {
border: 1px solid #e9aeae;
border: 1px solid var(--d2h-del-border-color);
border-radius: 0 5px 5px 0;
color: #c33;
color: var(--d2h-del-label-color);
margin-left: 1px;
padding: 2px;
text-align: left;
vertical-align: middle;
}
.d2h-file-name-wrapper {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
font-size: 15px;
width: 100%;
}
.d2h-file-name {
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.d2h-file-wrapper {
border: 1px solid #ddd;
border: 1px solid var(--d2h-border-color);
border-radius: 3px;
margin-bottom: 1em;
}
.d2h-file-collapse {
-webkit-box-pack: end;
-ms-flex-pack: end;
cursor: pointer;
display: none;
font-size: 12px;
justify-content: flex-end;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
border: 1px solid #ddd;
border: 1px solid var(--d2h-border-color);
border-radius: 3px;
padding: 4px 8px;
}
.d2h-file-collapse.d2h-selected {
background-color: #c8e1ff;
background-color: var(--d2h-selected-color);
}
.d2h-file-collapse-input {
margin: 0 4px 0 0;
}
.d2h-diff-table {
border-collapse: collapse;
font-family: Menlo, Consolas, monospace;
font-size: 13px;
width: 100%;
}
.d2h-files-diff {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
width: 100%;
}
.d2h-file-diff {
overflow-y: hidden;
}
.d2h-file-diff.d2h-d-none,
.d2h-files-diff.d2h-d-none {
display: none;
}
.d2h-file-side-diff {
display: inline-block;
overflow-x: scroll;
overflow-y: hidden;
width: 50%;
}
.d2h-code-line {
padding: 0 8em;
width: calc(100% - 16em);
}
.d2h-code-line,
.d2h-code-side-line {
display: inline-block;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
white-space: nowrap;
}
.d2h-code-side-line {
padding: 0 4.5em;
width: calc(100% - 9em);
}
.d2h-code-line-ctn {
background: none;
display: inline-block;
padding: 0;
word-wrap: normal;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
vertical-align: middle;
white-space: pre;
width: 100%;
}
.d2h-code-line del,
.d2h-code-side-line del {
background-color: #ffb6ba;
background-color: var(--d2h-del-highlight-bg-color);
}
.d2h-code-line del,
.d2h-code-line ins,
.d2h-code-side-line del,
.d2h-code-side-line ins {
border-radius: 0.2em;
display: inline-block;
margin-top: -1px;
-webkit-text-decoration: none;
text-decoration: none;
}
.d2h-code-line ins,
.d2h-code-side-line ins {
background-color: #97f295;
background-color: var(--d2h-ins-highlight-bg-color);
text-align: left;
}
.d2h-code-line-prefix {
background: none;
display: inline;
padding: 0;
word-wrap: normal;
white-space: pre;
}
.line-num1 {
float: left;
}
.line-num1,
.line-num2 {
-webkit-box-sizing: border-box;
box-sizing: border-box;
overflow: hidden;
padding: 0 0.5em;
text-overflow: ellipsis;
width: 3.5em;
}
.line-num2 {
float: right;
}
.d2h-code-linenumber {
background-color: #fff;
background-color: var(--d2h-bg-color);
border: solid #eee;
border: solid var(--d2h-line-border-color);
border-width: 0 1px;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: rgba(0, 0, 0, 0.3);
color: var(--d2h-dim-color);
cursor: pointer;
display: inline-block;
position: absolute;
text-align: right;
width: 7.5em;
}
.d2h-code-linenumber:after {
content: '\200b';
}
.d2h-code-side-linenumber {
background-color: #fff;
background-color: var(--d2h-bg-color);
border: solid #eee;
border: solid var(--d2h-line-border-color);
border-width: 0 1px;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: rgba(0, 0, 0, 0.3);
color: var(--d2h-dim-color);
cursor: pointer;
display: inline-block;
overflow: hidden;
padding: 0 0.5em;
position: absolute;
text-align: right;
text-overflow: ellipsis;
width: 4em;
}
.d2h-code-side-linenumber:after {
content: '\200b';
}
.d2h-code-side-emptyplaceholder,
.d2h-emptyplaceholder {
background-color: #f1f1f1;
background-color: var(--d2h-empty-placeholder-bg-color);
border-color: #e1e1e1;
border-color: var(--d2h-empty-placeholder-border-color);
}
.d2h-code-line-prefix,
.d2h-code-linenumber,
.d2h-code-side-linenumber,
.d2h-emptyplaceholder {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.d2h-code-linenumber,
.d2h-code-side-linenumber {
direction: rtl;
}
.d2h-del {
background-color: #fee8e9;
background-color: var(--d2h-del-bg-color);
border-color: #e9aeae;
border-color: var(--d2h-del-border-color);
}
.d2h-ins {
background-color: #dfd;
background-color: var(--d2h-ins-bg-color);
border-color: #b4e2b4;
border-color: var(--d2h-ins-border-color);
}
.d2h-info {
background-color: #f8fafd;
background-color: var(--d2h-info-bg-color);
border-color: #d5e4f2;
border-color: var(--d2h-info-border-color);
color: rgba(0, 0, 0, 0.3);
color: var(--d2h-dim-color);
}
.d2h-file-diff .d2h-del.d2h-change {
background-color: #fdf2d0;
background-color: var(--d2h-change-del-color);
}
.d2h-file-diff .d2h-ins.d2h-change {
background-color: #ded;
background-color: var(--d2h-change-ins-color);
}
.d2h-file-list-wrapper {
margin-bottom: 10px;
}
.d2h-file-list-wrapper a {
-webkit-text-decoration: none;
text-decoration: none;
}
.d2h-file-list-wrapper a,
.d2h-file-list-wrapper a:visited {
color: #3572b0;
color: var(--d2h-moved-label-color);
}
.d2h-file-list-header {
text-align: left;
}
.d2h-file-list-title {
font-weight: 700;
}
.d2h-file-list-line {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
text-align: left;
}
.d2h-file-list {
display: block;
list-style: none;
margin: 0;
padding: 0;
}
.d2h-file-list > li {
border-bottom: 1px solid #ddd;
border-bottom: 1px solid var(--d2h-border-color);
margin: 0;
padding: 5px 10px;
}
.d2h-file-list > li:last-child {
border-bottom: none;
}
.d2h-file-switch {
cursor: pointer;
display: none;
font-size: 10px;
}
.d2h-icon {
margin-right: 10px;
vertical-align: middle;
fill: currentColor;
}
.d2h-deleted {
color: #c33;
color: var(--d2h-del-label-color);
}
.d2h-added {
color: #399839;
color: var(--d2h-ins-label-color);
}
.d2h-changed {
color: #d0b44c;
color: var(--d2h-change-label-color);
}
.d2h-moved {
color: #3572b0;
color: var(--d2h-moved-label-color);
}
.d2h-tag {
background-color: #fff;
background-color: var(--d2h-bg-color);
display: -webkit-box;
display: -ms-flexbox;
display: flex;
font-size: 10px;
margin-left: 5px;
padding: 0 2px;
}
.d2h-deleted-tag {
border: 1px solid #c33;
border: 1px solid var(--d2h-del-label-color);
}
.d2h-added-tag {
border: 1px solid #399839;
border: 1px solid var(--d2h-ins-label-color);
}
.d2h-changed-tag {
border: 1px solid #d0b44c;
border: 1px solid var(--d2h-change-label-color);
}
.d2h-moved-tag {
border: 1px solid #3572b0;
border: 1px solid var(--d2h-moved-label-color);
}
.d2h-dark-color-scheme {
background-color: #0d1117;
background-color: var(--d2h-dark-bg-color);
color: #e6edf3;
color: var(--d2h-dark-color);
}
.d2h-dark-color-scheme .d2h-file-header {
background-color: #161b22;
background-color: var(--d2h-dark-file-header-bg-color);
border-bottom: #30363d;
border-bottom: var(--d2h-dark-file-header-border-color);
}
.d2h-dark-color-scheme .d2h-lines-added {
border: 1px solid rgba(46, 160, 67, 0.4);
border: 1px solid var(--d2h-dark-ins-border-color);
color: #3fb950;
color: var(--d2h-dark-ins-label-color);
}
.d2h-dark-color-scheme .d2h-lines-deleted {
border: 1px solid rgba(248, 81, 73, 0.4);
border: 1px solid var(--d2h-dark-del-border-color);
color: #f85149;
color: var(--d2h-dark-del-label-color);
}
.d2h-dark-color-scheme .d2h-code-line del,
.d2h-dark-color-scheme .d2h-code-side-line del {
background-color: rgba(248, 81, 73, 0.4);
background-color: var(--d2h-dark-del-highlight-bg-color);
}
.d2h-dark-color-scheme .d2h-code-line ins,
.d2h-dark-color-scheme .d2h-code-side-line ins {
background-color: rgba(46, 160, 67, 0.4);
background-color: var(--d2h-dark-ins-highlight-bg-color);
}
.d2h-dark-color-scheme .d2h-diff-tbody {
border-color: #30363d;
border-color: var(--d2h-dark-border-color);
}
.d2h-dark-color-scheme .d2h-code-side-linenumber {
background-color: #0d1117;
background-color: var(--d2h-dark-bg-color);
border-color: #21262d;
border-color: var(--d2h-dark-line-border-color);
color: #6e7681;
color: var(--d2h-dark-dim-color);
}
.d2h-dark-color-scheme .d2h-files-diff .d2h-code-side-emptyplaceholder,
.d2h-dark-color-scheme .d2h-files-diff .d2h-emptyplaceholder {
background-color: hsla(215, 8%, 47%, 0.1);
background-color: var(--d2h-dark-empty-placeholder-bg-color);
border-color: #30363d;
border-color: var(--d2h-dark-empty-placeholder-border-color);
}
.d2h-dark-color-scheme .d2h-code-linenumber {
background-color: #0d1117;
background-color: var(--d2h-dark-bg-color);
border-color: #21262d;
border-color: var(--d2h-dark-line-border-color);
color: #6e7681;
color: var(--d2h-dark-dim-color);
}
.d2h-dark-color-scheme .d2h-del {
background-color: rgba(248, 81, 73, 0.1);
background-color: var(--d2h-dark-del-bg-color);
border-color: rgba(248, 81, 73, 0.4);
border-color: var(--d2h-dark-del-border-color);
}
.d2h-dark-color-scheme .d2h-ins {
background-color: rgba(46, 160, 67, 0.15);
background-color: var(--d2h-dark-ins-bg-color);
border-color: rgba(46, 160, 67, 0.4);
border-color: var(--d2h-dark-ins-border-color);
}
.d2h-dark-color-scheme .d2h-info {
background-color: rgba(56, 139, 253, 0.1);
background-color: var(--d2h-dark-info-bg-color);
border-color: rgba(56, 139, 253, 0.4);
border-color: var(--d2h-dark-info-border-color);
color: #6e7681;
color: var(--d2h-dark-dim-color);
}
.d2h-dark-color-scheme .d2h-file-diff .d2h-del.d2h-change {
background-color: rgba(210, 153, 34, 0.2);
background-color: var(--d2h-dark-change-del-color);
}
.d2h-dark-color-scheme .d2h-file-diff .d2h-ins.d2h-change {
background-color: rgba(46, 160, 67, 0.25);
background-color: var(--d2h-dark-change-ins-color);
}
.d2h-dark-color-scheme .d2h-file-wrapper {
border: 1px solid #30363d;
border: 1px solid var(--d2h-dark-border-color);
}
.d2h-dark-color-scheme .d2h-file-collapse {
border: 1px solid #0d1117;
border: 1px solid var(--d2h-dark-bg-color);
}
.d2h-dark-color-scheme .d2h-file-collapse.d2h-selected {
background-color: rgba(56, 139, 253, 0.1);
background-color: var(--d2h-dark-selected-color);
}
.d2h-dark-color-scheme .d2h-file-list-wrapper a,
.d2h-dark-color-scheme .d2h-file-list-wrapper a:visited {
color: #3572b0;
color: var(--d2h-dark-moved-label-color);
}
.d2h-dark-color-scheme .d2h-file-list > li {
border-bottom: 1px solid #0d1117;
border-bottom: 1px solid var(--d2h-dark-bg-color);
}
.d2h-dark-color-scheme .d2h-deleted {
color: #f85149;
color: var(--d2h-dark-del-label-color);
}
.d2h-dark-color-scheme .d2h-added {
color: #3fb950;
color: var(--d2h-dark-ins-label-color);
}
.d2h-dark-color-scheme .d2h-changed {
color: #d29922;
color: var(--d2h-dark-change-label-color);
}
.d2h-dark-color-scheme .d2h-moved {
color: #3572b0;
color: var(--d2h-dark-moved-label-color);
}
.d2h-dark-color-scheme .d2h-tag {
background-color: #0d1117;
background-color: var(--d2h-dark-bg-color);
}
.d2h-dark-color-scheme .d2h-deleted-tag {
border: 1px solid #f85149;
border: 1px solid var(--d2h-dark-del-label-color);
}
.d2h-dark-color-scheme .d2h-added-tag {
border: 1px solid #3fb950;
border: 1px solid var(--d2h-dark-ins-label-color);
}
.d2h-dark-color-scheme .d2h-changed-tag {
border: 1px solid #d29922;
border: 1px solid var(--d2h-dark-change-label-color);
}
.d2h-dark-color-scheme .d2h-moved-tag {
border: 1px solid #3572b0;
border: 1px solid var(--d2h-dark-moved-label-color);
}
@media (prefers-color-scheme: dark) {
.d2h-auto-color-scheme {
background-color: #0d1117;
background-color: var(--d2h-dark-bg-color);
color: #e6edf3;
color: var(--d2h-dark-color);
}
.d2h-auto-color-scheme .d2h-file-header {
background-color: #161b22;
background-color: var(--d2h-dark-file-header-bg-color);
border-bottom: #30363d;
border-bottom: var(--d2h-dark-file-header-border-color);
}
.d2h-auto-color-scheme .d2h-lines-added {
border: 1px solid rgba(46, 160, 67, 0.4);
border: 1px solid var(--d2h-dark-ins-border-color);
color: #3fb950;
color: var(--d2h-dark-ins-label-color);
}
.d2h-auto-color-scheme .d2h-lines-deleted {
border: 1px solid rgba(248, 81, 73, 0.4);
border: 1px solid var(--d2h-dark-del-border-color);
color: #f85149;
color: var(--d2h-dark-del-label-color);
}
.d2h-auto-color-scheme .d2h-code-line del,
.d2h-auto-color-scheme .d2h-code-side-line del {
background-color: rgba(248, 81, 73, 0.4);
background-color: var(--d2h-dark-del-highlight-bg-color);
}
.d2h-auto-color-scheme .d2h-code-line ins,
.d2h-auto-color-scheme .d2h-code-side-line ins {
background-color: rgba(46, 160, 67, 0.4);
background-color: var(--d2h-dark-ins-highlight-bg-color);
}
.d2h-auto-color-scheme .d2h-diff-tbody {
border-color: #30363d;
border-color: var(--d2h-dark-border-color);
}
.d2h-auto-color-scheme .d2h-code-side-linenumber {
background-color: #0d1117;
background-color: var(--d2h-dark-bg-color);
border-color: #21262d;
border-color: var(--d2h-dark-line-border-color);
color: #6e7681;
color: var(--d2h-dark-dim-color);
}
.d2h-auto-color-scheme .d2h-files-diff .d2h-code-side-emptyplaceholder,
.d2h-auto-color-scheme .d2h-files-diff .d2h-emptyplaceholder {
background-color: hsla(215, 8%, 47%, 0.1);
background-color: var(--d2h-dark-empty-placeholder-bg-color);
border-color: #30363d;
border-color: var(--d2h-dark-empty-placeholder-border-color);
}
.d2h-auto-color-scheme .d2h-code-linenumber {
background-color: #0d1117;
background-color: var(--d2h-dark-bg-color);
border-color: #21262d;
border-color: var(--d2h-dark-line-border-color);
color: #6e7681;
color: var(--d2h-dark-dim-color);
}
.d2h-auto-color-scheme .d2h-del {
background-color: rgba(248, 81, 73, 0.1);
background-color: var(--d2h-dark-del-bg-color);
border-color: rgba(248, 81, 73, 0.4);
border-color: var(--d2h-dark-del-border-color);
}
.d2h-auto-color-scheme .d2h-ins {
background-color: rgba(46, 160, 67, 0.15);
background-color: var(--d2h-dark-ins-bg-color);
border-color: rgba(46, 160, 67, 0.4);
border-color: var(--d2h-dark-ins-border-color);
}
.d2h-auto-color-scheme .d2h-info {
background-color: rgba(56, 139, 253, 0.1);
background-color: var(--d2h-dark-info-bg-color);
border-color: rgba(56, 139, 253, 0.4);
border-color: var(--d2h-dark-info-border-color);
color: #6e7681;
color: var(--d2h-dark-dim-color);
}
.d2h-auto-color-scheme .d2h-file-diff .d2h-del.d2h-change {
background-color: rgba(210, 153, 34, 0.2);
background-color: var(--d2h-dark-change-del-color);
}
.d2h-auto-color-scheme .d2h-file-diff .d2h-ins.d2h-change {
background-color: rgba(46, 160, 67, 0.25);
background-color: var(--d2h-dark-change-ins-color);
}
.d2h-auto-color-scheme .d2h-file-wrapper {
border: 1px solid #30363d;
border: 1px solid var(--d2h-dark-border-color);
}
.d2h-auto-color-scheme .d2h-file-collapse {
border: 1px solid #0d1117;
border: 1px solid var(--d2h-dark-bg-color);
}
.d2h-auto-color-scheme .d2h-file-collapse.d2h-selected {
background-color: rgba(56, 139, 253, 0.1);
background-color: var(--d2h-dark-selected-color);
}
.d2h-auto-color-scheme .d2h-file-list-wrapper a,
.d2h-auto-color-scheme .d2h-file-list-wrapper a:visited {
color: #3572b0;
color: var(--d2h-dark-moved-label-color);
}
.d2h-auto-color-scheme .d2h-file-list > li {
border-bottom: 1px solid #0d1117;
border-bottom: 1px solid var(--d2h-dark-bg-color);
}
.d2h-dark-color-scheme .d2h-deleted {
color: #f85149;
color: var(--d2h-dark-del-label-color);
}
.d2h-auto-color-scheme .d2h-added {
color: #3fb950;
color: var(--d2h-dark-ins-label-color);
}
.d2h-auto-color-scheme .d2h-changed {
color: #d29922;
color: var(--d2h-dark-change-label-color);
}
.d2h-auto-color-scheme .d2h-moved {
color: #3572b0;
color: var(--d2h-dark-moved-label-color);
}
.d2h-auto-color-scheme .d2h-tag {
background-color: #0d1117;
background-color: var(--d2h-dark-bg-color);
}
.d2h-auto-color-scheme .d2h-deleted-tag {
border: 1px solid #f85149;
border: 1px solid var(--d2h-dark-del-label-color);
}
.d2h-auto-color-scheme .d2h-added-tag {
border: 1px solid #3fb950;
border: 1px solid var(--d2h-dark-ins-label-color);
}
.d2h-auto-color-scheme .d2h-changed-tag {
border: 1px solid #d29922;
border: 1px solid var(--d2h-dark-change-label-color);
}
.d2h-auto-color-scheme .d2h-moved-tag {
border: 1px solid #3572b0;
border: 1px solid var(--d2h-dark-moved-label-color);
}
}

View File

@@ -1,6 +1,6 @@
const darkTheme = {
'brand': '#546de5',
'text': 'rgb(52 52 52)',
brand: '#546de5',
text: 'rgb(52 52 52)',
'primary-text': '#ffffff',
'primary-theme': '#1e1e1e',
'secondary-text': '#929292',

View File

@@ -1,6 +1,6 @@
const lightTheme = {
'brand': '#546de5',
'text': 'rgb(52 52 52)',
brand: '#546de5',
text: 'rgb(52 52 52)',
'primary-text': 'rgb(52 52 52)',
'primary-theme': '#ffffff',
'secondary-text': '#929292',

View File

@@ -38,9 +38,6 @@ export default defineConfig({
dynamicImportMode: "eager",
},
},
rules: [
{ test: /\.md$/, type: 'asset/source' }
]
},
ignoreWarnings: [
(warning) => warning.message.includes('Critical dependency: the request of a dependency is an expression') && warning?.moduleDescriptor?.name?.includes('flow-parser')

View File

@@ -1,317 +0,0 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
position: absolute;
top: 6px;
right: 6px;
z-index: 10;
.ai-assist-trigger {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: ${(props) => props.theme.border.radius.sm};
border: 1px solid transparent;
background: transparent;
color: ${(props) => props.theme.colors.text.muted};
cursor: pointer;
transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease;
opacity: 0.7;
&:hover,
&.open {
opacity: 1;
color: ${(props) => props.theme.colors.accent};
background: ${(props) => props.theme.colors.accent}10;
border-color: ${(props) => props.theme.input.border};
}
&:focus-visible {
outline: 2px solid ${(props) => props.theme.colors.accent}55;
outline-offset: 1px;
}
}
`;
// Tippy renders the popup into document.body, outside StyledWrapper's subtree.
export const PopupWrapper = styled.div`
width: 360px;
background: ${(props) => props.theme.bg};
border: 1px solid ${(props) => props.theme.input.border};
border-radius: ${(props) => props.theme.border.radius.md};
overflow: hidden;
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 8px 10px;
border-bottom: 1px solid ${(props) => props.theme.input.border};
}
.popup-title {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
font-weight: 600;
color: ${(props) => props.theme.text};
text-transform: uppercase;
letter-spacing: 0.05em;
svg {
color: ${(props) => props.theme.colors.accent};
}
}
.popup-close {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: ${(props) => props.theme.border.radius.sm};
border: none;
background: transparent;
color: ${(props) => props.theme.colors.text.muted};
cursor: pointer;
transition: background-color 0.15s ease, color 0.15s ease;
&:hover {
background: ${(props) => props.theme.input.bg};
color: ${(props) => props.theme.text};
}
}
.popup-body {
padding: 10px;
display: flex;
flex-direction: column;
gap: 8px;
}
.popup-input {
width: 100%;
padding: 8px 10px;
font-size: 12px;
font-family: inherit;
line-height: 1.4;
border-radius: ${(props) => props.theme.border.radius.sm};
border: 1px solid ${(props) => props.theme.input.border};
background: ${(props) => props.theme.input.bg};
color: ${(props) => props.theme.text};
resize: vertical;
outline: none;
transition: border-color 0.15s ease;
&::placeholder {
color: ${(props) => props.theme.colors.text.muted};
opacity: 0.85;
}
&:focus {
border-color: ${(props) => props.theme.input.focusBorder};
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.popup-suggestions {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.suggestion-chip {
padding: 3px 8px;
font-size: 11px;
border: 1px solid ${(props) => props.theme.input.border};
border-radius: 999px;
background: ${(props) => props.theme.input.bg};
color: ${(props) => props.theme.colors.text.muted};
cursor: pointer;
transition: color 0.15s ease, border-color 0.15s ease, background-color 0.15s ease;
&:hover:not(:disabled) {
color: ${(props) => props.theme.text};
border-color: ${(props) => props.theme.colors.accent}80;
background: ${(props) => props.theme.colors.accent}10;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
.popup-error {
padding: 6px 8px;
font-size: 11px;
border-radius: ${(props) => props.theme.border.radius.sm};
color: ${(props) => props.theme.colors.text.danger};
background: ${(props) => props.theme.colors.bg.danger}15;
}
.popup-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 8px 10px;
border-top: 1px solid ${(props) => props.theme.input.border};
}
.popup-hint {
font-size: 11px;
color: ${(props) => props.theme.colors.text.muted};
}
.popup-loading {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: ${(props) => props.theme.colors.text.muted};
}
.loading-spinner {
width: 12px;
height: 12px;
border: 2px solid ${(props) => props.theme.input.border};
border-top-color: ${(props) => props.theme.colors.accent};
border-radius: 50%;
animation: ai-assist-spin 0.7s linear infinite;
}
@keyframes ai-assist-spin {
to { transform: rotate(360deg); }
}
.btn-generate {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 12px;
font-size: 12px;
font-weight: 500;
border-radius: ${(props) => props.theme.border.radius.sm};
border: 1px solid ${(props) => props.theme.colors.accent};
background: ${(props) => props.theme.colors.accent};
color: white;
cursor: pointer;
transition: opacity 0.15s ease;
&:hover:not(:disabled) {
opacity: 0.88;
}
&:disabled {
opacity: 0.45;
cursor: not-allowed;
}
}
.btn-stop {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 5px 12px;
font-size: 12px;
font-weight: 500;
border-radius: ${(props) => props.theme.border.radius.sm};
border: 1px solid ${(props) => props.theme.input.border};
background: ${(props) => props.theme.input.bg};
color: ${(props) => props.theme.text};
cursor: pointer;
transition: background-color 0.15s ease, border-color 0.15s ease;
&:hover {
border-color: ${(props) => props.theme.colors.text.danger};
color: ${(props) => props.theme.colors.text.danger};
}
}
.btn-secondary {
padding: 5px 12px;
font-size: 12px;
border-radius: ${(props) => props.theme.border.radius.sm};
border: 1px solid ${(props) => props.theme.input.border};
background: transparent;
color: ${(props) => props.theme.text};
cursor: pointer;
transition: background-color 0.15s ease;
&:hover:not(:disabled) {
background: ${(props) => props.theme.input.bg};
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
.preview-section {
display: flex;
flex-direction: column;
gap: 6px;
}
.preview-label {
font-size: 11px;
color: ${(props) => props.theme.colors.text.muted};
text-transform: uppercase;
letter-spacing: 0.05em;
}
.preview-code {
max-height: 220px;
overflow: auto;
padding: 8px 10px;
font-family: ${(props) => props.theme.font.monospace || 'monospace'};
font-size: 11.5px;
line-height: 1.5;
color: ${(props) => props.theme.text};
background: ${(props) => props.theme.input.bg};
border: 1px solid ${(props) => props.theme.input.border};
border-radius: ${(props) => props.theme.border.radius.sm};
white-space: pre;
}
.preview-modes {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: ${(props) => props.theme.colors.text.muted};
}
.preview-mode-btn {
padding: 2px 6px;
border-radius: ${(props) => props.theme.border.radius.sm};
border: 1px solid transparent;
background: transparent;
color: ${(props) => props.theme.colors.text.muted};
cursor: pointer;
font-size: 11px;
&.active {
color: ${(props) => props.theme.text};
border-color: ${(props) => props.theme.input.border};
background: ${(props) => props.theme.input.bg};
}
&:hover:not(.active) {
color: ${(props) => props.theme.text};
}
}
`;
export default StyledWrapper;

View File

@@ -1,302 +0,0 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import get from 'lodash/get';
import Tippy from '@tippyjs/react';
import { IconStars, IconX, IconArrowBackUp, IconPlayerStop } from '@tabler/icons';
import { aiGenerateScript, stopAiGeneration } from 'utils/ai';
import StyledWrapper, { PopupWrapper } from './StyledWrapper';
const SUGGESTIONS = {
'tests': [
{ label: 'Status 200', prompt: 'Add a test asserting the response status code is 200' },
{ label: 'JSON body', prompt: 'Add tests validating the JSON response body structure and key fields' },
{ label: 'Headers', prompt: 'Add a test checking the content-type response header' },
{ label: 'Response time', prompt: 'Add a test asserting the response time is below 1000ms' }
],
'pre-request': [
{ label: 'Auth header', prompt: 'Set an Authorization header from an environment token variable' },
{ label: 'Timestamp', prompt: 'Set a variable named "timestamp" containing the current epoch ms' },
{ label: 'Random ID', prompt: 'Set a variable named "requestId" containing a random UUID-style id' }
],
'post-response': [
{ label: 'Save token', prompt: 'Extract a token from the response body and save it to an environment variable' },
{ label: 'Save id', prompt: 'Extract the primary id from the response body and save it to a variable' },
{ label: 'Log response', prompt: 'Log the response status and a short summary of the body' }
],
'docs': [
{ label: 'Overview', prompt: 'Write an overview section describing the purpose and key features' },
{ label: 'Request', prompt: 'Document the request method, URL, headers, parameters, and body' },
{ label: 'Examples', prompt: 'Add request and response examples with sample JSON' },
{ label: 'Errors', prompt: 'Document common error responses and status codes' }
],
'app-request': [
{ label: 'Send button', prompt: 'Add a button that calls ctx.sendRequest() and displays the response status, headers, and pretty-printed body' },
{ label: 'Form for body', prompt: 'Build a form whose fields override the request body, then send it with ctx.sendRequest({ variables }) and show the result' },
{ label: 'Response viewer', prompt: 'Render ctx.response with collapsible JSON and a banner showing status and response time; update on ctx.onResponseUpdate' },
{ label: 'Test results', prompt: 'List ctx.testResults and ctx.assertionResults with pass/fail badges; refresh on ctx.onResultsUpdate' }
],
'app-collection': [
{ label: 'Request list', prompt: 'List all requests from ctx.listRequests() with their method and url, and a Run button next to each that calls ctx.runRequest(pathname)' },
{ label: 'Dashboard', prompt: 'Build a small dashboard that runs every request from ctx.listRequests() on load and shows status code, response time, and a pass/fail dot for each' },
{ label: 'Form runner', prompt: 'Render a form, and on submit call ctx.runRequest(pathname, { variables }) for a chosen request and display the response' },
{ label: 'Variables panel', prompt: 'Show ctx.variables in a table and allow editing values via ctx.setRuntimeVariable(key, value); react to ctx.onVariablesUpdate' }
]
};
const TITLES = {
'tests': 'Generate Tests',
'pre-request': 'Generate Pre-Request Script',
'post-response': 'Generate Post-Response Script',
'docs': 'Generate Documentation',
'app-request': 'Generate App',
'app-collection': 'Generate App'
};
const PREVIEW_LABELS = {
'docs': 'Preview · replaces current documentation',
'app-request': 'Preview · replaces current app',
'app-collection': 'Preview · replaces current app'
};
const isValidType = (t) => SUGGESTIONS[t] !== undefined;
const AIAssist = ({ scriptType, currentScript, requestContext, docsContext, variables, onApply }) => {
const [isOpen, setIsOpen] = useState(false);
const [prompt, setPrompt] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [generated, setGenerated] = useState(null);
const streamIdRef = useRef(null);
const tippyRef = useRef(null);
// Focus the prompt textarea when coming back from preview
useEffect(() => {
if (isOpen && generated == null) {
tippyRef.current?.popper?.querySelector('.popup-input')?.focus();
}
}, [isOpen, generated]);
// handle Escape key to close the popup
useEffect(() => {
if (!isOpen) return;
const onKeyDown = (e) => {
if (e.key === 'Escape') {
e.stopPropagation();
tippyRef.current?.hide();
}
};
document.addEventListener('keydown', onKeyDown, true);
return () => document.removeEventListener('keydown', onKeyDown, true);
}, [isOpen]);
const preferences = useSelector((state) => state.app.preferences);
const isAiEnabled = get(preferences, 'ai.enabled', false);
const suggestions = useMemo(() => SUGGESTIONS[scriptType] || [], [scriptType]);
const title = TITLES[scriptType] || 'Generate with AI';
const previewLabel = PREVIEW_LABELS[scriptType] || 'Preview · replaces current script';
const close = useCallback(() => {
tippyRef.current?.hide();
}, []);
const handleGenerate = useCallback(
async (overridePrompt) => {
const text = (overridePrompt ?? prompt).trim();
if (!text || isLoading) return;
setIsLoading(true);
setError(null);
const streamId = `sparkle-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
streamIdRef.current = streamId;
try {
const result = await aiGenerateScript({
scriptType,
prompt: text,
currentScript: currentScript || '',
requestContext,
docsContext,
variables,
streamId
});
if (result?.stopped) {
return;
}
if (result?.error) {
setError(result.error);
return;
}
if (result?.content) {
setGenerated(result.content);
} else {
setError('No content was generated. Try rephrasing your prompt.');
}
} catch (err) {
setError(err?.message || 'Failed to generate script');
} finally {
streamIdRef.current = null;
setIsLoading(false);
}
},
[prompt, isLoading, scriptType, currentScript, requestContext, docsContext, variables]
);
const handleStop = useCallback(() => {
if (streamIdRef.current) {
stopAiGeneration(streamIdRef.current);
}
}, []);
const handleApply = useCallback(() => {
if (generated == null) return;
onApply(generated);
setGenerated(null);
setPrompt('');
close();
}, [generated, onApply, close]);
const handleBackToPrompt = useCallback(() => {
setGenerated(null);
setError(null);
}, []);
if (!isAiEnabled || !isValidType(scriptType)) return null;
return (
<StyledWrapper>
<Tippy
interactive
trigger="click"
placement="bottom-end"
arrow={false}
animation={false}
maxWidth="none"
appendTo={() => document.body}
onCreate={(instance) => (tippyRef.current = instance)}
onShow={(instance) => {
setIsOpen(true);
// rAF so the popup content is in the DOM
requestAnimationFrame(() => instance.popper?.querySelector('.popup-input')?.focus());
}}
onHide={() => {
setIsOpen(false);
setError(null);
}}
render={(attrs) => (
<PopupWrapper className="ai-assist-popup" role="dialog" aria-label={title} tabIndex={-1} {...attrs}>
<div className="popup-header">
<span className="popup-title">
<IconStars size={12} strokeWidth={1.75} />
{title}
</span>
<button className="popup-close" onClick={close} type="button" aria-label="Close">
<IconX size={14} />
</button>
</div>
{generated == null ? (
<>
<div className="popup-body">
<textarea
className="popup-input"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleGenerate();
}
}}
placeholder="Describe what you want to generate..."
rows={3}
disabled={isLoading}
/>
{!isLoading && !prompt && suggestions.length > 0 && (
<div className="popup-suggestions">
{suggestions.map((s) => (
<button
key={s.label}
className="suggestion-chip"
type="button"
onClick={() => handleGenerate(s.prompt)}
disabled={isLoading}
>
{s.label}
</button>
))}
</div>
)}
{error && <div className="popup-error">{error}</div>}
</div>
<div className="popup-footer">
{isLoading ? (
<span className="popup-loading">
<span className="loading-spinner" />
Generating...
</span>
) : (
<span className="popup-hint">Enter to generate · Shift+Enter for newline</span>
)}
{isLoading ? (
<button
className="btn-stop"
type="button"
onClick={handleStop}
title="Stop generating"
>
<IconPlayerStop size={12} /> Stop
</button>
) : (
<button
className="btn-generate"
type="button"
onClick={() => handleGenerate()}
disabled={!prompt.trim()}
>
Generate
</button>
)}
</div>
</>
) : (
<>
<div className="popup-body">
<div className="preview-section">
<span className="preview-label">{previewLabel}</span>
<pre className="preview-code">{generated}</pre>
</div>
</div>
<div className="popup-footer">
<button className="btn-secondary" type="button" onClick={handleBackToPrompt}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
<IconArrowBackUp size={12} /> Back
</span>
</button>
<button className="btn-generate" type="button" onClick={handleApply}>
Apply
</button>
</div>
</>
)}
</PopupWrapper>
)}
>
<button
className={`ai-assist-trigger ${isOpen ? 'open' : ''}`}
title={title}
type="button"
aria-label={title}
>
<IconStars size={14} strokeWidth={1.75} />
</button>
</Tippy>
</StyledWrapper>
);
};
export default AIAssist;

View File

@@ -1,404 +0,0 @@
import '@testing-library/jest-dom';
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import { ThemeProvider } from 'styled-components';
import { aiGenerateScript, stopAiGeneration } from 'utils/ai';
import AIAssist from './index';
jest.mock('utils/ai', () => ({
aiGenerateScript: jest.fn(),
stopAiGeneration: jest.fn()
}));
const theme = {
bg: '#1e1e1e',
text: '#ffffff',
border: { radius: { sm: '4px', md: '6px' } },
colors: {
accent: '#6366f1',
text: { muted: '#9ca3af', danger: '#ef4444' },
bg: { danger: '#ef4444' }
},
input: {
border: '#374151',
bg: '#111827',
focusBorder: '#6366f1'
},
font: { monospace: 'monospace' }
};
const createStore = (aiEnabled = true) => configureStore({
reducer: {
app: (state = { preferences: { ai: { enabled: aiEnabled } } }) => state
}
});
const defaultProps = {
scriptType: 'tests',
currentScript: 'test("ok", () => {});',
onApply: jest.fn()
};
const renderAIAssist = ({
props = {},
aiEnabled = true
} = {}) => {
const mergedProps = { ...defaultProps, ...props };
return render(
<Provider store={createStore(aiEnabled)}>
<ThemeProvider theme={theme}>
<AIAssist {...mergedProps} />
</ThemeProvider>
</Provider>
);
};
const openPopup = () => {
fireEvent.click(screen.getByRole('button', { name: 'Generate Tests' }));
};
describe('AIAssist', () => {
beforeEach(() => {
jest.clearAllMocks();
aiGenerateScript.mockResolvedValue({ content: 'test("generated", () => {});' });
});
describe('visibility', () => {
it('renders nothing when AI is disabled', () => {
const { container } = renderAIAssist({ aiEnabled: false });
expect(container.firstChild).toBeNull();
});
it('renders nothing for an unsupported script type', () => {
const { container } = renderAIAssist({ props: { scriptType: 'unknown-type' } });
expect(container.firstChild).toBeNull();
});
it('renders the trigger when AI is enabled and the script type is supported', () => {
renderAIAssist();
expect(screen.getByRole('button', { name: 'Generate Tests' })).toBeInTheDocument();
});
});
describe('titles', () => {
it.each([
['tests', 'Generate Tests'],
['pre-request', 'Generate Pre-Request Script'],
['post-response', 'Generate Post-Response Script'],
['docs', 'Generate Documentation']
])('uses the correct title for %s', (scriptType, title) => {
renderAIAssist({ props: { scriptType } });
expect(screen.getByRole('button', { name: title })).toBeInTheDocument();
});
});
describe('popup interactions', () => {
it('opens and closes the popup from the trigger and close button', () => {
renderAIAssist();
openPopup();
expect(screen.getByRole('dialog', { name: 'Generate Tests' })).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Close' }));
expect(screen.queryByRole('dialog', { name: 'Generate Tests' })).not.toBeInTheDocument();
});
it('renders the popup into document.body as a portal', () => {
renderAIAssist();
openPopup();
const dialog = screen.getByRole('dialog', { name: 'Generate Tests' });
const tippyRoot = dialog.closest('[data-tippy-root]');
expect(tippyRoot).not.toBeNull();
expect(tippyRoot.parentElement).toBe(document.body);
});
it('closes the popup when Escape is pressed', () => {
renderAIAssist();
openPopup();
fireEvent.keyDown(document, { key: 'Escape' });
expect(screen.queryByRole('dialog', { name: 'Generate Tests' })).not.toBeInTheDocument();
});
it('closes the popup when clicking outside', () => {
renderAIAssist();
openPopup();
fireEvent.mouseDown(document.body);
expect(screen.queryByRole('dialog', { name: 'Generate Tests' })).not.toBeInTheDocument();
});
});
describe('prompt view', () => {
it('shows suggestion chips when the prompt is empty', () => {
renderAIAssist();
openPopup();
expect(screen.getByRole('button', { name: 'Status 200' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'JSON body' })).toBeInTheDocument();
});
it('shows docs suggestions for the docs script type', () => {
renderAIAssist({ props: { scriptType: 'docs', currentScript: '# Overview' } });
fireEvent.click(screen.getByRole('button', { name: 'Generate Documentation' }));
expect(screen.getByRole('button', { name: 'Overview' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Request' })).toBeInTheDocument();
});
it('hides suggestions once the user starts typing', () => {
renderAIAssist();
openPopup();
fireEvent.change(screen.getByPlaceholderText('Describe what you want to generate...'), {
target: { value: 'Add a status test' }
});
expect(screen.queryByRole('button', { name: 'Status 200' })).not.toBeInTheDocument();
});
it('keeps Generate disabled until the prompt has text', () => {
renderAIAssist();
openPopup();
expect(screen.getByRole('button', { name: 'Generate' })).toBeDisabled();
fireEvent.change(screen.getByPlaceholderText('Describe what you want to generate...'), {
target: { value: 'Add a status test' }
});
expect(screen.getByRole('button', { name: 'Generate' })).toBeEnabled();
});
});
describe('generation flow', () => {
it('generates from a suggestion chip', async () => {
renderAIAssist();
openPopup();
fireEvent.click(screen.getByRole('button', { name: 'Status 200' }));
await waitFor(() => {
expect(aiGenerateScript).toHaveBeenCalledWith(expect.objectContaining({
scriptType: 'tests',
prompt: 'Add a test asserting the response status code is 200',
currentScript: 'test("ok", () => {});',
requestContext: undefined,
streamId: expect.any(String)
}));
});
expect(screen.getByText('test("generated", () => {});')).toBeInTheDocument();
});
it('passes docs context for folder and collection documentation', async () => {
const docsContext = {
scope: 'folder',
name: 'Users',
collectionName: 'Pet Store API',
folders: [{ name: 'Admin', requestCount: 1, subfolderCount: 0 }],
requests: [{ name: 'List Users', method: 'GET', url: '{{base}}/users' }]
};
renderAIAssist({ props: { scriptType: 'docs', currentScript: '', docsContext } });
fireEvent.click(screen.getByRole('button', { name: 'Generate Documentation' }));
fireEvent.click(screen.getByRole('button', { name: 'Overview' }));
await waitFor(() => {
expect(aiGenerateScript).toHaveBeenCalledWith(expect.objectContaining({
scriptType: 'docs',
prompt: 'Write an overview section describing the purpose and key features',
currentScript: '',
requestContext: undefined,
docsContext
}));
});
});
it('generates from the prompt input and passes request context', async () => {
const requestContext = {
method: 'GET',
url: 'https://api.example.com/users',
headers: [],
params: [],
body: null
};
renderAIAssist({ props: { requestContext } });
openPopup();
fireEvent.change(screen.getByPlaceholderText('Describe what you want to generate...'), {
target: { value: 'Add auth header test' }
});
fireEvent.click(screen.getByRole('button', { name: 'Generate' }));
await waitFor(() => {
expect(aiGenerateScript).toHaveBeenCalledWith(expect.objectContaining({
scriptType: 'tests',
prompt: 'Add auth header test',
currentScript: 'test("ok", () => {});',
requestContext
}));
});
});
it('generates when pressing Enter', async () => {
renderAIAssist();
openPopup();
const textarea = screen.getByPlaceholderText('Describe what you want to generate...');
fireEvent.change(textarea, { target: { value: 'Add response time test' } });
fireEvent.keyDown(textarea, { key: 'Enter' });
await waitFor(() => {
expect(aiGenerateScript).toHaveBeenCalledWith(expect.objectContaining({
scriptType: 'tests',
prompt: 'Add response time test',
currentScript: 'test("ok", () => {});',
requestContext: undefined
}));
});
});
it('does not generate when pressing Shift+Enter (allows newline)', () => {
renderAIAssist();
openPopup();
const textarea = screen.getByPlaceholderText('Describe what you want to generate...');
fireEvent.change(textarea, { target: { value: 'Add response time test' } });
fireEvent.keyDown(textarea, { key: 'Enter', shiftKey: true });
expect(aiGenerateScript).not.toHaveBeenCalled();
});
it('shows a loading state while generation is in progress', async () => {
let resolveGenerate;
aiGenerateScript.mockImplementation(() => new Promise((resolve) => {
resolveGenerate = resolve;
}));
renderAIAssist();
openPopup();
fireEvent.click(screen.getByRole('button', { name: 'Status 200' }));
expect(screen.getByText('Generating...')).toBeInTheDocument();
resolveGenerate({ content: 'test("done", () => {});' });
await waitFor(() => {
expect(screen.getByText('test("done", () => {});')).toBeInTheDocument();
});
});
it('shows a Stop button during generation and cancels via streamId', async () => {
let resolveGenerate;
aiGenerateScript.mockImplementation(() => new Promise((resolve) => {
resolveGenerate = resolve;
}));
renderAIAssist();
openPopup();
fireEvent.click(screen.getByRole('button', { name: 'Status 200' }));
const stopButton = await screen.findByRole('button', { name: /stop/i });
expect(stopButton).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Generate' })).not.toBeInTheDocument();
const passedStreamId = aiGenerateScript.mock.calls[0][0].streamId;
expect(passedStreamId).toEqual(expect.any(String));
fireEvent.click(stopButton);
expect(stopAiGeneration).toHaveBeenCalledWith(passedStreamId);
resolveGenerate({ stopped: true });
await waitFor(() => {
expect(screen.queryByRole('button', { name: /stop/i })).not.toBeInTheDocument();
});
expect(screen.queryByRole('button', { name: 'Apply' })).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Generate' })).toBeInTheDocument();
});
it('shows an API error without entering preview mode', async () => {
aiGenerateScript.mockResolvedValue({ error: 'Provider unavailable' });
renderAIAssist();
openPopup();
fireEvent.click(screen.getByRole('button', { name: 'Status 200' }));
await waitFor(() => {
expect(screen.getByText('Provider unavailable')).toBeInTheDocument();
});
expect(screen.queryByRole('button', { name: 'Apply' })).not.toBeInTheDocument();
});
it('shows a fallback error when no content is returned', async () => {
aiGenerateScript.mockResolvedValue({});
renderAIAssist();
openPopup();
fireEvent.click(screen.getByRole('button', { name: 'Status 200' }));
await waitFor(() => {
expect(screen.getByText('No content was generated. Try rephrasing your prompt.')).toBeInTheDocument();
});
});
});
describe('preview and apply', () => {
const showPreview = async () => {
renderAIAssist();
openPopup();
fireEvent.click(screen.getByRole('button', { name: 'Status 200' }));
await waitFor(() => {
expect(screen.getByRole('button', { name: 'Apply' })).toBeInTheDocument();
});
};
it('uses the script preview label for script types', async () => {
await showPreview();
expect(screen.getByText('Preview · replaces current script')).toBeInTheDocument();
});
it('uses the documentation preview label for docs', async () => {
aiGenerateScript.mockResolvedValue({ content: '# API Docs' });
renderAIAssist({ props: { scriptType: 'docs', currentScript: '# Existing' } });
fireEvent.click(screen.getByRole('button', { name: 'Generate Documentation' }));
fireEvent.click(screen.getByRole('button', { name: 'Overview' }));
await waitFor(() => {
expect(screen.getByText('Preview · replaces current documentation')).toBeInTheDocument();
});
expect(screen.getByText('# API Docs')).toBeInTheDocument();
});
it('applies generated content and closes the popup', async () => {
const onApply = jest.fn();
renderAIAssist({ props: { onApply } });
openPopup();
fireEvent.click(screen.getByRole('button', { name: 'Status 200' }));
await waitFor(() => {
expect(screen.getByRole('button', { name: 'Apply' })).toBeInTheDocument();
});
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
expect(onApply).toHaveBeenCalledWith('test("generated", () => {});');
expect(screen.queryByRole('dialog', { name: 'Generate Tests' })).not.toBeInTheDocument();
});
it('returns to the prompt view when Back is clicked', async () => {
await showPreview();
fireEvent.click(screen.getByRole('button', { name: 'Back' }));
expect(screen.getByPlaceholderText('Describe what you want to generate...')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Status 200' })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Apply' })).not.toBeInTheDocument();
});
});
});

View File

@@ -4,7 +4,7 @@ import { AccordionItem, AccordionHeader, AccordionContent } from './styledWrappe
const AccordionContext = createContext();
const Accordion = ({ children, defaultIndex, dataTestId }) => {
const Accordion = ({ children, defaultIndex }) => {
const [openIndex, setOpenIndex] = useState(defaultIndex);
const toggleItem = (index) => {
@@ -13,7 +13,7 @@ const Accordion = ({ children, defaultIndex, dataTestId }) => {
return (
<AccordionContext.Provider value={{ openIndex, toggleItem }}>
<div data-testid={dataTestId}>{children}</div>
<div>{children}</div>
</AccordionContext.Provider>
);
};

View File

@@ -1,46 +0,0 @@
import { useState, useRef, useEffect } from 'react';
import { IconCopy, IconCheck } from '@tabler/icons';
const AssistantCodeBlock = ({ content, language, isOpen, isStreaming, isLast }) => {
const [isCopied, setIsCopied] = useState(false);
const preRef = useRef(null);
useEffect(() => {
if (isStreaming && isOpen && preRef.current) {
preRef.current.scrollTop = preRef.current.scrollHeight;
}
}, [content, isStreaming, isOpen]);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(content);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 1500);
} catch (err) {
console.error('Failed to copy:', err);
}
};
return (
<div className="assistant-code-block">
<div className="assistant-code-block__header">
<div className="assistant-code-block__meta">
<span className="assistant-code-block__lang">{language || 'code'}</span>
{isOpen && <span className="assistant-code-block__spinner" />}
</div>
<button className="assistant-code-block__btn" onClick={handleCopy} title="Copy">
{isCopied ? <IconCheck size={12} /> : <IconCopy size={12} />}
{isCopied ? 'Copied' : 'Copy'}
</button>
</div>
<pre ref={preRef} className="assistant-code-block__body">
<code className={`language-${language || 'text'}`}>
{content}
{isStreaming && isLast && <span className="cursor">|</span>}
</code>
</pre>
</div>
);
};
export default AssistantCodeBlock;

View File

@@ -1,298 +0,0 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
margin-top: 8px;
border-radius: ${(props) => props.theme.border.radius.base};
overflow: hidden;
border: 1px solid ${(props) => props.theme.border.border1};
background: ${(props) => props.theme.codemirror.bg};
&.accepted {
border-color: ${(props) => props.theme.colors.text.green};
}
&.rejected {
opacity: 0.5;
}
.diff-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
background: ${(props) => props.theme.background.mantle};
border-bottom: 1px solid ${(props) => props.theme.border.border1};
gap: 8px;
flex-wrap: nowrap;
}
.diff-title {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 500;
color: ${(props) => props.theme.colors.text.muted};
flex-shrink: 0;
.diff-icon {
color: ${(props) => props.theme.brand};
display: flex;
align-items: center;
}
}
.diff-content-type {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 1px 6px;
border-radius: 3px;
background: ${(props) => props.theme.background.surface0};
color: ${(props) => props.theme.colors.text.muted};
}
.diff-stats {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
font-weight: 500;
.stat {
padding: 1px 5px;
border-radius: 4px;
}
.additions {
background: ${(props) => props.theme.status.success.background};
color: ${(props) => props.theme.colors.text.green};
}
.deletions {
background: ${(props) => props.theme.status.danger.background};
color: ${(props) => props.theme.colors.text.danger};
}
}
.diff-actions {
display: flex;
gap: 6px;
flex-shrink: 0;
margin-left: auto;
}
.diff-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
padding: 4px 8px;
font-size: 11px;
font-weight: 500;
border: 1px solid transparent;
border-radius: ${(props) => props.theme.border.radius.base};
cursor: pointer;
white-space: nowrap;
&.accept {
background: ${(props) => props.theme.brand};
color: ${(props) => (props.theme.mode === 'dark' ? '#000' : '#fff')};
&:hover:not(:disabled) {
opacity: 0.9;
}
&:disabled {
opacity: 0.4;
cursor: not-allowed;
}
}
&.reject {
background: transparent;
color: ${(props) => props.theme.colors.text.muted};
border-color: ${(props) => props.theme.border.border1};
&:hover {
background: ${(props) => props.theme.status.danger.background};
color: ${(props) => props.theme.colors.text.danger};
border-color: ${(props) => props.theme.status.danger.background};
}
}
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 8px;
font-size: 11px;
border-radius: ${(props) => props.theme.border.radius.base};
font-weight: 500;
&.accepted {
background: ${(props) => props.theme.status.success.background};
color: ${(props) => props.theme.colors.text.green};
}
&.rejected {
background: ${(props) => props.theme.status.danger.background};
color: ${(props) => props.theme.colors.text.danger};
}
}
.diff-warning {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
font-size: 11px;
border-bottom: 1px solid ${(props) => props.theme.border.border1};
&.warn {
background: ${(props) => props.theme.status.warning.background};
color: ${(props) => props.theme.status.warning.text};
}
&.error {
background: ${(props) => props.theme.status.danger.background};
color: ${(props) => props.theme.colors.text.danger};
}
}
.diff-toggle {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
padding: 4px 8px;
font-size: 11px;
color: ${(props) => props.theme.colors.text.muted};
background: transparent;
border: none;
border-top: 1px solid ${(props) => props.theme.border.border1};
cursor: pointer;
width: 100%;
&:hover {
background: ${(props) => props.theme.background.surface0};
color: ${(props) => props.theme.text};
}
}
.diff-content {
max-height: 300px;
overflow: auto;
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
font-size: 11px;
line-height: 1.5;
&::-webkit-scrollbar {
width: 4px;
height: 4px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: ${(props) => props.theme.border.border1};
border-radius: 2px;
}
}
.diff-line {
padding: 0 8px 0 4px;
white-space: pre;
display: flex;
min-height: 18px;
line-height: 18px;
.line-number {
width: 24px;
text-align: right;
padding-right: 8px;
color: ${(props) => props.theme.colors.text.muted};
user-select: none;
flex-shrink: 0;
opacity: 0.5;
}
.line-prefix {
width: 12px;
flex-shrink: 0;
}
.line-content {
flex: 1;
overflow-x: auto;
}
&.added {
background: ${(props) => props.theme.status.success.background};
.line-content { color: ${(props) => props.theme.colors.text.green}; }
.line-prefix { color: ${(props) => props.theme.colors.text.green}; font-weight: 600; }
}
&.removed {
background: ${(props) => props.theme.status.danger.background};
.line-content { color: ${(props) => props.theme.colors.text.danger}; }
.line-prefix { color: ${(props) => props.theme.colors.text.danger}; font-weight: 600; }
}
&.unchanged {
.line-content { color: ${(props) => props.theme.colors.text.muted}; }
.line-prefix { opacity: 0; }
}
}
.expand-marker {
display: flex;
align-items: center;
padding: 0 8px 0 4px;
min-height: 22px;
background: ${(props) => props.theme.background.mantle};
.expand-gutter {
width: 24px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 4px;
}
.expand-buttons {
display: flex;
flex-direction: column;
gap: 0;
}
.expand-btn {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 11px;
padding: 0;
background: transparent;
border: none;
color: ${(props) => props.theme.colors.text.muted};
cursor: pointer;
opacity: 0.6;
&:hover {
color: ${(props) => props.theme.text};
opacity: 1;
}
}
.expand-line {
flex: 1;
height: 1px;
background: ${(props) => props.theme.border.border1};
margin-left: 8px;
}
}
`;
export default StyledWrapper;

View File

@@ -1,210 +0,0 @@
import React, { useMemo, useState } from 'react';
import { diffLines } from 'diff';
import { IconCheck, IconX, IconCode, IconChevronDown, IconChevronUp } from '@tabler/icons';
import StyledWrapper from './StyledWrapper';
const CONTEXT_LINES = 2;
const EXPAND_CHUNK_SIZE = 20;
const DiffView = ({ originalCode, newCode, onAccept, onReject, status, contentTypeLabel, warning, disableAccept }) => {
const [isExpanded, setIsExpanded] = useState(true);
const [expandedFromTop, setExpandedFromTop] = useState({});
const [expandedFromBottom, setExpandedFromBottom] = useState({});
const diffResult = useMemo(() => {
const changes = diffLines(originalCode || '', newCode || '');
let additions = 0;
let deletions = 0;
let lineNumber = 1;
const lines = changes.flatMap((part) => {
const partLines = part.value.split('\n');
if (partLines[partLines.length - 1] === '') partLines.pop();
return partLines.map((line) => {
const entry = { content: line, lineNumber: null };
if (part.added) {
additions += 1;
entry.type = 'added';
entry.lineNumber = lineNumber++;
} else if (part.removed) {
deletions += 1;
entry.type = 'removed';
} else {
entry.type = 'unchanged';
entry.lineNumber = lineNumber++;
}
return entry;
});
});
return { lines, additions, deletions };
}, [originalCode, newCode]);
const hunks = useMemo(() => {
const { lines } = diffResult;
if (lines.length === 0) return [];
const changedIndices = new Set();
lines.forEach((line, idx) => {
if (line.type === 'added' || line.type === 'removed') changedIndices.add(idx);
});
const visibleIndices = new Set();
changedIndices.forEach((idx) => {
for (let i = Math.max(0, idx - CONTEXT_LINES); i <= Math.min(lines.length - 1, idx + CONTEXT_LINES); i++) {
visibleIndices.add(i);
}
});
const result = [];
let i = 0;
while (i < lines.length) {
if (visibleIndices.has(i)) {
result.push({ type: 'line', data: lines[i], index: i });
i += 1;
} else {
const start = i;
while (i < lines.length && !visibleIndices.has(i)) i += 1;
result.push({
type: 'collapsed',
startIndex: start,
count: i - start,
lines: lines.slice(start, i)
});
}
}
return result;
}, [diffResult]);
const expandUp = (startIndex, totalLines) => {
setExpandedFromTop((prev) => {
const current = prev[startIndex] || 0;
const bottomExpanded = expandedFromBottom[startIndex] || 0;
const remaining = totalLines - current - bottomExpanded;
return { ...prev, [startIndex]: Math.min(current + EXPAND_CHUNK_SIZE, current + remaining) };
});
};
const expandDown = (startIndex, totalLines) => {
setExpandedFromBottom((prev) => {
const current = prev[startIndex] || 0;
const topExpanded = expandedFromTop[startIndex] || 0;
const remaining = totalLines - topExpanded - current;
return { ...prev, [startIndex]: Math.min(current + EXPAND_CHUNK_SIZE, current + remaining) };
});
};
if (diffResult.additions === 0 && diffResult.deletions === 0) return null;
const renderActions = () => {
if (status === 'accepted') {
return (
<span className="status-badge accepted">
<IconCheck size={12} /> Applied
</span>
);
}
if (status === 'rejected') {
return (
<span className="status-badge rejected">
<IconX size={12} /> Dismissed
</span>
);
}
return (
<div className="diff-actions">
<button className="diff-btn reject" onClick={onReject} title="Dismiss changes">
<IconX size={12} />
</button>
<button className="diff-btn accept" onClick={onAccept} title="Apply changes" disabled={disableAccept}>
<IconCheck size={12} /> Apply
</button>
</div>
);
};
const renderLine = (line, key) => (
<div key={key} className={`diff-line ${line.type}`}>
<span className="line-number">{line.type !== 'removed' ? line.lineNumber : ''}</span>
<span className="line-prefix">{line.type === 'added' ? '+' : line.type === 'removed' ? '-' : ' '}</span>
<span className="line-content">{line.content || ' '}</span>
</div>
);
const renderHunks = () =>
hunks.map((hunk, idx) => {
if (hunk.type === 'line') return renderLine(hunk.data, `line-${hunk.index}`);
const topCount = expandedFromTop[hunk.startIndex] || 0;
const bottomCount = expandedFromBottom[hunk.startIndex] || 0;
const remainingCount = hunk.count - topCount - bottomCount;
const topLines = hunk.lines.slice(0, topCount);
const bottomLines = hunk.lines.slice(hunk.count - bottomCount);
const isAtTop = idx === 0;
const isAtBottom = idx === hunks.length - 1;
return (
<React.Fragment key={`collapsed-${hunk.startIndex}`}>
{topLines.map((line, lineIdx) => renderLine(line, `top-${hunk.startIndex}-${lineIdx}`))}
{remainingCount > 0 && (
<div className="expand-marker">
<div className="expand-gutter">
<div className="expand-buttons">
{!isAtTop && (
<button className="expand-btn" onClick={() => expandUp(hunk.startIndex, hunk.count)} title="Expand up">
<IconChevronUp size={10} />
</button>
)}
{!isAtBottom && (
<button className="expand-btn" onClick={() => expandDown(hunk.startIndex, hunk.count)} title="Expand down">
<IconChevronDown size={10} />
</button>
)}
</div>
</div>
<div className="expand-line" />
</div>
)}
{bottomLines.map((line, lineIdx) => renderLine(line, `bottom-${hunk.startIndex}-${lineIdx}`))}
</React.Fragment>
);
});
return (
<StyledWrapper className={status || ''}>
<div className="diff-header">
<div className="diff-title">
<span className="diff-icon"><IconCode size={12} /></span>
{contentTypeLabel && <span className="diff-content-type">{contentTypeLabel}</span>}
<div className="diff-stats">
<span className="stat additions">+{diffResult.additions}</span>
<span className="stat deletions">-{diffResult.deletions}</span>
</div>
</div>
{renderActions()}
</div>
{warning && (
<div className={`diff-warning ${disableAccept ? 'error' : 'warn'}`}>
{warning}
</div>
)}
{isExpanded && <div className="diff-content">{renderHunks()}</div>}
<button className="diff-toggle" onClick={() => setIsExpanded((v) => !v)}>
{isExpanded ? (
<><IconChevronUp size={12} /> Hide</>
) : (
<><IconChevronDown size={12} /> Show ({diffResult.additions + diffResult.deletions})</>
)}
</button>
</StyledWrapper>
);
};
export default DiffView;

View File

@@ -1,831 +0,0 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
flex-shrink: 0;
height: 100%;
.ai-sidebar {
width: 420px;
height: 100%;
background: ${(props) => props.theme.bg};
border-left: 1px solid ${(props) => props.theme.border.border1};
display: flex;
flex-direction: column;
}
.ai-sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
border-bottom: 1px solid ${(props) => props.theme.border.border1};
.header-left {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.header-icon {
color: ${(props) => props.theme.brand};
flex-shrink: 0;
display: flex;
align-items: center;
}
.header-method {
font-size: 9px;
font-weight: 600;
text-transform: uppercase;
padding: 2px 6px;
border-radius: 4px;
flex-shrink: 0;
background: ${(props) => props.theme.background.surface0};
display: flex;
align-items: center;
&.method-get { color: ${(props) => props.theme.request.methods.get}; }
&.method-post { color: ${(props) => props.theme.request.methods.post}; }
&.method-put { color: ${(props) => props.theme.request.methods.put}; }
&.method-delete { color: ${(props) => props.theme.request.methods.delete}; }
&.method-patch { color: ${(props) => props.theme.request.methods.patch}; }
&.method-options { color: ${(props) => props.theme.request.methods.options}; }
&.method-head { color: ${(props) => props.theme.request.methods.head}; }
&.method-grpc { color: ${(props) => props.theme.request.grpc}; }
&.method-ws { color: ${(props) => props.theme.request.ws}; }
&.method-gql { color: ${(props) => props.theme.request.gql}; }
&.method-app { color: ${(props) => props.theme.brand}; }
}
.header-title {
font-size: 13px;
color: ${(props) => props.theme.text};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: flex;
align-items: center;
}
.chat-switcher-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 2px;
background: transparent;
border: none;
border-radius: 4px;
cursor: pointer;
color: ${(props) => props.theme.colors.text.muted};
flex-shrink: 0;
&:hover {
background: ${(props) => props.theme.background.surface0};
color: ${(props) => props.theme.text};
}
}
.header-actions {
display: flex;
align-items: center;
gap: 2px;
}
.history-wrap {
position: relative;
}
.icon-btn {
position: relative;
padding: 6px;
background: transparent;
border: none;
border-radius: 6px;
cursor: pointer;
color: ${(props) => props.theme.colors.text.muted};
display: flex;
align-items: center;
justify-content: center;
&:hover {
background: ${(props) => props.theme.background.surface0};
color: ${(props) => props.theme.text};
}
&.is-active {
background: ${(props) => props.theme.background.surface0};
color: ${(props) => props.theme.text};
}
&:disabled {
opacity: 0.4;
cursor: not-allowed;
}
&.close-btn:hover {
background: ${(props) => props.theme.status.danger.background};
color: ${(props) => props.theme.colors.text.danger};
}
}
}
.history-popover {
position: absolute;
top: calc(100% + 6px);
right: 0;
z-index: 20;
width: 300px;
max-height: 320px;
overflow-y: auto;
background: ${(props) => props.theme.bg};
border: 1px solid ${(props) => props.theme.border.border1};
border-radius: 8px;
box-shadow: ${(props) => props.theme.shadow.md};
padding: 4px;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
background: ${(props) => props.theme.scrollbar.color};
border-radius: 2px;
}
&__empty {
padding: 16px;
text-align: center;
font-size: 11px;
color: ${(props) => props.theme.colors.text.muted};
}
&__item {
display: flex;
align-items: stretch;
gap: 2px;
border-radius: 4px;
&:hover {
background: ${(props) => props.theme.background.surface0};
}
&.is-active {
background: ${(props) => props.theme.background.surface0};
}
}
&__title {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2px;
padding: 6px 8px;
background: transparent;
border: none;
cursor: pointer;
text-align: left;
color: ${(props) => props.theme.text};
}
&__title-text {
display: block;
width: 100%;
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__meta {
font-size: 10px;
color: ${(props) => props.theme.colors.text.muted};
}
&__delete {
display: flex;
align-items: center;
justify-content: center;
padding: 0 8px;
background: transparent;
border: none;
border-radius: 4px;
cursor: pointer;
color: ${(props) => props.theme.colors.text.muted};
&:hover {
background: ${(props) => props.theme.status.danger.background};
color: ${(props) => props.theme.colors.text.danger};
}
}
}
.ai-sidebar-messages {
flex: 1;
overflow-y: auto;
padding: 12px;
display: flex;
flex-direction: column;
gap: 12px;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: ${(props) => props.theme.scrollbar.color};
border-radius: 2px;
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 24px 16px;
animation: fadeIn 0.3s ease;
.empty-icon {
width: 40px;
height: 40px;
border-radius: 10px;
background: ${(props) => props.theme.brand};
display: flex;
align-items: center;
justify-content: center;
color: ${(props) => (props.theme.mode === 'dark' ? '#000' : '#fff')};
margin-bottom: 12px;
}
h3 {
font-size: 14px;
font-weight: 600;
margin: 0 0 4px 0;
color: ${(props) => props.theme.text};
}
> p {
color: ${(props) => props.theme.colors.text.muted};
font-size: 12px;
margin: 0 0 16px 0;
line-height: 1.4;
}
.suggestions-title {
font-size: 11px;
color: ${(props) => props.theme.colors.text.muted};
margin: 0 0 8px 0;
font-weight: 500;
}
.suggestion-chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
justify-content: center;
}
.suggestion-chip {
padding: 5px 10px;
background: ${(props) => props.theme.background.surface0};
border: 1px solid ${(props) => props.theme.border.border1};
border-radius: 12px;
font-size: 11px;
color: ${(props) => props.theme.text};
cursor: pointer;
&:hover {
border-color: ${(props) => props.theme.brand};
color: ${(props) => props.theme.brand};
}
}
}
.message {
animation: slideIn 0.25s ease;
&.user .message-content {
background: ${(props) => props.theme.background.mantle};
border: 1px solid ${(props) => props.theme.border.border1};
border-radius: 8px;
padding: 8px 12px;
font-size: 13px;
line-height: 1.4;
color: ${(props) => props.theme.text};
}
&.assistant .message-content {
color: ${(props) => props.theme.text};
}
}
.message-status {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
margin-bottom: 6px;
color: ${(props) => props.theme.colors.text.muted};
&__spinner {
width: 12px;
height: 12px;
border-radius: 50%;
border: 2px solid ${(props) => props.theme.brand};
border-top-color: transparent;
animation: spin 0.9s linear infinite;
flex-shrink: 0;
}
}
.tool-activity-log {
display: flex;
flex-direction: column;
gap: 1px;
margin: 6px 0;
padding: 4px 0;
&.completed {
opacity: 0.7;
}
}
.tool-activity-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: ${(props) => props.theme.colors.text.muted};
line-height: 1.6;
padding: 1px 0;
.tool-activity-indicator {
display: flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
flex-shrink: 0;
}
&.done .tool-activity-indicator {
color: ${(props) => props.theme.colors.text.green};
}
&.active {
color: ${(props) => props.theme.text};
.tool-activity-indicator {
color: ${(props) => props.theme.brand};
}
}
.tool-activity-spinner {
width: 10px;
height: 10px;
border-radius: 50%;
border: 1.5px solid ${(props) => props.theme.brand};
border-top-color: transparent;
animation: spin 0.9s linear infinite;
display: block;
}
}
.message-cancelled {
margin-top: 8px;
font-size: 11px;
color: ${(props) => props.theme.colors.text.muted};
}
.assistant-code-block {
border: 1px solid ${(props) => props.theme.border.border1};
border-radius: 8px;
background: ${(props) => props.theme.codemirror.bg};
overflow: hidden;
margin: 8px 0;
&__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 6px 8px;
border-bottom: 1px solid ${(props) => props.theme.border.border1};
}
&__meta {
display: flex;
align-items: center;
gap: 6px;
font-size: 10px;
color: ${(props) => props.theme.colors.text.muted};
}
&__lang {
text-transform: lowercase;
}
&__spinner {
width: 10px;
height: 10px;
border-radius: 50%;
border: 2px solid ${(props) => props.theme.brand};
border-top-color: transparent;
animation: spin 0.9s linear infinite;
flex-shrink: 0;
}
&__btn {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 6px;
border: 1px solid ${(props) => props.theme.border.border1};
border-radius: 4px;
background: ${(props) => props.theme.background.mantle};
font-size: 10px;
font-weight: 500;
color: ${(props) => props.theme.text};
cursor: pointer;
&:hover {
border-color: ${(props) => props.theme.brand};
color: ${(props) => props.theme.brand};
}
}
&__body {
margin: 0;
padding: 10px 12px;
overflow: auto;
max-height: 240px;
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
font-size: 11px;
line-height: 1.5;
white-space: pre;
}
.cursor {
display: inline-block;
animation: blink 1s infinite;
color: ${(props) => props.theme.brand};
margin-left: 1px;
}
}
.prose.markdown-body {
font-size: 13px;
line-height: 1.5;
.cursor {
display: inline-block;
animation: blink 1s infinite;
color: ${(props) => props.theme.brand};
margin-left: 1px;
}
p {
margin: 0 0 8px 0;
font-size: 13px;
&:last-child { margin-bottom: 0; }
}
h1, h2, h3, h4, h5, h6 {
margin: 10px 0 6px 0;
font-weight: 600;
line-height: 1.3;
&:first-child { margin-top: 0; }
}
h1 { font-size: 1.3em; }
h2 { font-size: 1.2em; }
h3 { font-size: 1.1em; }
ul, ol {
margin: 6px 0;
padding-left: 16px;
}
li {
margin: 4px 0;
font-size: 13px;
}
code {
background: ${(props) => props.theme.codemirror.bg};
padding: 2px 5px;
border-radius: 4px;
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
font-size: 0.9em;
}
pre, .code-block {
background: ${(props) => props.theme.codemirror.bg};
padding: 10px 12px;
border-radius: 6px;
overflow-x: auto;
margin: 8px 0;
border: 1px solid ${(props) => props.theme.border.border1};
code {
background: none;
padding: 0;
font-size: 11px;
line-height: 1.5;
}
}
blockquote {
border-left: 2px solid ${(props) => props.theme.brand};
margin: 8px 0;
padding: 4px 0 4px 10px;
color: ${(props) => props.theme.colors.text.muted};
background: ${(props) => props.theme.background.surface0};
border-radius: 0 4px 4px 0;
}
a {
color: ${(props) => props.theme.textLink};
text-decoration: none;
&:hover { text-decoration: underline; }
}
strong { font-weight: 600; }
em { font-style: italic; }
hr {
border: none;
border-top: 1px solid ${(props) => props.theme.border.border1};
margin: 10px 0;
}
table {
border-collapse: collapse;
width: 100%;
margin: 8px 0;
font-size: 12px;
}
th, td {
border: 1px solid ${(props) => props.theme.border.border1};
padding: 6px 8px;
text-align: left;
}
th {
background: ${(props) => props.theme.codemirror.bg};
font-weight: 600;
}
}
.processing-indicator {
padding: 8px 10px;
background: ${(props) => props.theme.background.surface0};
border: 1px solid ${(props) => props.theme.border.border1};
border-radius: 8px;
animation: slideIn 0.2s ease;
.processing-content {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.processing-icon {
width: 20px;
height: 20px;
border-radius: 4px;
background: ${(props) => props.theme.background.surface1};
display: flex;
align-items: center;
justify-content: center;
color: ${(props) => props.theme.brand};
}
.processing-label {
font-size: 12px;
font-weight: 500;
color: ${(props) => props.theme.text};
}
.processing-dots {
display: flex;
gap: 3px;
margin-left: 2px;
span {
width: 3px;
height: 3px;
background: ${(props) => props.theme.brand};
border-radius: 50%;
animation: dotBounce 1.4s infinite ease-in-out both;
&:nth-child(1) { animation-delay: -0.32s; }
&:nth-child(2) { animation-delay: -0.16s; }
}
}
.processing-bar {
height: 2px;
background: ${(props) => props.theme.border.border1};
border-radius: 1px;
overflow: hidden;
.processing-bar-fill {
height: 100%;
width: 30%;
background: ${(props) => props.theme.brand};
border-radius: 1px;
animation: progressSlide 1.5s infinite ease-in-out;
}
}
}
.error-message {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 8px 10px;
background: ${(props) => props.theme.status.danger.background};
border: 1px solid ${(props) => props.theme.status.danger.border};
border-radius: 6px;
.error-icon {
width: 18px;
height: 18px;
border-radius: 50%;
background: ${(props) => props.theme.colors.text.danger};
color: ${(props) => (props.theme.mode === 'dark' ? '#000' : '#fff')};
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 11px;
flex-shrink: 0;
}
.error-text {
color: ${(props) => props.theme.colors.text.danger};
font-size: 12px;
line-height: 1.4;
}
}
.ai-sidebar-input {
padding: 12px;
border-top: 1px solid ${(props) => props.theme.border.border1};
.no-models-warning {
padding: 10px 12px;
font-size: 12px;
color: ${(props) => props.theme.colors.text.muted};
background: ${(props) => props.theme.input.bg};
border: 1px dashed ${(props) => props.theme.border.border1};
border-radius: 6px;
text-align: center;
line-height: 1.4;
}
.input-container {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
background: ${(props) => props.theme.bg};
border: 1px solid ${(props) => props.theme.border.border1};
border-radius: 8px;
&:focus-within {
border-color: ${(props) => props.theme.brand};
}
}
textarea {
width: 100%;
padding: 0;
margin: 4px 0;
border: none;
background: transparent;
color: ${(props) => props.theme.text};
font-size: 13px;
font-family: inherit;
line-height: 1.4;
resize: none;
outline: none;
max-height: 100px;
&::placeholder {
color: ${(props) => props.theme.colors.text.muted};
}
&:disabled {
opacity: 0.6;
}
}
.input-actions {
display: flex;
align-items: center;
justify-content: space-between;
}
.model-selector {
position: relative;
}
.model-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 6px 4px 8px;
background: transparent;
border: 1px solid ${(props) => props.theme.border.border1};
border-radius: ${(props) => props.theme.border.radius.base};
font-size: 11px;
font-weight: 500;
color: ${(props) => props.theme.text};
cursor: pointer;
svg:first-child {
color: ${(props) => props.theme.brand};
}
&:hover {
border-color: ${(props) => props.theme.border.border2};
}
}
.send-btn, .stop-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
border: none;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
cursor: pointer;
}
.send-btn {
background: ${(props) => props.theme.brand};
color: ${(props) => (props.theme.mode === 'dark' ? '#000' : '#fff')};
&:hover {
opacity: 0.9;
}
&:disabled {
opacity: 0.4;
cursor: not-allowed;
}
}
.stop-btn {
background: ${(props) => props.theme.colors.text.danger};
color: ${(props) => (props.theme.mode === 'dark' ? '#000' : '#fff')};
&:hover {
opacity: 0.9;
}
}
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
@keyframes dotBounce {
0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
40% { transform: scale(1); opacity: 1; }
}
@keyframes progressSlide {
0% { transform: translateX(-100%); }
100% { transform: translateX(400%); }
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
`;
export default StyledWrapper;

View File

@@ -1,54 +0,0 @@
export const PROCESSING_STAGES = [
{ id: 'sending', label: 'Sending request', icon: 'send' },
{ id: 'thinking', label: 'AI is thinking', icon: 'sparkles' },
{ id: 'generating', label: 'Generating response', icon: 'wand' },
{ id: 'applying', label: 'Preparing changes', icon: 'code' }
];
export const CONTENT_TYPE_LABELS = {
'app': 'App',
'tests': 'Tests',
'pre-request': 'Script',
'post-response': 'Script',
'docs': 'Docs'
};
export const SUGGESTIONS_BY_TYPE = {
'app': [
{ label: 'Create a form for this request', prompt: 'Create a simple form to send this request' },
{ label: 'Add a loading spinner', prompt: 'Add a loading spinner while the request is pending' },
{ label: 'Show response in a table', prompt: 'Display the response data in a table' },
{ label: 'Add error handling', prompt: 'Add error handling with user-friendly messages' }
],
'tests': [
{ label: 'Generate basic tests', prompt: 'Generate tests for status code, response body, and headers' },
{ label: 'Test response structure', prompt: 'Write tests to validate the response body structure and data types' },
{ label: 'Test error cases', prompt: 'Write tests for common error scenarios' },
{ label: 'Test response time', prompt: 'Add a test to verify response time is acceptable' }
],
'pre-request': [
{ label: 'Add authentication', prompt: 'Add authorization header from environment variable' },
{ label: 'Set dynamic variables', prompt: 'Set dynamic request variables like timestamp or unique ID' },
{ label: 'Conditional logic', prompt: 'Add conditional logic to modify the request based on environment' }
],
'post-response': [
{ label: 'Extract to variables', prompt: 'Extract data from response and save to environment variables' },
{ label: 'Store auth token', prompt: 'Extract auth token from response and save for future requests' },
{ label: 'Log response', prompt: 'Log response status and body for debugging' },
{ label: 'Transform response', prompt: 'Transform and process the response data' }
],
'docs': [
{ label: 'Generate full docs', prompt: 'Generate comprehensive API documentation for this endpoint' },
{ label: 'Document parameters', prompt: 'Document all request parameters, headers, and body' },
{ label: 'Add examples', prompt: 'Add request and response examples' },
{ label: 'Document errors', prompt: 'Document common error responses and status codes' }
]
};
export const PLACEHOLDER_BY_TYPE = {
'tests': { empty: 'Describe the tests you want...', filled: 'Ask to modify or add tests...' },
'pre-request': { empty: 'Describe the script you want...', filled: 'Ask to modify the script...' },
'post-response': { empty: 'Describe the script you want...', filled: 'Ask to modify the script...' },
'docs': { empty: 'Describe the documentation...', filled: 'Ask to update the docs...' },
'app': { empty: 'Describe the app you want to create...', filled: 'Ask to modify your app...' }
};

View File

@@ -1,864 +0,0 @@
import React, { useState, useRef, useEffect, useCallback, useMemo, forwardRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
IconX,
IconPlayerStop,
IconCheck,
IconCode,
IconWand,
IconStars,
IconCornerDownLeft,
IconChevronDown,
IconHistory,
IconPlus,
IconTrash
} from '@tabler/icons';
import get from 'lodash/get';
import find from 'lodash/find';
import MenuDropdown from 'ui/MenuDropdown';
import { focusTab } from 'providers/ReduxStore/slices/tabs';
import {
closeAiSidebar,
sendAiMessage,
stopAiStream,
setChatBinding,
startNewConversation,
refreshChatHistory,
openConversation,
removeConversation,
setMessageCodeStatus
} from 'providers/ReduxStore/slices/chat';
import {
updateAppCode,
updateRequestTests,
updateRequestScript,
updateResponseScript,
updateRequestDocs,
updateFolderRequestScript,
updateFolderResponseScript,
updateFolderTests,
updateFolderDocs,
updateCollectionRequestScript,
updateCollectionResponseScript,
updateCollectionTests,
updateCollectionDocs
} from 'providers/ReduxStore/slices/collections';
import { findItemInCollection, isItemAFolder, isItemARequest } from 'utils/collections';
import { buildAiVariablesPayload, getAiStatus } from 'utils/ai';
import StyledWrapper from './StyledWrapper';
import DiffView from './DiffView';
import AssistantCodeBlock from './AssistantCodeBlock';
import { PROCESSING_STAGES, CONTENT_TYPE_LABELS, SUGGESTIONS_BY_TYPE, PLACEHOLDER_BY_TYPE } from './constants';
import { renderMarkdown, parseMessageSegments } from './utils';
const SELECTED_MODEL_LS_KEY = 'bruno.ai.chat.selectedModel';
const AUTO_MODEL_ID = '';
const ToolActivityGroup = ({ activities }) => {
if (!activities?.length) return null;
const allDone = activities.every((a) => a.done);
return (
<div className={`tool-activity-log ${allDone ? 'completed' : ''}`}>
{activities.map((activity, i) => (
<div key={i} className={`tool-activity-item ${activity.done ? 'done' : 'active'}`}>
<span className="tool-activity-indicator">
{activity.done ? <IconCheck size={10} /> : <span className="tool-activity-spinner" />}
</span>
<span>{activity.label}{!activity.done ? '…' : ''}</span>
</div>
))}
</div>
);
};
const buildMessageTimeline = (cleanedContent, activities) => {
if (!activities?.length) {
return cleanedContent ? [{ type: 'text', content: cleanedContent }] : [];
}
if (!cleanedContent) return [{ type: 'tools', activities }];
const groups = [];
for (const activity of activities) {
const offset = Math.min(activity.textOffset || 0, cleanedContent.length);
const last = groups[groups.length - 1];
if (last && last.offset === offset) last.activities.push(activity);
else groups.push({ offset, activities: [activity] });
}
const parts = [];
let cursor = 0;
for (const group of groups) {
if (group.offset > cursor) {
parts.push({ type: 'text', content: cleanedContent.substring(cursor, group.offset) });
}
parts.push({ type: 'tools', activities: group.activities });
cursor = Math.max(cursor, group.offset);
}
if (cursor < cleanedContent.length) {
parts.push({ type: 'text', content: cleanedContent.substring(cursor) });
}
return parts;
};
const formatRelativeTime = (timestamp) => {
if (!timestamp) return '';
const diff = Date.now() - timestamp;
const minute = 60 * 1000;
const hour = 60 * minute;
const day = 24 * hour;
if (diff < minute) return 'just now';
if (diff < hour) return `${Math.floor(diff / minute)}m ago`;
if (diff < day) return `${Math.floor(diff / hour)}h ago`;
if (diff < 7 * day) return `${Math.floor(diff / day)}d ago`;
return new Date(timestamp).toLocaleDateString();
};
const HistoryPopover = ({ items, activeId, onPick, onDelete, onClose }) => {
const popoverRef = useRef(null);
useEffect(() => {
const handleClick = (e) => {
if (popoverRef.current && !popoverRef.current.contains(e.target)) {
onClose();
}
};
const handleKey = (e) => {
if (e.key === 'Escape') onClose();
};
document.addEventListener('mousedown', handleClick);
document.addEventListener('keydown', handleKey);
return () => {
document.removeEventListener('mousedown', handleClick);
document.removeEventListener('keydown', handleKey);
};
}, [onClose]);
return (
<div className="history-popover" ref={popoverRef} role="menu">
{items.length === 0 ? (
<div className="history-popover__empty">No past conversations</div>
) : (
items.map((item) => (
<div
key={item.id}
className={`history-popover__item ${item.id === activeId ? 'is-active' : ''}`}
role="menuitem"
>
<button className="history-popover__title" onClick={() => onPick(item.id)} title={item.title}>
<span className="history-popover__title-text">{item.title || '(untitled)'}</span>
<span className="history-popover__meta">{formatRelativeTime(item.updatedAt)}</span>
</button>
<button
className="history-popover__delete"
onClick={(e) => {
e.stopPropagation(); onDelete(item.id);
}}
title="Delete conversation"
aria-label="Delete conversation"
>
<IconTrash size={12} />
</button>
</div>
))
)}
</div>
);
};
const AiChatSidebar = ({ collection }) => {
const dispatch = useDispatch();
const [input, setInput] = useState('');
const [processingStage, setProcessingStage] = useState(null);
const [availableModels, setAvailableModels] = useState([]);
const [selectedModel, setSelectedModel] = useState(() => {
try { return localStorage.getItem(SELECTED_MODEL_LS_KEY) ?? AUTO_MODEL_ID; } catch { return AUTO_MODEL_ID; }
});
const [historyOpen, setHistoryOpen] = useState(false);
const messagesEndRef = useRef(null);
const messagesContainerRef = useRef(null);
const isNearBottomRef = useRef(true);
const textareaRef = useRef(null);
const isOpen = useSelector((state) => state.chat.isOpen);
const allChats = useSelector((state) => state.chat.chats);
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const preferences = useSelector((state) => state.app.preferences);
const aiEnabled = get(preferences, 'ai.enabled', false);
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
const activeItem = focusedTab && collection ? findItemInCollection(collection, activeTabUid) : null;
const aiContext = useMemo(() => {
if (!focusedTab || !collection) return null;
if (activeItem && (isItemARequest(activeItem) || activeItem.type === 'app')) {
return { kind: 'request', item: activeItem, pathname: activeItem.pathname || '', name: activeItem.name || 'Untitled' };
}
if (activeItem && isItemAFolder(activeItem)) {
return { kind: 'folder', folder: activeItem, pathname: activeItem.pathname || '', name: activeItem.name || 'Untitled' };
}
// Anything else (collection-settings, runner, variables, openapi-sync,
// .js files in File Mode …) falls back to the collection root so the AI
// button always opens a useful chat instead of a no-op.
return { kind: 'collection', pathname: collection.pathname || '', name: collection.name || 'Untitled Collection' };
}, [focusedTab, collection, activeItem]);
const currentChat = allChats[activeTabUid] || { messages: [], isLoading: false, error: null, historyList: [] };
const { messages, isLoading, error, historyList, conversationId } = currentChat;
useEffect(() => {
if (!isOpen || !aiEnabled) return;
let cancelled = false;
getAiStatus()
.then((status) => {
if (cancelled) return;
setAvailableModels(status?.availableModels || []);
})
.catch(() => {
if (!cancelled) setAvailableModels([]);
});
return () => { cancelled = true; };
}, [isOpen, aiEnabled, preferences?.ai]);
// Auto = empty string. We don't auto-correct to the first model — let the
// backend pick, so users get smart defaults that adapt as providers change.
useEffect(() => {
if (selectedModel === AUTO_MODEL_ID) return;
if (availableModels.length === 0) return;
if (availableModels.some((m) => m.id === selectedModel)) return;
setSelectedModel(AUTO_MODEL_ID);
try { localStorage.setItem(SELECTED_MODEL_LS_KEY, AUTO_MODEL_ID); } catch {}
}, [availableModels, selectedModel]);
const requestName = aiContext?.name || activeItem?.name || 'Untitled';
const requestMethod = useMemo(() => {
if (aiContext?.kind === 'folder') return 'FOLDER';
if (aiContext?.kind === 'collection') return 'ROOT';
if (!activeItem) return 'GET';
if (activeItem.type === 'grpc-request') return 'GRPC';
if (activeItem.type === 'ws-request') return 'WS';
if (activeItem.type === 'graphql-request') return 'GQL';
if (activeItem.type === 'app') return 'APP';
const appOn = activeItem.draft
? get(activeItem, 'draft.app.enabled', false)
: get(activeItem, 'app.enabled', false);
if (appOn) return 'APP';
return activeItem.draft
? get(activeItem, 'draft.request.method', 'GET')
: get(activeItem, 'request.method', 'GET');
}, [aiContext?.kind, activeItem]);
// contentType drives the AI prompt, the diff target, and which entry of
// allContent the backend treats as "active". For requests it follows the
// request-pane tab. For folders / collections we read the settings sub-tab
// (and the inner pre/post script split for the Script sub-tab).
const requestPaneTab = focusedTab?.requestPaneTab;
const scriptPaneTab = focusedTab?.scriptPaneTab;
const contentType = useMemo(() => {
if (aiContext?.kind === 'folder') {
const sub = collection?.folderLevelSettingsSelectedTab?.[aiContext.folder.uid];
if (sub === 'test') return 'tests';
if (sub === 'docs') return 'docs';
if (sub === 'script') return scriptPaneTab === 'post-response' ? 'post-response' : 'pre-request';
return 'pre-request';
}
if (aiContext?.kind === 'collection') {
const sub = collection?.settingsSelectedTab;
if (sub === 'tests') return 'tests';
if (sub === 'overview') return 'docs';
if (sub === 'script') return scriptPaneTab === 'post-response' ? 'post-response' : 'pre-request';
return 'pre-request';
}
switch (requestPaneTab) {
case 'tests': return 'tests';
case 'script': return scriptPaneTab === 'post-response' ? 'post-response' : 'pre-request';
case 'docs': return 'docs';
default: return 'app';
}
}, [aiContext, collection?.folderLevelSettingsSelectedTab, collection?.settingsSelectedTab, requestPaneTab, scriptPaneTab]);
// Bind the chat to the active context's pathname so the history list
// reflects this specific request/folder/collection and persistence keys stay
// stable across sessions. Restoring the most recent conversation happens
// once per tab — if the user explicitly starts a new chat, we don't
// auto-replace it.
const restoredOnceRef = useRef({});
useEffect(() => {
if (!isOpen || !aiContext || !collection) return;
dispatch(setChatBinding({
tabUid: activeTabUid,
pathname: aiContext.pathname,
collectionUid: collection.uid,
contentType
}));
dispatch(refreshChatHistory(activeTabUid));
}, [isOpen, aiContext?.pathname, collection?.uid, activeTabUid, contentType, dispatch]);
// First-open restore: if this tab has no conversation yet and there's a
// saved one for the same file, load the most recent.
useEffect(() => {
if (!isOpen || !activeTabUid) return;
if (restoredOnceRef.current[activeTabUid]) return;
if (currentChat.conversationId) return;
if (currentChat.messages?.length > 0) return;
if (!historyList || historyList.length === 0) return;
restoredOnceRef.current[activeTabUid] = true;
dispatch(openConversation(activeTabUid, historyList[0].id));
}, [isOpen, activeTabUid, currentChat.conversationId, currentChat.messages?.length, historyList, dispatch]);
const allContent = useMemo(() => {
if (!aiContext) return {};
if (aiContext.kind === 'request') {
const item = aiContext.item;
const draft = item.draft;
const draftAppCode = get(item, 'draft.app.code');
return {
'app': draftAppCode != null ? draftAppCode : get(item, 'app.code', ''),
'tests': draft ? get(draft, 'request.tests', '') : get(item, 'request.tests', ''),
'pre-request': draft ? get(draft, 'request.script.req', '') : get(item, 'request.script.req', ''),
'post-response': draft ? get(draft, 'request.script.res', '') : get(item, 'request.script.res', ''),
'docs': draft ? get(draft, 'request.docs', '') : get(item, 'request.docs', '')
};
}
if (aiContext.kind === 'folder') {
const folder = aiContext.folder;
const root = folder.draft || folder.root || {};
return {
'tests': get(root, 'request.tests', ''),
'pre-request': get(root, 'request.script.req', ''),
'post-response': get(root, 'request.script.res', ''),
'docs': get(root, 'docs', '')
};
}
// collection
const root = collection?.draft?.root || collection?.root || {};
return {
'tests': get(root, 'request.tests', ''),
'pre-request': get(root, 'request.script.req', ''),
'post-response': get(root, 'request.script.res', ''),
'docs': get(root, 'docs', '')
};
}, [aiContext, collection?.draft?.root, collection?.root]);
const currentContent = allContent[contentType] || '';
// requestContext (URL/method/headers/response shape) only makes sense for
// HTTP-style request items. Folder, collection, and App chats skip it —
// App items live under kind: 'request' but have no URL/method to surface.
const requestContext = useMemo(() => {
if (aiContext?.kind !== 'request' || !isItemARequest(aiContext.item)) return null;
const item = aiContext.item;
const draft = item.draft;
return {
url: draft ? get(item, 'draft.request.url', '') : get(item, 'request.url', ''),
method: draft ? get(item, 'draft.request.method', '') : get(item, 'request.method', ''),
headers: draft ? get(item, 'draft.request.headers', []) : get(item, 'request.headers', []),
params: draft ? get(item, 'draft.request.params', []) : get(item, 'request.params', []),
body: draft ? get(item, 'draft.request.body', null) : get(item, 'request.body', null),
docs: draft ? get(item, 'draft.request.docs', null) : get(item, 'request.docs', null),
responseStatus: get(item, 'response.status', null),
responseData: get(item, 'response.data', null)
};
}, [aiContext]);
// Variables payload is collection-scoped — works for request, folder, and
// collection chats alike. Each entry is { name, value, scope, secret }; the
// model gets a name-only preview in the prompt and can call search_variables
// to fetch values (secrets come back redacted).
const aiVariables = useMemo(() => {
if (aiContext?.kind === 'request') return buildAiVariablesPayload(collection, aiContext.item);
if (aiContext?.kind === 'folder') return buildAiVariablesPayload(collection, aiContext.folder);
return buildAiVariablesPayload(collection, null);
}, [collection, aiContext]);
const chatsWithMessages = useMemo(() => {
if (!collection) return [];
return Object.entries(allChats)
.filter(([, chat]) => chat.messages?.length > 0)
.map(([tabUid, chat]) => {
if (tabUid === collection.uid) {
return { id: tabUid, name: collection.name || 'Untitled Collection', method: 'ROOT', messageCount: chat.messages.length };
}
const item = findItemInCollection(collection, tabUid);
if (!item) return null;
if (isItemAFolder(item)) {
return { id: tabUid, name: item.name || 'Untitled', method: 'FOLDER', messageCount: chat.messages.length };
}
const method = item.draft
? get(item, 'draft.request.method', 'GET')
: get(item, 'request.method', 'GET');
return {
id: tabUid,
name: item.name || 'Untitled',
method,
messageCount: chat.messages.length
};
})
.filter(Boolean);
}, [allChats, collection]);
const scrollToBottom = useCallback((behavior = 'smooth') => {
messagesEndRef.current?.scrollIntoView({ behavior });
}, []);
const handleMessagesScroll = useCallback(() => {
const el = messagesContainerRef.current;
if (!el) return;
isNearBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
}, []);
useEffect(() => {
if (!isNearBottomRef.current) return;
const behavior = messages.some((m) => m.isStreaming) ? 'auto' : 'smooth';
scrollToBottom(behavior);
}, [messages, scrollToBottom]);
useEffect(() => {
if (isOpen) textareaRef.current?.focus();
}, [isOpen]);
useEffect(() => {
if (!isLoading) {
setProcessingStage(null);
return;
}
const last = messages[messages.length - 1];
if (last?.isStreaming && last.content) setProcessingStage('generating');
else if (last?.isStreaming) setProcessingStage('thinking');
else setProcessingStage('sending');
}, [isLoading, messages]);
const handleTextareaChange = (e) => {
setInput(e.target.value);
const el = textareaRef.current;
if (el) {
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 150) + 'px';
}
};
const handleSubmit = async (e) => {
e?.preventDefault();
if (!input.trim() || isLoading || availableModels.length === 0) return;
const text = input.trim();
setInput('');
setProcessingStage('sending');
if (textareaRef.current) textareaRef.current.style.height = 'auto';
try {
await dispatch(sendAiMessage(activeTabUid, text, allContent, requestContext, selectedModel, contentType, aiVariables));
setProcessingStage('applying');
setTimeout(() => setProcessingStage(null), 500);
} catch (err) {
console.error('Failed to send AI message:', err);
setProcessingStage(null);
}
};
const handleKeyDown = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSubmit();
}
};
const handleStop = () => {
dispatch(stopAiStream(activeTabUid));
setProcessingStage(null);
};
const handleApplyCode = (code, originalCode, messageIndex, msgContentType, writeIndex) => {
if (!aiContext || code == null) return;
const targetType = msgContentType || contentType;
// Bail if the live buffer has drifted from what the AI based the diff on.
// The DiffView already disables the button in this case, but guarding here
// too means the keyboard / programmatic path can't blow away local edits.
const liveContent = allContent[targetType] || '';
if (originalCode != null && liveContent !== originalCode) {
return;
}
if (aiContext.kind === 'request') {
const payload = { itemUid: aiContext.item.uid, collectionUid: collection.uid };
switch (targetType) {
case 'tests': dispatch(updateRequestTests({ ...payload, tests: code })); break;
case 'pre-request': dispatch(updateRequestScript({ ...payload, script: code })); break;
case 'post-response': dispatch(updateResponseScript({ ...payload, script: code })); break;
case 'docs': dispatch(updateRequestDocs({ ...payload, docs: code })); break;
default: dispatch(updateAppCode({ ...payload, code })); break;
}
} else if (aiContext.kind === 'folder') {
const payload = { folderUid: aiContext.folder.uid, collectionUid: collection.uid };
switch (targetType) {
case 'tests': dispatch(updateFolderTests({ ...payload, tests: code })); break;
case 'pre-request': dispatch(updateFolderRequestScript({ ...payload, script: code })); break;
case 'post-response': dispatch(updateFolderResponseScript({ ...payload, script: code })); break;
case 'docs': dispatch(updateFolderDocs({ ...payload, docs: code })); break;
// Folders / collections have no 'app' equivalent. Bail rather than
// marking the diff accepted when nothing was dispatched.
default: return;
}
} else {
const payload = { collectionUid: collection.uid };
switch (targetType) {
case 'tests': dispatch(updateCollectionTests({ ...payload, tests: code })); break;
case 'pre-request': dispatch(updateCollectionRequestScript({ ...payload, script: code })); break;
case 'post-response': dispatch(updateCollectionResponseScript({ ...payload, script: code })); break;
case 'docs': dispatch(updateCollectionDocs({ ...payload, docs: code })); break;
default: return;
}
}
dispatch(setMessageCodeStatus({
tabUid: activeTabUid,
messageIndex,
status: 'accepted',
writeIndex
}));
};
const handleRejectCode = (messageIndex, writeIndex) => {
dispatch(setMessageCodeStatus({
tabUid: activeTabUid,
messageIndex,
status: 'rejected',
writeIndex
}));
};
const handleNewChat = () => {
setHistoryOpen(false);
restoredOnceRef.current[activeTabUid] = true; // suppress restore
dispatch(startNewConversation({ tabUid: activeTabUid, contentType }));
textareaRef.current?.focus();
};
const handlePickConversation = (id) => {
setHistoryOpen(false);
restoredOnceRef.current[activeTabUid] = true;
dispatch(openConversation(activeTabUid, id));
};
const handleDeleteConversation = (id) => {
dispatch(removeConversation(activeTabUid, id));
};
const handleClose = () => dispatch(closeAiSidebar());
const handleSwitchChat = (tabUid) => dispatch(focusTab({ uid: tabUid }));
const handleSuggestionClick = (suggestion) => {
setInput(suggestion);
textareaRef.current?.focus();
};
const handleModelSelect = (modelId) => {
setSelectedModel(modelId);
try { localStorage.setItem(SELECTED_MODEL_LS_KEY, modelId); } catch {}
};
const selectedModelLabel = useMemo(() => {
if (selectedModel === AUTO_MODEL_ID) return 'Auto';
return availableModels.find((m) => m.id === selectedModel)?.label || 'Auto';
}, [availableModels, selectedModel]);
const ModelSelectorTrigger = forwardRef((props, ref) => (
<div ref={ref} className="model-btn" {...props}>
<IconStars size={14} strokeWidth={1.75} />
<span>{selectedModelLabel}</span>
<IconChevronDown size={12} />
</div>
));
ModelSelectorTrigger.displayName = 'ModelSelectorTrigger';
const modelMenuItems = useMemo(
() => [
{ id: AUTO_MODEL_ID, label: 'Auto', onClick: () => handleModelSelect(AUTO_MODEL_ID) },
...availableModels.map((model) => ({
id: model.id,
label: model.label,
onClick: () => handleModelSelect(model.id)
}))
],
[availableModels]
);
const hasActiveStream = messages.some((m) => m.isStreaming);
const renderProcessingIndicator = () => {
if (!processingStage || processingStage === 'thinking' || hasActiveStream) return null;
const stage = PROCESSING_STAGES.find((s) => s.id === processingStage) || PROCESSING_STAGES[0];
return (
<div className="processing-indicator">
<div className="processing-content">
<div className="processing-icon">
{stage.icon === 'sparkles' && <IconStars size={12} />}
{stage.icon === 'wand' && <IconWand size={12} />}
{stage.icon === 'code' && <IconCode size={12} />}
{stage.icon === 'send' && <IconCornerDownLeft size={12} />}
</div>
<span className="processing-label">{stage.label}</span>
<div className="processing-dots"><span></span><span></span><span></span></div>
</div>
<div className="processing-bar"><div className="processing-bar-fill"></div></div>
</div>
);
};
const renderMessage = (msg, index) => {
const isUser = msg.role === 'user';
const isStreaming = msg.isStreaming;
const activities = msg.toolActivity || [];
const hasPendingTool = activities.some((a) => !a.done);
const content = msg.content || '';
const showThinking = isStreaming && !content && activities.length === 0;
const showWorking = isStreaming && activities.length > 0 && !hasPendingTool;
const timeline = buildMessageTimeline(content, activities);
return (
<div key={index} className={`message ${msg.role} ${isStreaming ? 'streaming' : ''}`}>
<div className="message-content">
{isUser ? content : (
<>
{showThinking && (
<div className="message-status">
<span className="message-status__spinner" />
<span>Thinking</span>
</div>
)}
{timeline.map((part, partIndex) => {
if (part.type === 'tools') {
return <ToolActivityGroup key={`tools-${partIndex}`} activities={part.activities} />;
}
const segments = parseMessageSegments(part.content);
const isLastTextPart = !timeline.slice(partIndex + 1).some((p) => p.type === 'text');
return (
<React.Fragment key={`text-${partIndex}`}>
{segments.map((segment, segIndex) => {
const isLastSegment = isLastTextPart && segIndex === segments.length - 1;
if (segment.type === 'code') {
return (
<AssistantCodeBlock
key={`p${partIndex}-s${segIndex}`}
content={segment.content}
language={segment.language}
isOpen={segment.isOpen}
isStreaming={isStreaming}
isLast={isLastSegment}
/>
);
}
return (
<div key={`p${partIndex}-s${segIndex}`} className="prose markdown-body">
<div dangerouslySetInnerHTML={{ __html: renderMarkdown(segment.content) }} />
{isStreaming && isLastSegment && <span className="cursor">|</span>}
</div>
);
})}
</React.Fragment>
);
})}
{showWorking && (
<div className="message-status">
<span className="message-status__spinner" />
<span>Working</span>
</div>
)}
{!isStreaming && msg.writes?.length > 0 && msg.writes.map((write, writeIdx) => {
if (write.content === write.originalContent) return null;
const liveContent = allContent[write.type] || '';
const isStale = liveContent !== write.originalContent;
const notRead = !write.wasRead;
return (
<DiffView
key={`write-${writeIdx}`}
originalCode={write.originalContent || ''}
newCode={write.content}
contentTypeLabel={CONTENT_TYPE_LABELS[write.type] || write.type}
warning={
notRead ? 'Content was not read first — changes may overwrite unrelated edits'
: isStale ? 'Content has been modified since AI read it'
: null
}
disableAccept={isStale || notRead}
onAccept={() => handleApplyCode(write.content, write.originalContent, index, write.type, writeIdx)}
onReject={() => handleRejectCode(index, writeIdx)}
status={write.status}
/>
);
})}
{!isStreaming && !msg.writes && msg.code && msg.originalCode && msg.code !== msg.originalCode && (
<DiffView
originalCode={msg.originalCode || ''}
newCode={msg.code}
onAccept={() => handleApplyCode(msg.code, msg.originalCode, index, msg.contentType)}
onReject={() => handleRejectCode(index)}
status={msg.codeStatus}
/>
)}
{!isStreaming && msg.cancelled && (
<div className="message-cancelled"><em>Cancelled</em></div>
)}
</>
)}
</div>
</div>
);
};
const renderEmptyState = () => {
const suggestions = SUGGESTIONS_BY_TYPE[contentType] || SUGGESTIONS_BY_TYPE.app;
return (
<div className="empty-state">
<div className="empty-icon"><IconStars size={20} /></div>
<h3>AI Assistant</h3>
<p>Ask me to generate or modify code, tests, scripts, and docs.</p>
<div className="suggestions">
<p className="suggestions-title">Try asking:</p>
<div className="suggestion-chips">
{suggestions.map((s, i) => (
<button key={i} className="suggestion-chip" onClick={() => handleSuggestionClick(s.prompt)}>
{s.label}
</button>
))}
</div>
</div>
</div>
);
};
if (!isOpen) return null;
if (!aiContext) return null;
const placeholders = PLACEHOLDER_BY_TYPE[contentType] || PLACEHOLDER_BY_TYPE.app;
const placeholder = currentContent ? placeholders.filled : placeholders.empty;
const historyCount = historyList?.length || 0;
return (
<StyledWrapper>
<div className="ai-sidebar">
<div className="ai-sidebar-header">
<div className="header-left">
<IconStars size={18} className="header-icon" />
<span className={`header-method method-${(requestMethod || 'get').toLowerCase()}`}>{requestMethod}</span>
<span className="header-title">{requestName}</span>
{chatsWithMessages.length > 1 && (
<MenuDropdown
items={chatsWithMessages.map((chat) => ({
id: chat.id,
label: `${chat.method} · ${chat.name}`,
onClick: () => handleSwitchChat(chat.id)
}))}
placement="bottom-start"
selectedItemId={activeTabUid}
>
<button className="chat-switcher-btn" title="Switch chat">
<IconChevronDown size={14} />
</button>
</MenuDropdown>
)}
</div>
<div className="header-actions">
<button
className="icon-btn"
onClick={handleNewChat}
title="New chat"
disabled={isLoading || messages.length === 0}
>
<IconPlus size={14} />
</button>
<div className="history-wrap">
<button
className={`icon-btn ${historyOpen ? 'is-active' : ''}`}
onClick={() => setHistoryOpen((v) => !v)}
title="History"
disabled={historyCount === 0}
>
<IconHistory size={14} />
</button>
{historyOpen && (
<HistoryPopover
items={historyList || []}
activeId={conversationId}
onPick={handlePickConversation}
onDelete={handleDeleteConversation}
onClose={() => setHistoryOpen(false)}
/>
)}
</div>
<button className="icon-btn close-btn" onClick={handleClose} title="Close">
<IconX size={14} />
</button>
</div>
</div>
<div className="ai-sidebar-messages" ref={messagesContainerRef} onScroll={handleMessagesScroll}>
{messages.length === 0 ? renderEmptyState() : (
<>
{messages.map(renderMessage)}
{renderProcessingIndicator()}
</>
)}
{error && (
<div className="error-message">
<div className="error-icon">!</div>
<div className="error-text">{error}</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
<div className="ai-sidebar-input">
{availableModels.length === 0 ? (
<div className="no-models-warning">
No AI models available. Configure a provider and enable models in Preferences &gt; AI.
</div>
) : (
<div className="input-container">
<textarea
ref={textareaRef}
value={input}
onChange={handleTextareaChange}
onKeyDown={handleKeyDown}
placeholder={placeholder}
disabled={isLoading}
rows={1}
/>
<div className="input-actions">
<div className="model-selector">
<MenuDropdown items={modelMenuItems} placement="top-start" selectedItemId={selectedModel}>
<ModelSelectorTrigger />
</MenuDropdown>
</div>
{isLoading ? (
<button className="stop-btn" onClick={handleStop} title="Stop generating">
<IconPlayerStop size={12} /> Stop
</button>
) : (
<button
className="send-btn"
onClick={handleSubmit}
title="Send (Enter)"
disabled={!input.trim()}
>
Send <IconCornerDownLeft size={12} />
</button>
)}
</div>
</div>
)}
</div>
</div>
</StyledWrapper>
);
};
export default AiChatSidebar;

View File

@@ -1,63 +0,0 @@
import MarkdownIt from 'markdown-it';
const SAFE_LANG = /^[a-z0-9_+#.-]+$/i;
const safeLanguage = (lang) => (lang && SAFE_LANG.test(lang) ? lang : 'text');
const md = new MarkdownIt({
html: false,
breaks: true,
linkify: true,
highlight: (str, lang) =>
`<pre class="code-block"><code class="language-${safeLanguage(lang)}">${md.utils.escapeHtml(str)}</code></pre>`
});
export const renderMarkdown = (content) => md.render(content || '');
export const parseMessageSegments = (content = '') => {
if (!content) return [];
const segments = [];
let cursor = 0;
let inCode = false;
let language = '';
while (cursor <= content.length) {
const fenceIndex = content.indexOf('```', cursor);
if (fenceIndex === -1) {
const chunk = content.slice(cursor);
if (inCode || chunk) {
segments.push({
type: inCode ? 'code' : 'text',
content: chunk,
language,
isOpen: inCode
});
}
break;
}
if (!inCode) {
const textChunk = content.slice(cursor, fenceIndex);
if (textChunk) {
segments.push({ type: 'text', content: textChunk });
}
const fenceEnd = fenceIndex + 3;
const lineEnd = content.indexOf('\n', fenceEnd);
language = (lineEnd === -1 ? content.slice(fenceEnd) : content.slice(fenceEnd, lineEnd)).trim();
inCode = true;
cursor = lineEnd === -1 ? content.length : lineEnd + 1;
} else {
const codeChunk = content.slice(cursor, fenceIndex);
if (codeChunk.trim()) {
segments.push({ type: 'code', content: codeChunk, language, isOpen: false });
}
inCode = false;
language = '';
cursor = fenceIndex + 3;
if (content[cursor] === '\n') cursor += 1;
}
}
return segments.filter((seg) => seg.content && seg.content.trim());
};

View File

@@ -1,129 +0,0 @@
const yamlPlugin = (cm) => {
cm.defineMode('yaml', function () {
var cons = ['true', 'false', 'on', 'off', 'yes', 'no'];
var keywordRegex = new RegExp('\\b((' + cons.join(')|(') + '))$', 'i');
return {
token: function (stream, state) {
var ch = stream.peek();
var esc = state.escaped;
state.escaped = false;
/* comments */
if (ch == '#' && (stream.pos == 0 || /\s/.test(stream.string.charAt(stream.pos - 1)))) {
stream.skipToEnd();
return 'comment';
}
if (stream.match(/^('([^']|\\.)*'?|"([^"]|\\.)*"?)/)) return 'string';
if (state.literal && stream.indentation() > state.keyCol) {
stream.skipToEnd();
return 'string';
} else if (state.literal) {
state.literal = false;
}
if (stream.sol()) {
state.keyCol = 0;
state.pair = false;
state.pairStart = false;
/* document start */
if (stream.match('---')) {
return 'def';
}
/* document end */
if (stream.match('...')) {
return 'def';
}
/* array list item */
if (stream.match(/\s*-\s+/)) {
return 'meta';
}
}
/* inline pairs/lists */
if (stream.match(/^(\{|\}|\[|\])/)) {
if (ch == '{') state.inlinePairs++;
else if (ch == '}') state.inlinePairs--;
else if (ch == '[') state.inlineList++;
else state.inlineList--;
return 'meta';
}
/* list separator */
if (state.inlineList > 0 && !esc && ch == ',') {
stream.next();
return 'meta';
}
/* pairs separator */
if (state.inlinePairs > 0 && !esc && ch == ',') {
state.keyCol = 0;
state.pair = false;
state.pairStart = false;
stream.next();
return 'meta';
}
/* start of value of a pair */
if (state.pairStart) {
/* block literals */
if (stream.match(/^\s*(\||\>)\s*/)) {
state.literal = true;
return 'meta';
}
/* references */
if (stream.match(/^\s*(\&|\*)[a-z0-9\._-]+\b/i)) {
return 'variable-2';
}
/* numbers */
if (state.inlinePairs == 0 && stream.match(/^\s*-?[0-9\.\,]+\s?$/)) {
return 'number';
}
if (state.inlinePairs > 0 && stream.match(/^\s*-?[0-9\.\,]+\s?(?=(,|}))/)) {
return 'number';
}
/* keywords */
if (stream.match(keywordRegex)) {
return 'keyword';
}
}
/* pairs (associative arrays) -> key */
if (
!state.pair
&& stream.match(/^\s*(?:[,\[\]{}&*!|>'"%@`][^\s'":]|[^\s,\[\]{}#&*!|>'"%@`])[^#:]*(?=:($|\s))/)
) {
state.pair = true;
state.keyCol = stream.indentation();
return 'atom';
}
if (state.pair && stream.match(/^:\s*/)) {
state.pairStart = true;
return 'meta';
}
/* nothing found, continue */
state.pairStart = false;
state.escaped = ch == '\\';
stream.next();
return null;
},
startState: function () {
return {
pair: false,
pairStart: false,
keyCol: 0,
inlinePairs: 0,
inlineList: 0,
literal: false,
escaped: false
};
},
lineComment: '#',
fold: 'indent'
};
});
cm.defineMIME('text/x-yaml', 'yaml');
cm.defineMIME('text/yaml', 'yaml');
};
export default yamlPlugin;

View File

@@ -1,77 +0,0 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.CodeMirror {
height: calc(100vh - 9rem);
background: ${(props) => props.theme.codemirror.bg};
border: solid 1px ${(props) => props.theme.codemirror.border};
font-family: ${(props) => (props.font ? props.font : 'default')};
font-size: ${(props) => props.theme.font.size.base};
line-break: anywhere;
}
.CodeMirror-dialog {
overflow: visible;
input {
background: transparent;
border: 1px solid #d3d6db;
outline: none;
border-radius: 0px;
}
}
.CodeMirror-overlayscroll-horizontal div,
.CodeMirror-overlayscroll-vertical div {
background: #d2d7db;
}
textarea.cm-editor {
position: relative;
}
// Todo: dark mode temporary fix
// Clean this
.CodeMirror.cm-s-monokai {
.CodeMirror-overlayscroll-horizontal div,
.CodeMirror-overlayscroll-vertical div {
background: #444444;
}
}
.cm-s-monokai span.cm-property,
.cm-s-monokai span.cm-attribute {
color: #9cdcfe !important;
}
.cm-s-monokai span.cm-string {
color: #ce9178 !important;
}
.cm-s-monokai span.cm-number {
color: #b5cea8 !important;
}
.cm-s-monokai span.cm-atom {
color: #569cd6 !important;
}
.cm-variable-valid {
color: ${(props) => props.theme.codemirror.variable.valid};
}
.cm-variable-invalid {
color: ${(props) => props.theme.codemirror.variable.invalid};
}
.CodeMirror-matchingbracket {
background: ${(props) => props.theme.status.success.background} !important;
text-decoration: unset;
}
.CodeMirror-nonmatchingbracket {
color: ${(props) => props.theme.colors.text.danger} !important;
background: ${(props) => props.theme.status.danger.background} !important;
text-decoration: unset;
}
`;
export default StyledWrapper;

View File

@@ -1,128 +0,0 @@
/**
* Copyright (c) 2021 GraphQL Contributors.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import StyledWrapper from './StyledWrapper';
import yamlPlugin from './Plugins/Yaml/index';
let CodeMirror;
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
if (!SERVER_RENDERED) {
CodeMirror = require('codemirror');
}
export default class CodeEditor extends React.Component {
constructor(props) {
super(props);
this.cachedValue = props.value || '';
this.variables = {};
this.lintOptions = {
esversion: 11,
expr: true,
asi: true
};
}
componentWillMount() {
switch (this.props.mode) {
case 'yaml':
// YAML linting and hightlighting plugin
yamlPlugin(CodeMirror);
break;
default:
break;
}
}
componentDidMount() {
const editor = (this.editor = CodeMirror(this._node, {
value: this.props.value || '',
lineNumbers: true,
lineWrapping: true,
tabSize: 2,
mode: this.props.mode || 'application/text',
keyMap: 'sublime',
autoCloseBrackets: true,
matchBrackets: true,
showCursorWhenSelecting: true,
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
lint: this.lintOptions,
readOnly: this.props.readOnly,
scrollbarStyle: 'overlay',
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
extraKeys: {
'Cmd-F': 'findPersistent',
'Ctrl-F': 'findPersistent',
'Cmd-H': 'replace',
'Ctrl-H': 'replace',
'Tab': function (cm) {
cm.getSelection().includes('\n') || editor.getLine(cm.getCursor().line) == cm.getSelection()
? cm.execCommand('indentMore')
: cm.replaceSelection(' ', 'end');
},
'Shift-Tab': 'indentLess',
'Ctrl-Space': 'autocomplete',
'Cmd-Space': 'autocomplete',
'Ctrl-Y': 'foldAll',
'Cmd-Y': 'foldAll',
'Ctrl-I': 'unfoldAll',
'Cmd-I': 'unfoldAll'
}
}));
if (editor) {
editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? this.lintOptions : false);
editor.on('change', this._onEdit);
}
}
componentDidUpdate(prevProps) {
this.ignoreChangeEvent = true;
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
this.cachedValue = this.props.value;
this.editor.setValue(this.props.value);
}
if (this.props.theme !== prevProps.theme && this.editor) {
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
}
this.ignoreChangeEvent = false;
}
componentWillUnmount() {
if (this.editor) {
this.editor.off('change', this._onEdit);
this.editor = null;
}
}
render() {
if (this.editor) {
this.editor.refresh();
}
return (
<StyledWrapper
className="h-full w-full graphiql-container"
aria-label="Code Editor"
font={this.props.font}
ref={(node) => {
this._node = node;
}}
/>
);
}
_onEdit = () => {
if (!this.ignoreChangeEvent && this.editor) {
this.editor.setOption('lint', this.editor.getValue().trim().length > 0 ? this.lintOptions : false);
this.cachedValue = this.editor.getValue();
if (this.props.onEdit) {
this.props.onEdit(this.cachedValue);
}
}
};
}

View File

@@ -1,870 +0,0 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.swagger-root {
height: calc(100vh - 7rem);
border-left: solid 1px ${(props) => props.theme.border.border1};
overflow-y: auto;
background: ${(props) => props.theme.bg};
padding-bottom: 20px;
/* ── Global reset ── */
.swagger-ui {
font-family: inherit;
font-size: ${(props) => props.theme.font.size.base};
color: ${(props) => props.theme.text};
* {
border-color: ${(props) => props.theme.border.border1};
}
.auth-container {
padding: 0;
}
select {
box-shadow: none !important;
}
.wrapper {
padding: 0 20px;
max-width: none;
}
/* ── Info section ── */
.info {
margin: 16px 0 12px;
hgroup.main {
margin: 0;
}
.title {
font-size: 16px;
font-weight: 600;
color: ${(props) => props.theme.text};
small {
padding: 2px 6px !important;
font-size: 10px;
vertical-align: middle;
border-radius: 3px;
pre {
color: ${(props) => props.theme.text} !important;
font-size: 10px;
}
}
}
.base-url {
font-size: ${(props) => props.theme.font.size.xs};
color: ${(props) => props.theme.colors.text.muted};
}
.description {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.colors.text.muted};
p, li {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.colors.text.muted};
margin: 3px 0;
line-height: 1.5;
}
h1, h2, h3, h4, h5, h6 {
color: ${(props) => props.theme.text};
}
a {
color: ${(props) => props.theme.textLink};
}
}
}
/* Version / OAS badges */
.version-stamp span.version {
background: ${(props) => props.theme.border.border1} !important;
border: 1px solid ${(props) => props.theme.colors.text.muted} !important;
color: ${(props) => props.theme.text} !important;
font-size: 9px;
padding: 2px 6px;
border-radius: 3px;
}
.version-pragma {
font-size: ${(props) => props.theme.font.size.xs};
color: ${(props) => props.theme.colors.text.muted};
}
/* ── Tag section headings ── */
.opblock-tag-section {
.opblock-tag {
font-size: ${(props) => props.theme.font.size.md};
color: ${(props) => props.theme.text};
border-bottom: none;
padding: 0;
&:hover {
background: ${(props) => props.theme.background.mantle};
}
a {
color: ${(props) => props.theme.text} !important;
}
small {
font-size: ${(props) => props.theme.font.size.xs};
color: ${(props) => props.theme.colors.text.muted};
padding: 0 10px;
}
}
}
/* ── Operation blocks (GET, POST, PUT, DELETE, PATCH) ── */
.opblock {
margin: 0 0 8px;
border-radius: 4px;
border: 1px solid ${(props) => props.theme.border.border1} !important;
background: ${(props) => props.theme.bg} !important;
box-shadow: none !important;
.opblock-summary {
padding: 6px 10px;
border: none !important;
background: transparent !important;
.opblock-summary-method {
font-size: 10px;
font-weight: 700;
padding: 3px 8px;
min-width: 50px;
text-align: center;
border-radius: 3px;
}
.opblock-summary-path {
font-size: ${(props) => props.theme.font.size.sm};
a, span {
color: ${(props) => props.theme.text} !important;
}
}
.opblock-summary-description {
font-size: ${(props) => props.theme.font.size.xs};
color: ${(props) => props.theme.colors.text.muted};
}
.opblock-summary-control {
svg {
fill: ${(props) => props.theme.colors.text.muted};
width: 14px;
height: 14px;
}
}
}
.opblock-body {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.text};
background: ${(props) => props.theme.bg};
border-top: 1px solid ${(props) => props.theme.border.border1};
.opblock-description-wrapper,
.opblock-section {
p {
color: ${(props) => props.theme.colors.text.muted};
font-size: ${(props) => props.theme.font.size.sm};
}
}
.tab-header .tab-item {
color: ${(props) => props.theme.colors.text.muted};
&.active {
color: ${(props) => props.theme.text};
}
}
select {
color: ${(props) => props.theme.text};
background: ${(props) => props.theme.bg};
border: 1px solid ${(props) => props.theme.border.border1};
border-radius: 3px;
font-size: ${(props) => props.theme.font.size.xs};
padding: 2px 6px;
}
input[type="text"] {
color: ${(props) => props.theme.text};
background: ${(props) => props.theme.bg};
border: 1px solid ${(props) => props.theme.border.border1};
border-radius: 3px;
font-size: ${(props) => props.theme.font.size.sm};
}
}
}
/* Method badge colors — keep them but tone down */
.opblock.opblock-get .opblock-summary-method { background: #61affe; color: #fff; }
.opblock.opblock-post .opblock-summary-method { background: #49cc90; color: #fff; }
.opblock.opblock-put .opblock-summary-method { background: #fca130; color: #fff; }
.opblock.opblock-delete .opblock-summary-method { background: #f93e3e; color: #fff; }
.opblock.opblock-patch .opblock-summary-method { background: #50e3c2; color: #000; }
/* Lock / authorization icons */
.authorization__btn {
svg {
fill: ${(props) => props.theme.colors.text.muted};
width: 14px;
height: 14px;
}
}
/* ── Tables ── */
table {
font-size: ${(props) => props.theme.font.size.sm};
thead {
tr {
th {
font-size: ${(props) => props.theme.font.size.xs} !important;
color: ${(props) => props.theme.colors.text.muted} !important;
border-bottom: 1px solid ${(props) => props.theme.border.border1} !important;
padding: 6px 0;
}
}
}
td {
padding: 6px 0;
border-bottom: 1px solid ${(props) => props.theme.border.border1};
color: ${(props) => props.theme.text};
}
}
.parameter__name {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.text};
&.required::after {
color: ${(props) => props.theme.colors.text.danger || '#c0392b'};
font-size: ${(props) => props.theme.font.size.xs};
}
}
.parameter__type {
font-size: ${(props) => props.theme.font.size.xs};
color: ${(props) => props.theme.colors.text.muted};
}
.parameter__in {
font-size: ${(props) => props.theme.font.size.xs};
color: ${(props) => props.theme.colors.text.muted};
}
/* ── Models / Schemas ── */
section.models {
border: 1px solid ${(props) => props.theme.border.border1};
border-radius: 4px;
background: ${(props) => props.theme.bg};
padding-bottom: 0px;
margin-bottom: 40px;
margin-top: 8px;
h4 {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.text};
border-bottom: none;
padding: 6px 10px;
margin: 0;
svg {
fill: ${(props) => props.theme.colors.text.muted};
width: 16px;
height: 16px;
}
}
.model-container {
background: ${(props) => props.theme.bg} !important;
margin: 0;
padding: 4px 8px;
border-bottom: 1px solid ${(props) => props.theme.border.border1};
&:last-child {
border-bottom: none;
}
.model-box {
background: ${(props) => props.theme.bg} !important;
padding: 2px 0;
}
}
}
.model {
font-size: 11px;
color: ${(props) => props.theme.text};
line-height: 1.4;
.prop-type {
color: ${(props) => props.theme.textLink};
font-size: 11px;
}
.prop-format {
color: ${(props) => props.theme.colors.text.muted};
font-size: 10px;
}
span.prop-enum {
display: block;
color: ${(props) => props.theme.colors.text.muted};
font-size: 10px;
}
}
.model-example {
.tab li {
color: ${(props) => props.theme.colors.text.muted} !important;
}
}
/* Model expand/collapse toggle */
.model-toggle {
cursor: pointer;
font-size: 10px;
color: ${(props) => props.theme.colors.text.muted};
&::after {
color: ${(props) => props.theme.colors.text.muted};
}
}
/* Model box inner styling */
.model-box {
background: ${(props) => props.theme.bg} !important;
color: ${(props) => props.theme.text};
}
/* Inner model details */
.inner-object {
color: ${(props) => props.theme.text};
}
/* Model title (schema name) */
.model-title {
color: ${(props) => props.theme.text};
font-size: 12px;
font-weight: 600;
}
/* ── JSON Schema 2020-12 (OpenAPI 3.1) schema overrides ── */
.json-schema-2020-12-accordion,
.json-schema-2020-12-expand-deep-button,
section.models h4 button,
.model-box button,
.models-control,
.opblock-summary,
.opblock-summary-control,
.opblock-tag {
outline: none !important;
box-shadow: none !important;
}
button:focus-visible,
.opblock-summary:focus-visible,
.opblock-tag:focus-visible,
.models-control:focus-visible {
outline: 2px solid ${(props) => props.theme.textLink} !important;
outline-offset: 2px;
}
.json-schema-2020-12__title {
font-size: 12px !important;
font-weight: 600;
color: ${(props) => props.theme.text} !important;
}
.json-schema-2020-12-head {
padding: 4px 8px !important;
background: ${(props) => props.theme.bg} !important;
.json-schema-2020-12-accordion {
padding: 0 !important;
color: ${(props) => props.theme.text} !important;
background: transparent !important;
}
/* chevron / arrow icon */
.json-schema-2020-12-accordion__icon {
fill: ${(props) => props.theme.colors.text.muted} !important;
}
button.json-schema-2020-12-expand-deep-button {
font-size: 10px !important;
color: ${(props) => props.theme.colors.text.muted} !important;
background: transparent !important;
padding: 0 4px !important;
}
strong.json-schema-2020-12__attribute--primary {
font-size: 11px !important;
color: ${(props) => props.theme.textLink} !important;
font-weight: normal;
}
}
.json-schema-2020-12-body {
font-size: 11px !important;
margin-left: 16px;
color: ${(props) => props.theme.text} !important;
.json-schema-2020-12-property {
margin-left: 8px;
color: ${(props) => props.theme.text} !important;
border-color: ${(props) => props.theme.border.border1} !important;
}
/* property names */
.json-schema-2020-12__title {
font-size: 11px !important;
font-weight: normal;
color: ${(props) => props.theme.text} !important;
}
/* type badges inside expanded schema */
strong.json-schema-2020-12__attribute--primary {
font-size: 10px !important;
color: ${(props) => props.theme.textLink} !important;
font-weight: normal;
}
strong.json-schema-2020-12__attribute {
font-size: 10px !important;
color: ${(props) => props.theme.colors.text.muted} !important;
font-weight: normal;
}
}
.json-schema-2020-12 {
font-size: 11px !important;
margin: 0 !important;
width: 100%;
height: 100%;
color: ${(props) => props.theme.text} !important;
background: ${(props) => props.theme.bg} !important;
}
/* JSON viewer (Examples section inside schema properties) */
.json-schema-2020-12-json-viewer {
background: transparent !important;
color: ${(props) => props.theme.text} !important;
}
.json-schema-2020-12-json-viewer__name {
color: ${(props) => props.theme.text} !important;
}
.json-schema-2020-12-json-viewer__name--secondary {
color: ${(props) => props.theme.colors.text.muted} !important;
font-weight: normal !important;
}
.json-schema-2020-12-json-viewer__value {
color: ${(props) => props.theme.text} !important;
}
.json-schema-2020-12-json-viewer__value--secondary {
color: ${(props) => props.theme.colors.text.subtext0} !important;
}
.json-schema-2020-12-json-viewer__value--string,
.json-schema-2020-12-json-viewer__value--string.json-schema-2020-12-json-viewer__value--secondary {
color: ${(props) => props.theme.colors.text.green} !important;
}
.json-schema-2020-12-json-viewer__value--number,
.json-schema-2020-12-json-viewer__value--bigint,
.json-schema-2020-12-json-viewer__value--number.json-schema-2020-12-json-viewer__value--secondary,
.json-schema-2020-12-json-viewer__value--bigint.json-schema-2020-12-json-viewer__value--secondary {
color: ${(props) => props.theme.textLink} !important;
}
.json-schema-2020-12-json-viewer__value--boolean,
.json-schema-2020-12-json-viewer__value--boolean.json-schema-2020-12-json-viewer__value--secondary {
color: ${(props) => props.theme.colors.text.warning} !important;
}
.json-schema-2020-12-json-viewer__value--null,
.json-schema-2020-12-json-viewer__value--undefined {
color: ${(props) => props.theme.colors.text.muted} !important;
}
/* enum/keyword example values container */
.json-schema-2020-12-keyword--examples,
[data-json-schema-keyword="examples"] {
color: ${(props) => props.theme.text} !important;
}
/* Model collapse/expand all link */
span.model-toggle {
color: ${(props) => props.theme.colors.text.muted};
font-size: 10px;
}
/* Brace styling in models */
.brace-open, .brace-close {
color: ${(props) => props.theme.colors.text.muted};
font-size: 11px;
}
/* ── Code / Response blocks ── */
.microlight {
background: ${(props) => props.theme.codemirror.bg} !important;
color: ${(props) => props.theme.text} !important;
font-size: ${(props) => props.theme.font.size.xs};
border-radius: 4px;
padding: 8px;
border: 1px solid ${(props) => props.theme.border.border1};
}
.highlight-code {
background: ${(props) => props.theme.codemirror.bg} !important;
> .microlight {
border: none;
}
}
pre {
color: ${(props) => props.theme.text};
font-size: ${(props) => props.theme.font.size.xs};
border-radius: 4px;
}
.response-col_status {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.text};
}
.response-col_description {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.colors.text.muted};
}
.responses-inner {
h4, h5 {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.text};
}
}
/* ── Buttons ── */
.btn {
font-size: ${(props) => props.theme.font.size.xs};
border-radius: 4px;
box-shadow: none !important;
color: ${(props) => props.theme.text};
border-color: ${(props) => props.theme.border.border1};
background: transparent;
}
.btn.authorize {
color: ${(props) => props.theme.text};
border-color: ${(props) => props.theme.border.border1};
background: transparent;
svg {
fill: ${(props) => props.theme.text};
}
span {
color: ${(props) => props.theme.text};
}
}
.btn.execute {
background: ${(props) => props.theme.primary?.solid || props.theme.textLink};
color: #fff;
border-color: transparent;
}
.btn-group {
.btn {
background: ${(props) => props.theme.bg};
color: ${(props) => props.theme.text};
}
}
/* ── Links ── */
a {
color: ${(props) => props.theme.textLink};
}
/* ── Servers / Scheme container ── */
.scheme-container {
background: ${(props) => props.theme.background.mantle} !important;
border-top: 1px solid ${(props) => props.theme.border.border1};
border-bottom: 1px solid ${(props) => props.theme.border.border1};
padding: 10px;
box-shadow: none !important;
.schemes-title {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.colors.text.muted};
}
label {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.colors.text.muted};
}
select {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.text};
background: ${(props) => props.theme.bg};
border: 1px solid ${(props) => props.theme.border.border1};
border-radius: 4px;
padding: 4px 8px;
}
}
/* ── SVGs / icons ── */
svg {
fill: ${(props) => props.theme.colors.text.muted};
}
svg.arrow {
fill: ${(props) => props.theme.text};
width: 12px;
height: 12px;
margin-left: 4px;
}
.expand-operation svg {
fill: ${(props) => props.theme.colors.text.muted};
width: 14px;
height: 14px;
}
/* ── Misc / catch-all ── */
.loading-container .loading::after {
color: ${(props) => props.theme.colors.text.muted};
font-size: ${(props) => props.theme.font.size.sm};
}
.renderedMarkdown p {
color: ${(props) => props.theme.colors.text.muted};
font-size: ${(props) => props.theme.font.size.sm};
}
.opblock-section-header {
background: ${(props) => props.theme.background.mantle} !important;
box-shadow: none !important;
border-bottom: 1px solid ${(props) => props.theme.border.border1};
padding: 6px 10px;
h4 {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.text};
}
label {
font-size: ${(props) => props.theme.font.size.xs};
color: ${(props) => props.theme.colors.text.muted};
}
}
.copy-to-clipboard {
button {
border-radius: 3px;
}
}
/* Dialog / modal overrides */
.dialog-ux {
.modal-ux {
background: ${(props) => props.theme.bg};
border: 1px solid ${(props) => props.theme.border.border1};
border-radius: 6px;
color: ${(props) => props.theme.text};
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
.modal-ux-header {
border-bottom: 1px solid ${(props) => props.theme.border.border1};
padding: 12px 0px;
h3 {
font-size: ${(props) => props.theme.font.size.md};
font-weight: 600;
color: ${(props) => props.theme.text};
}
.close-modal {
opacity: 0.6;
&:hover { opacity: 1; }
svg { fill: ${(props) => props.theme.text}; }
}
}
.modal-ux-content {
color: ${(props) => props.theme.text};
padding: 12px 16px;
p {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.colors.text.muted};
}
/* Section headings like "api_key (apiKey)" */
h4, h5, h6 {
font-size: ${(props) => props.theme.font.size.sm};
font-weight: 600;
color: ${(props) => props.theme.textLink};
margin: 12px 0 6px;
}
/* Labels: "Name:", "In:", "Flow:", "Value:", etc. */
label {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.text};
> span {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.colors.text.muted};
}
}
/* "Scopes:" heading */
.scopes h2 {
font-size: ${(props) => props.theme.font.size.sm} !important;
font-weight: 500;
color: ${(props) => props.theme.text} !important;
}
/* Scope item name + description */
.scopes .checkbox {
p.name {
font-size: ${(props) => props.theme.font.size.sm} !important;
color: ${(props) => props.theme.text} !important;
font-weight: 500;
margin: 0;
}
p.description {
font-size: ${(props) => props.theme.font.size.xs} !important;
color: ${(props) => props.theme.colors.text.muted} !important;
margin: 0;
}
}
/* Text inputs */
input[type="text"],
input[type="password"],
input[type="email"] {
background: ${(props) => props.theme.background.mantle} !important;
color: ${(props) => props.theme.text} !important;
border: 1px solid ${(props) => props.theme.border.border1} !important;
border-radius: 4px !important;
font-size: ${(props) => props.theme.font.size.sm} !important;
padding: 6px 10px !important;
outline: none !important;
box-shadow: none !important;
&:focus {
border-color: ${(props) => props.theme.textLink} !important;
outline: none !important;
box-shadow: none !important;
}
}
/* Checkboxes — custom styled to match theme */
input[type="checkbox"] {
appearance: none !important;
-webkit-appearance: none !important;
width: 14px !important;
height: 14px !important;
min-width: 14px;
border: 1px solid ${(props) => props.theme.border.border1} !important;
border-radius: 3px !important;
background: ${(props) => props.theme.background.mantle} !important;
cursor: pointer;
position: relative;
vertical-align: middle;
&:checked {
background: ${(props) => props.theme.textLink} !important;
border-color: ${(props) => props.theme.textLink} !important;
&::after {
content: '';
position: absolute;
left: 3px;
top: 1px;
width: 5px;
height: 8px;
border: 2px solid #fff;
border-top: none;
border-left: none;
transform: rotate(45deg);
}
}
}
/* "select all / select none" links */
a {
font-size: ${(props) => props.theme.font.size.xs};
color: ${(props) => props.theme.textLink};
}
/* Dividers between auth sections */
hr {
border-color: ${(props) => props.theme.border.border1};
margin: 12px 0;
}
/* Authorize / Close buttons */
.btn-done,
.auth-btn-wrapper .btn {
font-size: ${(props) => props.theme.font.size.sm};
border-radius: 4px;
padding: 6px 16px;
border: 1px solid ${(props) => props.theme.border.border1};
background: transparent;
color: ${(props) => props.theme.text};
cursor: pointer;
outline: none !important;
box-shadow: none !important;
&:hover {
background: ${(props) => props.theme.background.mantle};
}
&.modal-btn-operation {
background: ${(props) => props.theme.textLink};
color: #fff;
border-color: transparent;
&:hover {
opacity: 0.9;
}
}
}
}
}
.backdrop-ux {
background: rgba(0, 0, 0, 0.5);
}
}
}
}
`;
export default StyledWrapper;

View File

@@ -1,75 +0,0 @@
import { memo } from 'react';
import SwaggerUI from 'swagger-ui-react';
import StyledWrapper from './StyledWrapper';
import { serializeBody } from './serializeBody';
const serializeHeaders = (headers) => {
if (!headers) return {};
if (typeof headers.entries === 'function') {
const out = {};
for (const [k, v] of headers.entries()) out[k] = v;
return out;
}
return { ...headers };
};
const proxiedFetch = async (url, options = {}) => {
const result = await window.ipcRenderer.invoke('renderer:swagger-fetch', {
url,
method: options.method || 'GET',
headers: serializeHeaders(options.headers),
body: serializeBody(options.body)
});
if (result.error) {
const err = new TypeError(result.message);
err.code = result.code;
throw err;
}
// The Response constructor throws if a null-body status carries a body.
const nullBodyStatus = [101, 204, 205, 304].includes(result.status);
const bodyBytes = !nullBodyStatus && result.bodyBase64
? Uint8Array.from(atob(result.bodyBase64), (c) => c.charCodeAt(0))
: null;
// Build Headers manually so multi-value response headers (e.g. Set-Cookie,
// which axios returns as string[]) end up as repeated entries rather than
// joined via toString(). new Headers({ 'set-cookie': ['a','b'] }) coerces
// the array to "a,b", which is invalid Set-Cookie syntax.
const responseHeaders = new Headers();
for (const [name, value] of Object.entries(result.headers || {})) {
if (Array.isArray(value)) {
value.forEach((v) => responseHeaders.append(name, String(v)));
} else if (value != null) {
responseHeaders.append(name, String(value));
}
}
return new Response(bodyBytes, {
status: result.status,
statusText: result.statusText,
headers: responseHeaders
});
};
const requestInterceptor = (req) => {
req.userFetch = proxiedFetch;
return req;
};
const Swagger = ({ spec, onComplete }) => {
return (
<StyledWrapper>
<div className="swagger-root w-full">
<SwaggerUI
spec={spec}
onComplete={onComplete}
requestInterceptor={requestInterceptor}
/>
</div>
</StyledWrapper>
);
};
export default memo(Swagger);

Some files were not shown because too many files have changed in this diff Show More