Skip to content
4 changes: 2 additions & 2 deletions packages/bruno-app/src/components/OpenAPISpecTab/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import StyledWrapper from 'components/ApiSpecPanel/StyledWrapper';
*/
const prettyPrintSpec = (content) => {
if (!content) return content;
if (content.trimStart()[0] !== '{') return content;
try {
return fastJsonFormat(content);
} catch {
Expand All @@ -32,8 +33,7 @@ const OpenAPISpecTab = ({ collection }) => {
try {
const { ipcRenderer } = window;
const result = await ipcRenderer.invoke('renderer:read-openapi-spec', {
collectionPath: collection.pathname,
sourceUrl
collectionPath: collection.pathname
});
if (result.error) {
// Local file not found — fall back to fetching from remote URL
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
IconTrash,
IconArrowBackUp,
IconExternalLink,
IconClock,
IconAlertTriangle,
IconInfoCircle,
IconLoader2
} from '@tabler/icons';
Expand All @@ -25,7 +25,8 @@ const CollectionStatusSection = ({
storedSpec,
lastSyncDate,
onOpenEndpoint,
isLoading
isLoading,
onTabSelect
}) => {
const {
pendingAction, setPendingAction,
Expand Down Expand Up @@ -71,8 +72,6 @@ const CollectionStatusSection = ({
variant: 'muted',
message: 'Collection has changes since last sync',
badges: { modifiedCount, missingCount, localOnlyCount },
version,
lastSyncDate,
actions: ['revert-all']
};
}
Expand Down Expand Up @@ -117,7 +116,7 @@ const CollectionStatusSection = ({
{hasDrift && (
<div className="sync-info-notice mt-4">
<IconInfoCircle size={14} className="sync-info-icon" />
<span><span className="whats-updated-title">What's tracked:</span> Changes to URL, parameters, headers, body and auth compared to the synced spec. Your variables, scripts, tests, assertions, settings etc. are not tracked here.</span>
<span><span className="whats-updated-title">What's tracked:</span> Changes to parameters, headers, body and auth compared to the synced spec. Your variables, scripts, tests, assertions, settings etc. are not tracked here.</span>
</div>
)}

Expand Down Expand Up @@ -222,26 +221,15 @@ const CollectionStatusSection = ({
<p>Comparing your collection with the last synced spec...</p>
</div>
) : !hasStoredSpec ? (
<>
<div className="spec-update-banner warning">
<div className="banner-left">
<div className="status-dot warning" />
<span className="banner-title">
{lastSyncDate
? 'Last synced spec is required to show collection changes. Restore the latest spec from the source to track future changes.'
: 'Collection changes will be available after the initial sync'}
</span>
</div>
</div>
<div className="sync-review-empty-state mt-5">
<IconClock size={40} className="empty-state-icon" />
<h4>{lastSyncDate ? 'Last Synced Spec missing from storage' : 'Waiting for initial sync'}</h4>
<p>{lastSyncDate
? 'Restore the latest spec from the source to track future changes.'
: 'Once you sync your collection with the spec, changes will appear here.'}
</p>
</div>
</>
<div className="sync-review-empty-state mt-5">
<IconAlertTriangle size={40} className="empty-state-icon" />
<h4>{lastSyncDate ? 'Cannot track collection changes' : 'Waiting for initial sync'}</h4>
<p>{lastSyncDate
? 'The last synced spec is missing. Go to the \'Spec Updates\' tab to restore it, or sync the collection if updates are available to track future changes.'
: 'Once you sync your collection with the spec, local changes will appear here.'}
</p>
<Button variant="outline" size="sm" className="mt-4" onClick={() => onTabSelect('spec-updates')}>Go to Spec Updates</Button>
</div>
) : (
<div className="sync-review-empty-state mt-5">
<IconCheck size={40} className="empty-state-icon" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const SUMMARY_CARDS = [
key: 'inSync',
label: 'In Sync with Spec',
color: 'green',
tooltip: 'Endpoints that currently match the latest spec'
tooltip: 'Endpoints that currently match the latest spec from the source'
},
{
key: 'changed',
Expand All @@ -46,10 +46,8 @@ const OverviewSection = ({ collection, storedSpec, collectionDrift, specDrift, r
const specMeta = useSelector(selectStoredSpecMeta(collection.uid));
const activeError = error || reduxError;

const version = storedSpec?.info?.version ?? specMeta?.version;
const newVersion = specDrift?.newVersion;
const hasVersionChange = version && newVersion && version !== newVersion;
const endpointCount = countEndpoints(storedSpec) ?? specMeta?.endpointCount ?? null;
const version = specMeta?.version;
const endpointCount = specMeta?.endpointCount ?? null;
const lastSyncDate = openApiSyncConfig?.lastSyncDate;
const groupBy = openApiSyncConfig?.groupBy || 'tags';
const autoCheckEnabled = openApiSyncConfig?.autoCheck !== false;
Expand Down Expand Up @@ -90,7 +88,7 @@ const OverviewSection = ({ collection, storedSpec, collectionDrift, specDrift, r
};

const details = [
{ label: 'Spec Version', value: hasVersionChange ? `v${version} → v${newVersion}` : version ? `v${version}` : '–' },
{ label: 'Spec Version', value: version ? `v${version}` : '–' },
{ label: 'Endpoints in Spec', value: endpointCount != null ? endpointCount : '–' },
{ label: 'Last Synced At', value: lastSyncDate ? moment(lastSyncDate).fromNow() : '–', tooltip: lastSyncDate ? moment(lastSyncDate).format('MMMM D, YYYY [at] h:mm A') : undefined },
{ label: 'Folder Grouping', value: capitalize(groupBy) },
Expand Down Expand Up @@ -121,31 +119,31 @@ const OverviewSection = ({ collection, storedSpec, collectionDrift, specDrift, r
buttons: ['review']
};
}
if (specDrift?.storedSpecMissing && lastSyncDate) {
return {
variant: 'warning',
title: 'Last synced spec not found',
subtitle: 'The last synced spec is missing in the storage. Restore the latest spec from the source to track future changes.',
buttons: ['restore']
};
}
if (!hasDriftData) return null;
if (hasSpecUpdates && hasCollectionChanges) {
return {
variant: 'warning',
title: `The API spec has new updates${versionInfo} and the collection has changes`,
title: `OpenAPI spec has new updates${versionInfo} and the collection has changes`,
subtitle: 'New or changed requests are available. Some collection changes may be overwritten.',
buttons: ['sync', 'changes']
};
}
if (hasSpecUpdates) {
return {
variant: 'warning',
title: `The API spec has new updates${versionInfo}`,
title: `OpenAPI spec has new updates${versionInfo}`,
subtitle: 'New or changed requests are available.',
buttons: ['sync']
};
}
if (specDrift?.storedSpecMissing && lastSyncDate) {
return {
variant: 'warning',
title: 'Last synced spec not found',
subtitle: 'The last synced spec is missing in the storage. Restore the latest spec from the source to track collection changes.',
buttons: ['restore']
};
}
Comment on lines +138 to +145
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Delete-only spec changes still fall through to the “stored spec missing” banner.

This new branch relies on hasSpecUpdates, but in the stored-spec-missing path that count still ignores the remoteDrift.localOnly bucket. If the only upstream change is an endpoint removal, Overview will show “Last synced spec not found” instead of the higher-priority update state. SpecStatusSection in this PR already treats that bucket as an update, so the two tabs can disagree about the same collection.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bruno-app/src/components/OpenAPISyncTab/OverviewSection/index.js`
around lines 138 - 145, The stored-spec-missing branch in OverviewSection uses
hasSpecUpdates but ignores delete-only upstream changes in
remoteDrift.localOnly, causing the banner to show when SpecStatusSection
considers there are updates; update the condition so the "stored spec missing"
banner only shows when there are truly no updates including remote deletions —
for example, change the check around specDrift?.storedSpecMissing to also verify
that hasSpecUpdates considers remoteDrift.localOnly (e.g. call hasSpecUpdates
with a flag/include that counts remoteDrift.localOnly or explicitly check
specDrift.remoteDrift?.localOnly === 0) so that delete-only changes prevent the
stored-spec-missing banner from appearing and keep OverviewSection consistent
with SpecStatusSection.

if (!hasDriftData) return null;
if (hasCollectionChanges) {
return {
variant: 'muted',
Expand Down Expand Up @@ -197,7 +195,7 @@ const OverviewSection = ({ collection, storedSpec, collectionDrift, specDrift, r
)}
{bannerState.buttons.includes('restore') && (
<Button size="sm" onClick={() => onTabSelect('spec-updates')}>
Restore Spec File
View Details
</Button>
)}
{bannerState.buttons.includes('spec-details') && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { useSelector } from 'react-redux';
import {
IconCheck,
IconRefresh,
IconAlertTriangle
IconAlertTriangle,
IconClock
} from '@tabler/icons';
import Button from 'ui/Button';
import StatusBadge from 'ui/StatusBadge';
Expand All @@ -23,14 +24,20 @@ const SpecStatusSection = ({

const {
isSyncing, showConfirmModal, confirmGroups,
handleSyncNow, handleApplySync, cancelConfirmModal, handleConfirmModalSync
handleSyncNow, handleRestoreSpec, handleApplySync, cancelConfirmModal, handleConfirmModalSync
} = useSyncFlow({
collection, specDrift, remoteDrift, collectionDrift,
sourceUrl, setError, checkForUpdates: onCheck
setError, checkForUpdates: onCheck
});

const lastSyncedAt = openApiSyncConfig?.lastSyncDate;

const hasRemoteUpdates = remoteDrift && (
(remoteDrift.missing?.length || 0)
+ (remoteDrift.modified?.length || 0)
+ (remoteDrift.localOnly?.length || 0)
) > 0;
Comment on lines +35 to +39
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t derive “spec updated” from remoteDrift.

remoteDrift is a collection-vs-current-spec diff, so modified and localOnly also light up for purely local edits when the stored spec file is missing. That makes the new “No updates from the spec / Restore Spec File” state disappear even when the compare call found no upstream changes. checkForUpdates() already has that signal on result.hasChanges, so this should key off specDrift.hasChanges instead.

💡 Suggested change
-  const hasRemoteUpdates = remoteDrift && (
-    (remoteDrift.missing?.length || 0)
-    + (remoteDrift.modified?.length || 0)
-    + (remoteDrift.localOnly?.length || 0)
-  ) > 0;
+  const hasRemoteUpdates = specDrift?.hasChanges === true;

Also applies to: 51-56, 120-125

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bruno-app/src/components/OpenAPISyncTab/SpecStatusSection/index.js`
around lines 35 - 39, The current hasRemoteUpdates calculation derives "spec
updated" from remoteDrift which misclassifies local-only edits; replace the
remoteDrift length-sum logic used when computing hasRemoteUpdates (and the
similar checks around where remoteDrift is used at functions/blocks referencing
hasRemoteUpdates) with the spec-level change flag (use specDrift.hasChanges or
the result.hasChanges from checkForUpdates) so the UI state is driven by
specDrift.hasChanges rather than remoteDrift; update all occurrences (including
the other two similar blocks that reference remoteDrift/hasRemoteUpdates) to use
specDrift.hasChanges instead.


const bannerState = useMemo(() => {
if (fileNotFound) {
return { variant: 'danger', message: `Source file not found at ${sourceUrl}`, actions: ['open-settings'] };
Expand All @@ -41,13 +48,12 @@ const SpecStatusSection = ({
if (!specDrift) {
return null;
}
if (specDrift.storedSpecMissing) {
if (!lastSyncedAt) {
return { variant: 'warning', message: 'Initial sync required — your collection differs from the spec', actions: [] };
}
return { variant: 'warning', message: 'Last synced spec not found — Restore the latest spec from the source to track future changes.', actions: [] };
if (specDrift.storedSpecMissing && !hasRemoteUpdates) {
return null;
}
const hasEndpointUpdates = (specDrift.added?.length || 0) + (specDrift.modified?.length || 0) + (specDrift.removed?.length || 0) > 0;
const hasEndpointUpdates = specDrift.storedSpecMissing
? hasRemoteUpdates
: (specDrift.added?.length || 0) + (specDrift.modified?.length || 0) + (specDrift.removed?.length || 0) > 0;
if (hasEndpointUpdates) {
const versionInfo = (specDrift.storedVersion && specDrift.newVersion && specDrift.storedVersion !== specDrift.newVersion)
? ` (v${specDrift.storedVersion} → v${specDrift.newVersion})`
Expand All @@ -63,7 +69,7 @@ const SpecStatusSection = ({
// lastChecked: lastCheckedAt ? moment(lastCheckedAt).fromNow() : 'just now'
// };
return null;
}, [fileNotFound, error, sourceUrl, specDrift, lastSyncedAt, storedSpec, lastCheckedAt]);
}, [fileNotFound, error, sourceUrl, specDrift, lastSyncedAt, storedSpec, lastCheckedAt, hasRemoteUpdates]);
return (
<>
{bannerState && (
Expand Down Expand Up @@ -93,7 +99,7 @@ const SpecStatusSection = ({
</div>
<div className="banner-actions">
{bannerState.actions.includes('quick-sync') && (
<Button size="xs" onClick={handleSyncNow}>Restore Spec File</Button>
<Button size="xs" onClick={handleRestoreSpec}>Restore Spec File</Button>
)}
{bannerState.actions.includes('open-settings') && (
<Button variant="ghost" size="sm" onClick={onOpenSettings}>
Expand All @@ -111,12 +117,12 @@ const SpecStatusSection = ({
<h4>Unable to check for updates</h4>
<p>Fix the connection issue above and check again.</p>
</div>
) : specDrift?.storedSpecMissing && openApiSyncConfig?.lastSyncDate ? (
) : specDrift?.storedSpecMissing && openApiSyncConfig?.lastSyncDate && !hasRemoteUpdates ? (
<div className="sync-review-empty-state mt-5">
<IconRefresh size={40} className="empty-state-icon" />
<h4>Last Synced Spec not found in storage</h4>
<p>The last synced spec is missing in the storage. Restore the latest spec from the source to track future changes.</p>
<Button className="mt-4" color="warning" onClick={handleSyncNow} loading={isSyncing}>
<IconCheck size={40} className="empty-state-icon" />
<h4>No updates from the spec</h4>
<p>The spec endpoints have not been updated since the last sync. You can restore the spec file to track local collection changes.</p>
<Button className="mt-4" color="warning" onClick={handleRestoreSpec} loading={isSyncing}>
Restore Spec File
</Button>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,7 @@ const StyledWrapper = styled.div`
.settings-label {
font-size: 11px;
font-weight: 600;
color: ${(props) => props.theme.colors.text.subtext0};
color: ${(props) => props.theme.text};
display: block;
margin-bottom: 5px;
}
Expand Down Expand Up @@ -670,7 +670,7 @@ const StyledWrapper = styled.div`

.toggle-description {
font-size: 11px;
color: ${(props) => props.theme.colors.text.muted};
color: ${(props) => props.theme.text};
margin-top: 2px;
}

Expand Down Expand Up @@ -1251,7 +1251,6 @@ const StyledWrapper = styled.div`
.disconnect-modal {
.disconnect-message {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.colors.text.muted};
line-height: 1.5;
margin-bottom: 1.5rem;
}
Expand Down Expand Up @@ -1281,7 +1280,7 @@ const StyledWrapper = styled.div`
.action-confirm-modal {
.confirm-message {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.colors.text.muted};
color: ${(props) => props.theme.text};
line-height: 1.5;
margin-bottom: 1.5rem;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,12 @@ const useOpenAPISync = (collection) => {

const updateStoredSpec = (spec) => {
setStoredSpec(spec);
if (spec) {
dispatch(setStoredSpecMeta({
collectionUid: collection.uid,
title: spec.info?.title || null,
version: spec.info?.version || null,
endpointCount: countEndpoints(spec)
}));
}
dispatch(setStoredSpecMeta({
collectionUid: collection.uid,
title: spec?.info?.title || null,
version: spec?.info?.version || null,
endpointCount: spec ? countEndpoints(spec) : null
}));
};

// Flatten collection items including nested items in folders
Expand Down Expand Up @@ -100,8 +98,7 @@ const useOpenAPISync = (collection) => {
try {
const { ipcRenderer } = window;
const result = await ipcRenderer.invoke('renderer:get-collection-drift', {
collectionPath: collection.pathname,
brunoConfig: collection.brunoConfig
collectionPath: collection.pathname
});

if (!result.error) {
Expand Down Expand Up @@ -150,9 +147,7 @@ const useOpenAPISync = (collection) => {
}

setSpecDrift(result);
if (result.storedSpec) {
updateStoredSpec(result.storedSpec);
}
updateStoredSpec(result.storedSpec || null);

// Update Redux store so toolbar status stays in sync
dispatch(setCollectionUpdate({
Expand All @@ -166,7 +161,6 @@ const useOpenAPISync = (collection) => {
if (result.newSpec) {
const remoteComparison = await ipcRenderer.invoke('renderer:get-collection-drift', {
collectionPath: collection.pathname,
brunoConfig: collection.brunoConfig,
compareSpec: result.newSpec
});
if (remoteComparison.error) {
Expand Down Expand Up @@ -271,7 +265,6 @@ const useOpenAPISync = (collection) => {
if (result.newSpec) {
const drift = await ipcRenderer.invoke('renderer:get-collection-drift', {
collectionPath: collection.pathname,
brunoConfig: collection.brunoConfig,
compareSpec: result.newSpec
});

Expand All @@ -284,8 +277,7 @@ const useOpenAPISync = (collection) => {
// Collection matches — save spec file silently to complete setup
await ipcRenderer.invoke('renderer:save-openapi-spec', {
collectionPath: collection.pathname,
specContent: result.newSpecContent || JSON.stringify(result.newSpec, null, 2),
sourceUrl: trimmedUrl
specContent: result.newSpecContent || JSON.stringify(result.newSpec, null, 2)
});
}
}
Expand All @@ -304,7 +296,6 @@ const useOpenAPISync = (collection) => {
const { ipcRenderer } = window;
await ipcRenderer.invoke('renderer:remove-openapi-sync-config', {
collectionPath: collection.pathname,
sourceUrl: openApiSyncConfig?.sourceUrl || sourceUrl,
deleteSpecFile: true
});
setSourceUrl('');
Expand Down Expand Up @@ -343,7 +334,6 @@ const useOpenAPISync = (collection) => {
const { ipcRenderer } = window;
const remoteComparison = await ipcRenderer.invoke('renderer:get-collection-drift', {
collectionPath: collection.pathname,
brunoConfig: collection.brunoConfig,
compareSpec: currentSpecDrift.newSpec
});
if (!remoteComparison.error) {
Expand Down Expand Up @@ -380,7 +370,6 @@ const useOpenAPISync = (collection) => {

await ipcRenderer.invoke('renderer:update-openapi-sync-config', {
collectionPath: collection.pathname,
oldSourceUrl: openApiSyncConfig?.sourceUrl,
config: {
sourceUrl: newUrl,
autoCheck,
Expand Down
Loading
Loading