mirror of
https://github.com/cline/cline.git
synced 2026-09-06 12:28:08 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 370b774c14 | |||
| 557c1549be |
@@ -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,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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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