Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2639,7 +2639,7 @@
"pageId": "application.shortcuts"
},
"evidence": [
"source:src/web-ui/src/app/scenes/settings/components/KeyboardShortcutsTab.tsx#lists every known shortcut"
"source:src/web-ui/src/app/scenes/settings/pages/application/KeyboardShortcutsSection.tsx#lists every known shortcut"
]
},
{
Expand All @@ -2655,8 +2655,8 @@
"pageId": "application.shortcuts"
},
"evidence": [
"source:src/web-ui/src/app/scenes/settings/components/KeyboardShortcutsTab.tsx#buildFinalConflictMap",
"source:src/web-ui/src/app/scenes/settings/components/KeyboardShortcutsTab.tsx#buildStoredKeybindings({})"
"source:src/web-ui/src/app/scenes/settings/pages/application/KeyboardShortcutsSection.tsx#buildFinalConflictMap",
"source:src/web-ui/src/app/scenes/settings/pages/application/KeyboardShortcutsSection.tsx#buildStoredKeybindings({})"
]
},
{
Expand Down Expand Up @@ -2795,9 +2795,9 @@
"pageId": "ai.models"
},
"evidence": [
"source:src/web-ui/src/infrastructure/config/components/ModelSettingsPage.tsx#custom_headers",
"source:src/web-ui/src/infrastructure/config/components/ModelSettingsPage.tsx#custom_request_body",
"source:src/web-ui/src/infrastructure/config/components/ModelSettingsPage.tsx#skip_ssl_verify"
"source:src/web-ui/src/app/scenes/settings/pages/ai/ModelSettingsPage.tsx#custom_headers",
"source:src/web-ui/src/app/scenes/settings/pages/ai/ModelSettingsPage.tsx#custom_request_body",
"source:src/web-ui/src/app/scenes/settings/pages/ai/ModelSettingsPage.tsx#skip_ssl_verify"
]
},
{
Expand Down Expand Up @@ -2882,7 +2882,7 @@
"pageId": "workspace.session"
},
"evidence": [
"source:src/web-ui/src/infrastructure/config/components/DefaultHarnessConfig.tsx#DefaultHarnessConfig",
"source:src/web-ui/src/app/scenes/settings/pages/ai/DefaultHarnessSection.tsx#DefaultHarnessSection",
"source:src/web-ui/src/locales/zh-CN/settings/runtime.json#defaultHarness.title"
]
},
Expand Down
53 changes: 43 additions & 10 deletions scripts/generate-interactive-capabilities.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ const APPEARANCE_BOOTSTRAP_PATH = path.join(
);
const SETTINGS_REGISTRY = path.join(REPO_ROOT, 'src/web-ui/src/app/scenes/settings/settingsRegistry.ts');
const SETTINGS_TYPES = path.join(REPO_ROOT, 'src/web-ui/src/app/scenes/settings/settingsTypes.ts');
const SETTINGS_MIGRATIONS = JSON.parse(readFileSync(
path.join(REPO_ROOT, 'src/web-ui/src/app/scenes/settings/settingsDestinationMigrations.json'),
'utf8',
));
const PRODUCT_ACTION_CATALOG = path.join(REPO_ROOT, 'src/web-ui/src/app/global-search/productActionCatalog.ts');
const SCENE_TYPES = path.join(REPO_ROOT, 'src/web-ui/src/app/components/SceneBar/types.ts');
const TOOL_PROVIDER_GROUPS = path.join(
Expand Down Expand Up @@ -381,6 +385,23 @@ function controlCoverage(source) {
return coverage;
}

// Keep historical destinations on the wire for older presentation surfaces.
// Validate coverage against the same migration data used by the current Web UI.
export function projectSettingsCatalogDestination(capabilityId, destination, itemId) {
if (destination.kind !== 'settings') return destination;
const capability = SETTINGS_MIGRATIONS.capabilities[capabilityId];
const input = capability?.items?.[itemId] ?? capability?.destination ?? destination;
const migratedView = SETTINGS_MIGRATIONS.views[input.pageId]?.[input.viewId];
const migrated = migratedView ?? SETTINGS_MIGRATIONS.pages[input.pageId];
const sectionId = input.sectionId ?? migrated?.sectionId;
return {
kind: 'settings',
pageId: migrated?.pageId ?? input.pageId,
...(sectionId ? { sectionId } : {}),
...(!migratedView && input.viewId ? { viewId: input.viewId } : {}),
};
}

function validateSource(source) {
if (source.schemaVersion !== 5) throw new Error('Interactive capability source must use schemaVersion 5');
const capabilityIds = new Set();
Expand All @@ -389,21 +410,27 @@ function validateSource(source) {
[...settingsSource.matchAll(/definePage\(\{\s*id:\s*'([^']+)'/gu)].map((match) => match[1]),
);
const settingsViewIds = extractTypeUnionIds(readFileSync(SETTINGS_TYPES, 'utf8'), 'SettingsViewId');
const settingsSectionIds = extractTypeUnionIds(readFileSync(SETTINGS_TYPES, 'utf8'), 'SettingsSectionId');
const actionIds = extractSingleQuotedIds(readFileSync(PRODUCT_ACTION_CATALOG, 'utf8'));
const sceneIds = new Set([
...readFileSync(SCENE_TYPES, 'utf8').matchAll(/^\s*\|\s*'([^']+)'/gmu),
].map((match) => match[1]));
const productToolNames = extractProductToolNames(readFileSync(TOOL_PROVIDER_GROUPS, 'utf8'));
const delegatedToolNames = new Set();

const validateDestination = (destination, owner) => {
const validateDestination = (original, capabilityId, itemId) => {
const owner = itemId ? `${capabilityId}.${itemId}` : capabilityId;
const destination = projectSettingsCatalogDestination(capabilityId, original, itemId);
if (destination.kind === 'settings') {
if (!settingsIds.has(destination.pageId)) {
throw new Error(`${owner} targets unknown settings page ${destination.pageId}`);
}
if (destination.viewId && !settingsViewIds.has(destination.viewId)) {
throw new Error(`${owner} targets unknown settings view ${destination.viewId}`);
}
if (destination.sectionId && !settingsSectionIds.has(destination.sectionId)) {
throw new Error(`${owner} targets unknown settings section ${destination.sectionId}`);
}
return;
}
if (destination.kind === 'action' && !actionIds.has(destination.actionId)) {
Expand Down Expand Up @@ -470,7 +497,7 @@ function validateSource(source) {
itemIds.add(item.id);
assertBilingual({ ...item, id: `${capability.id}.${item.id}` }, 'title');
if (item.destination) {
validateDestination(item.destination, `${capability.id}.${item.id}`);
validateDestination(item.destination, capability.id, item.id);
if (
item.destination.kind === 'settings'
&& capability.destination.kind === 'settings'
Expand Down Expand Up @@ -691,9 +718,16 @@ function validateSource(source) {
throw new Error('User-facing capabilities may not silently ship as unsupported Agent controls');
}

const semanticSettings = new Set(source.capabilities
const projectedSettingsDestinations = source.capabilities
.filter(({ kind }) => kind === 'setting')
.map(({ destination }) => destination.kind === 'settings' ? destination.pageId : null)
.flatMap((capability) => [
projectSettingsCatalogDestination(capability.id, capability.destination),
...capability.items.map((item) => projectSettingsCatalogDestination(
capability.id, item.destination ?? capability.destination, item.id,
)),
]);
const semanticSettings = new Set(projectedSettingsDestinations
.map((destination) => destination.kind === 'settings' ? destination.pageId : null)
.filter(Boolean));
const missingSettings = [...settingsIds].filter((id) => !semanticSettings.has(id));
const staleSettings = [...semanticSettings].filter((id) => !settingsIds.has(id));
Expand All @@ -704,10 +738,9 @@ function validateSource(source) {
].filter(Boolean).join('\n'));
}

const semanticSettingsViews = new Set(source.capabilities.flatMap((capability) =>
capability.items
.map(({ destination }) => destination?.kind === 'settings' ? destination.viewId : null)
.filter(Boolean)));
const semanticSettingsViews = new Set(projectedSettingsDestinations
.map((destination) => destination.kind === 'settings' ? destination.viewId : null)
.filter(Boolean));
const missingSettingsViews = [...settingsViewIds].filter((id) => !semanticSettingsViews.has(id));
const staleSettingsViews = [...semanticSettingsViews].filter((id) => !settingsViewIds.has(id));
if (missingSettingsViews.length || staleSettingsViews.length) {
Expand Down Expand Up @@ -1034,7 +1067,7 @@ function loadOwnerDefinitions() {
const result = spawnSync(
'cargo',
['run', '--quiet', '-p', 'openbitfun-product-domains', '--bin', PRODUCT_CONTROL_OWNER_EXPORT],
{ cwd: REPO_ROOT, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 },
{ cwd: REPO_ROOT, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, windowsHide: true },
);
if (result.status !== 0) {
throw new Error([
Expand Down Expand Up @@ -1067,7 +1100,7 @@ export function loadRemoteSurfaceRegistry() {
const result = spawnSync(
'cargo',
['run', '--quiet', '-p', 'openbitfun-product-domains', '--bin', REMOTE_SURFACE_EXPORT],
{ cwd: REPO_ROOT, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 },
{ cwd: REPO_ROOT, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, windowsHide: true },
);
if (result.status !== 0) {
throw new Error([
Expand Down
2 changes: 1 addition & 1 deletion scripts/i18n-dynamic-key-allowlist.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
"id": "web-editor-option-metadata",
"surface": "web-ui",
"namespace": "settings/editor",
"owner": "src/web-ui/src/infrastructure/config/components/EditorConfig.tsx",
"owner": "src/web-ui/src/app/scenes/settings/pages/development/EditorSettingsPage.tsx",
"description": "Editor option descriptors store labelKey metadata resolved with the settings/editor namespace.",
"sourceReferences": [
"labelKey: 'appearance.cursorStyles.line'",
Expand Down
25 changes: 24 additions & 1 deletion scripts/interactive-capabilities.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,33 @@ import {
buildCapabilityCatalog,
loadRemoteSurfaceRegistry,
parseRegisteredCommands,
projectSettingsCatalogDestination,
renderRemoteSurfaceTsBindings,
} from './generate-interactive-capabilities.mjs';

const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const read = (relativePath) => readFile(path.join(repositoryRoot, relativePath), 'utf8');

test('historical settings wire targets resolve through the current presentation migration', () => {
const { runtimeCatalog } = buildCapabilityCatalog();
const execution = runtimeCatalog.capabilities.find(({ id }) => id === 'setting.tools.execution');
const permissions = execution.items.find(({ id }) => id === 'permission-mode');
assert.deepEqual(permissions.destination, { kind: 'settings', pageId: 'tools.execution', viewId: 'common' });
assert.deepEqual(projectSettingsCatalogDestination(execution.id, permissions.destination, permissions.id), {
kind: 'settings', pageId: 'ai.permissions',
});
const automation = runtimeCatalog.capabilities.find(({ id }) => id === 'setting.tools.automation');
const hooks = automation.items.find(({ id }) => id === 'hooks-enabled');
assert.deepEqual(hooks.destination, { kind: 'settings', pageId: 'tools.automation', viewId: 'hooks' });
assert.deepEqual(projectSettingsCatalogDestination(automation.id, hooks.destination, hooks.id), {
kind: 'settings', pageId: 'tools.automation', sectionId: 'hooks',
});
const computerUse = runtimeCatalog.capabilities.find(({ id }) => id === 'feature.computer-use');
assert.deepEqual(projectSettingsCatalogDestination(computerUse.id, computerUse.destination), {
kind: 'settings', pageId: 'tools.desktop-control',
});
});

test('the public contract is a compact feature-and-settings manual', () => {
const { publicCatalog, runtimeCatalog } = buildCapabilityCatalog();
const featureCount = publicCatalog.capabilities.filter(({ kind }) => kind === 'feature').length;
Expand Down Expand Up @@ -352,7 +373,9 @@ test('docs, runtime, and technical views are generated projections of one semant
assert.ok(interactionAudit.files.some(({ sourceFile }) =>
sourceFile.endsWith('/AssistantDefaultsPage.tsx')));
assert.ok(interactionAudit.files.some(({ sourceFile }) =>
sourceFile.endsWith('/AppearanceSettingsPage.tsx')));
sourceFile.endsWith('/LanguageSettingsSection.tsx')));
assert.ok(interactionAudit.files.some(({ sourceFile }) =>
sourceFile.endsWith('/AppearancePackageConfigSection.tsx')));
assert.equal(publicCatalog.source, 'src/shared/interactive-capabilities/catalog.json');

const appearance = runtimeCatalog.capabilities
Expand Down
18 changes: 18 additions & 0 deletions src/crates/adapters/ai-adapters/src/client/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,21 @@ impl ApiFormat {
}
}
}

#[cfg(test)]
mod tests {
use super::ApiFormat;

#[test]
fn gemini_api_key_format_is_distinct_from_code_assist() {
for format in ["gemini", "google", " GEMINI "] {
assert_eq!(ApiFormat::parse(format).unwrap(), ApiFormat::Gemini);
}
for format in ["gemini-code-assist", "gemini_code_assist", "code-assist"] {
assert_eq!(
ApiFormat::parse(format).unwrap(),
ApiFormat::GeminiCodeAssist
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ pub(crate) async fn list_models(client: &AIClient) -> Result<Vec<RemoteModelInfo
let url = resolve_models_url(client);
debug!("Gemini models list URL: {}", url);

let response = apply_headers(client, client.client.get(&url))
let response = apply_headers(client, client.client.get(&url), &url)
.send()
.await?
.error_for_status()?;
Expand Down
Loading
Loading