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
15 changes: 10 additions & 5 deletions e2e/demo/login-dialog.demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ test.describe('Login Dialog Demo', () => {
// Verify dialog title
await expect(webviewPage.getByText('SSO 认证', { exact: true })).toBeVisible();

// Verify server URL is displayed
await expect(webviewPage.getByText('https://wave-ai.example.com')).toBeVisible();
// Verify server URL input is pre-filled
const serverUrlInput = webviewPage.locator('#login-serverUrl');
await expect(serverUrlInput).toHaveValue('https://wave-ai.example.com');

// Verify login button is visible
await expect(webviewPage.getByText('SSO 登录', { exact: true })).toBeVisible();
Expand Down Expand Up @@ -77,7 +78,7 @@ test.describe('Login Dialog Demo', () => {
await dialog.screenshot({ path: 'docs/public/screenshots/spec-login-dialog-authenticated.png' });
});

test('should show warning when no server URL configured', async ({ webviewPage }) => {
test('should show editable serverUrl input when no server URL configured', async ({ webviewPage }) => {
await webviewPage.evaluate(() => {
(window as any).simulateExtensionMessage({
command: 'showDialog',
Expand All @@ -102,9 +103,13 @@ test.describe('Login Dialog Demo', () => {
});

await expect(webviewPage.getByText('SSO 认证', { exact: true })).toBeVisible();
await expect(webviewPage.getByText('请先在 /config 中配置服务端链接')).toBeVisible();

// Login button should be disabled
// ServerUrl input should be visible and empty
const serverUrlInput = webviewPage.locator('#login-serverUrl');
await expect(serverUrlInput).toBeVisible();
await expect(serverUrlInput).toHaveValue('');

// Login button should be disabled when serverUrl is empty
const loginBtn = webviewPage.getByText('SSO 登录', { exact: true });
await expect(loginBtn).toBeDisabled();

Expand Down
56 changes: 54 additions & 2 deletions e2e/webview/model-status-login-commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,9 @@ test.describe('Model/Status/Login Slash Commands', () => {
// Verify login dialog is visible
await expect(webviewPage.getByText('SSO 认证', { exact: true })).toBeVisible();

// Verify server URL is displayed
await expect(webviewPage.getByText('https://wave.example.com')).toBeVisible();
// Verify server URL input is pre-filled
const serverUrlInput = webviewPage.locator('#login-serverUrl');
await expect(serverUrlInput).toHaveValue('https://wave.example.com');

// Verify getAuthStatus message was sent to extension
const messages = await webviewPage.evaluate(() => (window as any).getTestMessages());
Expand Down Expand Up @@ -221,4 +222,55 @@ test.describe('Model/Status/Login Slash Commands', () => {
await expect(webviewPage.getByText('test@example.com')).toBeVisible();
await expect(webviewPage.getByText('登出', { exact: true })).toBeVisible();
});

test('should save serverUrl from login dialog via updateConfiguration', async ({ webviewPage }) => {
const input = webviewPage.getByTestId('message-input');
await input.focus();

// Simulate extension sending configuration data with empty serverUrl
await webviewPage.evaluate(() => {
(window as any).simulateExtensionMessage({
command: 'configurationResponse',
configurationData: {
serverUrl: ''
}
});
});

// Open login dialog
await input.type('/login');
await webviewPage.keyboard.press('Enter');

// Verify login dialog is visible
await expect(webviewPage.getByText('SSO 认证', { exact: true })).toBeVisible();

// Type serverUrl into the input
const serverUrlInput = webviewPage.locator('#login-serverUrl');
await serverUrlInput.fill('https://wave.example.com');

// Click save button for serverUrl
await webviewPage.locator('#login-save-serverUrl').click();

// Verify updateConfiguration message was sent with serverUrl
const messages = await webviewPage.evaluate(() => (window as any).getTestMessages());
const updateConfigMsg = messages.find((m: any) => m.command === 'updateConfiguration');
expect(updateConfigMsg).toBeDefined();
expect(updateConfigMsg.configurationData.serverUrl).toBe('https://wave.example.com');
});

test('should not have serverUrl field in config dialog', async ({ webviewPage }) => {
const input = webviewPage.getByTestId('message-input');
await input.focus();

// Open config dialog
await input.type('/config');
await webviewPage.keyboard.press('Enter');

// Verify config dialog is visible
await expect(webviewPage.getByText('配置设置', { exact: true })).toBeVisible();

// Verify serverUrl input is NOT present in config dialog
const serverUrlInput = webviewPage.locator('#serverUrl');
await expect(serverUrlInput).not.toBeVisible();
});
});
5 changes: 4 additions & 1 deletion webview/src/components/ChatApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,10 @@ export const ChatApp: React.FC<ChatAppProps> = ({ vscode }) => {
dispatch({ type: 'SHOW_DIALOG', payload: { type: message.dialogType } });
break;
case 'configurationUpdated':
dispatch({ type: 'HIDE_DIALOG' });
// Only close config dialog; keep login dialog open for continued SSO flow
if (stateRef.current.activeDialog !== 'login') {
dispatch({ type: 'HIDE_DIALOG' });
}
break;
case 'statusResponse':
if (message.configurationData) {
Expand Down
12 changes: 0 additions & 12 deletions webview/src/components/ConfigDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,18 +82,6 @@ const ConfigDialog: React.FC<ConfigDialogProps & { vscode: any }> = ({

<form onSubmit={handleSubmit} className="configuration-form">
<div className="configuration-fields-scroll-area">
<div className="configuration-field">
<label htmlFor="serverUrl">服务端链接 (SSO):</label>
<input
id="serverUrl"
type="url"
value={formData.serverUrl || ''}
onChange={(e) => handleInputChange('serverUrl', e.target.value)}
placeholder={configurationData?.envServerUrl || 'WAVE_SERVER_URL'}
disabled={isLoading}
/>
</div>

<div className="configuration-field">
<label htmlFor="baseURL">Base URL:</label>
<input
Expand Down
45 changes: 39 additions & 6 deletions webview/src/components/LoginDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,16 @@ const LoginDialog: React.FC<LoginDialogProps & { vscode: any }> = ({
const [authUser, setAuthUser] = useState<{ id: string; email?: string } | null>(null);
const [authLoading, setAuthLoading] = useState(false);
const [authMessage, setAuthMessage] = useState('');
const [serverUrlInput, setServerUrlInput] = useState('');
const [serverUrlSaved, setServerUrlSaved] = useState(false);
const dialogRef = useRef<HTMLDivElement>(null);

// Initialize serverUrl input from configuration
useEffect(() => {
setServerUrlInput(configurationData?.serverUrl || '');
setServerUrlSaved(!!configurationData?.serverUrl);
}, [configurationData?.serverUrl]);

// Fetch auth status on mount
useEffect(() => {
vscode?.postMessage({ command: 'getAuthStatus' });
Expand Down Expand Up @@ -66,6 +74,17 @@ const LoginDialog: React.FC<LoginDialogProps & { vscode: any }> = ({
vscode?.postMessage({ command: 'logout' });
};

const handleSaveServerUrl = () => {
vscode?.postMessage({
command: 'updateConfiguration',
configurationData: {
...configurationData,
serverUrl: serverUrlInput
}
});
setServerUrlSaved(true);
};

// Click outside / Escape to close
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
Expand All @@ -86,7 +105,7 @@ const LoginDialog: React.FC<LoginDialogProps & { vscode: any }> = ({
};
}, [onClose]);

const serverUrl = configurationData?.serverUrl || configurationData?.envServerUrl;
const serverUrl = serverUrlSaved ? serverUrlInput : (configurationData?.serverUrl || configurationData?.envServerUrl || '');

return (
<div className="configuration-dialog-overlay">
Expand All @@ -98,8 +117,25 @@ const LoginDialog: React.FC<LoginDialogProps & { vscode: any }> = ({
<div className="configuration-form">
<div className="configuration-fields-scroll-area">
<div className="configuration-field">
<label>服务端链接:</label>
<div className="status-info-value">{serverUrl || '未配置'}</div>
<label htmlFor="login-serverUrl">服务端链接:</label>
<div className="login-serverUrl-row">
<input
id="login-serverUrl"
type="url"
value={serverUrlInput}
onChange={(e) => { setServerUrlInput(e.target.value); setServerUrlSaved(false); }}
placeholder={configurationData?.envServerUrl || 'WAVE_SERVER_URL'}
/>
<button
type="button"
id="login-save-serverUrl"
className="login-save-serverUrl-btn"
onClick={handleSaveServerUrl}
disabled={!serverUrlInput || serverUrlSaved}
>
{serverUrlSaved ? '已保存' : '保存'}
</button>
</div>
</div>

<div className="configuration-field sso-auth-section">
Expand Down Expand Up @@ -137,9 +173,6 @@ const LoginDialog: React.FC<LoginDialogProps & { vscode: any }> = ({
{authMessage}
</div>
)}
{!serverUrl && !isAuthenticated && (
<div className="sso-message error">请先在 /config 中配置服务端链接</div>
)}
</div>
</div>

Expand Down
33 changes: 33 additions & 0 deletions webview/src/styles/ConfigurationDialog.css
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,39 @@ input:checked + .slider:before {
color: var(--vscode-errorForeground);
}

/* Login Dialog - Server URL row */
.login-serverUrl-row {
display: flex;
gap: 6px;
align-items: center;
}

.login-serverUrl-row input {
flex: 1;
min-width: 0;
}

.login-save-serverUrl-btn {
background-color: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
padding: 6px 10px;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
white-space: nowrap;
flex-shrink: 0;
}

.login-save-serverUrl-btn:hover {
background-color: var(--vscode-button-hoverBackground);
}

.login-save-serverUrl-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}

/* MCP Server Management */
.mcp-container {
display: flex;
Expand Down