diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnectionTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnectionTests.java new file mode 100644 index 00000000..3c4f881d --- /dev/null +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnectionTests.java @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package com.microsoft.copilot.eclipse.core.lsp; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.lsp4e.LanguageServerWrapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class CopilotLanguageServerConnectionTests { + + @Mock + private LanguageServerWrapper languageServerWrapper; + + private CopilotLanguageServerConnection connection; + + @BeforeEach + void setUp() { + connection = new CopilotLanguageServerConnection(languageServerWrapper); + } + + @Test + void testCreateConversation_noActiveModel_failsWithoutRequest() { + CompletableFuture result = connection.createConversation("token", "hello", List.of(), null, null, null, null, + null, null, "Ask", null, null, null, null, null, null, null); + + assertTrue(result.isCompletedExceptionally()); + verify(languageServerWrapper, never()).execute(any()); + } + + @Test + void testAddConversationTurn_noActiveModel_failsWithoutRequest() { + CompletableFuture result = connection.addConversationTurn("token", "conversation", "hello", List.of(), null, + null, null, null, null, "Ask", null, null, null, null, null); + + assertTrue(result.isCompletedExceptionally()); + verify(languageServerWrapper, never()).execute(any()); + } +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java index 08f27df0..ee65010f 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java @@ -273,6 +273,9 @@ public CompletableFuture createConversation(String workDoneTok String reasoningEffort, Integer contextSize, String chatModeName, String customChatModeId, List todos, String agentSlug, String agentJobWorkspaceFolder, String conversationId, String restoreToTurnId, List workspaceFolders) { + if (activeModel == null) { + return noActiveModelFailure(); + } boolean supportVision = activeModel.getCapabilities().supports().vision(); Either> messageWithImages = ChatMessageUtils @@ -331,6 +334,9 @@ public CompletableFuture addConversationTurn(String workDoneToke String reasoningEffort, Integer contextSize, String chatModeName, String customChatModeId, List todoList, String agentSlug, String agentJobWorkspaceFolder, List workspaceFolders) { + if (activeModel == null) { + return noActiveModelFailure(); + } boolean supportVision = activeModel.getCapabilities().supports().vision(); Either> messageWithImages = ChatMessageUtils @@ -732,6 +738,11 @@ public void stop() { this.languageServerWrapper.stop(); } + private static CompletableFuture noActiveModelFailure() { + // The language server rejects chat requests without a model. + return CompletableFuture.failedFuture(new IllegalStateException("No active chat model available")); + } + private String getModelName(CopilotModel activeModel) { return activeModel == null ? null : activeModel.isChatFallback() ? activeModel.getId() : activeModel.getModelFamily(); diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/chat/ActionBarTest.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/chat/ActionBarTest.java new file mode 100644 index 00000000..aaa56444 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/chat/ActionBarTest.java @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package com.microsoft.copilot.eclipse.ui.chat; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mockStatic; + +import java.lang.reflect.Field; +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.microsoft.copilot.eclipse.core.CopilotCore; +import com.microsoft.copilot.eclipse.core.FeatureFlags; +import com.microsoft.copilot.eclipse.core.lsp.protocol.ChatMode; +import com.microsoft.copilot.eclipse.ui.CopilotUi; +import com.microsoft.copilot.eclipse.ui.chat.contextwindow.ContextWindowService; +import com.microsoft.copilot.eclipse.ui.chat.services.AgentToolService; +import com.microsoft.copilot.eclipse.ui.chat.services.ChatFontService; +import com.microsoft.copilot.eclipse.ui.chat.services.ChatServiceManager; +import com.microsoft.copilot.eclipse.ui.chat.services.McpConfigService; +import com.microsoft.copilot.eclipse.ui.chat.services.ModelService; +import com.microsoft.copilot.eclipse.ui.chat.services.ReferencedFileService; +import com.microsoft.copilot.eclipse.ui.chat.services.UserPreferenceService; +import com.microsoft.copilot.eclipse.ui.chat.tools.JavaDebuggerToolAdapter; +import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; + +@ExtendWith(MockitoExtension.class) +class ActionBarTest { + + @Mock + private UserPreferenceService mockUserPreferenceService; + @Mock + private ModelService mockModelService; + @Mock + private ChatServiceManager mockChatServiceManager; + @Mock + private ReferencedFileService mockReferencedFileService; + @Mock + private CopilotUi mockCopilotUi; + @Mock + private McpConfigService mockMcpConfigService; + @Mock + private AgentToolService mockAgentToolService; + @Mock + private ChatFontService mockChatFontService; + @Mock + private ContextWindowService mockContextWindowService; + + private Shell shell; + private ActionBar actionBar; + private MockedStatic mockedCopilotUi; + private FeatureFlags featureFlags; + private boolean previewFeaturesEnabled; + + @BeforeEach + void setUp() { + featureFlags = CopilotCore.getPlugin().getFeatureFlags(); + previewFeaturesEnabled = featureFlags.isClientPreviewFeatureEnabled(); + featureFlags.setClientPreviewFeatureEnabled(true); + + mockedCopilotUi = mockStatic(CopilotUi.class); + mockedCopilotUi.when(CopilotUi::getPlugin).thenReturn(mockCopilotUi); + lenient().when(mockCopilotUi.getChatServiceManager()).thenReturn(mockChatServiceManager); + lenient().when(mockChatServiceManager.getUserPreferenceService()).thenReturn(mockUserPreferenceService); + lenient().when(mockChatServiceManager.getModelService()).thenReturn(mockModelService); + lenient().when(mockChatServiceManager.getReferencedFileService()).thenReturn(mockReferencedFileService); + lenient().when(mockChatServiceManager.getMcpConfigService()).thenReturn(mockMcpConfigService); + lenient().when(mockChatServiceManager.getAgentToolService()).thenReturn(mockAgentToolService); + lenient().when(mockChatServiceManager.getChatFontService()).thenReturn(mockChatFontService); + lenient().when(mockChatServiceManager.getContextWindowService()).thenReturn(mockContextWindowService); + lenient().when(mockAgentToolService.getTool(JavaDebuggerToolAdapter.TOOL_NAME)).thenReturn(null); + lenient().when(mockUserPreferenceService.getActiveChatMode()).thenReturn(ChatMode.Ask); + + runOnDisplayThread(() -> { + shell = new Shell(Display.getDefault()); + actionBar = new ActionBar(shell, SWT.NONE, mockChatServiceManager); + }); + } + + @AfterEach + void tearDown() { + SwtUtils.invokeOnDisplayThread(() -> { + if (shell != null && !shell.isDisposed()) { + shell.dispose(); + } + }); + mockedCopilotUi.close(); + featureFlags.setClientPreviewFeatureEnabled(previewFeaturesEnabled); + } + + @Test + void testUpdateButtonsLayout_noModelWithText_sendButtonsDisabled() { + AtomicReference sendEnabled = new AtomicReference<>(); + AtomicReference delegateEnabled = new AtomicReference<>(); + runOnDisplayThread(() -> { + actionBar.setInputTextViewerContent("hello"); + + // Recreates the send buttons, e.g. on chat mode switch + actionBar.updateButtonsLayout(); + sendEnabled.set(getButton("btnMsgToggle").getEnabled()); + delegateEnabled.set(getButton("sendToJobButton").getEnabled()); + }); + + assertFalse(sendEnabled.get(), "Send must be disabled without a model"); + assertFalse(delegateEnabled.get(), "Delegate must be disabled without a model"); + } + + // The workbench's syncExec swallows exceptions, so rethrow them on the test thread + private static void runOnDisplayThread(Runnable runnable) { + AtomicReference error = new AtomicReference<>(); + SwtUtils.invokeOnDisplayThread(() -> { + try { + runnable.run(); + } catch (Throwable t) { + error.set(t); + } + }); + assertNull(error.get(), () -> "Unexpected error on display thread: " + error.get()); + } + + private Button getButton(String fieldName) { + try { + Field field = ActionBar.class.getDeclaredField(fieldName); + field.setAccessible(true); + return (Button) field.get(actionBar); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Failed to read " + fieldName, e); + } + } +} diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/chat/services/ModelServiceTests.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/chat/services/ModelServiceTests.java index 1626af9a..703c4d3e 100644 --- a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/chat/services/ModelServiceTests.java +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/chat/services/ModelServiceTests.java @@ -9,6 +9,10 @@ import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.IOException; @@ -42,6 +46,7 @@ import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotModel; import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotScope; import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokListModelResponse; +import com.microsoft.copilot.eclipse.ui.chat.ActionBar; @ExtendWith(MockitoExtension.class) class ModelServiceTests { @@ -231,6 +236,23 @@ void testSetActiveModel_AlreadyActiveModelDoesNotPersistPreference() throws Inte assertFalse(Files.exists(getPreferenceFile())); } + @Test + void testBindActionBarForModelAvailability_modelsPending_enablesSendOnlyAfterModelSet() { + CopilotModel defaultModel = createModel("gpt-4o", "GPT-4o", true); + CompletableFuture copilotModels = new CompletableFuture<>(); + when(lsConnection.listModels()).thenReturn(copilotModels); + ActionBar actionBar = mock(ActionBar.class); + + modelService = new ModelService(lsConnection, authStatusManager); + modelService.bindActionBarForModelAvailability(actionBar); + + verify(actionBar, timeout(5000)).setModelAvailable(false); + verify(actionBar, never()).setModelAvailable(true); + + copilotModels.complete(new CopilotModel[] { defaultModel }); + verify(actionBar, timeout(5000)).setModelAvailable(true); + } + private static CopilotModel createModel(String id, String name, boolean isChatDefault) { CopilotModel model = new CopilotModel(); model.setId(id); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ActionBar.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ActionBar.java index ed5a5d9c..705e8ec1 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ActionBar.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ActionBar.java @@ -98,6 +98,7 @@ public class ActionBar extends Composite implements NewConversationListener { private Image sendImage; private Image sendDisabledImage; private boolean isSendButton = true; + private boolean modelAvailable; private LinkedHashSet messageListeners = new LinkedHashSet<>(); private Button mcpToolButton; private Image mcpToolImage; @@ -206,6 +207,7 @@ public ActionBar(Composite parent, int style, ChatServiceManager chatServiceMana ModelService modelService = chatServiceManager.getModelService(); modelService.bindActionBarForSupportVisionChange(this); + modelService.bindActionBarForModelAvailability(this); ChatInputTextViewer tv = new ChatInputTextViewer(borderedActionBar, chatServiceManager); tv.setEditable(true); @@ -229,15 +231,11 @@ public void textChanged(TextEvent event) { if (!isSendButton) { return; } - if (tv.getDocument().get().equals(StringUtils.EMPTY)) { - updateButtonState(SendOrCancelButtonStates.SEND_DISABLED); - } else { - updateButtonState(SendOrCancelButtonStates.SEND_ENABLED); - } + refreshSendButtonState(); } }); tv.setSendMessageHandler((message) -> { - if (isSendButton) { + if (isSendButton && modelAvailable) { handleSendMessage(); } }); @@ -424,9 +422,9 @@ public void updateButtonsLayout() { this.sendToJobButton = UiUtils.createIconButton(this.bottomRightButtonsComposite, SWT.PUSH | SWT.FLAT); - boolean hasText = !StringUtils.isBlank(this.inputTextViewer.getContent()); - this.sendToJobButton.setEnabled(hasText); - this.sendToJobButton.setImage(hasText ? sendToJobImage : sendToJobDisabledImage); + boolean isEnabled = canSend(); + this.sendToJobButton.setEnabled(isEnabled); + this.sendToJobButton.setImage(isEnabled ? sendToJobImage : sendToJobDisabledImage); this.sendToJobButton.setToolTipText(Messages.chat_actionBar_sendToJobButton_Tooltip); AccessibilityUtils.addAccessibilityNameForUiComponent(this.sendToJobButton, Messages.chat_actionBar_sendToJobButton_Tooltip); @@ -448,7 +446,7 @@ public void widgetSelected(SelectionEvent e) { this.sendImage = CopilotImages.getImage(CopilotImages.IMG_CHAT_SEND); this.sendDisabledImage = CopilotImages.getImage(CopilotImages.IMG_CHAT_SEND_DISABLED); this.btnMsgToggle = UiUtils.createIconButton(bottomRightButtonsComposite, SWT.PUSH | SWT.FLAT); - boolean isEnabled = !StringUtils.isBlank(this.inputTextViewer.getContent()); + boolean isEnabled = canSend(); this.btnMsgToggle.setEnabled(isEnabled); this.btnMsgToggle.setImage(isEnabled ? this.sendImage : this.sendDisabledImage); this.btnMsgToggle.setToolTipText(Messages.chat_actionBar_sendButton_Tooltip); @@ -713,12 +711,38 @@ public void onNewConversation() { * Handles the cancel message event. */ public void resetSendButton() { - if (this.inputTextViewer.getContent().isEmpty()) { - updateButtonState(SendOrCancelButtonStates.SEND_DISABLED); - } else { + refreshSendButtonState(); + this.chatServiceManager.getFileToolService().setWorkingSetBarButtonStatus(true); + } + + /** + * Updates whether a chat model is available. Sending is disabled while no model is available. + * + * @param available true if an active model is set + */ + public void setModelAvailable(boolean available) { + SwtUtils.invokeOnDisplayThreadAsync(() -> { + if (isDisposed()) { + return; + } + modelAvailable = available; + if (isSendButton) { + refreshSendButtonState(); + } + refreshPlaceholder(); + }, this); + } + + private void refreshSendButtonState() { + if (canSend()) { updateButtonState(SendOrCancelButtonStates.SEND_ENABLED); + } else { + updateButtonState(SendOrCancelButtonStates.SEND_DISABLED); } - this.chatServiceManager.getFileToolService().setWorkingSetBarButtonStatus(true); + } + + private boolean canSend() { + return modelAvailable && !StringUtils.isBlank(this.inputTextViewer.getContent()); } /** @@ -794,11 +818,7 @@ private void handleSendToJob() { notifySendWithSlug(workDoneToken, message, UiConstants.GITHUB_COPILOT_CODING_AGENT_SLUG, selectedProjectPath); } else { // Dialog was cancelled, reset button state based on current input content - if (this.inputTextViewer.getContent().isEmpty()) { - updateButtonState(SendOrCancelButtonStates.SEND_DISABLED); - } else { - updateButtonState(SendOrCancelButtonStates.SEND_ENABLED); - } + refreshSendButtonState(); } } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ChatInputTextViewer.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ChatInputTextViewer.java index adec34dc..f1d8558d 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ChatInputTextViewer.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ChatInputTextViewer.java @@ -355,6 +355,9 @@ private void handleSendMessage() { } private String getPlaceholderText() { + if (!chatServiceManager.getModelService().hasActiveModel()) { + return Messages.chat_actionBar_noModel_placeholder; + } switch (userPreferenceService.getActiveChatMode()) { case Agent: // Get the active mode name or ID from observable (not from disk to avoid stale data) diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ChatView.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ChatView.java index fdea9af1..26c9747a 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ChatView.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ChatView.java @@ -1027,6 +1027,16 @@ public void setFocus() { private void onSendInternal(String workDoneToken, String message, String agentSlug, String agentJobWorkspaceFolder, boolean createNewTurn) { + if (!chatServiceManager.getModelService().hasActiveModel()) { + // Send is disabled without a model; keep the input if a request slips through. + CopilotCore.LOGGER.info("Chat message not sent: no active model available"); + SwtUtils.invokeOnDisplayThread(() -> { + actionBar.setInputTextViewerContent(message); + actionBar.resetSendButton(); + }, parent); + return; + } + // Persist the user input to history chatServiceManager.getUserPreferenceService().addInputToHistory(message); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ReferencedFile.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ReferencedFile.java index 050d5d48..c6bd9f5d 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ReferencedFile.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ReferencedFile.java @@ -27,6 +27,7 @@ import org.eclipse.ui.model.WorkbenchLabelProvider; import com.microsoft.copilot.eclipse.core.Constants; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotModel; import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.chat.services.ReferencedFileService; @@ -198,9 +199,9 @@ private void setupUnsupportedFileDisplay() { // Set warning icon lblfileIcon.setImage(CopilotImages.getSharedImage(ISharedImages.IMG_OBJS_WARN_TSK)); // Set tooltip with model name - String modelName = CopilotUi.getPlugin().getChatServiceManager().getModelService().getActiveModel() - .getModelName(); - String tooltipText = String.format(Messages.chat_referencedFile_noVision_tooltip, modelName); + CopilotModel activeModel = CopilotUi.getPlugin().getChatServiceManager().getModelService().getActiveModel(); + String tooltipText = activeModel == null ? Messages.chat_referencedFile_noModel_tooltip + : String.format(Messages.chat_referencedFile_noVision_tooltip, activeModel.getModelName()); lblfileIcon.setToolTipText(tooltipText); lblFileName.setToolTipText(tooltipText); lblClose.setToolTipText(tooltipText); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ModelService.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ModelService.java index 88bcdf71..0cfe6fe2 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ModelService.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ModelService.java @@ -66,6 +66,7 @@ public class ModelService extends ChatBaseService { private final Map modelButtonSideEffects = new HashMap<>(); private ISideEffect actionBarSideEffect; + private ISideEffect modelAvailabilitySideEffect; // Event communication private IEventBroker eventBroker; @@ -403,6 +404,15 @@ public CopilotModel getActiveModel() { return activeModelObservable.getValue(); } + /** + * Check if an active model is set. Chat requests require one. + * + * @return true if an active model is set + */ + public boolean hasActiveModel() { + return getActiveModel() != null; + } + /** * Get all available models for the current chat mode. * @@ -792,6 +802,36 @@ public void unbindActionBarForSupportVisionChange(ActionBar actionBar) { } } + /** + * Bind the action bar to enable sending only while an active model is set. + * + * @param actionBar the action bar to bind + */ + public void bindActionBarForModelAvailability(ActionBar actionBar) { + unbindActionBarForModelAvailability(actionBar); + + ensureRealm(() -> { + modelAvailabilitySideEffect = ISideEffect.create(this::hasActiveModel, (Boolean available) -> { + if (!actionBar.isDisposed()) { + actionBar.setModelAvailable(available); + } + }); + actionBar.addDisposeListener(e -> unbindActionBarForModelAvailability(actionBar)); + }); + } + + /** + * Unbind the model availability side effect for a specific action bar. + * + * @param actionBar the action bar to unbind + */ + public void unbindActionBarForModelAvailability(ActionBar actionBar) { + if (modelAvailabilitySideEffect != null) { + modelAvailabilitySideEffect.dispose(); + modelAvailabilitySideEffect = null; + } + } + private void disposeAllSideEffects() { ensureRealm(() -> { for (ISideEffect[] effects : modelButtonSideEffects.values()) { diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java index 3f16e332..6a421e30 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java @@ -92,6 +92,7 @@ public final class Messages extends NLS { public static String chat_actionBar_initialContent; public static String chat_actionBar_initialContentForAgent; public static String chat_actionBar_sendButton_Tooltip; + public static String chat_actionBar_noModel_placeholder; public static String chat_actionBar_sendToJobButton_Tooltip; public static String chat_actionBar_sendToJob_noProject; public static String chat_actionBar_cancelButton_Tooltip; @@ -169,6 +170,7 @@ public final class Messages extends NLS { public static String chat_customModels; public static String chat_addPremiumModels; public static String chat_referencedFile_noVision_tooltip; + public static String chat_referencedFile_noModel_tooltip; public static String agent_tool_compareEditor_titlePrefix; public static String agent_tool_compareEditor_proposedChangesTitle; public static String agentFileEditor_contentAssist_statusMessage; diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties index f377e310..b3a92706 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties @@ -81,6 +81,7 @@ chat_topBanner_chatHistoryItem_currentConversation_label=(Current) chat_actionBar_initialContent=Ask Copilot chat_actionBar_initialContentForAgent=Edit files in your workspace in agent mode chat_actionBar_sendButton_Tooltip=Send +chat_actionBar_noModel_placeholder=Waiting for models to load... chat_actionBar_sendToJobButton_Tooltip=Delegate to coding agent chat_actionBar_sendToJob_noProject=No GitHub repository was found in the workspace. Please open a project that contains a Git repository to use the coding agent. chat_actionBar_cancelButton_Tooltip=Cancel @@ -171,6 +172,7 @@ chat_premiumModels=Premium Models chat_customModels=Custom Models chat_addPremiumModels=Add Premium Models chat_referencedFile_noVision_tooltip=%s does not support images. +chat_referencedFile_noModel_tooltip=Images can be attached once models are loaded. agent_tool_compareEditor_titlePrefix=Changes from GitHub Copilot: agent_tool_compareEditor_proposedChangesTitle=Proposed Changes