// ***************************************************************************** // Copyright (C) 2026 EclipseSource and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import * as React from '@theia/core/shared/react'; import { nls, PreferenceService } from '@theia/core'; import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { codicon, Message, ReactWidget } from '@theia/core/lib/browser'; import { AiConfigurationService } from '@theia/ai-core'; import { AiConfigurationCategoryRegistry } from '@theia/ai-core-ui/lib/browser/ai-configuration/ai-configuration-category-registry'; import { AiConfigurationSelectionModel } from '@theia/ai-core-ui/lib/browser/ai-configuration/ai-configuration-selection-model'; import { AiConfigurationItemDetailHeader } from '@theia/ai-core-ui/lib/browser/ai-configuration/components/ai-configuration-primitives'; import { AiConfigurationCategory, AiConfigurationRenderContext, AiConfigurationScope, AiConfigurationSelection } from '@theia/ai-core-ui/lib/browser/ai-configuration/ai-configuration-category'; @injectable() export class AiConfigurationDetailWidget extends ReactWidget { static readonly ID = 'ai-configuration-detail'; /** Frames a pending search highlight waits for its target row to be mounted before it is dropped. */ static readonly HIGHLIGHT_RETRY_FRAMES = 10; @inject(AiConfigurationCategoryRegistry) protected readonly registry: AiConfigurationCategoryRegistry; @inject(AiConfigurationSelectionModel) protected readonly selectionModel: AiConfigurationSelectionModel; @inject(AiConfigurationService) protected readonly aiConfigurationService: AiConfigurationService; @inject(PreferenceService) protected readonly preferenceService: PreferenceService; /** Scope every row on the page reads from and writes to; per-scope editing is not wired up yet. */ protected scope: AiConfigurationScope = 'user'; protected resetScroll = false; protected pendingHighlight?: AiConfigurationSelection['highlight']; /** Frames a pending highlight has already waited for its target row; see {@link applyScrollAndHighlight}. */ protected pendingHighlightFrames = 0; @postConstruct() protected init(): void { this.id = AiConfigurationDetailWidget.ID; this.addClass('ai-configuration-detail-widget'); // The page scrolls in its own inner `.ai-configuration-detail-body` (driven by // `applyScrollAndHighlight`). Clear the default `ReactWidget` scroll options so that // `BaseWidget` does not attach a PerfectScrollbar to the outer node, whose forced // `overflow: hidden` and wheel handling would otherwise swallow the inner body's scroll. this.scrollOptions = undefined; this.toDispose.push(this.selectionModel.onDidChangeSelection(() => this.onSelectionChanged())); this.toDispose.push(this.registry.onDidChange(() => this.update())); // Also covers the workspace-trust transitions this service reports, which are not preference changes. this.toDispose.push(this.aiConfigurationService.onDidChange(() => this.update())); // Reflect preference changes made outside the current render pass — most notably the gear menu's // Reset command, which runs as a command and so never calls `ctx.update()`. Listening here rather // than only to `AiConfigurationService.onDidChange` (scoped to `ai-features.*`) keeps contributed // categories up to date too: their preferences carry no AI prefix, so a reset of one used to leave // the row's `modified` state stale until the page was re-rendered for some other reason. this.toDispose.push(this.preferenceService.onPreferenceChanged(() => this.update())); this.update(); } protected onSelectionChanged(): void { this.resetScroll = true; this.pendingHighlight = this.selectionModel.getSelection()?.highlight; this.pendingHighlightFrames = 0; this.update(); } protected createRenderContext(): AiConfigurationRenderContext { return { scope: this.scope, navigate: selection => this.selectionModel.select(selection), update: () => this.update() }; } protected render(): React.ReactNode { const selection = this.selectionModel.getSelection(); const category = selection && this.registry.getCategory(selection.categoryId); if (!selection || !category) { return this.renderPlaceholder(); } const ctx = this.createRenderContext(); // The category/item path is shown as real Theia breadcrumbs in the shell tab bar // (see AiConfigurationBreadcrumbsContribution), so no in-pane breadcrumb is rendered here. return