mirror of
https://github.com/Kong/insomnia.git
synced 2026-08-03 19:32:31 -04:00
feat: UI cleanup for the API Designer's spec editor [INS-2667] (#10141)
* Modify the view of workspace * Add tests * Fix height * Modify style
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../../playwright/test';
|
||||
|
||||
test.describe('Spec editor toolbar', () => {
|
||||
test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms');
|
||||
|
||||
test('generate dropdown, format switch, and preview toggle', async ({ page }) => {
|
||||
// Setup: create a design document from the Pet Store example
|
||||
await page.getByRole('button', { name: 'Create document' }).click();
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'Create' }).click();
|
||||
await page.click('text=Use example');
|
||||
await page.click('text=Pet Store');
|
||||
|
||||
const codeEditor = page.locator('.pane-one').getByTestId('CodeEditor');
|
||||
await expect.soft(codeEditor).toContainText('openapi: 3.0.4');
|
||||
|
||||
// The toolbar shows the OpenAPI version label
|
||||
await expect.soft(page.locator('.pane-one').getByText('OpenAPI 3.0.4')).toBeVisible();
|
||||
|
||||
// The Generate dropdown exposes the Collection option
|
||||
await page.getByRole('button', { name: 'Generate' }).click();
|
||||
await expect.soft(page.getByRole('menuitemradio', { name: 'Collection' })).toBeVisible();
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
// The format dropdown converts the spec between YAML and JSON
|
||||
const formatButton = page.getByRole('button', { name: 'Spec format' });
|
||||
await expect.soft(formatButton).toContainText('YAML');
|
||||
await formatButton.click();
|
||||
await page.getByRole('menuitemradio', { name: 'JSON' }).click();
|
||||
await expect.soft(codeEditor).toContainText('"openapi": "3.0.4"');
|
||||
await expect.soft(formatButton).toContainText('JSON');
|
||||
|
||||
// The single preview toggle shows/hides the docs preview pane.
|
||||
// A freshly created document starts with the preview collapsed.
|
||||
const previewToggle = page.getByTestId('preview-toggle');
|
||||
await expect.soft(page.locator('.pane-two')).toBeHidden();
|
||||
await previewToggle.click();
|
||||
await expect.soft(page.locator('.pane-two')).toBeVisible();
|
||||
await previewToggle.click();
|
||||
await expect.soft(page.locator('.pane-two')).toBeHidden();
|
||||
});
|
||||
});
|
||||
@@ -289,7 +289,8 @@ export type RendererOnChannels =
|
||||
| 'hide-oauth-authorization-modal'
|
||||
| 'mcp-auth-confirmation'
|
||||
| 'git.db-synced'
|
||||
| 'git.file-problems-changed';
|
||||
| 'git.file-problems-changed'
|
||||
| 'llm.changed';
|
||||
|
||||
export const ipcMainOn = (
|
||||
channel: MainOnChannels,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import { app } from 'electron';
|
||||
import { app, BrowserWindow } from 'electron';
|
||||
import { services } from 'insomnia-data';
|
||||
|
||||
import { LLM_BACKENDS } from '~/common/constants';
|
||||
@@ -152,18 +152,36 @@ export interface LLMConfigServiceAPI {
|
||||
setAIFeatureEnabled: typeof setAIFeatureEnabled;
|
||||
}
|
||||
|
||||
// Notify every renderer that the LLM config or AI feature flags changed so that
|
||||
// any mounted consumer (e.g. `useAIFeatureStatus`) re-reads the latest values.
|
||||
// Main owns the data, so it also owns the change signal — this keeps every
|
||||
// window consistent instead of only the one that performed the mutation.
|
||||
const broadcastLLMChanged = () => {
|
||||
for (const w of BrowserWindow.getAllWindows()) {
|
||||
w.webContents.send('llm.changed');
|
||||
}
|
||||
};
|
||||
|
||||
export const registerLLMConfigServiceAPI = () => {
|
||||
ipcMainHandle('llm.getActiveBackend', async () => getActiveBackend());
|
||||
ipcMainHandle('llm.setActiveBackend', async (_, backend: LLMBackend) => setActiveBackend(backend));
|
||||
ipcMainHandle('llm.clearActiveBackend', async () => clearActiveBackend());
|
||||
ipcMainHandle('llm.setActiveBackend', async (_, backend: LLMBackend) => {
|
||||
await setActiveBackend(backend);
|
||||
broadcastLLMChanged();
|
||||
});
|
||||
ipcMainHandle('llm.clearActiveBackend', async () => {
|
||||
await clearActiveBackend();
|
||||
broadcastLLMChanged();
|
||||
});
|
||||
ipcMainHandle('llm.getBackendConfig', async (_, backend: LLMBackend) => getBackendConfig(backend));
|
||||
ipcMainHandle('llm.updateBackendConfig', async (_, backend: LLMBackend, config: Partial<LLMConfig>) =>
|
||||
updateBackendConfig(backend, config),
|
||||
);
|
||||
ipcMainHandle('llm.updateBackendConfig', async (_, backend: LLMBackend, config: Partial<LLMConfig>) => {
|
||||
await updateBackendConfig(backend, config);
|
||||
broadcastLLMChanged();
|
||||
});
|
||||
ipcMainHandle('llm.getAllConfigurations', async () => getAllConfigurations());
|
||||
ipcMainHandle('llm.getCurrentConfig', async () => getCurrentConfig());
|
||||
ipcMainHandle('llm.getAIFeatureEnabled', async (_, feature: AIFeatureNames) => getAIFeatureEnabled(feature));
|
||||
ipcMainHandle('llm.setAIFeatureEnabled', async (_, feature: AIFeatureNames, enabled: boolean) =>
|
||||
setAIFeatureEnabled(feature, enabled),
|
||||
);
|
||||
ipcMainHandle('llm.setAIFeatureEnabled', async (_, feature: AIFeatureNames, enabled: boolean) => {
|
||||
await setAIFeatureEnabled(feature, enabled);
|
||||
broadcastLLMChanged();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -50,6 +50,7 @@ import { useSpecUpdateActionFetcher } from '~/routes/organization.$organizationI
|
||||
import { useStorageRulesLoaderFetcher } from '~/routes/organization.$organizationId.storage-rules';
|
||||
import { AnalyticsEvent } from '~/ui/analytics';
|
||||
import { CodeEditor, type CodeEditorHandle } from '~/ui/components/.client/codemirror/code-editor';
|
||||
import { Badge } from '~/ui/components/base/badge';
|
||||
import { DesignEmptyState } from '~/ui/components/design-empty-state';
|
||||
import { DocumentTab } from '~/ui/components/document-tab';
|
||||
import { Icon } from '~/ui/components/icon';
|
||||
@@ -165,7 +166,8 @@ interface GroupedLintMessage {
|
||||
interface SpecActionItem {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: ReactNode;
|
||||
icon?: ReactNode;
|
||||
badge?: ReactNode;
|
||||
isDisabled?: boolean;
|
||||
action: () => void;
|
||||
}
|
||||
@@ -554,11 +556,15 @@ const Component = ({ params }: Route.ComponentProps) => {
|
||||
});
|
||||
};
|
||||
|
||||
const specActionList: SpecActionItem[] = [
|
||||
const generateActionList: SpecActionItem[] = [
|
||||
{
|
||||
id: 'generate-request-collection',
|
||||
name: 'Generate collection',
|
||||
icon: <Icon className="w-3" icon="file-code" />,
|
||||
name: 'Collection',
|
||||
icon: (
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-sm bg-(--color-surprise) text-(--color-font-surprise)">
|
||||
<Icon className="w-3" icon="bars" />
|
||||
</span>
|
||||
),
|
||||
isDisabled: !apiSpec.contents || lintErrors.length > 0 || generateRequestCollectionFetcher.state !== 'idle',
|
||||
action: () =>
|
||||
generateRequestCollectionFetcher.submit({
|
||||
@@ -568,41 +574,38 @@ const Component = ({ params }: Route.ComponentProps) => {
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'toggle-preview',
|
||||
name: 'Toggle preview',
|
||||
icon: <Icon className="w-3" icon={isSpecPaneOpen ? 'eye' : 'eye-slash'} />,
|
||||
id: 'generate-mock-server',
|
||||
name: 'Mock Server',
|
||||
icon: (
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-sm bg-(--color-warning) text-(--color-font-warning)">
|
||||
<Icon className="w-3" icon="server" />
|
||||
</span>
|
||||
),
|
||||
badge: <Badge icon="sparkles" color="surprise" label="AI" style={{ marginLeft: 'auto', marginRight: '0' }} />,
|
||||
isDisabled: !apiSpec.contents || !isGenerateMockServersWithAIEnabled,
|
||||
action: () => {
|
||||
window.main.trackAnalyticsEvent({
|
||||
event: AnalyticsEvent.designerPreviewToggled,
|
||||
properties: {
|
||||
status: !isSpecPaneOpen ? 'open' : 'collapsed',
|
||||
},
|
||||
event: AnalyticsEvent.designerGenerateMockClicked,
|
||||
});
|
||||
setIsSpecPaneOpen(!isSpecPaneOpen);
|
||||
setNewMockServerModalOpen(true);
|
||||
},
|
||||
},
|
||||
...(specFormat === 'json'
|
||||
? [
|
||||
{
|
||||
id: 'convert-to-yaml',
|
||||
name: 'Convert to YAML',
|
||||
icon: <Icon className="w-3" icon="sync-alt" />,
|
||||
action: () => switchFormat('yaml'),
|
||||
},
|
||||
]
|
||||
: specFormat === 'yaml'
|
||||
? [
|
||||
{
|
||||
id: 'convert-to-json',
|
||||
name: 'Convert to JSON',
|
||||
icon: <Icon className="w-3" icon="sync-alt" />,
|
||||
action: () => switchFormat('json'),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
const disabledKeys = specActionList.filter(item => item.isDisabled).map(item => item.id);
|
||||
const specFormatActionList: SpecActionItem[] = [
|
||||
{
|
||||
id: 'json',
|
||||
name: 'JSON',
|
||||
action: () => switchFormat('json'),
|
||||
},
|
||||
{
|
||||
id: 'yaml',
|
||||
name: 'YAML',
|
||||
action: () => switchFormat('yaml'),
|
||||
},
|
||||
];
|
||||
|
||||
const generateDisabledKeys = generateActionList.filter(item => item.isDisabled).map(item => item.id);
|
||||
|
||||
const uniquenessKey = `${apiSpec?._id}::${apiSpec?.created}::${gitVersion}::${vcsVersion}`;
|
||||
|
||||
@@ -658,6 +661,117 @@ const Component = ({ params }: Route.ComponentProps) => {
|
||||
</div>
|
||||
);
|
||||
|
||||
const specPaneToolbar = apiSpec.contents ? (
|
||||
<div className="flex h-[40px] shrink-0 items-center gap-2 overflow-hidden border-b border-solid border-(--hl-md) px-(--padding-sm)">
|
||||
<span className="truncate text-sm text-(--hl) italic">
|
||||
{parsedSpec?.openapi ? `OpenAPI ${parsedSpec.openapi}` : ''}
|
||||
</span>
|
||||
<span className="flex-1" />
|
||||
<MenuTrigger>
|
||||
<Button
|
||||
aria-label="Generate"
|
||||
className="flex shrink-0 items-center justify-center gap-2 rounded-md border border-solid border-(--hl-md) px-2.5 py-1 text-sm text-(--color-font) ring-1 ring-transparent transition-all hover:bg-(--hl-xs) focus:ring-(--hl-md) focus:ring-inset aria-pressed:bg-(--hl-sm)"
|
||||
>
|
||||
<span>Generate</span>
|
||||
<Icon icon="chevron-down" className="w-2.5 text-(--hl)" />
|
||||
</Button>
|
||||
<Popover className="flex min-w-max flex-col overflow-y-hidden">
|
||||
<Menu
|
||||
aria-label="Generate menu"
|
||||
selectionMode="single"
|
||||
disabledKeys={generateDisabledKeys}
|
||||
onAction={key => {
|
||||
const item = generateActionList.find(item => item.id === key);
|
||||
if (item) {
|
||||
item.action();
|
||||
}
|
||||
}}
|
||||
items={generateActionList}
|
||||
className="min-w-max overflow-y-auto rounded-md border border-solid border-(--hl-sm) bg-(--color-bg) py-2 text-sm shadow-lg select-none focus:outline-hidden"
|
||||
>
|
||||
{item => (
|
||||
<MenuItem
|
||||
className="flex h-(--line-height-xs) w-full items-center gap-2 bg-transparent px-(--padding-sm) whitespace-nowrap text-(--color-font) transition-colors hover:bg-(--hl-sm) focus:bg-(--hl-xs) focus:outline-hidden disabled:cursor-not-allowed aria-disabled:cursor-not-allowed aria-disabled:text-(--hl-md) aria-selected:font-bold"
|
||||
aria-label={item.name}
|
||||
>
|
||||
{item.icon}
|
||||
<span>{item.name}</span>
|
||||
{item.badge && (
|
||||
<span className="flex origin-left scale-90 items-center pl-2 text-xs">{item.badge}</span>
|
||||
)}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
</Popover>
|
||||
</MenuTrigger>
|
||||
<MenuTrigger>
|
||||
<Button
|
||||
aria-label="Spec format"
|
||||
className="flex shrink-0 items-center justify-center gap-2 rounded-md border border-solid border-(--hl-md) px-2.5 py-1 text-sm text-(--color-font) ring-1 ring-transparent transition-all hover:bg-(--hl-xs) focus:ring-(--hl-md) focus:ring-inset aria-pressed:bg-(--hl-sm)"
|
||||
>
|
||||
<span>{specFormat ? specFormat.toUpperCase() : 'JSON'}</span>
|
||||
<Icon icon="chevron-down" className="w-2.5 text-(--hl)" />
|
||||
</Button>
|
||||
<Popover className="flex min-w-max flex-col overflow-y-hidden">
|
||||
<div className="min-w-max overflow-y-auto rounded-md border border-solid border-(--hl-sm) bg-(--color-bg) py-2 text-sm shadow-lg select-none">
|
||||
<span className="block px-(--padding-md) py-1 text-xs text-(--hl)">Spec format</span>
|
||||
<Menu
|
||||
aria-label="Spec format menu"
|
||||
selectionMode="single"
|
||||
selectedKeys={specFormat ? [specFormat] : []}
|
||||
onAction={key => {
|
||||
const item = specFormatActionList.find(item => item.id === key);
|
||||
if (item) {
|
||||
item.action();
|
||||
}
|
||||
}}
|
||||
items={specFormatActionList}
|
||||
className="min-w-max focus:outline-hidden"
|
||||
>
|
||||
{item => (
|
||||
<MenuItem
|
||||
className="flex h-(--line-height-xs) w-full items-center gap-2 bg-transparent px-(--padding-sm) whitespace-nowrap text-(--color-font) transition-colors hover:bg-(--hl-sm) focus:bg-(--hl-xs) focus:outline-hidden aria-selected:font-bold"
|
||||
aria-label={item.name}
|
||||
>
|
||||
<span>{item.name}</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
</div>
|
||||
</Popover>
|
||||
</MenuTrigger>
|
||||
<TooltipTrigger delay={0}>
|
||||
<ToggleButton
|
||||
aria-label="Toggle preview"
|
||||
data-testid="preview-toggle"
|
||||
isSelected={isSpecPaneOpen}
|
||||
className="flex shrink-0 items-center justify-center rounded-md border border-solid border-(--hl-md) px-2 py-1 text-sm text-(--color-font) ring-1 ring-transparent transition-all hover:bg-(--hl-xs) focus:ring-(--hl-md) focus:ring-inset"
|
||||
onChange={value => {
|
||||
setIsSpecPaneOpen(value);
|
||||
window.main.trackAnalyticsEvent({
|
||||
event: AnalyticsEvent.designerPreviewToggled,
|
||||
properties: {
|
||||
status: !value ? 'open' : 'collapsed',
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{({ isSelected }) => (
|
||||
<span className="flex h-5 w-5 items-center justify-center">
|
||||
<Icon icon={isSelected ? 'eye-slash' : 'eye'} fixedWidth />
|
||||
</span>
|
||||
)}
|
||||
</ToggleButton>
|
||||
<Tooltip
|
||||
offset={8}
|
||||
className="rounded-md border border-solid border-(--hl-sm) bg-(--color-bg) px-2 py-1 text-sm text-(--color-font) shadow-lg"
|
||||
>
|
||||
{isSpecPaneOpen ? 'Hide docs preview' : 'Show docs preview'}
|
||||
</Tooltip>
|
||||
</TooltipTrigger>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const lintToolbar = (
|
||||
<div
|
||||
className={`flex h-(--line-height-sm) items-center gap-2 overflow-hidden border-solid border-(--hl-md) px-(--padding-sm) ${isLintPaneOpen ? 'border-b' : ''}`}
|
||||
@@ -862,80 +976,6 @@ const Component = ({ params }: Route.ComponentProps) => {
|
||||
workspaceId={workspaceId}
|
||||
className="border-b border-solid border-(--hl-sm)"
|
||||
/>
|
||||
<div className="flex shrink-0 items-center gap-2 p-(--padding-sm)">
|
||||
<Heading className="text-(--hl) uppercase">Spec</Heading>
|
||||
<span className="flex-1" />
|
||||
{isGenerateMockServersWithAIEnabled && (
|
||||
<Button
|
||||
onPress={() => {
|
||||
window.main.trackAnalyticsEvent({
|
||||
event: AnalyticsEvent.designerGenerateMockClicked,
|
||||
});
|
||||
setNewMockServerModalOpen(true);
|
||||
}}
|
||||
isDisabled={!apiSpec.contents}
|
||||
className="flex max-w-full flex-1 items-center justify-center gap-2 truncate rounded-xs px-4 py-1 text-sm text-(--color-font) ring-1 ring-transparent transition-all hover:bg-(--hl-xs) focus:ring-(--hl-md) focus:ring-inset disabled:cursor-not-allowed disabled:opacity-50 aria-pressed:bg-(--hl-sm)"
|
||||
>
|
||||
<Icon icon="server" className="w-5 shrink-0" />
|
||||
<span className="truncate">Generate Mock</span>
|
||||
</Button>
|
||||
)}
|
||||
<ToggleButton
|
||||
aria-label="Toggle preview"
|
||||
data-testid="preview-toggle"
|
||||
isSelected={isSpecPaneOpen}
|
||||
className="flex h-full items-center justify-center gap-2 rounded-xs px-2 text-sm text-(--color-font) ring-1 ring-transparent transition-all hover:bg-(--hl-xs) focus:ring-(--hl-md) focus:ring-inset aria-pressed:bg-(--hl-sm)"
|
||||
onChange={value => {
|
||||
setIsSpecPaneOpen(value);
|
||||
window.main.trackAnalyticsEvent({
|
||||
event: AnalyticsEvent.designerPreviewToggled,
|
||||
properties: {
|
||||
status: !value ? 'open' : 'collapsed',
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{({ isSelected }) => (
|
||||
<>
|
||||
<Icon icon={isSelected ? 'eye' : 'eye-slash'} />
|
||||
<span>Preview</span>
|
||||
</>
|
||||
)}
|
||||
</ToggleButton>
|
||||
<MenuTrigger>
|
||||
<Button
|
||||
aria-label="Spec actions"
|
||||
className="flex aspect-square h-full items-center justify-center rounded-xs text-sm text-(--color-font) ring-1 ring-transparent transition-all hover:bg-(--hl-xs) focus:ring-(--hl-md) focus:ring-inset aria-pressed:bg-(--hl-sm)"
|
||||
>
|
||||
<Icon icon="gear" />
|
||||
</Button>
|
||||
<Popover className="flex min-w-max flex-col overflow-y-hidden">
|
||||
<Menu
|
||||
aria-label="Spec actions menu"
|
||||
selectionMode="single"
|
||||
disabledKeys={disabledKeys}
|
||||
onAction={key => {
|
||||
const item = specActionList.find(item => item.id === key);
|
||||
if (item) {
|
||||
item.action();
|
||||
}
|
||||
}}
|
||||
items={specActionList}
|
||||
className="min-w-max overflow-y-auto rounded-md border border-solid border-(--hl-sm) bg-(--color-bg) py-2 text-sm shadow-lg select-none focus:outline-hidden"
|
||||
>
|
||||
{item => (
|
||||
<MenuItem
|
||||
className="flex h-(--line-height-xs) w-full items-center gap-2 bg-transparent px-(--padding-md) whitespace-nowrap text-(--color-font) transition-colors hover:bg-(--hl-sm) focus:bg-(--hl-xs) focus:outline-hidden disabled:cursor-not-allowed aria-disabled:cursor-not-allowed aria-disabled:text-(--hl-md) aria-selected:font-bold"
|
||||
aria-label={item.name}
|
||||
>
|
||||
{item.icon}
|
||||
<span>{item.name}</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
</Popover>
|
||||
</MenuTrigger>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col divide-y divide-solid divide-(--hl-md) overflow-y-auto">
|
||||
{/* Info */}
|
||||
{info && (
|
||||
@@ -1344,7 +1384,8 @@ const Component = ({ params }: Route.ComponentProps) => {
|
||||
</Panel>
|
||||
<PanelResizeHandle className="h-full w-px bg-(--hl-md)" />
|
||||
<Panel id="workspace-content" className="flex flex-col">
|
||||
<PanelGroup autoSaveId="insomnia-panels" direction={direction}>
|
||||
{specPaneToolbar}
|
||||
<PanelGroup autoSaveId="insomnia-panels" direction={direction} className="min-h-0 flex-1">
|
||||
<Panel id="pane-one" minSize={10} className="pane-one theme--pane">
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<PanelGroup autoSaveId="insomnia-spec-vertical" direction="vertical" className="min-h-0 flex-1">
|
||||
|
||||
@@ -6,9 +6,10 @@ export interface BadgeProps {
|
||||
color: keyof typeof ThemeEnum;
|
||||
icon?: IconId;
|
||||
label: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export const Badge: FC<BadgeProps> = ({ color, icon, label }) => {
|
||||
export const Badge: FC<BadgeProps> = ({ color, icon, label, style }) => {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
@@ -23,6 +24,7 @@ export const Badge: FC<BadgeProps> = ({ color, icon, label }) => {
|
||||
top: '-1px',
|
||||
color: `rgb(var(--color-${color}-rgb))`,
|
||||
borderColor: `rgb(var(--color-${color}-rgb))`,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{icon && <SvgIcon icon={icon} />}
|
||||
|
||||
@@ -56,6 +56,8 @@ export const AISettings = () => {
|
||||
|
||||
const toggleAIFeature = useCallback(async (feature: AIFeatureNames, enabled: boolean) => {
|
||||
setAIFeatures(prev => ({ ...prev, [feature]: enabled }));
|
||||
// Main broadcasts an `llm.changed` event after the write, which consumers
|
||||
// (e.g. the spec view Generate dropdown) listen for to re-read the status.
|
||||
await window.main.llm.setAIFeatureEnabled(feature, enabled);
|
||||
}, []);
|
||||
|
||||
@@ -64,6 +66,8 @@ export const AISettings = () => {
|
||||
await window.main.llm.updateBackendConfig(backend, extras);
|
||||
|
||||
if (setCurrent) {
|
||||
// Activating an LLM can enable AI features that require an active LLM;
|
||||
// main broadcasts `llm.changed` so consumers re-read the status.
|
||||
await window.main.llm.setActiveBackend(backend);
|
||||
const newCurrentConfig = await window.main.llm.getCurrentConfig();
|
||||
setCurrentLLM(newCurrentConfig);
|
||||
|
||||
@@ -75,6 +75,15 @@ export function useAIFeatureStatus(): AIFeatureStatus {
|
||||
loadFeatureStatus();
|
||||
}, [loadFeatureStatus]);
|
||||
|
||||
// Re-read the status when the AI settings change in the main process (the
|
||||
// source of truth), since this hook would otherwise keep a stale snapshot
|
||||
// taken at mount time. Main broadcasts to every window, so all consumers stay
|
||||
// consistent regardless of which window performed the change.
|
||||
useEffect(() => {
|
||||
const unsubscribe = window.main.on('llm.changed', loadFeatureStatus);
|
||||
return unsubscribe;
|
||||
}, [loadFeatureStatus]);
|
||||
|
||||
const generateMockServersWithAIAllowedByOrg = features.aiMockServers ? features.aiMockServers.enabled : true;
|
||||
const generateCommitMessagesWithAIAllowedByOrg = features.aiCommitMessages ? features.aiCommitMessages.enabled : true;
|
||||
const mcpClientWithAIAllowedByOrg = features.aiMcpClient ? features.aiMcpClient.enabled : true;
|
||||
|
||||
Reference in New Issue
Block a user