+ {/* Consistent header pattern with back button */}
+
+
+
+
+ {isOnboarding ? 'Other providers' : 'Provider Configuration Settings'}
+
+ {isOnboarding && (
+
+ Select an AI model provider to get started with goose. You'll need to use API keys
+ generated by each provider which will be encrypted and stored locally. You can
+ change your provider at any time in settings.
+
+ )}
- )}
-
- {/* Only show back button if not in onboarding mode */}
- {!isOnboarding &&
}
-
- {isOnboarding ? 'Configure your providers' : 'Provider Configuration Settings'}
-
- {isOnboarding && (
-
- Select an AI model provider to get started with goose. You'll need to use API keys
- generated by each provider which will be encrypted and stored locally. You can change
- your provider at any time in settings.
-
- )}
diff --git a/ui/desktop/src/styles/main.css b/ui/desktop/src/styles/main.css
index a329dabbc327..9aded362616a 100644
--- a/ui/desktop/src/styles/main.css
+++ b/ui/desktop/src/styles/main.css
@@ -741,3 +741,32 @@ p > code.bg-inline-code {
[data-state='collapsed'] [data-slot='sidebar-gap'] {
will-change: width;
}
+
+
+
+
+/* Subtle back-and-forth shimmer animation for onboarding card */
+@keyframes shimmer {
+ 0% {
+ transform: translateX(-100%);
+ opacity: 0;
+ }
+ 20% {
+ opacity: 0.15;
+ }
+ 50% {
+ transform: translateX(100%);
+ opacity: 0.05;
+ }
+ 80% {
+ opacity: 0.15;
+ }
+ 100% {
+ transform: translateX(-100%);
+ opacity: 0;
+ }
+}
+
+.animate-shimmer {
+ animation: shimmer 6s ease-in-out infinite;
+}
diff --git a/ui/desktop/tests/e2e/app.spec.ts b/ui/desktop/tests/e2e/app.spec.ts
index 227b703e9075..3b5ce2b91cb6 100644
--- a/ui/desktop/tests/e2e/app.spec.ts
+++ b/ui/desktop/tests/e2e/app.spec.ts
@@ -37,11 +37,11 @@ test.beforeEach(async ({ }, testInfo) => {
if (mainWindow) {
// Get a clean test name without the full hierarchy
const testName = testInfo.titlePath[testInfo.titlePath.length - 1];
-
+
// Get provider name if we're in a provider suite
const providerSuite = testInfo.titlePath.find(t => t.startsWith('Provider:'));
const providerName = providerSuite ? providerSuite.split(': ')[1] : undefined;
-
+
console.log(`Setting overlay for test: "${testName}"${providerName ? ` (Provider: ${providerName})` : ''}`);
await showTestName(mainWindow, testName, providerName);
}
@@ -56,9 +56,9 @@ test.afterEach(async () => {
// Helper function to select a provider
async function selectProvider(mainWindow: any, provider: Provider) {
console.log(`Selecting provider: ${provider.name}`);
-
+
// If we're already in the chat interface, we need to reset providers
- const chatTextarea = await mainWindow.waitForSelector('[data-testid="chat-input"]', {
+ const chatTextarea = await mainWindow.waitForSelector('[data-testid="chat-input"]', {
timeout: 2000
}).catch(() => null);
@@ -76,7 +76,7 @@ async function selectProvider(mainWindow: any, provider: Provider) {
timeout: 5000,
state: 'visible'
});
-
+
const modelsTab = await mainWindow.waitForSelector('[data-testid="settings-models-tab"]');
await modelsTab.click();
@@ -90,7 +90,7 @@ async function selectProvider(mainWindow: any, provider: Provider) {
state: 'visible'
});
await resetButton.click();
-
+
// Wait for the reset to complete
await mainWindow.waitForTimeout(1000);
}
@@ -117,13 +117,13 @@ async function selectProvider(mainWindow: any, provider: Provider) {
}
// Check if we need to click "configure other providers (advanced)" button
- const configureAdvancedButton = await mainWindow.waitForSelector('button:has-text("configure other providers (advanced)")', {
+ const configureAdvancedButton = await mainWindow.waitForSelector('h3:has-text("Other providers")', {
timeout: 3000,
state: 'visible'
}).catch(() => null);
if (configureAdvancedButton) {
- console.log('Found "configure other providers (advanced)" button, clicking it...');
+ console.log('Found "configure other providers" button, clicking it...');
await configureAdvancedButton.click();
await mainWindow.waitForTimeout(1500);
}
@@ -213,7 +213,7 @@ test.describe('Goose App', () => {
// Get the main window once for all tests
mainWindow = await electronApp.firstWindow();
await mainWindow.waitForLoadState('domcontentloaded');
-
+
// Try to wait for networkidle, but don't fail if it times out due to MCP activity
try {
await mainWindow.waitForLoadState('networkidle', { timeout: 10000 });
@@ -307,13 +307,13 @@ test.describe('Goose App', () => {
timeout: 5000,
state: 'visible'
});
-
+
const appTab = await mainWindow.waitForSelector('[data-testid="settings-app-tab"]');
await appTab.click();
// Wait for the theme selector to be visible
await mainWindow.waitForTimeout(1000);
-
+
// Find and click the dark mode toggle button
const darkModeButton = await mainWindow.waitForSelector('[data-testid="dark-mode-button"]');
const lightModeButton = await mainWindow.waitForSelector('[data-testid="light-mode-button"]');
@@ -341,13 +341,13 @@ test.describe('Goose App', () => {
// check that system mode is clickable
await systemModeButton.click();
-
+
// Toggle back to light mode
await lightModeButton.click();
-
+
// Pause to show return to original state
await mainWindow.waitForTimeout(2000);
-
+
// Navigate back to home
const homeButton = await mainWindow.waitForSelector('[data-testid="sidebar-home-button"]');
await homeButton.click();
@@ -364,75 +364,75 @@ test.describe('Goose App', () => {
test.describe('Chat', () => {
test('chat interaction', async () => {
console.log(`Testing chat interaction with ${provider.name}...`);
-
+
// Find the chat input
const chatInput = await mainWindow.waitForSelector('[data-testid="chat-input"]');
expect(await chatInput.isVisible()).toBe(true);
-
+
// Type a message
await chatInput.fill('Hello, can you help me with a simple task?');
-
+
// Take screenshot before sending
await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-before-send.png` });
-
+
// Send message
await chatInput.press('Enter');
-
+
// Wait for loading indicator to appear
console.log('Waiting for loading indicator...');
const loadingGoose = await mainWindow.waitForSelector('[data-testid="loading-indicator"]',
{ timeout: 2000 });
expect(await loadingGoose.isVisible()).toBe(true);
-
+
// Take screenshot of loading state
await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-loading-state.png` });
-
+
// Wait for loading indicator to disappear
console.log('Waiting for response...');
await mainWindow.waitForSelector('[data-testid="loading-indicator"]',
{ state: 'hidden', timeout: 30000 });
-
+
// Get the latest response
const response = await mainWindow.locator('[data-testid="message-container"]').last();
expect(await response.isVisible()).toBe(true);
-
+
// Verify response has content
const responseText = await response.textContent();
expect(responseText).toBeTruthy();
expect(responseText.length).toBeGreaterThan(0);
-
+
// Take screenshot of response
await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-chat-response.png` });
});
-
+
test('verify chat history', async () => {
console.log(`Testing chat history with ${provider.name}...`);
-
+
// Find the chat input again
const chatInput = await mainWindow.waitForSelector('[data-testid="chat-input"]');
-
+
// Test message sending with a specific question
await chatInput.fill('What is 2+2?');
-
+
// Send message
await chatInput.press('Enter');
-
+
// Wait for loading indicator and response
await mainWindow.waitForSelector('[data-testid="loading-indicator"]',
{ state: 'hidden', timeout: 30000 });
-
+
// Get the latest response
const response = await mainWindow.locator('[data-testid="message-container"]').last();
const responseText = await response.textContent();
expect(responseText).toBeTruthy();
-
+
// Check for message history
const messages = await mainWindow.locator('[data-testid="message-container"]').all();
expect(messages.length).toBeGreaterThanOrEqual(2);
-
+
// Take screenshot of chat history
await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-chat-history.png` });
-
+
// Test command history (up arrow)
await chatInput.press('Control+ArrowUp');
const inputValue = await chatInput.inputValue();
@@ -443,7 +443,7 @@ test.describe('Goose App', () => {
test.describe('MCP Integration', () => {
test('running quotes MCP server integration', async () => {
console.log(`Testing Running Quotes MCP server integration with ${provider.name}...`);
-
+
// Create test-results directory if it doesn't exist
const fs = require('fs');
if (!fs.existsSync('test-results')) {
@@ -460,7 +460,7 @@ test.describe('Goose App', () => {
console.log('NetworkIdle timeout (likely due to MCP activity), continuing with test...');
}
await mainWindow.waitForLoadState('domcontentloaded');
-
+
// Wait for React app to be ready
await mainWindow.waitForFunction(() => {
const root = document.getElementById('root');
@@ -477,47 +477,47 @@ test.describe('Goose App', () => {
state: 'visible'
});
await extensionsButton.click();
-
+
// Wait for extensions page to load
await mainWindow.waitForTimeout(1000);
-
+
// Look for Running Quotes extension card
console.log('Looking for existing Running Quotes extension...');
const existingExtension = await mainWindow.$('div.flex:has-text("Running Quotes")');
-
+
if (existingExtension) {
console.log('Found existing Running Quotes extension, removing it...');
-
+
// Find and click the settings gear icon next to Running Quotes
const settingsButton = await existingExtension.$('button[aria-label="Extension settings"]');
if (settingsButton) {
await settingsButton.click();
-
+
// Wait for modal to appear
await mainWindow.waitForTimeout(500);
-
+
// Click the Remove Extension button
const removeButton = await mainWindow.waitForSelector('button:has-text("Remove Extension")', {
timeout: 2000,
state: 'visible'
});
await removeButton.click();
-
+
// Wait for confirmation modal
await mainWindow.waitForTimeout(500);
-
+
// Click the Remove button in confirmation dialog
const confirmButton = await mainWindow.waitForSelector('button:has-text("Remove")', {
timeout: 2000,
state: 'visible'
});
await confirmButton.click();
-
+
// Wait for extension to be removed
await mainWindow.waitForTimeout(1000);
}
}
-
+
// Now proceed with adding the extension
console.log('Proceeding with adding Running Quotes extension...');
@@ -527,11 +527,11 @@ test.describe('Goose App', () => {
timeout: 2000,
state: 'visible'
});
-
+
// Verify add extension button is visible
const isAddExtensionVisible = await addExtensionButton.isVisible();
console.log('Add custom extension button visible:', isAddExtensionVisible);
-
+
await addExtensionButton.click();
console.log('Clicked Add custom extension');
@@ -541,21 +541,21 @@ test.describe('Goose App', () => {
// Fill the form
console.log('Filling form fields...');
-
+
// Fill Extension Name
const nameInput = await mainWindow.waitForSelector('input[placeholder="Enter extension name..."]', {
timeout: 2000,
state: 'visible'
});
await nameInput.fill('Running Quotes');
-
+
// Fill Description
const descriptionInput = await mainWindow.waitForSelector('input[placeholder="Optional description..."]', {
timeout: 2000,
state: 'visible'
});
await descriptionInput.fill('Inspirational running quotes MCP server');
-
+
// Fill Command
const mcpScriptPath = join(__dirname, 'basic-mcp.ts');
const commandInput = await mainWindow.waitForSelector('input[placeholder="e.g. npx -y @modelcontextprotocol/my-extension "]', {
@@ -576,50 +576,50 @@ test.describe('Goose App', () => {
timeout: 2000,
state: 'visible'
});
-
+
// Verify button is visible
const isModalAddButtonVisible = await modalAddButton.isVisible();
console.log('Add Extension button visible:', isModalAddButtonVisible);
// Click the button
await modalAddButton.click();
-
+
console.log('Clicked Add Extension button');
// Wait for the Running Quotes extension to appear in the list
console.log('Waiting for Running Quotes extension to appear...');
try {
const extensionCard = await mainWindow.waitForSelector(
- 'div.flex:has-text("Running Quotes")',
+ 'div.flex:has-text("Running Quotes")',
{
timeout: 30000,
state: 'visible'
}
);
-
+
// Verify the extension is enabled
await mainWindow.waitForTimeout(1000);
const toggleButton = await extensionCard.$('button[role="switch"][data-state="checked"]');
const isEnabled = !!toggleButton;
console.log('Extension enabled:', isEnabled);
-
+
if (!isEnabled) {
throw new Error('Running Quotes extension was added but not enabled');
}
-
+
await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-extension-added.png` });
console.log('Extension added successfully');
} catch (error) {
console.error('Error verifying extension:', error);
-
+
// Get any error messages that might be visible
- const errorElements = await mainWindow.$$eval('.text-red-500, .text-error',
+ const errorElements = await mainWindow.$$eval('.text-red-500, .text-error',
elements => elements.map(el => el.textContent)
);
if (errorElements.length > 0) {
console.log('Found error messages:', errorElements);
}
-
+
throw error;
}
@@ -631,36 +631,36 @@ test.describe('Goose App', () => {
} catch (error) {
// Take error screenshot and log details
await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-error.png` });
-
+
// Get page content
const pageContent = await mainWindow.evaluate(() => document.body.innerHTML);
console.log('Page content at error:', pageContent);
-
+
console.error('Test failed:', error);
throw error;
}
});
-
+
test('test running quotes functionality', async () => {
console.log(`Testing running quotes functionality with ${provider.name}...`);
-
+
// Find the chat input
const chatInput = await mainWindow.waitForSelector('[data-testid="chat-input"]');
expect(await chatInput.isVisible()).toBe(true);
-
+
// Type a message requesting a running quote
await chatInput.fill('Can you give me an inspirational running quote using the runningQuotes tool?');
-
+
// Take screenshot before sending
await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-before-quote-request.png` });
-
+
// Send message
await chatInput.press('Enter');
// Get the latest response
const response = await mainWindow.waitForSelector('.goose-message-tool', { timeout: 5000 });
expect(await response.isVisible()).toBe(true);
-
+
// Click the Output dropdown to reveal the actual quote
await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-quote-response-debug.png` });
@@ -668,12 +668,12 @@ test.describe('Goose App', () => {
const outputContent = await mainWindow.waitForSelector('.whitespace-pre-wrap', { timeout: 5000 });
const outputText = await outputContent.textContent();
console.log('Output text:', outputText);
-
+
// Take screenshot of expanded response
await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-quote-response.png` });
-
+
// Check if the output contains one of our known quotes
- const containsKnownQuote = runningQuotes.some(({ quote, author }) =>
+ const containsKnownQuote = runningQuotes.some(({ quote, author }) =>
outputText.includes(`"${quote}" - ${author}`)
);
expect(containsKnownQuote).toBe(true);