mirror of
https://github.com/cline/cline.git
synced 2026-09-07 22:16:30 +08:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
370b774c14 | ||
|
|
557c1549be | ||
|
|
c535a5ec73 | ||
|
|
644280bbb4 | ||
|
|
43357c1100 | ||
|
|
eb19731843 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Put all the navigation state and message handling and navigation functions in the extension state context instead of the app.tsx
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
taskCompletionViewChanges protobus migration
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
copyToClipboard protobus migration
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Migrate restartMcpServer to protobus
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
cd webview-ui
|
||||
# Ensure coverage dependency is installed
|
||||
npm install --no-save @vitest/coverage-v8
|
||||
npm run test:coverage > webview_coverage.txt 2>&1 || true
|
||||
npm run test:coverage > webview_coverage.txt 2>&1
|
||||
cd ..
|
||||
PYTHONPATH=.github/scripts python -m coverage_check extract-coverage webview-ui/webview_coverage.txt --type=webview --github-output --verbose
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ import "common.proto";
|
||||
|
||||
// Service for file-related operations
|
||||
service FileService {
|
||||
// Copies text to clipboard
|
||||
rpc copyToClipboard(StringRequest) returns (Empty);
|
||||
|
||||
// Opens a file in the editor
|
||||
rpc openFile(StringRequest) returns (Empty);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ service McpService {
|
||||
rpc updateMcpTimeout(UpdateMcpTimeoutRequest) returns (McpServers);
|
||||
rpc addRemoteMcpServer(AddRemoteMcpServerRequest) returns (McpServers);
|
||||
rpc downloadMcp(StringRequest) returns (Empty);
|
||||
rpc restartMcpServer(StringRequest) returns (McpServers);
|
||||
}
|
||||
|
||||
message ToggleMcpServerRequest {
|
||||
|
||||
@@ -29,6 +29,8 @@ service TaskService {
|
||||
rpc askResponse(AskResponseRequest) returns (Empty);
|
||||
// Records task feedback (thumbs up/down)
|
||||
rpc taskFeedback(StringRequest) returns (Empty);
|
||||
// Shows task completion changes diff in a view
|
||||
rpc taskCompletionViewChanges(Int64Request) returns (Empty);
|
||||
}
|
||||
|
||||
// Request message for creating a new task
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import * as vscode from "vscode"
|
||||
import { Controller } from ".."
|
||||
import { Empty, StringRequest } from "../../../shared/proto/common"
|
||||
|
||||
/**
|
||||
* Copies text to the system clipboard
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the text to copy
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function copyToClipboard(controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
try {
|
||||
if (request.value) {
|
||||
await vscode.env.clipboard.writeText(request.value)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error copying to clipboard:", error)
|
||||
}
|
||||
return Empty.create()
|
||||
}
|
||||
@@ -341,12 +341,6 @@ export class Controller {
|
||||
case "openMention":
|
||||
openMention(message.text)
|
||||
break
|
||||
case "taskCompletionViewChanges": {
|
||||
if (message.number) {
|
||||
await this.task?.presentMultifileDiff(message.number, true)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "accountLogoutClicked": {
|
||||
await this.handleSignOut()
|
||||
break
|
||||
@@ -488,14 +482,6 @@ export class Controller {
|
||||
this.refreshTotalTasksSize()
|
||||
break
|
||||
}
|
||||
case "restartMcpServer": {
|
||||
try {
|
||||
await this.mcpHub?.restartConnection(message.text!)
|
||||
} catch (error) {
|
||||
console.error(`Failed to retry connection for ${message.text}:`, error)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "deleteMcpServer": {
|
||||
if (message.serverName) {
|
||||
this.mcpHub?.deleteServer(message.serverName)
|
||||
@@ -621,14 +607,6 @@ export class Controller {
|
||||
break
|
||||
}
|
||||
|
||||
case "copyToClipboard": {
|
||||
try {
|
||||
await vscode.env.clipboard.writeText(message.text || "")
|
||||
} catch (error) {
|
||||
console.error("Error copying to clipboard:", error)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "updateTerminalConnectionTimeout": {
|
||||
if (message.shellIntegrationTimeout !== undefined) {
|
||||
const timeout = message.shellIntegrationTimeout
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { registerMethod } from "./index"
|
||||
import { addRemoteMcpServer } from "./addRemoteMcpServer"
|
||||
import { downloadMcp } from "./downloadMcp"
|
||||
import { restartMcpServer } from "./restartMcpServer"
|
||||
import { toggleMcpServer } from "./toggleMcpServer"
|
||||
import { updateMcpTimeout } from "./updateMcpTimeout"
|
||||
|
||||
@@ -13,6 +14,7 @@ export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("addRemoteMcpServer", addRemoteMcpServer)
|
||||
registerMethod("downloadMcp", downloadMcp)
|
||||
registerMethod("restartMcpServer", restartMcpServer)
|
||||
registerMethod("toggleMcpServer", toggleMcpServer)
|
||||
registerMethod("updateMcpTimeout", updateMcpTimeout)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { McpServers } from "@shared/proto/mcp"
|
||||
import type { Controller } from "../index"
|
||||
import { convertMcpServersToProtoMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
|
||||
import { StringRequest } from "@/shared/proto/common"
|
||||
|
||||
/**
|
||||
* Restarts an MCP server connection
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the server name
|
||||
* @returns The updated list of MCP servers
|
||||
*/
|
||||
export async function restartMcpServer(controller: Controller, request: StringRequest): Promise<McpServers> {
|
||||
try {
|
||||
const mcpServers = await controller.mcpHub?.restartConnectionRPC(request.value)
|
||||
|
||||
// Convert from McpServer[] to ProtoMcpServer[] ensuring all required fields are set
|
||||
const protoServers = convertMcpServersToProtoMcpServers(mcpServers)
|
||||
|
||||
return { mcpServers: protoServers }
|
||||
} catch (error) {
|
||||
console.error(`Failed to restart MCP server ${request.value}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { exportTaskWithId } from "./exportTaskWithId"
|
||||
import { getTaskHistory } from "./getTaskHistory"
|
||||
import { newTask } from "./newTask"
|
||||
import { showTaskWithId } from "./showTaskWithId"
|
||||
import { taskCompletionViewChanges } from "./taskCompletionViewChanges"
|
||||
import { taskFeedback } from "./taskFeedback"
|
||||
import { toggleTaskFavorite } from "./toggleTaskFavorite"
|
||||
|
||||
@@ -27,6 +28,7 @@ export function registerAllMethods(): void {
|
||||
registerMethod("getTaskHistory", getTaskHistory)
|
||||
registerMethod("newTask", newTask)
|
||||
registerMethod("showTaskWithId", showTaskWithId)
|
||||
registerMethod("taskCompletionViewChanges", taskCompletionViewChanges)
|
||||
registerMethod("taskFeedback", taskFeedback)
|
||||
registerMethod("toggleTaskFavorite", toggleTaskFavorite)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Controller } from ".."
|
||||
import { Empty } from "../../../shared/proto/common"
|
||||
import { Int64Request } from "../../../shared/proto/common"
|
||||
|
||||
/**
|
||||
* Shows task completion changes in a diff view
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the timestamp of the message
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function taskCompletionViewChanges(controller: Controller, request: Int64Request): Promise<Empty> {
|
||||
try {
|
||||
if (request.value && controller.task) {
|
||||
await controller.task.presentMultifileDiff(request.value, true)
|
||||
}
|
||||
return Empty.create()
|
||||
} catch (error) {
|
||||
console.error("Error in taskCompletionViewChanges handler:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -103,6 +103,7 @@ export class McpHub {
|
||||
|
||||
getServers(): McpServer[] {
|
||||
// Only return enabled servers
|
||||
|
||||
return this.connections.filter((conn) => !conn.server.disabled).map((conn) => conn.server)
|
||||
}
|
||||
|
||||
@@ -180,6 +181,120 @@ export class McpHub {
|
||||
}
|
||||
}
|
||||
|
||||
private async connectToServerRPC(
|
||||
name: string,
|
||||
config: z.infer<typeof StdioConfigSchema> | z.infer<typeof SseConfigSchema>,
|
||||
): Promise<void> {
|
||||
// Remove existing connection if it exists (should never happen, the connection should be deleted beforehand)
|
||||
this.connections = this.connections.filter((conn) => conn.server.name !== name)
|
||||
|
||||
try {
|
||||
// Each MCP server requires its own transport connection and has unique capabilities, configurations, and error handling. Having separate clients also allows proper scoping of resources/tools and independent server management like reconnection.
|
||||
const client = new Client(
|
||||
{
|
||||
name: "Cline",
|
||||
version: this.clientVersion,
|
||||
},
|
||||
{
|
||||
capabilities: {},
|
||||
},
|
||||
)
|
||||
|
||||
let transport: StdioClientTransport | SSEClientTransport
|
||||
|
||||
if (config.transportType === "sse") {
|
||||
transport = new SSEClientTransport(new URL(config.url), {})
|
||||
} else {
|
||||
transport = new StdioClientTransport({
|
||||
command: config.command,
|
||||
args: config.args,
|
||||
env: {
|
||||
...config.env,
|
||||
...(process.env.PATH ? { PATH: process.env.PATH } : {}),
|
||||
// ...(process.env.NODE_PATH ? { NODE_PATH: process.env.NODE_PATH } : {}),
|
||||
},
|
||||
stderr: "pipe", // necessary for stderr to be available
|
||||
})
|
||||
}
|
||||
|
||||
transport.onerror = async (error) => {
|
||||
console.error(`Transport error for "${name}":`, error)
|
||||
const connection = this.connections.find((conn) => conn.server.name === name)
|
||||
if (connection) {
|
||||
connection.server.status = "disconnected"
|
||||
this.appendErrorMessage(connection, error.message)
|
||||
}
|
||||
}
|
||||
|
||||
transport.onclose = async () => {
|
||||
const connection = this.connections.find((conn) => conn.server.name === name)
|
||||
if (connection) {
|
||||
connection.server.status = "disconnected"
|
||||
}
|
||||
}
|
||||
|
||||
const connection: McpConnection = {
|
||||
server: {
|
||||
name,
|
||||
config: JSON.stringify(config),
|
||||
status: "connecting",
|
||||
disabled: config.disabled,
|
||||
},
|
||||
client,
|
||||
transport,
|
||||
}
|
||||
this.connections.push(connection)
|
||||
|
||||
if (config.transportType === "stdio") {
|
||||
// transport.stderr is only available after the process has been started. However we can't start it separately from the .connect() call because it also starts the transport. And we can't place this after the connect call since we need to capture the stderr stream before the connection is established, in order to capture errors during the connection process.
|
||||
// As a workaround, we start the transport ourselves, and then monkey-patch the start method to no-op so that .connect() doesn't try to start it again.
|
||||
await transport.start()
|
||||
const stderrStream = (transport as StdioClientTransport).stderr
|
||||
if (stderrStream) {
|
||||
stderrStream.on("data", async (data: Buffer) => {
|
||||
const output = data.toString()
|
||||
// Check if output contains INFO level log
|
||||
const isInfoLog = !/\berror\b/i.test(output)
|
||||
|
||||
if (isInfoLog) {
|
||||
// Log normal informational messages
|
||||
console.info(`Server "${name}" info:`, output)
|
||||
} else {
|
||||
// Treat as error log
|
||||
console.error(`Server "${name}" stderr:`, output)
|
||||
const connection = this.connections.find((conn) => conn.server.name === name)
|
||||
if (connection) {
|
||||
this.appendErrorMessage(connection, output)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.error(`No stderr stream for ${name}`)
|
||||
}
|
||||
transport.start = async () => {} // No-op now, .connect() won't fail
|
||||
}
|
||||
|
||||
// Connect
|
||||
await client.connect(transport)
|
||||
|
||||
connection.server.status = "connected"
|
||||
connection.server.error = ""
|
||||
|
||||
// Initial fetch of tools and resources
|
||||
connection.server.tools = await this.fetchToolsList(name)
|
||||
connection.server.resources = await this.fetchResourcesList(name)
|
||||
connection.server.resourceTemplates = await this.fetchResourceTemplatesList(name)
|
||||
} catch (error) {
|
||||
// Update status with error
|
||||
const connection = this.connections.find((conn) => conn.server.name === name)
|
||||
if (connection) {
|
||||
connection.server.status = "disconnected"
|
||||
this.appendErrorMessage(connection, error instanceof Error ? error.message : String(error))
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private async connectToServer(
|
||||
name: string,
|
||||
config: z.infer<typeof StdioConfigSchema> | z.infer<typeof SseConfigSchema>,
|
||||
@@ -494,6 +609,36 @@ export class McpHub {
|
||||
this.fileWatchers.clear()
|
||||
}
|
||||
|
||||
async restartConnectionRPC(serverName: string): Promise<McpServer[]> {
|
||||
this.isConnecting = true
|
||||
|
||||
// Get existing connection and update its status
|
||||
const connection = this.connections.find((conn) => conn.server.name === serverName)
|
||||
const inMemoryConfig = connection?.server.config
|
||||
if (inMemoryConfig) {
|
||||
connection.server.status = "connecting"
|
||||
connection.server.error = ""
|
||||
await setTimeoutPromise(500) // artificial delay to show user that server is restarting
|
||||
try {
|
||||
await this.deleteConnection(serverName)
|
||||
// Try to connect again using existing config
|
||||
await this.connectToServerRPC(serverName, JSON.parse(inMemoryConfig))
|
||||
} catch (error) {
|
||||
console.error(`Failed to restart connection for ${serverName}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
this.isConnecting = false
|
||||
|
||||
const config = await this.readAndValidateMcpSettingsFile()
|
||||
if (!config) {
|
||||
throw new Error("Failed to read or validate MCP settings")
|
||||
}
|
||||
|
||||
const serverOrder = Object.keys(config.mcpServers || {})
|
||||
return this.getSortedMcpServers(serverOrder)
|
||||
}
|
||||
|
||||
async restartConnection(serverName: string): Promise<void> {
|
||||
this.isConnecting = true
|
||||
|
||||
|
||||
@@ -20,12 +20,10 @@ export interface WebviewMessage {
|
||||
| "showChatView"
|
||||
| "refreshClineRules"
|
||||
| "openMcpSettings"
|
||||
| "restartMcpServer"
|
||||
| "deleteMcpServer"
|
||||
| "autoApprovalSettings"
|
||||
| "browserRelaunchResult"
|
||||
| "togglePlanActMode"
|
||||
| "taskCompletionViewChanges"
|
||||
| "openExtensionSettings"
|
||||
| "requestVsCodeLmModels"
|
||||
| "toggleToolAutoApprove"
|
||||
@@ -55,7 +53,6 @@ export interface WebviewMessage {
|
||||
| "toggleWindsurfRule"
|
||||
| "toggleWorkflow"
|
||||
| "deleteClineRule"
|
||||
| "copyToClipboard"
|
||||
| "updateTerminalConnectionTimeout"
|
||||
| "setActiveQuote"
|
||||
|
||||
|
||||
@@ -906,6 +906,15 @@ export const FileServiceDefinition = {
|
||||
name: "FileService",
|
||||
fullName: "cline.FileService",
|
||||
methods: {
|
||||
/** Copies text to clipboard */
|
||||
copyToClipboard: {
|
||||
name: "copyToClipboard",
|
||||
requestType: StringRequest,
|
||||
requestStream: false,
|
||||
responseType: Empty,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
/** Opens a file in the editor */
|
||||
openFile: {
|
||||
name: "openFile",
|
||||
|
||||
@@ -1012,6 +1012,14 @@ export const McpServiceDefinition = {
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
restartMcpServer: {
|
||||
name: "restartMcpServer",
|
||||
requestType: StringRequest,
|
||||
requestStream: false,
|
||||
responseType: McpServers,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
/* eslint-disable */
|
||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
|
||||
import { Empty, EmptyRequest, Metadata, StringArrayRequest, StringRequest } from "./common"
|
||||
import { Empty, EmptyRequest, Int64Request, Metadata, StringArrayRequest, StringRequest } from "./common"
|
||||
|
||||
export const protobufPackage = "cline"
|
||||
|
||||
@@ -1187,6 +1187,15 @@ export const TaskServiceDefinition = {
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
/** Shows task completion changes diff in a view */
|
||||
taskCompletionViewChanges: {
|
||||
name: "taskCompletionViewChanges",
|
||||
requestType: Int64Request,
|
||||
requestStream: false,
|
||||
responseType: Empty,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import { checkpointDiff } from "../core/controller/checkpoints/checkpointDiff"
|
||||
import { checkpointRestore } from "../core/controller/checkpoints/checkpointRestore"
|
||||
|
||||
// File Service
|
||||
import { copyToClipboard } from "../core/controller/file/copyToClipboard"
|
||||
import { openFile } from "../core/controller/file/openFile"
|
||||
import { openImage } from "../core/controller/file/openImage"
|
||||
import { deleteRuleFile } from "../core/controller/file/deleteRuleFile"
|
||||
@@ -33,6 +34,7 @@ import { toggleMcpServer } from "../core/controller/mcp/toggleMcpServer"
|
||||
import { updateMcpTimeout } from "../core/controller/mcp/updateMcpTimeout"
|
||||
import { addRemoteMcpServer } from "../core/controller/mcp/addRemoteMcpServer"
|
||||
import { downloadMcp } from "../core/controller/mcp/downloadMcp"
|
||||
import { restartMcpServer } from "../core/controller/mcp/restartMcpServer"
|
||||
|
||||
// Models Service
|
||||
import { getOllamaModels } from "../core/controller/models/getOllamaModels"
|
||||
@@ -64,6 +66,7 @@ import { deleteNonFavoritedTasks } from "../core/controller/task/deleteNonFavori
|
||||
import { getTaskHistory } from "../core/controller/task/getTaskHistory"
|
||||
import { askResponse } from "../core/controller/task/askResponse"
|
||||
import { taskFeedback } from "../core/controller/task/taskFeedback"
|
||||
import { taskCompletionViewChanges } from "../core/controller/task/taskCompletionViewChanges"
|
||||
|
||||
// Web Service
|
||||
import { checkIsImageUrl } from "../core/controller/web/checkIsImageUrl"
|
||||
@@ -98,6 +101,7 @@ export function addServices(
|
||||
|
||||
// File Service
|
||||
server.addService(proto.cline.FileService.service, {
|
||||
copyToClipboard: wrapper(copyToClipboard, controller),
|
||||
openFile: wrapper(openFile, controller),
|
||||
openImage: wrapper(openImage, controller),
|
||||
deleteRuleFile: wrapper(deleteRuleFile, controller),
|
||||
@@ -114,6 +118,7 @@ export function addServices(
|
||||
updateMcpTimeout: wrapper(updateMcpTimeout, controller),
|
||||
addRemoteMcpServer: wrapper(addRemoteMcpServer, controller),
|
||||
downloadMcp: wrapper(downloadMcp, controller),
|
||||
restartMcpServer: wrapper(restartMcpServer, controller),
|
||||
})
|
||||
|
||||
// Models Service
|
||||
@@ -153,6 +158,7 @@ export function addServices(
|
||||
getTaskHistory: wrapper(getTaskHistory, controller),
|
||||
askResponse: wrapper(askResponse, controller),
|
||||
taskFeedback: wrapper(taskFeedback, controller),
|
||||
taskCompletionViewChanges: wrapper(taskCompletionViewChanges, controller),
|
||||
})
|
||||
|
||||
// Web Service
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
---
|
||||
title: "UI Navigation Refactoring Guide for Cline"
|
||||
description: "A comprehensive guide for refactoring Cline's UI navigation system from extension message-based navigation to React state management"
|
||||
---
|
||||
|
||||
|
||||
|
||||
This guide provides context for refactoring Cline's UI navigation system, moving from extension message-based navigation to React state management using context and custom hooks.
|
||||
|
||||
## Key Files and Structure
|
||||
|
||||
### Message Types
|
||||
|
||||
Message types define the communication between the extension and webview.
|
||||
|
||||
- `src/shared/WebviewMessage.ts` - TypeScript interface defining the messages sent from webview to extension
|
||||
- Contains message types that should be removed in favor of direct navigation functions
|
||||
- Example: `"showChatView"` should be replaced with `navigateToChat()`
|
||||
|
||||
- `src/shared/ExtensionMessage.ts` - TypeScript interface defining the messages sent from extension to extension
|
||||
- Contains navigation actions that will be handled by ExtensionStateContext
|
||||
- Example: `action: "mcpButtonClicked"` triggers MCP view navigation
|
||||
|
||||
### Controller and Context
|
||||
|
||||
- `src/core/controller/index.ts` - Main controller that handles messages from the webview
|
||||
- Processes incoming WebviewMessages
|
||||
- Sends navigation commands via ExtensionMessages
|
||||
- Contains handlers that will be removed after refactoring
|
||||
|
||||
- `webview-ui/src/context/ExtensionStateContext.tsx` - React context for managing UI state
|
||||
- Maintains navigation state (which views are visible)
|
||||
- Provides navigation functions through the context (navigateToMcp, navigateToSettings, etc.)
|
||||
- Handles incoming extension messages for navigation
|
||||
|
||||
### Components
|
||||
|
||||
- `webview-ui/src/components/cline-rules/ClineRulesToggleModal.tsx` - Example component using direct message posting
|
||||
- Currently uses `vscode.postMessage()` directly for some actions
|
||||
- Should be updated to use navigation functions from ExtensionStateContext directly
|
||||
|
||||
- `webview-ui/src/components/chat/ServersToggleModal.tsx` - Example of refactored component
|
||||
- Uses `navigateToMcp()` directly from ExtensionStateContext
|
||||
- No longer depends on separate navigation hooks
|
||||
|
||||
## Understanding the Navigation Patterns
|
||||
|
||||
### Pattern 1: Webview-to-Extension-to-Webview (To Be Refactored)
|
||||
|
||||
This pattern occurs when a component wants to navigate to a different view:
|
||||
|
||||
1. Component sends a message to the extension: `vscode.postMessage({ type: "showChatView" })`
|
||||
2. Extension controller processes it: `case "showChatView"`
|
||||
3. Controller sends a navigation action back: `this.postMessageToWebview({ type: "action", action: "chatButtonClicked" })`
|
||||
4. ExtensionStateContext handles the action and updates state: `case "chatButtonClicked": navigateToChat()`
|
||||
|
||||
**Example from the code:**
|
||||
|
||||
```typescript
|
||||
// WebviewMessage.ts - Message type to be removed
|
||||
| "showChatView"
|
||||
|
||||
// controller/index.ts - Handler to be removed
|
||||
case "showChatView": {
|
||||
this.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
// ExtensionStateContext.tsx - Handling that will remain
|
||||
case "chatButtonClicked":
|
||||
navigateToChat()
|
||||
break
|
||||
```
|
||||
|
||||
### Pattern 2: Extension-Initiated Navigation (Must Remain)
|
||||
|
||||
This pattern occurs when the extension needs to control navigation:
|
||||
|
||||
1. Extension sends a message: `this.postMessageToWebview({ type: "action", action: "settingsButtonClicked" })`
|
||||
2. ExtensionStateContext handles it and updates state: `case "settingsButtonClicked": navigateToSettings()`
|
||||
|
||||
**Example from the code:**
|
||||
|
||||
```typescript
|
||||
// ExtensionMessage.ts
|
||||
action?:
|
||||
| "chatButtonClicked"
|
||||
| "mcpButtonClicked"
|
||||
| "settingsButtonClicked"
|
||||
| "historyButtonClicked"
|
||||
| "didBecomeVisible"
|
||||
| "accountLogoutClicked"
|
||||
| "accountButtonClicked"
|
||||
| "focusChatInput"
|
||||
|
||||
// ExtensionStateContext.tsx
|
||||
case "settingsButtonClicked":
|
||||
navigateToSettings()
|
||||
break
|
||||
```
|
||||
|
||||
## The Refactoring Process
|
||||
|
||||
For each UI action that follows Pattern 1, follow these steps:
|
||||
|
||||
1. **Identify UI state variables needed**
|
||||
- ExtensionStateContext already includes view state variables:
|
||||
```typescript
|
||||
const [showMcp, setShowMcp] = useState(false)
|
||||
const [mcpTab, setMcpTab] = useState<McpViewTab | undefined>(undefined)
|
||||
const [showSettings, setShowSettings] = useState(false)
|
||||
const [showHistory, setShowHistory] = useState(false)
|
||||
const [showAccount, setShowAccount] = useState(false)
|
||||
```
|
||||
|
||||
2. **Use navigation functions from ExtensionStateContext directly**
|
||||
- ExtensionStateContext provides navigation functions:
|
||||
```typescript
|
||||
const navigateToMcp = useCallback(/*...*/)
|
||||
const navigateToSettings = useCallback(/*...*/)
|
||||
const navigateToHistory = useCallback(/*...*/)
|
||||
const navigateToAccount = useCallback(/*...*/)
|
||||
const navigateToChat = useCallback(/*...*/)
|
||||
```
|
||||
- Use these functions directly without intermediate hooks
|
||||
|
||||
3. **Update components to use the context**
|
||||
- Replace `vscode.postMessage()` calls with context functions:
|
||||
```typescript
|
||||
// Before:
|
||||
vscode.postMessage({ type: "showChatView" })
|
||||
|
||||
// After:
|
||||
const { navigateToChat } = useExtensionState()
|
||||
navigateToChat()
|
||||
```
|
||||
|
||||
4. **Clean up message interfaces and controller**
|
||||
- Remove the action type from WebviewMessage interface
|
||||
- Remove the case handler from controller/index.ts
|
||||
|
||||
## Messages to Remove vs. Keep
|
||||
|
||||
### Messages to Remove (Pattern 1)
|
||||
|
||||
These messages from WebviewMessage.ts should be refactored to use direct navigation functions:
|
||||
|
||||
```typescript
|
||||
| "showChatView" // Use navigateToChat() instead
|
||||
| "openMcpSettings" // Use navigateToMcp() instead
|
||||
| "openSettings" // Use navigateToSettings() instead
|
||||
```
|
||||
|
||||
### Messages to Keep (Pattern 2)
|
||||
|
||||
These actions in ExtensionMessage.ts must remain as they're initiated by the extension:
|
||||
|
||||
```typescript
|
||||
action?:
|
||||
| "chatButtonClicked" // Extension initiates chat view
|
||||
| "mcpButtonClicked" // Extension initiates MCP view
|
||||
| "settingsButtonClicked" // Extension initiates settings view
|
||||
| "historyButtonClicked" // Extension initiates history view
|
||||
| "accountButtonClicked" // Extension initiates account view
|
||||
```
|
||||
|
||||
## Example Refactoring
|
||||
|
||||
Let's look at how `showChatView` would be refactored:
|
||||
|
||||
### Before Refactoring
|
||||
|
||||
```typescript
|
||||
// Component.tsx
|
||||
const handleButtonClick = () => {
|
||||
vscode.postMessage({ type: "showChatView" })
|
||||
}
|
||||
|
||||
// controller/index.ts
|
||||
case "showChatView": {
|
||||
this.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
// ExtensionStateContext.tsx
|
||||
case "chatButtonClicked":
|
||||
navigateToChat()
|
||||
break
|
||||
```
|
||||
|
||||
### After Refactoring
|
||||
|
||||
```typescript
|
||||
// Component.tsx
|
||||
const { navigateToChat } = useExtensionState()
|
||||
const handleButtonClick = () => {
|
||||
navigateToChat()
|
||||
}
|
||||
|
||||
// controller/index.ts
|
||||
// The showChatView case is removed
|
||||
|
||||
// ExtensionStateContext.tsx
|
||||
// The chatButtonClicked case remains for extension-initiated navigation
|
||||
case "chatButtonClicked":
|
||||
navigateToChat()
|
||||
break
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Direct Navigation**: Always use the navigation functions from ExtensionStateContext directly
|
||||
- **Clean Views**: When navigating to a view, ensure other views are properly hidden
|
||||
- **Consistent Naming**: Use consistent naming for navigation functions (`navigateTo<View>`)
|
||||
- **Keep Extension Actions**: Maintain extension-initiated navigation actions in ExtensionMessage.ts
|
||||
- **Document Dependencies**: Use proper dependency arrays in useCallback for navigation functions
|
||||
- **Centralized Logic**: Keep all navigation logic in ExtensionStateContext
|
||||
- **Avoid Extra Hooks**: Don't create separate hooks that merely wrap navigation functions
|
||||
+23
-75
@@ -1,6 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { useEvent } from "react-use"
|
||||
import { ExtensionMessage } from "@shared/ExtensionMessage"
|
||||
import { useEffect } from "react"
|
||||
import ChatView from "./components/chat/ChatView"
|
||||
import HistoryView from "./components/history/HistoryView"
|
||||
import SettingsView from "./components/settings/SettingsView"
|
||||
@@ -12,67 +10,24 @@ import McpView from "./components/mcp/configuration/McpConfigurationView"
|
||||
import { Providers } from "./Providers"
|
||||
|
||||
const AppContent = () => {
|
||||
const { didHydrateState, showWelcome, shouldShowAnnouncement, showMcp, mcpTab } = useExtensionState()
|
||||
const [showSettings, setShowSettings] = useState(false)
|
||||
const hideSettings = useCallback(() => setShowSettings(false), [])
|
||||
const [showHistory, setShowHistory] = useState(false)
|
||||
const [showAccount, setShowAccount] = useState(false)
|
||||
const [showAnnouncement, setShowAnnouncement] = useState(false)
|
||||
|
||||
const { setShowMcp, setMcpTab } = useExtensionState()
|
||||
|
||||
const closeMcpView = useCallback(() => {
|
||||
setShowMcp(false)
|
||||
setMcpTab(undefined)
|
||||
}, [setShowMcp, setMcpTab])
|
||||
|
||||
const handleMessage = useCallback(
|
||||
(e: MessageEvent) => {
|
||||
const message: ExtensionMessage = e.data
|
||||
switch (message.type) {
|
||||
case "action":
|
||||
switch (message.action!) {
|
||||
case "settingsButtonClicked":
|
||||
setShowSettings(true)
|
||||
setShowHistory(false)
|
||||
closeMcpView()
|
||||
setShowAccount(false)
|
||||
break
|
||||
case "historyButtonClicked":
|
||||
setShowSettings(false)
|
||||
setShowHistory(true)
|
||||
closeMcpView()
|
||||
setShowAccount(false)
|
||||
break
|
||||
case "mcpButtonClicked":
|
||||
setShowSettings(false)
|
||||
setShowHistory(false)
|
||||
if (message.tab) {
|
||||
setMcpTab(message.tab)
|
||||
}
|
||||
setShowMcp(true)
|
||||
setShowAccount(false)
|
||||
break
|
||||
case "accountButtonClicked":
|
||||
setShowSettings(false)
|
||||
setShowHistory(false)
|
||||
closeMcpView()
|
||||
setShowAccount(true)
|
||||
break
|
||||
case "chatButtonClicked":
|
||||
setShowSettings(false)
|
||||
setShowHistory(false)
|
||||
closeMcpView()
|
||||
setShowAccount(false)
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
},
|
||||
[setShowMcp, setMcpTab, closeMcpView],
|
||||
)
|
||||
|
||||
useEvent("message", handleMessage)
|
||||
const {
|
||||
didHydrateState,
|
||||
showWelcome,
|
||||
shouldShowAnnouncement,
|
||||
showMcp,
|
||||
mcpTab,
|
||||
showSettings,
|
||||
showHistory,
|
||||
showAccount,
|
||||
showAnnouncement,
|
||||
setShowAnnouncement,
|
||||
closeMcpView,
|
||||
navigateToHistory,
|
||||
hideSettings,
|
||||
hideHistory,
|
||||
hideAccount,
|
||||
hideAnnouncement,
|
||||
} = useExtensionState()
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldShowAnnouncement) {
|
||||
@@ -92,22 +47,15 @@ const AppContent = () => {
|
||||
) : (
|
||||
<>
|
||||
{showSettings && <SettingsView onDone={hideSettings} />}
|
||||
{showHistory && <HistoryView onDone={() => setShowHistory(false)} />}
|
||||
{showHistory && <HistoryView onDone={hideHistory} />}
|
||||
{showMcp && <McpView initialTab={mcpTab} onDone={closeMcpView} />}
|
||||
{showAccount && <AccountView onDone={() => setShowAccount(false)} />}
|
||||
{showAccount && <AccountView onDone={hideAccount} />}
|
||||
{/* Do not conditionally load ChatView, it's expensive and there's state we don't want to lose (user input, disableInput, askResponse promise, etc.) */}
|
||||
<ChatView
|
||||
showHistoryView={() => {
|
||||
setShowSettings(false)
|
||||
closeMcpView()
|
||||
setShowAccount(false)
|
||||
setShowHistory(true)
|
||||
}}
|
||||
showHistoryView={navigateToHistory}
|
||||
isHidden={showSettings || showHistory || showMcp || showAccount}
|
||||
showAnnouncement={showAnnouncement}
|
||||
hideAnnouncement={() => {
|
||||
setShowAnnouncement(false)
|
||||
}}
|
||||
hideAnnouncement={hideAnnouncement}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -36,7 +36,7 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
const minorVersion = version.split(".").slice(0, 2).join(".") // 2.0.0 -> 2.0
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<VSCodeButton appearance="icon" onClick={hideAnnouncement} style={closeIconStyle}>
|
||||
<VSCodeButton data-testid="close-button" appearance="icon" onClick={hideAnnouncement} style={closeIconStyle}>
|
||||
<span className="codicon codicon-close"></span>
|
||||
</VSCodeButton>
|
||||
<h3 style={h3TitleStyle}>
|
||||
|
||||
@@ -18,7 +18,7 @@ import { COMMAND_OUTPUT_STRING, COMMAND_REQ_APP_STRING } from "@shared/combineCo
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { findMatchingResourceOrTemplate, getMcpServerDisplayName } from "@/utils/mcp"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { FileServiceClient } from "@/services/grpc-client"
|
||||
import { FileServiceClient, TaskServiceClient } from "@/services/grpc-client"
|
||||
import { CheckmarkControl } from "@/components/common/CheckmarkControl"
|
||||
|
||||
interface CopyButtonProps {
|
||||
@@ -1226,10 +1226,9 @@ export const ChatRowContent = ({
|
||||
disabled={seeNewChangesDisabled}
|
||||
onClick={() => {
|
||||
setSeeNewChangesDisabled(true)
|
||||
vscode.postMessage({
|
||||
type: "taskCompletionViewChanges",
|
||||
number: message.ts,
|
||||
})
|
||||
TaskServiceClient.taskCompletionViewChanges({
|
||||
value: message.ts,
|
||||
}).catch((err) => console.error("Failed to show task completion view changes:", err))
|
||||
}}
|
||||
style={{
|
||||
cursor: seeNewChangesDisabled ? "wait" : "pointer",
|
||||
@@ -1390,10 +1389,11 @@ export const ChatRowContent = ({
|
||||
disabled={seeNewChangesDisabled}
|
||||
onClick={() => {
|
||||
setSeeNewChangesDisabled(true)
|
||||
vscode.postMessage({
|
||||
type: "taskCompletionViewChanges",
|
||||
number: message.ts,
|
||||
})
|
||||
TaskServiceClient.taskCompletionViewChanges({
|
||||
value: message.ts,
|
||||
}).catch((err) =>
|
||||
console.error("Failed to show task completion view changes:", err),
|
||||
)
|
||||
}}>
|
||||
<i
|
||||
className="codicon codicon-new-file"
|
||||
|
||||
@@ -199,8 +199,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
}
|
||||
|
||||
if (textToCopy !== null) {
|
||||
vscode.postMessage({ type: "copyToClipboard", text: textToCopy })
|
||||
e.preventDefault()
|
||||
try {
|
||||
FileServiceClient.copyToClipboard({ value: textToCopy }).catch((err) => {
|
||||
console.error("Error copying to clipboard:", err)
|
||||
})
|
||||
e.preventDefault()
|
||||
} catch (error) {
|
||||
console.error("Error copying to clipboard:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useRef, useState, useEffect } from "react"
|
||||
import { useClickAway, useWindowSize } from "react-use"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { useNavigator } from "@/hooks/useNavigator"
|
||||
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
import ServersToggleList from "@/components/mcp/configuration/tabs/installed/ServersToggleList"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
@@ -9,8 +8,7 @@ import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import Tooltip from "@/components/common/Tooltip"
|
||||
|
||||
const ServersToggleModal: React.FC = () => {
|
||||
const { mcpServers } = useExtensionState()
|
||||
const { navigateToMcp } = useNavigator()
|
||||
const { mcpServers, navigateToMcp } = useExtensionState()
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
const buttonRef = useRef<HTMLDivElement>(null)
|
||||
const modalRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -20,13 +20,7 @@ describe("Announcement", () => {
|
||||
|
||||
it("calls hideAnnouncement when close button is clicked", () => {
|
||||
render(<Announcement version="2.0.0" hideAnnouncement={hideAnnouncement} />)
|
||||
fireEvent.click(screen.getByRole("button"))
|
||||
fireEvent.click(screen.getByTestId("close-button"))
|
||||
expect(hideAnnouncement).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("renders the enhanced MCP support announcement", () => {
|
||||
render(<Announcement version="2.0.0" hideAnnouncement={hideAnnouncement} />)
|
||||
// Updated text based on actual component output
|
||||
expect(screen.getByText(/Enhanced MCP Support:/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
+24
-8
@@ -19,6 +19,7 @@ import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { McpServiceClient } from "@/services/grpc-client"
|
||||
import { convertProtoMcpServersToMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
|
||||
import { McpServers, UpdateMcpTimeoutRequest } from "@shared/proto/mcp"
|
||||
import { StringRequest } from "@shared/proto/common"
|
||||
// constant JSX.Elements
|
||||
const TimeoutOptions = [
|
||||
{ value: "30", label: "30 seconds" },
|
||||
@@ -46,6 +47,7 @@ const ServerRow = ({
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
const [isRestarting, setIsRestarting] = useState(false)
|
||||
|
||||
const getStatusColor = useCallback((status: McpServer["status"]) => {
|
||||
switch (status) {
|
||||
@@ -93,10 +95,24 @@ const ServerRow = ({
|
||||
}
|
||||
|
||||
const handleRestart = () => {
|
||||
vscode.postMessage({
|
||||
type: "restartMcpServer",
|
||||
text: server.name,
|
||||
})
|
||||
// Set local state to show "connecting" status
|
||||
setIsRestarting(true)
|
||||
|
||||
// Make the gRPC call
|
||||
McpServiceClient.restartMcpServer({
|
||||
value: server.name,
|
||||
} as StringRequest)
|
||||
.then((response: McpServers) => {
|
||||
// Update with the final state from the server
|
||||
const mcpServers = convertProtoMcpServersToMcpServers(response.mcpServers)
|
||||
setMcpServers(mcpServers)
|
||||
setIsRestarting(false)
|
||||
})
|
||||
.catch((error) => {
|
||||
// Reset the restarting state
|
||||
setIsRestarting(false)
|
||||
console.error("Error restarting MCP server", error)
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
@@ -171,7 +187,7 @@ const ServerRow = ({
|
||||
e.stopPropagation()
|
||||
handleRestart()
|
||||
}}
|
||||
disabled={server.status === "connecting"}>
|
||||
disabled={server.status === "connecting" || isRestarting}>
|
||||
<span className="codicon codicon-sync"></span>
|
||||
</VSCodeButton>
|
||||
{hasTrashIcon && (
|
||||
@@ -267,7 +283,7 @@ const ServerRow = ({
|
||||
width: "calc(100% - 20px)",
|
||||
margin: "0 10px 10px 10px",
|
||||
}}>
|
||||
{server.status === "connecting" ? "Retrying..." : "Retry Connection"}
|
||||
{server.status === "connecting" || isRestarting ? "Retrying..." : "Retry Connection"}
|
||||
</VSCodeButton>
|
||||
|
||||
<DangerButton
|
||||
@@ -363,12 +379,12 @@ const ServerRow = ({
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
onClick={handleRestart}
|
||||
disabled={server.status === "connecting"}
|
||||
disabled={server.status === "connecting" || isRestarting}
|
||||
style={{
|
||||
width: "calc(100% - 14px)",
|
||||
margin: "0 7px 3px 7px",
|
||||
}}>
|
||||
{server.status === "connecting" ? "Restarting..." : "Restart Server"}
|
||||
{server.status === "connecting" || isRestarting ? "Restarting..." : "Restart Server"}
|
||||
</VSCodeButton>
|
||||
|
||||
<DangerButton
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { render, screen, fireEvent } from "@testing-library/react"
|
||||
import { describe, it, expect, vi } from "vitest"
|
||||
import ApiOptions from "../ApiOptions"
|
||||
import { ExtensionStateContextProvider } from "@/context/ExtensionStateContext"
|
||||
import { ExtensionStateContextProvider, useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { ApiConfiguration } from "@shared/api"
|
||||
|
||||
vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => {
|
||||
const actual = await importOriginal()
|
||||
@@ -16,10 +17,20 @@ vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => {
|
||||
},
|
||||
setApiConfiguration: vi.fn(),
|
||||
uriScheme: "vscode",
|
||||
requestyModels: {},
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
const mockExtensionState = (apiConfiguration: Partial<ApiConfiguration>) => {
|
||||
vi.mocked(useExtensionState).mockReturnValue({
|
||||
apiConfiguration,
|
||||
setApiConfiguration: vi.fn(),
|
||||
uriScheme: "vscode",
|
||||
requestyModels: {},
|
||||
} as any)
|
||||
}
|
||||
|
||||
describe("ApiOptions Component", () => {
|
||||
vi.clearAllMocks()
|
||||
const mockPostMessage = vi.fn()
|
||||
@@ -27,6 +38,9 @@ describe("ApiOptions Component", () => {
|
||||
beforeEach(() => {
|
||||
//@ts-expect-error - vscode is not defined in the global namespace in test environment
|
||||
global.vscode = { postMessage: mockPostMessage }
|
||||
mockExtensionState({
|
||||
apiProvider: "requesty",
|
||||
})
|
||||
})
|
||||
|
||||
it("renders Requesty API Key input", () => {
|
||||
@@ -45,28 +59,11 @@ describe("ApiOptions Component", () => {
|
||||
<ApiOptions showModelOptions={true} />
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
const modelIdInput = screen.getByPlaceholderText("Enter Model ID...")
|
||||
const modelIdInput = screen.getByPlaceholderText("Search and select a model...")
|
||||
expect(modelIdInput).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => {
|
||||
const actual = await importOriginal()
|
||||
return {
|
||||
...(actual || {}),
|
||||
// your mocked methods
|
||||
useExtensionState: vi.fn(() => ({
|
||||
apiConfiguration: {
|
||||
apiProvider: "together",
|
||||
requestyApiKey: "",
|
||||
requestyModelId: "",
|
||||
},
|
||||
setApiConfiguration: vi.fn(),
|
||||
uriScheme: "vscode",
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
describe("ApiOptions Component", () => {
|
||||
vi.clearAllMocks()
|
||||
const mockPostMessage = vi.fn()
|
||||
@@ -74,6 +71,9 @@ describe("ApiOptions Component", () => {
|
||||
beforeEach(() => {
|
||||
//@ts-expect-error - vscode is not defined in the global namespace in test environment
|
||||
global.vscode = { postMessage: mockPostMessage }
|
||||
mockExtensionState({
|
||||
apiProvider: "together",
|
||||
})
|
||||
})
|
||||
|
||||
it("renders Together API Key input", () => {
|
||||
@@ -97,24 +97,6 @@ describe("ApiOptions Component", () => {
|
||||
})
|
||||
})
|
||||
|
||||
vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => {
|
||||
const actual = await importOriginal()
|
||||
return {
|
||||
...(actual || {}),
|
||||
useExtensionState: vi.fn(() => ({
|
||||
apiConfiguration: {
|
||||
apiProvider: "fireworks",
|
||||
fireworksApiKey: "",
|
||||
fireworksModelId: "",
|
||||
fireworksModelMaxCompletionTokens: 2000,
|
||||
fireworksModelMaxTokens: 4000,
|
||||
},
|
||||
setApiConfiguration: vi.fn(),
|
||||
uriScheme: "vscode",
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
describe("ApiOptions Component", () => {
|
||||
vi.clearAllMocks()
|
||||
const mockPostMessage = vi.fn()
|
||||
@@ -122,6 +104,14 @@ describe("ApiOptions Component", () => {
|
||||
beforeEach(() => {
|
||||
//@ts-expect-error - vscode is not defined in the global namespace in test environment
|
||||
global.vscode = { postMessage: mockPostMessage }
|
||||
|
||||
mockExtensionState({
|
||||
apiProvider: "fireworks",
|
||||
fireworksApiKey: "",
|
||||
fireworksModelId: "",
|
||||
fireworksModelMaxCompletionTokens: 2000,
|
||||
fireworksModelMaxTokens: 4000,
|
||||
})
|
||||
})
|
||||
|
||||
it("renders Fireworks API Key input", () => {
|
||||
@@ -165,23 +155,6 @@ describe("ApiOptions Component", () => {
|
||||
})
|
||||
})
|
||||
|
||||
vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => {
|
||||
const actual = await importOriginal()
|
||||
return {
|
||||
...(actual || {}),
|
||||
// your mocked methods
|
||||
useExtensionState: vi.fn(() => ({
|
||||
apiConfiguration: {
|
||||
apiProvider: "openai",
|
||||
requestyApiKey: "",
|
||||
requestyModelId: "",
|
||||
},
|
||||
setApiConfiguration: vi.fn(),
|
||||
uriScheme: "vscode",
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
describe("OpenApiInfoOptions", () => {
|
||||
const mockPostMessage = vi.fn()
|
||||
|
||||
@@ -189,6 +162,9 @@ describe("OpenApiInfoOptions", () => {
|
||||
vi.clearAllMocks()
|
||||
//@ts-expect-error - vscode is not defined in the global namespace in test environment
|
||||
global.vscode = { postMessage: mockPostMessage }
|
||||
mockExtensionState({
|
||||
apiProvider: "openai",
|
||||
})
|
||||
})
|
||||
|
||||
it("renders OpenAI Supports Images input", () => {
|
||||
|
||||
@@ -31,9 +31,14 @@ interface ExtensionStateContextType extends ExtensionState {
|
||||
mcpMarketplaceCatalog: McpMarketplaceCatalog
|
||||
filePaths: string[]
|
||||
totalTasksSize: number | null
|
||||
|
||||
// View state
|
||||
showMcp: boolean
|
||||
mcpTab?: McpViewTab
|
||||
showSettings: boolean
|
||||
showHistory: boolean
|
||||
showAccount: boolean
|
||||
showAnnouncement: boolean
|
||||
|
||||
// Setters
|
||||
setApiConfiguration: (config: ApiConfiguration) => void
|
||||
@@ -47,9 +52,23 @@ interface ExtensionStateContextType extends ExtensionState {
|
||||
setChatSettings: (value: ChatSettings) => void
|
||||
setMcpServers: (value: McpServer[]) => void
|
||||
|
||||
// Navigation
|
||||
// Navigation state setters
|
||||
setShowMcp: (value: boolean) => void
|
||||
setMcpTab: (tab?: McpViewTab) => void
|
||||
|
||||
// Navigation functions
|
||||
navigateToMcp: (tab?: McpViewTab) => void
|
||||
navigateToSettings: () => void
|
||||
navigateToHistory: () => void
|
||||
navigateToAccount: () => void
|
||||
navigateToChat: () => void
|
||||
|
||||
// Hide functions
|
||||
hideSettings: () => void
|
||||
hideHistory: () => void
|
||||
hideAccount: () => void
|
||||
hideAnnouncement: () => void
|
||||
closeMcpView: () => void
|
||||
}
|
||||
|
||||
const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
|
||||
@@ -60,6 +79,64 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
// UI view state
|
||||
const [showMcp, setShowMcp] = useState(false)
|
||||
const [mcpTab, setMcpTab] = useState<McpViewTab | undefined>(undefined)
|
||||
const [showSettings, setShowSettings] = useState(false)
|
||||
const [showHistory, setShowHistory] = useState(false)
|
||||
const [showAccount, setShowAccount] = useState(false)
|
||||
const [showAnnouncement, setShowAnnouncement] = useState(false)
|
||||
|
||||
// Helper for MCP view
|
||||
const closeMcpView = useCallback(() => {
|
||||
setShowMcp(false)
|
||||
setMcpTab(undefined)
|
||||
}, [setShowMcp, setMcpTab])
|
||||
|
||||
// Hide functions
|
||||
const hideSettings = useCallback(() => setShowSettings(false), [setShowSettings])
|
||||
const hideHistory = useCallback(() => setShowHistory(false), [setShowHistory])
|
||||
const hideAccount = useCallback(() => setShowAccount(false), [setShowAccount])
|
||||
const hideAnnouncement = useCallback(() => setShowAnnouncement(false), [setShowAnnouncement])
|
||||
|
||||
// Navigation functions
|
||||
const navigateToMcp = useCallback(
|
||||
(tab?: McpViewTab) => {
|
||||
setShowSettings(false)
|
||||
setShowHistory(false)
|
||||
setShowAccount(false)
|
||||
if (tab) {
|
||||
setMcpTab(tab)
|
||||
}
|
||||
setShowMcp(true)
|
||||
},
|
||||
[setShowMcp, setMcpTab, setShowSettings, setShowHistory, setShowAccount],
|
||||
)
|
||||
|
||||
const navigateToSettings = useCallback(() => {
|
||||
setShowHistory(false)
|
||||
closeMcpView()
|
||||
setShowAccount(false)
|
||||
setShowSettings(true)
|
||||
}, [setShowSettings, setShowHistory, closeMcpView, setShowAccount])
|
||||
|
||||
const navigateToHistory = useCallback(() => {
|
||||
setShowSettings(false)
|
||||
closeMcpView()
|
||||
setShowAccount(false)
|
||||
setShowHistory(true)
|
||||
}, [setShowSettings, closeMcpView, setShowAccount, setShowHistory])
|
||||
|
||||
const navigateToAccount = useCallback(() => {
|
||||
setShowSettings(false)
|
||||
closeMcpView()
|
||||
setShowHistory(false)
|
||||
setShowAccount(true)
|
||||
}, [setShowSettings, closeMcpView, setShowHistory, setShowAccount])
|
||||
|
||||
const navigateToChat = useCallback(() => {
|
||||
setShowSettings(false)
|
||||
closeMcpView()
|
||||
setShowHistory(false)
|
||||
setShowAccount(false)
|
||||
}, [setShowSettings, closeMcpView, setShowHistory, setShowAccount])
|
||||
|
||||
const [state, setState] = useState<ExtensionState>({
|
||||
version: "",
|
||||
@@ -100,6 +177,26 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
const handleMessage = useCallback((event: MessageEvent) => {
|
||||
const message: ExtensionMessage = event.data
|
||||
switch (message.type) {
|
||||
case "action": {
|
||||
switch (message.action!) {
|
||||
case "mcpButtonClicked":
|
||||
navigateToMcp(message.tab)
|
||||
break
|
||||
case "settingsButtonClicked":
|
||||
navigateToSettings()
|
||||
break
|
||||
case "historyButtonClicked":
|
||||
navigateToHistory()
|
||||
break
|
||||
case "accountButtonClicked":
|
||||
navigateToAccount()
|
||||
break
|
||||
case "chatButtonClicked":
|
||||
navigateToChat()
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
case "state": {
|
||||
// Handler for direct state messages
|
||||
if (message.state) {
|
||||
@@ -335,12 +432,29 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
totalTasksSize,
|
||||
showMcp,
|
||||
mcpTab,
|
||||
showSettings,
|
||||
showHistory,
|
||||
showAccount,
|
||||
showAnnouncement,
|
||||
globalClineRulesToggles: state.globalClineRulesToggles || {},
|
||||
localClineRulesToggles: state.localClineRulesToggles || {},
|
||||
localCursorRulesToggles: state.localCursorRulesToggles || {},
|
||||
localWindsurfRulesToggles: state.localWindsurfRulesToggles || {},
|
||||
workflowToggles: state.workflowToggles || {},
|
||||
enableCheckpointsSetting: state.enableCheckpointsSetting,
|
||||
|
||||
// Navigation functions
|
||||
navigateToMcp,
|
||||
navigateToSettings,
|
||||
navigateToHistory,
|
||||
navigateToAccount,
|
||||
navigateToChat,
|
||||
|
||||
// Hide functions
|
||||
hideSettings,
|
||||
hideHistory,
|
||||
hideAccount,
|
||||
hideAnnouncement,
|
||||
setApiConfiguration: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
@@ -383,6 +497,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
})),
|
||||
setMcpServers: (mcpServers: McpServer[]) => setMcpServers(mcpServers),
|
||||
setShowMcp,
|
||||
closeMcpView,
|
||||
setChatSettings: (value) => {
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { useExtensionState } from "../context/ExtensionStateContext"
|
||||
import { McpViewTab } from "@shared/mcp"
|
||||
|
||||
/**
|
||||
* Hook for navigating between different views in the application.
|
||||
*/
|
||||
export const useNavigator = () => {
|
||||
const { setShowMcp, setMcpTab } = useExtensionState()
|
||||
|
||||
/**
|
||||
* Navigate to the MCP view
|
||||
* @param tab Optional tab to show in the MCP view
|
||||
*/
|
||||
const navigateToMcp = (tab?: McpViewTab) => {
|
||||
if (tab) {
|
||||
setMcpTab(tab)
|
||||
}
|
||||
setShowMcp(true)
|
||||
}
|
||||
|
||||
return {
|
||||
navigateToMcp,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user