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
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ This file provides guidance to Wave Code when working with code in this reposito
### 前端 (Webview)
- **`webview/src/index.tsx`**: React 应用程序的入口点。
- **`webview/src/components/ChatApp.tsx`**: 使用 `useReducer` 模式管理聊天状态的主要组件。
- **`webview/src/components/ConfigurationDialog.tsx`**: 用于管理 AI 设置(API 密钥、模型、语言等)和插件管理的 UI。包含三个主要标签页:
- **常规设置**: API 配置、模型选择、语言设置
- **插件管理**: 探索新插件、已安装插件、插件市场
- **`webview/src/components/ConfigDialog.tsx`**: 常规设置弹窗(通过 `/config` 命令唤起),管理 AI 设置(API 密钥、模型、语言等)和 SSO 认证。
- **`webview/src/components/PluginDialog.tsx`**: 插件管理弹窗(通过 `/plugin` 命令唤起),包含探索新插件、已安装插件、插件市场三个子选项卡。
- **`webview/src/components/McpDialog.tsx`**: MCP 服务器管理弹窗(通过 `/mcp` 命令唤起),显示服务器状态和连接控制。
- **通信 (Communication)**: 使用 `vscode.postMessage` 和 `window.addEventListener('message', ...)` 与后端通信。
- **VS Code API 限制**: `acquireVsCodeApi()` 在整个 Webview 生命周期内只能被调用一次。必须在根组件(如 `index.tsx`)调用并作为 Prop 传递给子组件,严禁在子组件中重复获取,否则会导致 Webview 崩溃。

Expand Down
54 changes: 38 additions & 16 deletions docs/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,16 @@ _输入框中的代码选中标签_

### 2.4 指令系统 (Slash Commands) {#slash-commands}

通过输入 `/`,用户可以快速调用预设的指令,如 `/explain`、`/fix` 等,提高操作效率。
通过输入 `/`,用户可以快速调用预设的指令,如 `/explain`、`/fix` 等提高操作效率。此外,Wave 还提供以下内置指令来快速打开管理弹窗:

- **`/config`**:打开常规设置弹窗(详见 [第 10.1 节 配置设置](#configuration-settings))
- **`/plugin`**:打开插件管理弹窗(详见 [第 11 章 插件系统](#plugin-system))
- **`/mcp`**:打开 MCP 服务器管理弹窗(详见 [第 6.3 节 MCP 协议集成](#mcp-integration))
- **`/model`**:打开模型设置弹窗,快速切换主模型和快速模型(详见 [第 10.1 节 配置设置](#configuration-settings))
- **`/status`**:打开状态信息弹窗,查看版本、会话 ID、工作目录、模型配置和认证状态
- **`/login`**:打开 SSO 认证弹窗,进行登录/登出操作(详见 [第 10.1 节 配置设置](#configuration-settings))

选择这些指令后会直接弹出对应弹窗,无需按回车发送。

![指令系统](/screenshots/spec-slash-commands.png)
_指令系统_
Expand Down Expand Up @@ -388,7 +397,7 @@ _Skill 系统_

### 6.3 MCP 协议集成 {#mcp-integration}

支持 Model Context Protocol (MCP),允许 AI 连接到外部的 MCP 服务器,从而获取更多的上下文信息或调用外部工具。用户可通过配置弹窗中的 **MCP 服务器** 标签页查看和管理所有已配置的 MCP 服务器。
支持 Model Context Protocol (MCP),允许 AI 连接到外部的 MCP 服务器,从而获取更多的上下文信息或调用外部工具。用户可通过 `/mcp` 命令唤起的 MCP 服务器管理弹窗查看和管理所有已配置的 MCP 服务器。

**主要特性:**

Expand All @@ -405,7 +414,7 @@ _Skill 系统_
_MCP 集成_

![MCP 服务器管理](/screenshots/spec-mcp-server-tab.png)
_MCP 服务器管理(配置弹窗中的 MCP 标签页)_
_MCP 服务器管理(通过 `/mcp` 命令唤起)_

### 6.4 内置 Skills {#builtin-skills}

Expand Down Expand Up @@ -614,25 +623,38 @@ Wave 在后台自动维护项目记忆,帮助 AI 持续了解项目演变:

### 10.1 配置设置 {#configuration-settings}

用户可以自定义 AI 模型、API Key、Base URL 等关键参数,以适配不同的 AI 服务提供商。配置界面包含三个标签页:**常规设置**、**插件** 和 **MCP 服务器**。配置界面中的表单字段仅显示用户手动输入的值,不会被环境变量填充;但如果设置了相应的环境变量(如 `WAVE_BASE_URL`),其值会作为 placeholder 提示显示在输入框中。服务端链接字段默认 placeholder 为"请联系管理员获取"。
用户可以自定义 AI 模型、API Key、Base URL 等关键参数,以适配不同的 AI 服务提供商。通过 `/config`、`/model`、`/login`、`/status`、`/plugin`、`/mcp` 六个斜杠命令分别唤起独立的弹窗进行管理。配置界面中的表单字段仅显示用户手动输入的值,不会被环境变量填充;但如果设置了相应的环境变量(如 `WAVE_BASE_URL`),其值会作为 placeholder 提示显示在输入框中。服务端链接字段默认 placeholder 为"请联系管理员获取"。

**主要特性:**

- **常规设置**:配置 AI 模型、API Key、Base URL、Headers、语言等。
- **常规设置(`/config`)**:配置服务端链接、API Key、Base URL、Headers、语言等。
- **服务端链接**:配置 Wave AI 服务端地址,用于 SSO 认证。支持环境变量 `WAVE_SERVER_URL` 作为 fallback,默认 placeholder 提示"请联系管理员获取"。
- **Model / Fast Model**:SSO 模式下也可配置主模型和快速模型名称,支持环境变量 `WAVE_MODEL` / `WAVE_FAST_MODEL` 作为 placeholder 提示。
- **SSO 登录/登出**:当配置了服务端链接后,用户可通过 SSO 认证进行登录,无需手动配置 API Key。登录后所有 API 请求自动通过 Wave AI 代理路由。
- **浏览器登录**:点击"SSO 登录"后自动打开浏览器,用户在 Wave AI 登录页完成认证(支持 SSO 企业身份提供商或账号密码登录),授权码通过 localhost 回调自动交换为 JWT 并保存。VS Code Remote SSH 环境会自动转发端口,远程服务器体验与本地一致。
- **登录状态显示**:已认证时显示用户邮箱/ID 和登出按钮;登出后自动恢复为直接 LLM API 模式。
- **插件**:管理插件的安装、更新、卸载和插件市场(详见 [第 11 章 插件系统](#plugin-system))。
- **MCP 服务器**:查看和管理 MCP 服务器连接状态(详见 [第 6.3 节 MCP 协议集成](#mcp-integration))。
- **模型设置(`/model`)**:快速切换主模型和快速模型,支持环境变量 `WAVE_MODEL` / `WAVE_FAST_MODEL` 作为 placeholder 提示。

![模型设置](/screenshots/spec-model-dialog.png)
_模型设置弹窗_

- **SSO 认证(`/login`)**:当配置了服务端链接后,用户可通过 SSO 认证进行登录,无需手动配置 API Key。登录后所有 API 请求自动通过 Wave AI 代理路由。
- **浏览器登录**:点击"SSO 登录"后自动打开浏览器,用户在 Wave AI 登录页完成认证(支持 SSO 企业身份提供商或账号密码登录),授权码通过 localhost 回调自动交换为 JWT 并保存。VS Code Remote SSH 环境会自动转发端口,远程服务器体验与本地一致。
- **登录状态显示**:已认证时显示用户邮箱/ID 和登出按钮;登出后自动恢复为直接 LLM API 模式。

![SSO 认证](/screenshots/spec-login-dialog.png)
_SSO 认证弹窗_

- **状态信息(`/status`)**:查看当前 Wave 的运行状态,包括版本号、Session ID、工作目录、服务端链接、Base URL、Model、Fast Model 及认证状态。

![状态信息](/screenshots/spec-status-dialog.png)
_状态信息弹窗_

- **插件管理(`/plugin`)**:管理插件的安装、更新、卸载和插件市场(详见 [第 11 章 插件系统](#plugin-system))。
- **MCP 服务器(`/mcp`)**:查看和管理 MCP 服务器连接状态(详见 [第 6.3 节 MCP 协议集成](#mcp-integration))。

![配置设置](/screenshots/spec-configuration.png)
_配置设置_

### 10.2 语言设置 {#language-settings}

用户可以在配置界面中选择偏好的语言(中文或英文),AI 代理将根据该设置使用相应的语言进行回复。
用户可以在常规设置弹窗(通过 `/config` 命令唤起)中选择偏好的语言(中文或英文),AI 代理将根据该设置使用相应的语言进行回复。

![语言设置](/screenshots/language-config-ui.png)
_语言设置_
Expand Down Expand Up @@ -816,7 +838,7 @@ SDK 默认对名称包含 `claude` 的模型自动启用 Prompt Cache(提示

### 11.1 概述 {#plugin-overview}

Wave 通过插件系统扩展 AI 能力,用户可以在配置界面中管理插件的安装、更新和卸载。
Wave 通过插件系统扩展 AI 能力,用户可以通过 `/plugin` 命令唤起的插件管理弹窗管理插件的安装、更新和卸载。

**主要特性:**

Expand All @@ -828,7 +850,7 @@ Wave 通过插件系统扩展 AI 能力,用户可以在配置界面中管理

### 11.2 探索新插件 {#explore-plugins}

"探索新插件"标签页中,用户可以通过顶部的搜索框按关键词过滤插件列表。搜索支持对插件名称和描述进行不区分大小写的匹配。对于尚未在当前环境激活的插件,用户可以选择安装作用域后点击"安装"按钮。已安装但未在当前作用域激活的插件也会在此列出,并带有"已安装"标识。
在插件管理弹窗的"探索新插件"选项卡中,用户可以通过顶部的搜索框按关键词过滤插件列表。搜索支持对插件名称和描述进行不区分大小写的匹配。对于尚未在当前环境激活的插件,用户可以选择安装作用域后点击"安装"按钮。已安装但未在当前作用域激活的插件也会在此列出,并带有"已安装"标识。

![探索新插件(支持关键词搜索)](/screenshots/spec-plugin-explore.png)
_探索新插件(支持关键词搜索)_
Expand All @@ -838,7 +860,7 @@ _关键词过滤效果_

### 11.3 已激活插件 {#installed-plugins}

"已激活插件"标签页中,用户可以查看当前作用域下所有已激活的插件。用户可以对这些插件进行"更新"或"卸载"操作。每个插件会显示其激活的作用域(User、Project 或 Local)。
在插件管理弹窗的"已激活插件"选项卡中,用户可以查看当前作用域下所有已激活的插件。用户可以对这些插件进行"更新"或"卸载"操作。每个插件会显示其激活的作用域(User、Project 或 Local)。

![已激活插件管理](/screenshots/spec-plugin-installed.png)
_已激活插件管理_
Expand Down Expand Up @@ -891,7 +913,7 @@ LCAP 低代码平台扩展组件开发指南。包含约 **17 个技能**,覆

创建独特的、生产级前端界面设计技能。注重美学品质,避免千篇一律的 AI 审美风格,生成具有设计感的 Web 前端代码。

"插件市场"标签页中,用户还可以添加自定义的插件市场源,支持 GitHub 仓库(owner/repo 格式)、Git URL 或本地文件路径。添加后可以从该市场源安装插件,也可以更新或移除市场源。
在插件管理弹窗的"插件市场"选项卡中,用户还可以添加自定义的插件市场源,支持 GitHub 仓库(owner/repo 格式)、Git URL 或本地文件路径。添加后可以从该市场源安装插件,也可以更新或移除市场源。

![插件市场管理](/screenshots/spec-plugin-marketplaces.png)
_插件市场管理_
Expand Down
4 changes: 2 additions & 2 deletions e2e/demo/config-popup.demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { MessageInjector } from '../utils/messageInjector.js';

test.describe('Configuration Popup Demo', () => {
test('should show configuration dialog when config is missing', async ({ webviewPage }) => {
// Simulate the extension sending the showConfiguration command
// Simulate the extension sending the showConfiguration command (opens ConfigDialog)
await webviewPage.evaluate(() => {
(window as any).simulateExtensionMessage({
command: 'showConfiguration',
Expand All @@ -25,7 +25,7 @@ test.describe('Configuration Popup Demo', () => {
});

test('should NOT show configuration dialog when config is provided (simulated)', async ({ webviewPage }) => {
// In this case, the extension wouldn't send showConfiguration
// The extension wouldn't send showConfiguration when config is valid
// We just verify the normal chat interface is there
await expect(webviewPage.getByTestId('chat-container')).toBeVisible();
await expect(webviewPage.getByText('配置设置', { exact: true })).not.toBeVisible(); // The dialog title
Expand Down
2 changes: 1 addition & 1 deletion e2e/demo/language-config.demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { test, expect } from '../utils/webviewTestHarness.js';

test.describe('Language Configuration Demo', () => {
test('should show language field in configuration dialog', async ({ webviewPage }) => {
// Simulate the extension sending the showConfiguration command with language
// Simulate the extension sending showConfiguration (opens ConfigDialog via /config)
await webviewPage.evaluate(() => {
(window as any).simulateExtensionMessage({
command: 'showConfiguration',
Expand Down
114 changes: 114 additions & 0 deletions e2e/demo/login-dialog.demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { test, expect } from '../utils/webviewTestHarness.js';

test.describe('Login Dialog Demo', () => {
test('should show login dialog with not-authenticated state', async ({ webviewPage }) => {
// 1. Open the login dialog via showDialog
await webviewPage.evaluate(() => {
(window as any).simulateExtensionMessage({
command: 'showDialog',
dialogType: 'login'
});
});

// 2. Simulate extension sending configuration data with serverUrl
await webviewPage.evaluate(() => {
(window as any).simulateExtensionMessage({
command: 'configurationResponse',
configurationData: {
serverUrl: 'https://wave-ai.example.com'
}
});
});

// 3. Simulate auth status (not authenticated)
await webviewPage.evaluate(() => {
(window as any).simulateExtensionMessage({
command: 'authStatusResponse',
isAuthenticated: false,
user: null
});
});

// 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 login button is visible
await expect(webviewPage.getByText('SSO 登录', { exact: true })).toBeVisible();

// Take screenshot
const dialog = webviewPage.locator('.configuration-dialog');
await dialog.screenshot({ path: 'docs/public/screenshots/spec-login-dialog.png' });
});

test('should show login dialog with authenticated state', async ({ webviewPage }) => {
await webviewPage.evaluate(() => {
(window as any).simulateExtensionMessage({
command: 'showDialog',
dialogType: 'login'
});
});

await webviewPage.evaluate(() => {
(window as any).simulateExtensionMessage({
command: 'configurationResponse',
configurationData: {
serverUrl: 'https://wave-ai.example.com'
}
});
});

// Simulate authenticated state
await webviewPage.evaluate(() => {
(window as any).simulateExtensionMessage({
command: 'authStatusResponse',
isAuthenticated: true,
user: { id: 'user-xyz789', email: 'alice@example.com' }
});
});

await expect(webviewPage.getByText('SSO 认证', { exact: true })).toBeVisible();
await expect(webviewPage.getByText('alice@example.com')).toBeVisible();
await expect(webviewPage.getByText('登出', { exact: true })).toBeVisible();

const dialog = webviewPage.locator('.configuration-dialog');
await dialog.screenshot({ path: 'docs/public/screenshots/spec-login-dialog-authenticated.png' });
});

test('should show warning when no server URL configured', async ({ webviewPage }) => {
await webviewPage.evaluate(() => {
(window as any).simulateExtensionMessage({
command: 'showDialog',
dialogType: 'login'
});
});

// No serverUrl configured
await webviewPage.evaluate(() => {
(window as any).simulateExtensionMessage({
command: 'configurationResponse',
configurationData: {}
});
});

await webviewPage.evaluate(() => {
(window as any).simulateExtensionMessage({
command: 'authStatusResponse',
isAuthenticated: false,
user: null
});
});

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

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

const dialog = webviewPage.locator('.configuration-dialog');
await dialog.screenshot({ path: 'docs/public/screenshots/spec-login-dialog-no-url.png' });
});
});
Loading