// ***************************************************************************** // Copyright (C) 2025 EclipseSource GmbH. // // 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 { ChatWelcomeMessageProvider } from '@theia/ai-chat-ui/lib/browser/chat-tree-view'; import * as React from '@theia/core/shared/react'; import { nls } from '@theia/core/lib/common/nls'; import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { codicon, LocalizedMarkdown, MarkdownRenderer } from '@theia/core/lib/browser'; import { CommandRegistry, DisposableCollection, Emitter, Event, PreferenceScope } from '@theia/core'; import { AgentService, FrontendLanguageModelRegistry } from '@theia/ai-core/lib/common'; import { PreferenceService } from '@theia/core/lib/common'; import { DEFAULT_CHAT_AGENT_PREF, BYPASS_MODEL_REQUIREMENT_PREF, PERSISTED_SESSION_LIMIT_PREF, SESSION_STORAGE_PREF } from '@theia/ai-chat/lib/common/ai-chat-preferences'; import { ChatAgentRecommendationService, ChatAgentService, ChatService } from '@theia/ai-chat/lib/common'; import { ToolConfirmationManager } from '@theia/ai-chat/lib/browser/chat-tool-preference-bindings'; import { DEFAULT_TOOL_CONFIRMATION_PREFERENCE, TOOL_CONFIRMATION_PREFERENCE, ToolConfirmationMode } from '@theia/ai-chat/lib/common/chat-tool-preferences'; import { OPEN_AI_CONFIG_VIEW, OPEN_AI_CONFIG_VIEW_TOOLS } from './ai-configuration/ai-configuration-view-contribution'; import { AIActivationService } from '@theia/ai-core/lib/browser'; import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; import { WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands'; const TheiaIdeAiLogo = ({ width = 120, height = 120, className = '' }) => {/* Head: outline only */} {/* Antenna */} {/* Ears: small strokes */} {/* Eyes: small dots */} {/* Mouth: subtle line */} {/* Chat dots */} ; @injectable() export class IdeChatWelcomeMessageProvider implements ChatWelcomeMessageProvider { readonly priority = 100; @inject(MarkdownRenderer) protected readonly markdownRenderer: MarkdownRenderer; @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry; @inject(FrontendLanguageModelRegistry) protected languageModelRegistry: FrontendLanguageModelRegistry; @inject(PreferenceService) protected preferenceService: PreferenceService; @inject(ChatAgentRecommendationService) protected recommendationService: ChatAgentRecommendationService; @inject(ChatAgentService) protected chatAgentService: ChatAgentService; @inject(AgentService) protected agentService: AgentService; @inject(ToolConfirmationManager) protected readonly toolConfirmationManager: ToolConfirmationManager; @inject(AIActivationService) protected readonly activationService: AIActivationService; @inject(ChatService) protected readonly chatService: ChatService; protected readonly toDispose = new DisposableCollection(); protected _hasReadyModels = false; protected _modelRequirementBypassed = false; protected _defaultAgent = ''; protected modelConfig: { hasModels: boolean; errorMessages: string[] } | undefined; /** True once the user has any chat (active or persisted). Drives the compact "has-chats" welcome variant. */ protected hasAnyChat = false; protected readonly onStateChangedEmitter = new Emitter(); get onStateChanged(): Event { return this.onStateChangedEmitter.event; } @postConstruct() protected init(): void { this.checkLanguageModelStatus(); this.toDispose.push( this.languageModelRegistry.onChange(() => { this.checkLanguageModelStatus(); }) ); this.toDispose.push( this.preferenceService.onPreferenceChanged(e => { if (e.preferenceName === DEFAULT_CHAT_AGENT_PREF) { const effectiveValue = this.preferenceService.get(DEFAULT_CHAT_AGENT_PREF, ''); if (this._defaultAgent !== effectiveValue) { this._defaultAgent = effectiveValue; this.notifyStateChanged(); } } else if (e.preferenceName === BYPASS_MODEL_REQUIREMENT_PREF) { const effectiveValue = this.preferenceService.get(BYPASS_MODEL_REQUIREMENT_PREF, false); if (this._modelRequirementBypassed !== effectiveValue) { this._modelRequirementBypassed = effectiveValue; this.notifyStateChanged(); } } else if (e.preferenceName === DEFAULT_TOOL_CONFIRMATION_PREFERENCE || e.preferenceName === TOOL_CONFIRMATION_PREFERENCE) { // Re-render so the tool-confirmation explainer is hidden once the user configures confirmation behavior. this.notifyStateChanged(); } else if (e.preferenceName === SESSION_STORAGE_PREF || e.preferenceName === PERSISTED_SESSION_LIMIT_PREF) { this.refreshHasAnyChat(); } }) ); this.toDispose.push( this.chatService.onSessionEvent(() => this.refreshHasAnyChat()) ); this.refreshHasAnyChat(); this.toDispose.push( this.agentService.onDidChangeAgents(() => { this.notifyStateChanged(); }) ); this.analyzeModelConfiguration().then(config => { this.modelConfig = config; this.notifyStateChanged(); }); this.preferenceService.ready.then(() => { const defaultAgentValue = this.preferenceService.get(DEFAULT_CHAT_AGENT_PREF, ''); const bypassValue = this.preferenceService.get(BYPASS_MODEL_REQUIREMENT_PREF, false); this._defaultAgent = defaultAgentValue; this._modelRequirementBypassed = bypassValue; this.notifyStateChanged(); }); // Listen to both canRun and activeStatus changes. They may change independent from each other. this.toDispose.push( this.activationService.onDidChangeCanRun(() => { this.notifyStateChanged(); }) ); this.toDispose.push( this.activationService.onDidChangeActiveStatus(() => { this.notifyStateChanged(); }) ); } protected async checkLanguageModelStatus(): Promise { const models = await this.languageModelRegistry.getLanguageModels(); this._hasReadyModels = models.some(model => model.status.status === 'ready'); this.modelConfig = await this.analyzeModelConfiguration(); this.notifyStateChanged(); } protected async analyzeModelConfiguration(): Promise<{ hasModels: boolean; errorMessages: string[] }> { const models = await this.languageModelRegistry.getLanguageModels(); const hasModels = models.length > 0; const unavailableModels = models.filter(model => model.status.status === 'unavailable'); const errorMessages = unavailableModels .map(model => model.status.message) .filter((msg): msg is string => !!msg); const uniqueErrorMessages = [...new Set(errorMessages)]; return { hasModels, errorMessages: uniqueErrorMessages }; } protected notifyStateChanged(): void { this.onStateChangedEmitter.fire(); } /** * Recomputes {@link hasAnyChat} (true when the user has any titled active session or any * persisted session on disk). Fires a state change when the flag flips so the welcome * variant updates between the rich onboarding (no chats yet) and the compact mode. */ protected async refreshHasAnyChat(): Promise { const hasTitledActive = this.chatService.getSessions().some(s => !!s.title); let hasPersisted = false; if (!hasTitledActive) { try { hasPersisted = await this.chatService.hasPersistedSessions(); } catch { hasPersisted = false; } } const next = hasTitledActive || hasPersisted; if (this.hasAnyChat !== next) { this.hasAnyChat = next; this.notifyStateChanged(); } } get hasReadyModels(): boolean { return this._hasReadyModels; } get modelRequirementBypassed(): boolean { return this._modelRequirementBypassed; } get defaultAgent(): string { return this._defaultAgent; } /** * Whether to show the tool-confirmation explainer on the welcome screen. * * Only shown while the user is still in the default state, i.e. every tool call is confirmed and * no per-tool overrides exist. Once the user has changed the default mode (e.g. to always allow * or to disable tools) or pre-approved individual tools (including via bulk approval), they are * already aware of the mechanism, so the explainer is suppressed. */ protected get shouldShowToolConfirmationInfo(): boolean { return this.toolConfirmationManager.getDefaultConfirmationMode() === ToolConfirmationMode.CONFIRM && Object.keys(this.toolConfirmationManager.getAllConfirmationSettings()).length === 0; } protected setModelRequirementBypassed(bypassed: boolean): void { this.preferenceService.set(BYPASS_MODEL_REQUIREMENT_PREF, bypassed, PreferenceScope.User); } protected setDefaultAgent(agentId: string): void { this.preferenceService.set(DEFAULT_CHAT_AGENT_PREF, agentId, PreferenceScope.User); } dispose(): void { this.toDispose.dispose(); this.onStateChangedEmitter.dispose(); } renderWelcomeMessage(): React.ReactNode { if (!this._hasReadyModels && !this._modelRequirementBypassed) { return this.renderModelConfigurationScreen(); } if (!this._defaultAgent) { return this.renderAgentSelectionScreen(); } return this.renderWelcomeScreen(); } protected renderWelcomeScreen(): React.ReactNode { if (this.hasAnyChat) { // Returning user: keep a slim banner (logo + heading + one-line tip) above the // sessions list so the chat view still has identity, but skip the verbose // multi-paragraph tutorial. return ' ]} markdownRenderer={this.markdownRenderer} className="theia-WelcomeMessage-Content" markdownOptions={{ supportHtml: true }} /> {this.renderToolConfirmationAlert()} ; } // First-time user: original full-length onboarding with the larger logo and the // verbose agent/context paragraphs so the empty panel feels welcoming. return ', FrontendApplicationConfigProvider.get().applicationName]} markdownRenderer={this.markdownRenderer} className="theia-WelcomeMessage-Content" markdownOptions={{ supportHtml: true }} /> {this.renderToolConfirmationAlert()} ; } protected renderToolConfirmationAlert(): React.ReactNode { if (!this.shouldShowToolConfirmationInfo) { return undefined; } return ( {nls.localize('theia/ai/ide/toolConfirmationInfo/header', 'Tool confirmation')} ); } protected renderModelConfigurationScreen(): React.ReactNode { const config = this.modelConfig ?? { hasModels: false, errorMessages: [] }; const { hasModels, errorMessages } = config; if (!hasModels) { return ⚠️ this.setModelRequirementBypassed(true)}> {nls.localize('theia/ai/ide/continueAnyway', 'Continue Anyway')} {nls.localize('theia/ai/ide/bypassHint', 'Some agents like Claude Code don\'t require Theia Language Models')} ; } return {errorMessages.length > 0 && ( {nls.localize('theia/ai/ide/configurationState', 'Configuration issues')} {errorMessages.map((msg, idx) => {msg})} )} this.commandRegistry.executeCommand(OPEN_AI_CONFIG_VIEW.id)}> {nls.localize('theia/ai/ide/openAiConfiguration', 'Open AI Configuration')} this.setModelRequirementBypassed(true)}> {nls.localize('theia/ai/ide/continueAnyway', 'Continue Anyway')} ; } protected renderAgentSelectionScreen(): React.ReactNode { const recommendedAgents = this.recommendationService.getRecommendedAgents() .filter(agent => this.chatAgentService.getAgent(agent.id) !== undefined); return {recommendedAgents.length > 0 ? ( {recommendedAgents.map(agent => ( this.setDefaultAgent(agent.id)} title={agent.description}> {agent.label} ))} ) : ( {nls.localize('theia/ai/ide/noRecommendedAgents', 'No recommended agents are available.')} )} {recommendedAgents.length > 0 ? nls.localize('theia/ai/ide/moreAgentsAvailable/header', 'More agents are available') : nls.localize('theia/ai/ide/configureAgent/header', 'Configure a default agent')} ; } renderDisabledMessage(): React.ReactNode { if (this.activationService.isActive && !this.activationService.canRun) { return this.renderTrustRestrictedMessage(); } return this.renderPreferenceDisabledMessage(); } protected renderTrustRestrictedMessage(): React.ReactNode { return {nls.localize('theia/ai/ide/chatRestrictedMessage/title', 'AI Features are Restricted')} {nls.localizeByDefault('Restricted Mode')} this.commandRegistry.executeCommand(WorkspaceCommands.MANAGE_WORKSPACE_TRUST.id)}> {nls.localizeByDefault('Manage Workspace Trust')} ; } protected renderPreferenceDisabledMessage(): React.ReactNode { const openAiHistory = 'aiHistory:open'; return {nls.localize('theia/ai/ide/chatDisabledMessage/title', 'AI Features are Disabled')} {nls.localize('theia/ai/ide/howToGetStarted', 'How to get started')} this.commandRegistry.executeCommand(OPEN_AI_CONFIG_VIEW.id)}> {nls.localize('theia/ai/ide/openAiConfiguration', 'Open AI Configuration')} ; } }
{nls.localize('theia/ai/ide/noRecommendedAgents', 'No recommended agents are available.')}