Compare commits

...

3 Commits

Author SHA1 Message Date
Trevor Hudson 5d1636145e change to state name 2025-05-22 14:07:38 -07:00
Trevor Hudson 20ee6dca29 migrate to zustand 2025-05-21 17:40:46 -07:00
Trevor Hudson b2ffaf1026 fix telemetry 2025-05-20 20:51:25 -07:00
52 changed files with 1229 additions and 1034 deletions
+69
View File
@@ -0,0 +1,69 @@
# Workflow: Create Pull Request
This workflow outlines the steps for creating a pull request (PR) after development work is completed on a feature branch. The AI should follow these steps precisely.
## Prerequisites:
- Work is completed on the current feature branch.
- All changes are saved.
## 1. Add Changes for Commit
Stage all relevant changes for the commit.
- **Action:** Execute `git add .` to stage all changes in the current directory.
- **Alternative:** If specific files need to be staged, the AI can ask the user or attempt to identify them based on the work done. For example, `git add src/feature-file.ts webview-ui/src/components/NewComponent.tsx`.
## 2. Create a Changeset (If Applicable)
If the project uses `changesets` (e.g., `npx changeset` or `yarn changeset`), create a new changeset.
- **Action:** Check for `changeset` CLI in `package.json` scripts or as a dev dependency.
- **If `changesets` is used:**
- Execute `npx changeset` (or `yarn changeset`).
- The AI should guide the user through the changeset prompts if possible, or inform the user that they will need to complete the prompts in their terminal.
- **AI Note:** The AI might need to describe what a changeset is and why it's being created (e.g., "This project uses changesets to manage versioning and changelogs. I'll run the command to create one. Please follow the prompts in your terminal to describe the changes.").
- **If `changesets` is not used:** Skip this step.
## 3. Commit Changes
Commit the staged changes with a descriptive message.
- **Action:** Ask the user for a commit message or suggest one based on the task.
- **Guidance for AI:** Follow conventional commit message format if the project seems to use it (e.g., `feat: add user login functionality`, `fix: resolve issue with form validation`).
- **Action:** Execute `git commit -m "<commit-message>"`.
*If changesets were added in the previous step, the commit message could be something like `chore: add changeset` or the AI can ask the user for a more specific message.*
## 4. Rebase Main Branch onto Feature Branch
Ensure the feature branch is up-to-date with the latest changes from the main branch.
- **Action:** Identify the main branch (e.g., `main` or `master`).
- **Action:** Execute `git fetch origin <main-branch-name>`.
- **Action:** Execute `git rebase origin/<main-branch-name>`.
- **Conflict Resolution:** If rebase conflicts occur:
- Inform the user: "Rebase conflicts detected. Please resolve them in your editor. After resolving, run `git rebase --continue`. If you get stuck, you can run `git rebase --abort` to cancel the rebase."
- The AI should pause and wait for the user to confirm conflicts are resolved before proceeding.
## 5. Push Changes
Push the (potentially rebased) feature branch to the remote repository.
- **Action:** Get the current branch name: `git branch --show-current`.
- **Action:** Execute `git push origin <current-branch-name> --force-with-lease`.
*`--force-with-lease` is generally safer than `--force` when pushing rebased branches.*
## 6. Create Pull Request
Open a pull request on the repository hosting platform (e.g., GitHub, GitLab).
- **Action:** Use the `gh` CLI if available and authenticated.
- **Check for `gh`:** `gh --version`.
- **Check auth status:** `gh auth status`. If not authenticated, inform the user.
- **Create PR:** `gh pr create --fill --web` (opens in web browser to finalize) or `gh pr create --title "<PR Title>" --body "<PR Body>"`.
- The AI should ask the user for a PR title and body, or suggest them based on the commit messages/changesets.
- **Alternative (if `gh` CLI is not available/configured):**
- Provide the user with a link to create the PR. The link format depends on the platform (e.g., GitHub: `https://github.com/<owner>/<repo>/compare/<main-branch>...<feature-branch>?expand=1`). The AI will need to infer owner/repo from `git remote -v`.
- Instruct the user: "Please open the following link in your browser to create the Pull Request: [link]"
## AI Instructions & Considerations:
* **Error Handling:** If any Git command fails (other than expected rebase conflicts), report the error to the user and ask for guidance.
* **User Interaction:** Clearly communicate each step. Wait for user input for commit messages, PR titles/bodies, and confirmation of conflict resolution.
* **Platform Specifics:** Be mindful that `gh` is GitHub-specific. If the remote URL suggests GitLab or Bitbucket, the PR creation step will need to be adjusted (e.g., providing a generic link or different CLI commands if known).
* **Changeset Tooling:** The exact command for changesets might vary (`yarn changeset`, `pnpm changeset`, etc.). The AI should try to infer this from `package.json`.
* **Tool Usage:** Use the `execute_command` tool for Git and `gh` operations. Set `requires_approval` to `true` for commands that modify state or create PRs.
* **Idempotency:** If a PR already exists for the branch, `gh pr create` might fail or offer to update. The AI should handle this gracefully.
+48
View File
@@ -0,0 +1,48 @@
# Workflow: Start New Work
This workflow outlines the steps for starting a new piece of work or feature development. The AI should follow these steps precisely.
## 1. Ensure a Clean Working Directory
Before starting, ensure there are no uncommitted changes.
- **Action:** Execute `git status` to check for modifications.
- **If changes exist:** Ask the user if they want to stash them (`git stash`) or commit them. Proceed only when the working directory is clean.
## 2. Switch to the Main Branch
Switch to the primary development branch (commonly `main` or `master`).
- **Action:** Execute `git checkout main`.
*If `main` doesn't exist, try `master`. If neither exists, ask the user for the correct main branch name.*
## 3. Pull Latest Changes
Update the local main branch with the latest changes from the remote repository.
- **Action:** Execute `git pull origin main` (or the identified main branch name).
*Ensure this step completes successfully before proceeding.*
## 4. Create a New Feature Branch
Create a new branch for the work. The branch name should be descriptive.
- **Action:** Ask the user for a concise branch name (e.g., `feature/user-authentication` or `fix/login-bug`).
- **Guidance for AI:** Suggest a branch name based on the task if the user doesn't provide one.
- **Example format:** `feature/<short-description>`, `fix/<short-description>`, `chore/<short-description>`.
- **Action:** Execute `git checkout -b <branch-name>`.
## 5. Confirm Branch Creation
Verify that the new branch has been created and is currently active.
- **Action:** Execute `git branch --show-current`.
- **Expected Output:** The name of the newly created branch.
## 6. Understand the Task
Once the branch is set up, clarify the development task.
- **Action:** Ask the user: "Your new branch `<branch-name>` is ready. What would you like to build or work on?"
## AI Instructions & Considerations:
* **Error Handling:** If any Git command fails, report the error to the user and ask for guidance before retrying or proceeding.
* **User Interaction:** Clearly communicate each step being performed. Wait for user confirmation or input where specified (e.g., branch name, task description).
* **Project Context:** Be aware of the project structure (from `environment_details`) to understand potential impacts of the new work.
* **Idempotency:** If the workflow is re-run and the branch already exists, ask the user if they want to switch to it or delete and recreate it.
* **Tool Usage:** Use the `execute_command` tool for all Git operations. Set `requires_approval` to `false` for read-only commands like `git status` or `git branch --show-current`, and `true` for commands that modify state like `git checkout`, `git pull`, `git stash`.
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.16.0",
"version": "3.16.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.16.0",
"version": "3.16.1",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.12.4",
+2 -9
View File
@@ -256,13 +256,6 @@ export class Controller {
}
}
})
// If user already opted in to telemetry, enable telemetry service
this.getStateToPostToWebview().then((state) => {
const { telemetrySetting } = state
const isOptedIn = telemetrySetting !== "disabled"
telemetryService.updateTelemetryState(isOptedIn)
})
break
case "showChatView": {
this.postMessageToWebview({
@@ -1288,7 +1281,8 @@ export class Controller {
const workflowToggles = ((await getWorkspaceState(this.context, "workflowToggles")) as ClineRulesToggles) || {}
return {
version: this.context.extension?.packageJSON?.version ?? "",
version: this.context.extension.packageJSON.version,
vscMachineId: vscode.env.machineId,
apiConfiguration,
customInstructions,
uriScheme: vscode.env.uriScheme,
@@ -1309,7 +1303,6 @@ export class Controller {
telemetrySetting,
planActSeparateModelsSetting,
enableCheckpointsSetting: enableCheckpointsSetting ?? true,
vscMachineId: vscode.env.machineId,
globalClineRulesToggles: globalClineRulesToggles || {},
localClineRulesToggles: localClineRulesToggles || {},
localWindsurfRulesToggles: localWindsurfRulesToggles || {},
+24
View File
@@ -68,6 +68,7 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
localResourceRoots: [this.context.extensionUri],
}
// Original logic:
webviewView.webview.html =
this.context.extensionMode === vscode.ExtensionMode.Development
? await this.getHMRHtmlContent(webviewView.webview)
@@ -235,6 +236,15 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
*/
const nonce = getNonce()
// Data you want to pass to the React app
const initialData = {
vscMachineId: vscode.env.machineId,
extensionVersion: this.context.extension.packageJSON.version,
}
// Serialize the data to a JSON string to safely embed it
const serializedInitialData = JSON.stringify(initialData)
// Tip: Install the es6-string-html VS Code extension to enable code highlighting below
return /*html*/ `
<!DOCTYPE html>
@@ -248,6 +258,9 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
<link href="${katexCssUri}" rel="stylesheet" />
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; connect-src https://*.posthog.com https://*.firebaseauth.com https://*.firebaseio.com https://*.googleapis.com https://*.firebase.com; font-src ${webview.cspSource} data:; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} https: data:; script-src 'nonce-${nonce}' 'unsafe-eval';">
<title>Cline</title>
<script nonce="${nonce}">
window.__INITIAL_DATA__ = ${serializedInitialData};
</script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
@@ -327,6 +340,14 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
const scriptEntrypoint = "src/main.tsx"
const scriptUri = `http://${localServerUrl}/${scriptEntrypoint}`
// Data you want to pass to the React app
const initialData = {
vscMachineId: vscode.env.machineId,
extensionVersion: this.context.extension.packageJSON.version,
}
// Serialize the data to a JSON string to safely embed it
const serializedInitialData = JSON.stringify(initialData)
const reactRefresh = /*html*/ `
<script nonce="${nonce}" type="module">
import RefreshRuntime from "http://${localServerUrl}/@react-refresh"
@@ -358,6 +379,9 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
<link href="${codiconsUri}" rel="stylesheet" />
<link href="${katexCssUri}" rel="stylesheet" />
<title>Cline</title>
<script nonce="${nonce}">
window.__INITIAL_DATA__ = ${serializedInitialData};
</script>
</head>
<body>
<div id="root"></div>
@@ -9,7 +9,6 @@ class PostHogClientProvider {
this.client = new PostHog(posthogConfig.apiKey, {
host: posthogConfig.host,
enableExceptionAutocapture: false,
defaultOptIn: false,
})
}
@@ -7,7 +7,7 @@ import type { BrowserSettings } from "@shared/BrowserSettings"
import { posthogClientProvider } from "../PostHogClientProvider"
/**
* PostHogClient handles telemetry event tracking for the Cline extension
* TelemetryService handles telemetry event tracking for the Cline extension
* Uses PostHog analytics to track user interactions and system events
* Respects user privacy settings and VSCode's global telemetry configuration
*/
@@ -29,7 +29,7 @@ interface Collection {
*/
type TelemetryCategory = "checkpoints" | "browser"
class PostHogClient {
class TelemetryService {
// Map to control specific telemetry categories (event types)
private telemetryCategoryEnabled: Map<TelemetryCategory, boolean> = new Map([
["checkpoints", false], // Checkpoints telemetry disabled
@@ -83,33 +83,15 @@ class PostHogClient {
},
// UI interaction events for tracking user engagement
UI: {
// Tracks when user switches between API providers
PROVIDER_SWITCH: "ui.provider_switch",
// Tracks when images are attached to a conversation
IMAGE_ATTACHED: "ui.image_attached",
// Tracks general button click interactions
BUTTON_CLICK: "ui.button_click",
// Tracks when the marketplace view is opened
MARKETPLACE_OPENED: "ui.marketplace_opened",
// Tracks when settings panel is opened
SETTINGS_OPENED: "ui.settings_opened",
// Tracks when task history view is opened
HISTORY_OPENED: "ui.history_opened",
// Tracks when a task is removed from history
TASK_POPPED: "ui.task_popped",
// Tracks when a different model is selected
MODEL_SELECTED: "ui.model_selected",
// Tracks when planning mode is toggled on
PLAN_MODE_TOGGLED: "ui.plan_mode_toggled",
// Tracks when action mode is toggled on
ACT_MODE_TOGGLED: "ui.act_mode_toggled",
// Tracks when users use the "favorite" button in the model picker
MODEL_FAVORITE_TOGGLED: "ui.model_favorite_toggled",
},
}
/** Singleton instance of the PostHogClient */
private static instance: PostHogClient
/** Singleton instance of the TelemetryService */
private static instance: TelemetryService
/** PostHog client instance for sending analytics events */
private client: PostHog
/** Unique identifier for the current VSCode instance */
@@ -135,8 +117,6 @@ class PostHogClient {
* @param didUserOptIn Whether the user has explicitly opted into telemetry
*/
public updateTelemetryState(didUserOptIn: boolean): void {
this.telemetryEnabled = false
// First check global telemetry level - telemetry should only be enabled when level is "all"
const telemetryLevel = vscode.workspace.getConfiguration("telemetry").get<string>("telemetryLevel", "all")
const globalTelemetryEnabled = telemetryLevel === "all"
@@ -144,25 +124,47 @@ class PostHogClient {
// We only enable telemetry if global vscode telemetry is enabled
if (globalTelemetryEnabled) {
this.telemetryEnabled = didUserOptIn
} else {
// Show warning to user that global telemetry is disabled
void vscode.window
.showWarningMessage(
"VSCode telemetry is disabled. To enable telemetry for this extension, first enable VSCode telemetry in settings.",
"Open Settings",
)
.then((selection) => {
if (selection === "Open Settings") {
void vscode.commands.executeCommand("workbench.action.openSettings", "telemetry.telemetryLevel")
}
})
}
// Update PostHog client state based on telemetry preference
if (this.telemetryEnabled) {
if (this.telemetryEnabled && this.client) {
this.client.optIn()
this.client.identify({ distinctId: this.distinctId })
} else {
this.client.capture({ distinctId: this.distinctId, event: "Opt out", properties: this.addProperties({}) })
this.client.optOut()
}
}
/**
* Gets or creates the singleton instance of PostHogClient
* @returns The PostHogClient instance
* Gets or creates the singleton instance of TelemetryService
* @returns The TelemetryService instance
*/
public static getInstance(): PostHogClient {
if (!PostHogClient.instance) {
PostHogClient.instance = new PostHogClient()
public static getInstance(): TelemetryService {
if (!TelemetryService.instance) {
TelemetryService.instance = new TelemetryService()
}
return TelemetryService.instance
}
private addProperties(properties: any): any {
return {
...properties,
extension_version: this.version,
is_dev: this.isDev,
}
return PostHogClient.instance
}
/**
@@ -171,13 +173,14 @@ class PostHogClient {
* @param collect If true, store the event in collectedEvents instead of sending to PostHog
*/
public capture(event: { event: string; properties?: any }, collect: boolean = false): void {
const taskId = event.properties.taskId
const propertiesWithVersion = {
...event.properties,
extension_version: this.version,
is_dev: this.isDev,
if (!this.telemetryEnabled) {
return
}
if (collect) {
const taskId = event.properties.taskId
const propertiesWithVersion = this.addProperties(event.properties)
if (collect && taskId) {
const existingTask = this.collectedTasks.find((task) => task.taskId === taskId)
if (existingTask) {
existingTask.collection.push({
@@ -195,7 +198,7 @@ class PostHogClient {
],
})
}
} else if (this.telemetryEnabled) {
} else {
this.client.capture({ distinctId: this.distinctId, event: event.event, properties: propertiesWithVersion })
}
}
@@ -210,7 +213,7 @@ class PostHogClient {
public captureTaskCreated(taskId: string, apiProvider?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.CREATED,
event: TelemetryService.EVENTS.TASK.CREATED,
properties: { taskId, apiProvider },
},
collect,
@@ -226,7 +229,7 @@ class PostHogClient {
public captureTaskRestarted(taskId: string, apiProvider?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.RESTARTED,
event: TelemetryService.EVENTS.TASK.RESTARTED,
properties: { taskId, apiProvider },
},
collect,
@@ -241,7 +244,7 @@ class PostHogClient {
public captureTaskCompleted(taskId: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.COMPLETED,
event: TelemetryService.EVENTS.TASK.COMPLETED,
properties: { taskId },
},
collect,
@@ -278,7 +281,7 @@ class PostHogClient {
this.capture(
{
event: PostHogClient.EVENTS.TASK.CONVERSATION_TURN,
event: TelemetryService.EVENTS.TASK.CONVERSATION_TURN,
properties,
},
collect,
@@ -295,7 +298,7 @@ class PostHogClient {
public captureTokenUsage(taskId: string, tokensIn: number, tokensOut: number, model: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.TOKEN_USAGE,
event: TelemetryService.EVENTS.TASK.TOKEN_USAGE,
properties: {
taskId,
tokensIn,
@@ -315,7 +318,7 @@ class PostHogClient {
public captureModeSwitch(taskId: string, mode: "plan" | "act", collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.MODE_SWITCH,
event: TelemetryService.EVENTS.TASK.MODE_SWITCH,
properties: {
taskId,
mode,
@@ -334,7 +337,7 @@ class PostHogClient {
console.info("TelemetryService: Capturing task feedback", { taskId, feedbackType })
this.capture(
{
event: PostHogClient.EVENTS.TASK.FEEDBACK,
event: TelemetryService.EVENTS.TASK.FEEDBACK,
properties: {
taskId,
feedbackType,
@@ -355,7 +358,7 @@ class PostHogClient {
public captureToolUsage(taskId: string, tool: string, autoApproved: boolean, success: boolean, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.TOOL_USED,
event: TelemetryService.EVENTS.TASK.TOOL_USED,
properties: {
taskId,
tool,
@@ -385,7 +388,7 @@ class PostHogClient {
this.capture(
{
event: PostHogClient.EVENTS.TASK.CHECKPOINT_USED,
event: TelemetryService.EVENTS.TASK.CHECKPOINT_USED,
properties: {
taskId,
action,
@@ -396,135 +399,6 @@ class PostHogClient {
)
}
// UI events
/**
* Records when the user switches between different API providers
* @param from Previous provider name
* @param to New provider name
* @param location Where the switch occurred (settings panel or bottom bar)
* @param taskId Optional task identifier if switch occurred during a task
*/
public captureProviderSwitch(
from: string,
to: string,
location: "settings" | "bottom",
taskId?: string,
collect: boolean = false,
) {
this.capture(
{
event: PostHogClient.EVENTS.UI.PROVIDER_SWITCH,
properties: {
from,
to,
location,
taskId,
},
},
collect,
)
}
/**
* Records when images are attached to a conversation
* @param taskId Unique identifier for the task
* @param imageCount Number of images attached
*/
public captureImageAttached(taskId: string, imageCount: number, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.IMAGE_ATTACHED,
properties: {
taskId,
imageCount,
},
},
collect,
)
}
/**
* Records general button click interactions in the UI
* @param button Identifier for the button that was clicked
* @param taskId Optional task identifier if click occurred during a task
*/
public captureButtonClick(button: string, taskId?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.BUTTON_CLICK,
properties: {
button,
taskId,
},
},
collect,
)
}
/**
* Records when the marketplace view is opened
* @param taskId Optional task identifier if marketplace was opened during a task
*/
public captureMarketplaceOpened(taskId?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.MARKETPLACE_OPENED,
properties: {
taskId,
},
},
collect,
)
}
/**
* Records when the settings panel is opened
* @param taskId Optional task identifier if settings were opened during a task
*/
public captureSettingsOpened(taskId?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.SETTINGS_OPENED,
properties: {
taskId,
},
},
collect,
)
}
/**
* Records when the task history view is opened
* @param taskId Optional task identifier if history was opened during a task
*/
public captureHistoryOpened(taskId?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.HISTORY_OPENED,
properties: {
taskId,
},
},
collect,
)
}
/**
* Records when a task is removed from the task history
* @param taskId Unique identifier for the task being removed
*/
public captureTaskPopped(taskId: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.TASK_POPPED,
properties: {
taskId,
},
},
collect,
)
}
/**
* Records when a diff edit (replace_in_file) operation fails
* @param taskId Unique identifier for the task
@@ -533,7 +407,7 @@ class PostHogClient {
public captureDiffEditFailure(taskId: string, modelId: string, errorType?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.DIFF_EDIT_FAILED,
event: TelemetryService.EVENTS.TASK.DIFF_EDIT_FAILED,
properties: {
taskId,
errorType,
@@ -553,7 +427,7 @@ class PostHogClient {
public captureModelSelected(model: string, provider: string, taskId?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.MODEL_SELECTED,
event: TelemetryService.EVENTS.UI.MODEL_SELECTED,
properties: {
model,
provider,
@@ -571,7 +445,7 @@ class PostHogClient {
public captureHistoricalTaskLoaded(taskId: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.HISTORICAL_LOADED,
event: TelemetryService.EVENTS.TASK.HISTORICAL_LOADED,
properties: {
taskId,
},
@@ -587,7 +461,7 @@ class PostHogClient {
public captureRetryClicked(taskId: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.RETRY_CLICKED,
event: TelemetryService.EVENTS.TASK.RETRY_CLICKED,
properties: {
taskId,
},
@@ -608,7 +482,7 @@ class PostHogClient {
this.capture(
{
event: PostHogClient.EVENTS.TASK.BROWSER_TOOL_START,
event: TelemetryService.EVENTS.TASK.BROWSER_TOOL_START,
properties: {
taskId,
viewport: browserSettings.viewport,
@@ -641,7 +515,7 @@ class PostHogClient {
this.capture(
{
event: PostHogClient.EVENTS.TASK.BROWSER_TOOL_END,
event: TelemetryService.EVENTS.TASK.BROWSER_TOOL_END,
properties: {
taskId,
actionCount: stats.actionCount,
@@ -679,7 +553,7 @@ class PostHogClient {
this.capture(
{
event: PostHogClient.EVENTS.TASK.BROWSER_ERROR,
event: TelemetryService.EVENTS.TASK.BROWSER_ERROR,
properties: {
taskId,
errorType,
@@ -701,7 +575,7 @@ class PostHogClient {
public captureOptionSelected(taskId: string, qty: number, mode: "plan" | "act", collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.OPTION_SELECTED,
event: TelemetryService.EVENTS.TASK.OPTION_SELECTED,
properties: {
taskId,
qty,
@@ -721,7 +595,7 @@ class PostHogClient {
public captureOptionsIgnored(taskId: string, qty: number, mode: "plan" | "act", collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.OPTIONS_IGNORED,
event: TelemetryService.EVENTS.TASK.OPTIONS_IGNORED,
properties: {
taskId,
qty,
@@ -758,7 +632,7 @@ class PostHogClient {
) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.GEMINI_API_PERFORMANCE,
event: TelemetryService.EVENTS.TASK.GEMINI_API_PERFORMANCE,
properties: {
taskId,
modelId,
@@ -777,7 +651,7 @@ class PostHogClient {
public captureModelFavoritesUsage(model: string, isFavorited: boolean, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.MODEL_FAVORITE_TOGGLED,
event: TelemetryService.EVENTS.UI.MODEL_FAVORITE_TOGGLED,
properties: {
model,
isFavorited,
@@ -806,13 +680,17 @@ class PostHogClient {
}
public async sendCollectedEvents(taskId?: string): Promise<void> {
if (!this.telemetryEnabled) {
return
}
if (this.collectedTasks.length > 0) {
if (taskId) {
const task = this.collectedTasks.find((t) => t.taskId === taskId)
if (task) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.TASK_COLLECTION,
event: TelemetryService.EVENTS.TASK.TASK_COLLECTION,
properties: { taskId, events: task.collection },
},
false,
@@ -823,7 +701,7 @@ class PostHogClient {
for (const task of this.collectedTasks) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.TASK_COLLECTION,
event: TelemetryService.EVENTS.TASK.TASK_COLLECTION,
properties: { taskId: task.taskId, events: task.collection },
},
false,
@@ -839,4 +717,4 @@ class PostHogClient {
}
}
export const telemetryService = PostHogClient.getInstance()
export const telemetryService = TelemetryService.getInstance()
+2 -2
View File
@@ -112,6 +112,8 @@ export type Platform = "aix" | "darwin" | "freebsd" | "linux" | "openbsd" | "sun
export const DEFAULT_PLATFORM = "unknown"
export interface ExtensionState {
version: string
vscMachineId: string
isNewUser: boolean
apiConfiguration?: ApiConfiguration
autoApprovalSettings: AutoApprovalSettings
@@ -136,8 +138,6 @@ export interface ExtensionState {
email: string | null
photoURL: string | null
}
version: string
vscMachineId: string
globalClineRulesToggles: ClineRulesToggles
localClineRulesToggles: ClineRulesToggles
workflowToggles: ClineRulesToggles
+44 -3
View File
@@ -38,7 +38,8 @@
"remark-stringify": "^11.0.0",
"styled-components": "^6.1.15",
"unified": "^11.0.5",
"uuid": "^9.0.1"
"uuid": "^9.0.1",
"zustand": "^5.0.5"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
@@ -49,6 +50,7 @@
"@types/dompurify": "^3.0.5",
"@types/jest": "^29.5.14",
"@types/katex": "^0.16.7",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.13.4",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
@@ -7089,6 +7091,16 @@
"integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==",
"license": "MIT"
},
"node_modules/@types/lodash-es": {
"version": "4.17.12",
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/lodash.debounce": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz",
@@ -7132,14 +7144,14 @@
"version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@@ -17563,6 +17575,35 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zustand": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.5.tgz",
"integrity": "sha512-mILtRfKW9xM47hqxGIxCv12gXusoY/xTSHBYApXozR0HmQv299whhBeeAcRy+KrPPybzosvJBCOmVjq6x12fCg==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
"@types/react": ">=18.0.0",
"immer": ">=9.0.6",
"react": ">=18.0.0",
"use-sync-external-store": ">=1.2.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
},
"use-sync-external-store": {
"optional": true
}
}
},
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+3 -1
View File
@@ -45,7 +45,8 @@
"remark-stringify": "^11.0.0",
"styled-components": "^6.1.15",
"unified": "^11.0.5",
"uuid": "^9.0.1"
"uuid": "^9.0.1",
"zustand": "^5.0.5"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
@@ -56,6 +57,7 @@
"@types/dompurify": "^3.0.5",
"@types/jest": "^29.5.14",
"@types/katex": "^0.16.7",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.13.4",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
+44 -27
View File
@@ -1,37 +1,38 @@
import { useEffect } from "react"
import { useEffect, useRef, useCallback } from "react" // Added useCallback
import { useEvent } from "react-use" // Added useEvent
import ChatView from "./components/chat/ChatView"
import HistoryView from "./components/history/HistoryView"
import SettingsView from "./components/settings/SettingsView"
import WelcomeView from "./components/welcome/WelcomeView"
import AccountView from "./components/account/AccountView"
import { useExtensionState } from "./context/ExtensionStateContext"
import { useExtensionState } from "./store/extensionStore" // Changed import
import { vscode } from "./utils/vscode"
import McpView from "./components/mcp/configuration/McpConfigurationView"
import { Providers } from "./Providers"
import { logger } from "./utils/logger"
import { ExtensionMessage } from "@shared/ExtensionMessage" // Added for typing
const AppContent = () => {
const {
didHydrateState,
showWelcome,
shouldShowAnnouncement,
showMcp,
mcpTab,
showSettings,
showHistory,
showAccount,
showAnnouncement,
setShowAnnouncement,
closeMcpView,
navigateToHistory,
hideSettings,
hideHistory,
hideAccount,
hideAnnouncement,
} = useExtensionState()
const didHydrateState = useExtensionState((state) => state.didHydrateState)
const showWelcome = useExtensionState((state) => state.showWelcome)
const shouldShowAnnouncement = useExtensionState((state) => state.shouldShowAnnouncement)
const showMcp = useExtensionState((state) => state.showMcp)
const mcpTab = useExtensionState((state) => state.mcpTab)
const showSettings = useExtensionState((state) => state.showSettings)
const showHistory = useExtensionState((state) => state.showHistory)
const showAccount = useExtensionState((state) => state.showAccount)
const showAnnouncementView = useExtensionState((state) => state.showAnnouncementView) // Renamed in store
const setShowAnnouncementView = useExtensionState((state) => state.setShowAnnouncementView) // Renamed in store
const closeMcpView = useExtensionState((state) => state.closeMcpView)
const navigateToHistory = useExtensionState((state) => state.navigateToHistory)
const hideSettings = useExtensionState((state) => state.hideSettings)
const hideHistory = useExtensionState((state) => state.hideHistory)
const hideAccount = useExtensionState((state) => state.hideAccount)
// hideAnnouncement is now setShowAnnouncementView(false) or navigating away
useEffect(() => {
if (shouldShowAnnouncement) {
setShowAnnouncement(true)
setShowAnnouncementView(true) // Use the store action
vscode.postMessage({ type: "didShowAnnouncement" })
}
}, [shouldShowAnnouncement])
@@ -54,8 +55,8 @@ const AppContent = () => {
<ChatView
showHistoryView={navigateToHistory}
isHidden={showSettings || showHistory || showMcp || showAccount}
showAnnouncement={showAnnouncement}
hideAnnouncement={hideAnnouncement}
showAnnouncement={showAnnouncementView} // Use renamed state
hideAnnouncement={() => setShowAnnouncementView(false)} // Use renamed action
/>
</>
)}
@@ -63,12 +64,28 @@ const AppContent = () => {
)
}
import { useMemo } from "react" // Added useMemo
const App = () => {
return (
<Providers>
<AppContent />
</Providers>
const renderCountRef = useRef(0)
renderCountRef.current += 1
logger.debug(`[App.tsx] App component Render #${renderCountRef.current}`)
const processMessage = useExtensionState((state) => state.processMessage)
const handleMessage = useCallback(
(event: MessageEvent) => {
const message = event.data as ExtensionMessage // Cast to known type
logger.debug("[App.tsx] Received message from extension:", message.type, message)
processMessage(message)
},
[processMessage],
)
useEvent("message", handleMessage) // Setup the event listener
const appContentElement = useMemo(() => <AppContent />, [])
return <Providers>{appContentElement}</Providers>
}
export default App
+78 -23
View File
@@ -1,36 +1,91 @@
import { useEffect, type ReactNode } from "react"
import React, { useEffect, type ReactNode, memo, useCallback, useRef } from "react" // Removed useMemo
import { PostHogProvider } from "posthog-js/react"
import posthog from "posthog-js"
// import equal from "fast-deep-equal" // We'll use shallow from zustand
// import { shallow } from "zustand/shallow" // Import shallow - not needed if selecting individually
import { posthogConfig } from "@shared/services/config/posthog-config"
import { useExtensionState } from "./context/ExtensionStateContext"
import { useExtensionState } from "./store/extensionStore" // Changed import
import { logger } from "./utils/logger"
export function CustomPostHogProvider({ children }: { children: ReactNode }) {
const { telemetrySetting, vscMachineId } = useExtensionState()
const isTelemetryEnabled = telemetrySetting !== "disabled"
posthog.init(posthogConfig.apiKey, {
api_host: posthogConfig.host,
ui_host: posthogConfig.uiHost,
disable_session_recording: true,
capture_pageview: false,
capture_dead_clicks: true,
})
function CustomPostHogProviderComponent({ children }: { children: ReactNode }) {
const renderCountRef = useRef(0)
renderCountRef.current += 1
logger.debug(`[PostHogProvider] Component Render #${renderCountRef.current}`)
const telemetrySetting = useExtensionState((state) => state.telemetrySetting)
const vscMachineId = useExtensionState((state) => state.vscMachineId)
const version = useExtensionState((state) => state.version)
const prevTelemetryRef = useRef(telemetrySetting)
const telemetryChanged = telemetrySetting !== prevTelemetryRef.current
logger.debug(
"[PostHogProvider] Selected telemetrySetting:",
telemetrySetting,
"vscMachineId:",
vscMachineId,
"version:",
version,
)
logger.debug("[PostHogProvider] telemetrySetting changed since last render? ->", telemetryChanged)
useEffect(() => {
if (vscMachineId.length === 0) {
return
}
// Update ref after render
prevTelemetryRef.current = telemetrySetting
}, [telemetrySetting]) // Only update when telemetrySetting itself changes
posthog.init(posthogConfig.apiKey, {
api_host: posthogConfig.host,
ui_host: posthogConfig.uiHost,
opt_out_capturing_by_default: true,
disable_session_recording: true,
capture_pageview: false,
capture_dead_clicks: true,
bootstrap: {
distinctID: vscMachineId,
},
const isTelemetryEnabled = telemetrySetting !== "disabled"
// Memoize beforeSendCb
const beforeSendCb = useCallback(
(payload: any) => {
if (payload?.properties) {
payload.properties.extension_version = version
}
return payload
},
[version], // Dependency: version
)
useEffect(() => {
logger.debug("[PostHogProvider] useEffect RUNNING. Deps:", {
isTelemetryEnabled,
vscMachineId,
versionForCb: version, // To see the version that determines beforeSendCb's identity
})
if (isTelemetryEnabled) {
posthog.opt_in_capturing()
} else {
posthog.opt_out_capturing()
// It's crucial that vscMachineId and version are stable and available here.
// If they are initially empty and then populate, this effect will run again.
if (!vscMachineId || vscMachineId.length === 0 || !version || version.length === 0) {
logger.warn("[PostHogProvider] useEffect: vscMachineId or version is empty/null. Skipping PostHog config.")
return // Skip PostHog config if essential IDs are missing
}
}, [isTelemetryEnabled, vscMachineId])
posthog.set_config({
before_send: beforeSendCb,
})
logger.debug("[PostHogProvider] useEffect: posthog.set_config called.")
if (isTelemetryEnabled && !posthog.has_opted_in_capturing()) {
posthog.opt_in_capturing()
posthog.identify(vscMachineId)
logger.info("[PostHogProvider] useEffect: Opted IN to capturing.")
} else if (!isTelemetryEnabled && !posthog.has_opted_out_capturing()) {
posthog.opt_out_capturing()
logger.info("[PostHogProvider] useEffect: Opted OUT of capturing.")
}
}, [isTelemetryEnabled, vscMachineId, beforeSendCb, version])
logger.debug("[PostHogProvider] Rendering <PostHogProvider client={posthog}>")
return <PostHogProvider client={posthog}>{children}</PostHogProvider>
}
export const CustomPostHogProvider = memo(CustomPostHogProviderComponent)
+20 -12
View File
@@ -1,18 +1,26 @@
import { type ReactNode } from "react"
import { memo, type ReactNode, useMemo, useRef } from "react" // Added useRef
import { ExtensionStateContextProvider } from "./context/ExtensionStateContext"
// Removed import for ExtensionStateProviderWrapper
import { FirebaseAuthProvider } from "./context/FirebaseAuthContext"
import { HeroUIProvider } from "@heroui/react"
import { CustomPostHogProvider } from "./CustomPostHogProvider"
import { logger } from "./utils/logger"
export function Providers({ children }: { children: ReactNode }) {
return (
<ExtensionStateContextProvider>
<CustomPostHogProvider>
<FirebaseAuthProvider>
<HeroUIProvider>{children}</HeroUIProvider>
</FirebaseAuthProvider>
</CustomPostHogProvider>
</ExtensionStateContextProvider>
export const Providers = memo(function Providers({ children }: { children: ReactNode }) {
const renderCountRef = useRef(0)
renderCountRef.current += 1
logger.debug(
`[Providers.tsx] Providers inner function Render #${renderCountRef.current}. Children changed: ${children !== (useRef(children).current = children)}`,
)
}
const memoizedPostHogChildren = useMemo(
() => (
<FirebaseAuthProvider>
<HeroUIProvider>{children}</HeroUIProvider>
</FirebaseAuthProvider>
),
[children],
)
return <CustomPostHogProvider>{memoizedPostHogChildren}</CustomPostHogProvider>
})
@@ -7,7 +7,7 @@ import ClineLogoWhite from "../../assets/ClineLogoWhite"
import CountUp from "react-countup"
import CreditsHistoryTable from "./CreditsHistoryTable"
import { UsageTransaction, PaymentTransaction } from "@shared/ClineAccount"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useExtensionState } from "@/store/extensionStore" // Changed import
import { AccountServiceClient } from "@/services/grpc-client"
import { EmptyRequest } from "@shared/proto/common"
@@ -33,7 +33,8 @@ const AccountView = ({ onDone }: AccountViewProps) => {
export const ClineAccountView = () => {
const { user: firebaseUser, handleSignOut } = useFirebaseAuth()
const { userInfo, apiConfiguration } = useExtensionState()
const userInfo = useExtensionState((state) => state.userInfo)
const apiConfiguration = useExtensionState((state) => state.apiConfiguration)
let user = apiConfiguration?.clineApiKey ? firebaseUser || userInfo : undefined
@@ -1,7 +1,7 @@
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { useEffect, useRef, useState } from "react"
import styled from "styled-components"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useExtensionState } from "@/store/extensionStore" // Changed import
import { vscode } from "@/utils/vscode"
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
import { BrowserServiceClient, UiServiceClient } from "../../services/grpc-client"
@@ -13,7 +13,7 @@ interface ConnectionInfo {
}
export const BrowserSettingsMenu = () => {
const { browserSettings } = useExtensionState()
const browserSettings = useExtensionState((state) => state.browserSettings)
const containerRef = useRef<HTMLDivElement>(null)
const [showInfoPopover, setShowInfoPopover] = useState(false)
const [connectionInfo, setConnectionInfo] = useState<ConnectionInfo>({
@@ -5,7 +5,7 @@ import { useSize } from "react-use"
import styled from "styled-components"
import { BROWSER_VIEWPORT_PRESETS } from "@shared/BrowserSettings"
import { BrowserAction, BrowserActionResult, ClineMessage, ClineSayBrowserAction } from "@shared/ExtensionMessage"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useExtensionState } from "@/store/extensionStore" // Changed import
import { FileServiceClient } from "@/services/grpc-client"
import { BrowserSettingsMenu } from "@/components/browser/BrowserSettingsMenu"
import { CheckpointControls } from "@/components/common/CheckpointControls"
@@ -111,7 +111,7 @@ const headerStyle: CSSProperties = {
const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
const { messages, isLast, onHeightChange, lastModifiedMessage, onSetQuote } = props
const { browserSettings } = useExtensionState()
const browserSettings = useExtensionState((state) => state.browserSettings)
const prevHeightRef = useRef(0)
const [maxActionHeight, setMaxActionHeight] = useState(0)
const [consoleLogsExpanded, setConsoleLogsExpanded] = useState(false)
+3 -2
View File
@@ -15,7 +15,7 @@ import {
ExtensionMessage,
} from "@shared/ExtensionMessage"
import { COMMAND_OUTPUT_STRING, COMMAND_REQ_APP_STRING } from "@shared/combineCommandSequences"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useExtensionState } from "@/store/extensionStore" // Changed import
import { findMatchingResourceOrTemplate, getMcpServerDisplayName } from "@/utils/mcp"
import { vscode } from "@/utils/vscode"
import { FileServiceClient, TaskServiceClient } from "@/services/grpc-client"
@@ -199,7 +199,8 @@ export const ChatRowContent = ({
sendMessageFromChatRow,
onSetQuote,
}: ChatRowContentProps) => {
const { mcpServers, mcpMarketplaceCatalog } = useExtensionState()
const mcpServers = useExtensionState((state) => state.mcpServers)
const mcpMarketplaceCatalog = useExtensionState((state) => state.mcpMarketplaceCatalog)
const [seeNewChangesDisabled, setSeeNewChangesDisabled] = useState(false)
const [quoteButtonState, setQuoteButtonState] = useState<QuoteButtonState>({
visible: false,
@@ -5,7 +5,7 @@ import { useClickAway, useEvent, useWindowSize } from "react-use"
import styled from "styled-components"
import { mentionRegex, mentionRegexGlobal } from "@shared/context-mentions"
import { ExtensionMessage } from "@shared/ExtensionMessage"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useExtensionState } from "@/store/extensionStore" // Changed import
import {
ContextMenuOptionType,
getContextMenuOptions,
@@ -259,7 +259,12 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
},
ref,
) => {
const { filePaths, chatSettings, apiConfiguration, openRouterModels, platform, workflowToggles } = useExtensionState()
const filePaths = useExtensionState((state) => state.filePaths)
const chatSettings = useExtensionState((state) => state.chatSettings)
const apiConfiguration = useExtensionState((state) => state.apiConfiguration)
const openRouterModels = useExtensionState((state) => state.openRouterModels)
const platform = useExtensionState((state) => state.platform)
const workflowToggles = useExtensionState((state) => state.workflowToggles)
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
const [isDraggingOver, setIsDraggingOver] = useState(false)
const [gitCommits, setGitCommits] = useState<GitCommit[]>([])
+6 -2
View File
@@ -16,7 +16,7 @@ import { findLast } from "@shared/array"
import { combineApiRequests } from "@shared/combineApiRequests"
import { combineCommandSequences } from "@shared/combineCommandSequences"
import { getApiMetrics } from "@shared/getApiMetrics"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useExtensionState } from "@/store/extensionStore" // Changed import
import { vscode } from "@/utils/vscode"
import { TaskServiceClient, SlashServiceClient, FileServiceClient } from "@/services/grpc-client"
import HistoryPreview from "@/components/history/HistoryPreview"
@@ -89,7 +89,11 @@ async function convertHtmlToMarkdown(html: string) {
export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryView }: ChatViewProps) => {
const { version, clineMessages: messages, taskHistory, apiConfiguration, telemetrySetting } = useExtensionState()
const version = useExtensionState((state) => state.version)
const messages = useExtensionState((state) => state.clineMessages)
const taskHistory = useExtensionState((state) => state.taskHistory)
const apiConfiguration = useExtensionState((state) => state.apiConfiguration)
const telemetrySetting = useExtensionState((state) => state.telemetrySetting)
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined
const task = useMemo(() => messages.at(0), [messages]) // leaving this less safe version here since if the first message is not a task, then the extension is in a bad state and needs to be debugged (see Cline.abort)
@@ -1,6 +1,6 @@
import React, { useRef, useState, useEffect } from "react"
import { useClickAway, useWindowSize } from "react-use"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useExtensionState } from "@/store/extensionStore" // Changed import
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import ServersToggleList from "@/components/mcp/configuration/tabs/installed/ServersToggleList"
import { vscode } from "@/utils/vscode"
@@ -8,7 +8,8 @@ import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import Tooltip from "@/components/common/Tooltip"
const ServersToggleModal: React.FC = () => {
const { mcpServers, navigateToMcp } = useExtensionState()
const mcpServers = useExtensionState((state) => state.mcpServers)
const navigateToMcp = useExtensionState((state) => state.navigateToMcp)
const [isVisible, setIsVisible] = useState(false)
const buttonRef = useRef<HTMLDivElement>(null)
const modalRef = useRef<HTMLDivElement>(null)
@@ -3,7 +3,7 @@ import React, { memo, useEffect, useMemo, useRef, useState } from "react"
import { useWindowSize } from "react-use"
import { mentionRegexGlobal } from "@shared/context-mentions"
import { ClineMessage } from "@shared/ExtensionMessage"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useExtensionState } from "@/store/extensionStore" // Changed import
import { formatLargeNumber } from "@/utils/format"
import { formatSize } from "@/utils/format"
import { vscode } from "@/utils/vscode"
@@ -37,7 +37,10 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
lastApiReqTotalTokens,
onClose,
}) => {
const { apiConfiguration, currentTaskItem, checkpointTrackerErrorMessage, clineMessages } = useExtensionState()
const apiConfiguration = useExtensionState((state) => state.apiConfiguration)
const currentTaskItem = useExtensionState((state) => state.currentTaskItem)
const checkpointTrackerErrorMessage = useExtensionState((state) => state.checkpointTrackerErrorMessage)
const clineMessages = useExtensionState((state) => state.clineMessages)
const [isTaskExpanded, setIsTaskExpanded] = useState(true)
const [isTextExpanded, setIsTextExpanded] = useState(false)
const [showSeeMore, setShowSeeMore] = useState(false)
@@ -2,7 +2,7 @@ import React, { useState, useRef, forwardRef, useCallback } from "react"
import Thumbnails from "@/components/common/Thumbnails"
import { highlightText } from "./TaskHeader"
import DynamicTextArea from "react-textarea-autosize"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useExtensionState } from "@/store/extensionStore" // Changed import
import { CheckpointsServiceClient } from "@/services/grpc-client"
import { ClineCheckpointRestore } from "@shared/WebviewMessage"
@@ -17,7 +17,7 @@ const UserMessage: React.FC<UserMessageProps> = ({ text, images, messageTs, send
const [isEditing, setIsEditing] = useState(false)
const [editedText, setEditedText] = useState(text || "")
const textAreaRef = useRef<HTMLTextAreaElement>(null)
const { checkpointTrackerErrorMessage } = useExtensionState()
const checkpointTrackerErrorMessage = useExtensionState((state) => state.checkpointTrackerErrorMessage)
// Create refs for the buttons to check in the blur handler
const restoreAllButtonRef = useRef<HTMLButtonElement>(null)
@@ -9,12 +9,24 @@ import React from "react"
import { render, fireEvent } from "@testing-library/react"
import { describe, it, expect, vi } from "vitest"
vi.mock("@/context/ExtensionStateContext", () => ({
vi.mock("@/store/extensionStore", () => ({
__esModule: true,
useExtensionState: () => ({
state: {},
dispatch: vi.fn(),
}),
useExtensionState: (selector: (state: any) => any) => {
// This mock needs to simulate how the UserMessage component might use the store.
// For this specific test, it seems UserMessage doesn't rely on any specific state
// from the store, as the original mock returned a simple object.
// If UserMessage *does* select specific state, this mock needs to provide it.
// For now, let's assume it doesn't need specific state for this IME test.
const mockState = {
// Provide any state UserMessage might actually select, e.g.:
// apiConfiguration: { selectedProvider: "anthropic", anthropicModelId: "claude-3-opus-20240229" },
// chatSettings: { mode: "act" },
}
if (typeof selector === "function") {
return selector(mockState)
}
return mockState // Fallback if no selector is used (though components should use selectors)
},
}))
import UserMessage from "../UserMessage"
@@ -1,5 +1,5 @@
import { useRef, useState, useMemo } from "react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useExtensionState } from "@/store/extensionStore" // Changed import
import { useAutoApproveActions } from "@/hooks/useAutoApproveActions"
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import { getAsVar, VSC_TITLEBAR_INACTIVE_FOREGROUND } from "@/utils/vscStyles"
@@ -12,7 +12,7 @@ interface AutoApproveBarProps {
}
const AutoApproveBar = ({ style }: AutoApproveBarProps) => {
const { autoApprovalSettings } = useExtensionState()
const autoApprovalSettings = useExtensionState((state) => state.autoApprovalSettings)
const { isChecked, isFavorited, updateAction } = useAutoApproveActions()
const [isModalVisible, setIsModalVisible] = useState(false)
@@ -1,6 +1,6 @@
import React, { useRef, useState, useEffect } from "react"
import { useClickAway, useWindowSize } from "react-use"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useExtensionState } from "@/store/extensionStore" // Changed import
import { useAutoApproveActions } from "@/hooks/useAutoApproveActions"
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import { VSCodeTextField, VSCodeButton } from "@vscode/webview-ui-toolkit/react"
@@ -26,7 +26,7 @@ const AutoApproveModal: React.FC<AutoApproveModalProps> = ({
ACTION_METADATA,
NOTIFICATIONS_SETTING,
}) => {
const { autoApprovalSettings } = useExtensionState()
const autoApprovalSettings = useExtensionState((state) => state.autoApprovalSettings)
const { isChecked, isFavorited, toggleFavorite, updateAction, updateMaxRequests } = useAutoApproveActions()
const modalRef = useRef<HTMLDivElement>(null)
@@ -1,6 +1,6 @@
import React, { useRef, useState, useEffect } from "react"
import { useClickAway, useWindowSize } from "react-use"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useExtensionState } from "@/store/extensionStore" // Changed import
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import { vscode } from "@/utils/vscode"
import { FileServiceClient } from "@/services/grpc-client"
@@ -11,17 +11,18 @@ import styled from "styled-components"
import { ClineRulesToggles, ToggleWindsurfRuleRequest } from "@shared/proto/file"
const ClineRulesToggleModal: React.FC = () => {
const {
globalClineRulesToggles = {},
localClineRulesToggles = {},
localCursorRulesToggles = {},
localWindsurfRulesToggles = {},
workflowToggles = {},
setGlobalClineRulesToggles,
setLocalClineRulesToggles,
setLocalCursorRulesToggles,
setLocalWindsurfRulesToggles,
} = useExtensionState()
const globalClineRulesToggles = useExtensionState((state) => state.globalClineRulesToggles) || {}
const localClineRulesToggles = useExtensionState((state) => state.localClineRulesToggles) || {}
const localCursorRulesToggles = useExtensionState((state) => state.localCursorRulesToggles) || {}
const localWindsurfRulesToggles = useExtensionState((state) => state.localWindsurfRulesToggles) || {}
const workflowToggles = useExtensionState((state) => state.workflowToggles) || {}
const setGlobalClineRulesToggles = useExtensionState((state) => state.setGlobalClineRulesToggles)
const setLocalClineRulesToggles = useExtensionState((state) => state.setLocalClineRulesToggles)
const setLocalCursorRulesToggles = useExtensionState((state) => state.setLocalCursorRulesToggles)
const setLocalWindsurfRulesToggles = useExtensionState((state) => state.setLocalWindsurfRulesToggles)
// Note: There's no setWorkflowToggles in the store, assuming this is handled via postMessage as in toggleWorkflow
const [isVisible, setIsVisible] = useState(false)
const buttonRef = useRef<HTMLDivElement>(null)
const modalRef = useRef<HTMLDivElement>(null)
@@ -3,7 +3,7 @@ import { useRemark } from "react-remark"
import rehypeHighlight, { Options } from "rehype-highlight"
import styled from "styled-components"
import { visit } from "unist-util-visit"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useExtensionState } from "@/store/extensionStore" // Changed import
export const CODE_BLOCK_BG_COLOR = "var(--vscode-editor-background, --vscode-sideBar-background, rgb(30 30 30))"
@@ -111,7 +111,7 @@ const StyledPre = styled.pre<{ theme: any }>`
`
const CodeBlock = memo(({ source, forceWrap = false }: CodeBlockProps) => {
const { theme } = useExtensionState()
const theme = useExtensionState((state) => state.theme)
const [reactContent, setMarkdownSource] = useRemark({
remarkPlugins: [
() => {
@@ -7,7 +7,7 @@ import remarkMath from "remark-math"
import styled from "styled-components"
import { visit } from "unist-util-visit"
import type { Node } from "unist"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useExtensionState } from "@/store/extensionStore" // Changed import
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import MermaidBlock from "@/components/common/MermaidBlock"
@@ -280,7 +280,7 @@ const PreWithCopyButton = ({
}
const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
const { theme } = useExtensionState()
const theme = useExtensionState((state) => state.theme)
const [reactContent, setMarkdown] = useRemark({
remarkPlugins: [
@@ -1,5 +1,5 @@
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useExtensionState } from "@/store/extensionStore" // Changed import
import { vscode } from "@/utils/vscode"
import { memo, useState } from "react"
import { TaskServiceClient } from "@/services/grpc-client"
@@ -10,7 +10,7 @@ type HistoryPreviewProps = {
}
const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
const { taskHistory } = useExtensionState()
const taskHistory = useExtensionState((state) => state.taskHistory)
const [isExpanded, setIsExpanded] = useState(true)
const handleHistorySelect = (id: string) => {
@@ -1,5 +1,5 @@
import { VSCodeButton, VSCodeTextField, VSCodeRadioGroup, VSCodeRadio, VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useExtensionState } from "@/store/extensionStore" // Changed import
import { vscode } from "@/utils/vscode"
import { Virtuoso } from "react-virtuoso"
import { memo, useMemo, useState, useEffect, useCallback } from "react"
@@ -47,7 +47,9 @@ const CustomFilterRadio = ({ checked, onChange, icon, label }: CustomFilterRadio
}
const HistoryView = ({ onDone }: HistoryViewProps) => {
const { taskHistory, totalTasksSize, filePaths } = useExtensionState()
const taskHistory = useExtensionState((state) => state.taskHistory)
const totalTasksSize = useExtensionState((state) => state.totalTasksSize)
// filePaths is not used in this component, so we can remove it.
const [searchQuery, setSearchQuery] = useState("")
const [sortOption, setSortOption] = useState<SortOption>("newest")
const [lastNonRelevantSort, setLastNonRelevantSort] = useState<SortOption | null>("newest")
@@ -1,7 +1,7 @@
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { useEffect, useState } from "react"
import styled from "styled-components"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useExtensionState } from "@/store/extensionStore" // Changed import
import { vscode } from "@/utils/vscode"
import { McpServiceClient } from "@/services/grpc-client"
import AddRemoteServerForm from "./tabs/add-server/AddRemoteServerForm"
@@ -15,7 +15,7 @@ type McpViewProps = {
}
const McpConfigurationView = ({ onDone, initialTab }: McpViewProps) => {
const { mcpMarketplaceEnabled } = useExtensionState()
const mcpMarketplaceEnabled = useExtensionState((state) => state.mcpMarketplaceEnabled)
const [activeTab, setActiveTab] = useState<McpViewTab>(initialTab || (mcpMarketplaceEnabled ? "marketplace" : "installed"))
const handleTabChange = (tab: McpViewTab) => {
@@ -30,7 +30,7 @@ const McpConfigurationView = ({ onDone, initialTab }: McpViewProps) => {
}, [mcpMarketplaceEnabled, activeTab])
// Get setter for MCP marketplace catalog from context
const { setMcpMarketplaceCatalog } = useExtensionState()
const setMcpMarketplaceCatalog = useExtensionState((state) => state.setStoreMcpMarketplaceCatalog) // Renamed in store
useEffect(() => {
if (mcpMarketplaceEnabled) {
@@ -4,7 +4,7 @@ import { VSCodeButton, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-to
import { LINKS } from "@/constants"
import { McpServiceClient } from "@/services/grpc-client"
import { convertProtoMcpServersToMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useExtensionState } from "@/store/extensionStore" // Changed import
const AddRemoteServerForm = ({ onServerAdded }: { onServerAdded: () => void }) => {
const [serverName, setServerName] = useState("")
@@ -12,7 +12,7 @@ const AddRemoteServerForm = ({ onServerAdded }: { onServerAdded: () => void }) =
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState("")
const [showConnectingMessage, setShowConnectingMessage] = useState(false)
const { setMcpServers } = useExtensionState()
const setMcpServers = useExtensionState((state) => state.setStoreMcpServers) // Changed hook and action name
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
@@ -1,9 +1,9 @@
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { vscode } from "@/utils/vscode"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useExtensionState } from "@/store/extensionStore" // Changed import
import ServersToggleList from "./ServersToggleList"
const InstalledServersView = () => {
const { mcpServers: servers } = useExtensionState()
const servers = useExtensionState((state) => state.mcpServers) // Changed hook
return (
<div style={{ padding: "16px 20px" }}>
@@ -1,6 +1,6 @@
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { McpTool } from "@shared/mcp"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useExtensionState } from "@/store/extensionStore" // Changed import
import { McpServiceClient } from "@/services/grpc-client"
import { convertProtoMcpServersToMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
@@ -10,9 +10,9 @@ type McpToolRowProps = {
}
const McpToolRow = ({ tool, serverName }: McpToolRowProps) => {
const { autoApprovalSettings } = useExtensionState()
const autoApprovalSettings = useExtensionState((state) => state.autoApprovalSettings)
const { setMcpServers } = useExtensionState()
const setMcpServers = useExtensionState((state) => state.setStoreMcpServers) // Renamed in store
// Accept the event object
const handleAutoApproveChange = (event: any) => {
@@ -15,7 +15,7 @@ import { getMcpServerDisplayName } from "@/utils/mcp"
import DangerButton from "@/components/common/DangerButton"
import McpToolRow from "./McpToolRow"
import McpResourceRow from "./McpResourceRow"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useExtensionState } from "@/store/extensionStore" // Changed import
import { McpServiceClient } from "@/services/grpc-client"
import { convertProtoMcpServersToMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
import { McpServers, UpdateMcpTimeoutRequest } from "@shared/proto/mcp"
@@ -43,7 +43,9 @@ const ServerRow = ({
isExpandable?: boolean
hasTrashIcon?: boolean
}) => {
const { mcpMarketplaceCatalog, autoApprovalSettings, setMcpServers } = useExtensionState()
const mcpMarketplaceCatalog = useExtensionState((state) => state.mcpMarketplaceCatalog)
const autoApprovalSettings = useExtensionState((state) => state.autoApprovalSettings)
const setMcpServers = useExtensionState((state) => state.setStoreMcpServers) // Renamed in store
const [isExpanded, setIsExpanded] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
@@ -9,12 +9,12 @@ import {
VSCodeTextField,
} from "@vscode/webview-ui-toolkit/react"
import { McpMarketplaceItem } from "@shared/mcp"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useExtensionState } from "@/store/extensionStore" // Changed import
import { vscode } from "@/utils/vscode"
import McpMarketplaceCard from "./McpMarketplaceCard"
import McpSubmitCard from "./McpSubmitCard"
const McpMarketplaceView = () => {
const { mcpServers } = useExtensionState()
const mcpServers = useExtensionState((state) => state.mcpServers)
const [items, setItems] = useState<McpMarketplaceItem[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
@@ -54,7 +54,7 @@ import {
liteLlmModelInfoSaneDefaults,
} from "@shared/api"
import { ExtensionMessage } from "@shared/ExtensionMessage"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useExtensionState } from "@/store/extensionStore" // Changed import
import { vscode } from "@/utils/vscode"
import { ModelsServiceClient } from "@/services/grpc-client"
import { getAsVar, VSC_DESCRIPTION_FOREGROUND } from "@/utils/vscStyles"
@@ -142,7 +142,7 @@ const ApiOptions = ({
saveImmediately = false, // Default to false
}: ApiOptionsProps) => {
// Use full context state for immediate save payload
const extensionState = useExtensionState()
const extensionState = useExtensionState() // Changed hook
const { apiConfiguration, setApiConfiguration, uriScheme } = extensionState
const [ollamaModels, setOllamaModels] = useState<string[]>([])
const [lmStudioModels, setLmStudioModels] = useState<string[]>([])
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useCallback } from "react"
import { VSCodeButton, VSCodeCheckbox, VSCodeDropdown, VSCodeOption, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import debounce from "debounce"
import { BROWSER_VIEWPORT_PRESETS } from "../../../../src/shared/BrowserSettings"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { useExtensionState } from "../../store/extensionStore" // Changed import
import styled from "styled-components"
import { BrowserServiceClient } from "../../services/grpc-client"
@@ -50,7 +50,7 @@ const CollapsibleContent = styled.div<{ isOpen: boolean }>`
`
export const BrowserSettingsSection: React.FC = () => {
const { browserSettings } = useExtensionState()
const browserSettings = useExtensionState((state) => state.browserSettings)
const [localChromePath, setLocalChromePath] = useState(browserSettings.chromeExecutablePath || "")
const [isCheckingConnection, setIsCheckingConnection] = useState(false)
const [connectionStatus, setConnectionStatus] = useState<boolean | null>(null)
@@ -1,13 +1,14 @@
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { useFirebaseAuth } from "@/context/FirebaseAuthContext"
import { vscode } from "@/utils/vscode"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useExtensionState } from "@/store/extensionStore" // Changed import
import { AccountServiceClient } from "@/services/grpc-client"
import { EmptyRequest } from "@shared/proto/common"
export const ClineAccountInfoCard = () => {
const { user: firebaseUser, handleSignOut } = useFirebaseAuth()
const { userInfo, apiConfiguration } = useExtensionState()
const userInfo = useExtensionState((state) => state.userInfo)
const apiConfiguration = useExtensionState((state) => state.apiConfiguration)
let user = apiConfiguration?.clineApiKey ? firebaseUser || userInfo : undefined
@@ -1,17 +1,15 @@
import { VSCodeCheckbox, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useExtensionState } from "@/store/extensionStore" // Changed import
import { memo } from "react"
import { OpenAIReasoningEffort } from "@shared/ChatSettings"
const FeatureSettingsSection = () => {
const {
enableCheckpointsSetting,
setEnableCheckpointsSetting,
mcpMarketplaceEnabled,
setMcpMarketplaceEnabled,
chatSettings,
setChatSettings,
} = useExtensionState()
const enableCheckpointsSetting = useExtensionState((state) => state.enableCheckpointsSetting)
const setEnableCheckpointsSetting = useExtensionState((state) => state.setEnableCheckpointsSetting)
const mcpMarketplaceEnabled = useExtensionState((state) => state.mcpMarketplaceEnabled)
const setMcpMarketplaceEnabled = useExtensionState((state) => state.setMcpMarketplaceEnabled)
const chatSettings = useExtensionState((state) => state.chatSettings)
const setChatSettings = useExtensionState((state) => state.setChatSettings)
return (
<div style={{ marginBottom: 20, borderTop: "1px solid var(--vscode-panel-border)", paddingTop: 15 }}>
@@ -5,7 +5,7 @@ import { useRemark } from "react-remark"
import { useMount } from "react-use"
import styled from "styled-components"
import { openRouterDefaultModelId } from "@shared/api"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useExtensionState } from "@/store/extensionStore" // Changed import
import { ModelsServiceClient, StateServiceClient } from "@/services/grpc-client"
import { vscode } from "@/utils/vscode"
import { highlight } from "../history/HistoryView"
@@ -59,7 +59,9 @@ const featuredModels = [
]
const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }) => {
const { apiConfiguration, setApiConfiguration, openRouterModels } = useExtensionState()
const apiConfiguration = useExtensionState((state) => state.apiConfiguration)
const setApiConfiguration = useExtensionState((state) => state.setApiConfiguration)
const openRouterModels = useExtensionState((state) => state.openRouterModels)
const [searchTerm, setSearchTerm] = useState(apiConfiguration?.openRouterModelId || openRouterDefaultModelId)
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
const [selectedIndex, setSelectedIndex] = useState(-1)
@@ -5,7 +5,7 @@ import { useRemark } from "react-remark"
import { useMount } from "react-use"
import styled from "styled-components"
import { requestyDefaultModelId } from "../../../../src/shared/api"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { useExtensionState } from "../../store/extensionStore" // Changed import
import { ModelsServiceClient } from "../../services/grpc-client"
import { highlight } from "../history/HistoryView"
import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
@@ -17,7 +17,9 @@ export interface RequestyModelPickerProps {
}
const RequestyModelPicker: React.FC<RequestyModelPickerProps> = ({ isPopup }) => {
const { apiConfiguration, setApiConfiguration, requestyModels } = useExtensionState()
const apiConfiguration = useExtensionState((state) => state.apiConfiguration)
const setApiConfiguration = useExtensionState((state) => state.setApiConfiguration)
const requestyModels = useExtensionState((state) => state.requestyModels)
const [searchTerm, setSearchTerm] = useState(apiConfiguration?.requestyModelId || requestyDefaultModelId)
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
const [selectedIndex, setSelectedIndex] = useState(-1)
@@ -1,18 +1,13 @@
import {
VSCodeButton,
VSCodeCheckbox,
VSCodeDropdown,
VSCodeLink,
VSCodeOption,
VSCodeTextArea,
} from "@vscode/webview-ui-toolkit/react"
import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"
import { memo, useCallback, useEffect, useState } from "react"
import { isEqual } from "lodash-es" // Added for deep comparison
import PreferredLanguageSetting from "./PreferredLanguageSetting" // Added import
import { OpenAIReasoningEffort } from "@shared/ChatSettings"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ChatSettings, OpenAIReasoningEffort } from "@shared/ChatSettings"
import { ApiConfiguration } from "@shared/api" // Corrected import for ApiConfiguration
import { TelemetrySetting } from "@shared/TelemetrySetting" // Added import
import { useExtensionState } from "@/store/extensionStore" // Changed import
import { validateApiConfiguration, validateModelId } from "@/utils/validate"
import { vscode } from "@/utils/vscode"
import SettingsButton from "@/components/common/SettingsButton"
import ApiOptions from "./ApiOptions"
import { TabButton } from "../mcp/configuration/McpConfigurationView"
import { useEvent } from "react-use"
@@ -28,25 +23,74 @@ type SettingsViewProps = {
onDone: () => void
}
type InitialSettings = {
apiConfiguration: ApiConfiguration | undefined
customInstructions: string | null | undefined
telemetrySetting: TelemetrySetting
chatSettings: ChatSettings
planActSeparateModelsSetting: boolean
enableCheckpointsSetting: boolean | undefined
mcpMarketplaceEnabled: boolean | undefined
} | null
const SettingsView = ({ onDone }: SettingsViewProps) => {
const {
apiConfiguration,
version,
customInstructions,
setCustomInstructions,
openRouterModels,
telemetrySetting,
setTelemetrySetting,
chatSettings,
setChatSettings,
planActSeparateModelsSetting,
setPlanActSeparateModelsSetting,
enableCheckpointsSetting,
mcpMarketplaceEnabled,
} = useExtensionState()
const apiConfiguration = useExtensionState((state) => state.apiConfiguration)
const version = useExtensionState((state) => state.version)
const customInstructions = useExtensionState((state) => state.customInstructions)
const setCustomInstructions = useExtensionState((state) => state.setCustomInstructions)
const openRouterModels = useExtensionState((state) => state.openRouterModels)
const telemetrySetting = useExtensionState((state) => state.telemetrySetting)
const setTelemetrySetting = useExtensionState((state) => state.setTelemetrySetting)
const chatSettings = useExtensionState((state) => state.chatSettings)
const setChatSettings = useExtensionState((state) => state.setChatSettings)
const planActSeparateModelsSetting = useExtensionState((state) => state.planActSeparateModelsSetting)
const setPlanActSeparateModelsSetting = useExtensionState((state) => state.setPlanActSeparateModelsSetting)
const enableCheckpointsSetting = useExtensionState((state) => state.enableCheckpointsSetting)
const mcpMarketplaceEnabled = useExtensionState((state) => state.mcpMarketplaceEnabled)
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
const [pendingTabChange, setPendingTabChange] = useState<"plan" | "act" | null>(null)
const [initialSettings, setInitialSettings] = useState<InitialSettings>(null)
const [hasChanges, setHasChanges] = useState(false)
// Capture initial settings on mount
useEffect(() => {
setInitialSettings({
apiConfiguration,
customInstructions,
telemetrySetting,
chatSettings,
planActSeparateModelsSetting,
enableCheckpointsSetting,
mcpMarketplaceEnabled,
})
}, []) // Deliberately empty to run only once on mount
// Detect changes compared to initial settings
useEffect(() => {
if (initialSettings) {
const currentSettingsMatchInitial =
isEqual(apiConfiguration, initialSettings.apiConfiguration) &&
customInstructions === initialSettings.customInstructions &&
telemetrySetting === initialSettings.telemetrySetting &&
isEqual(chatSettings, initialSettings.chatSettings) &&
planActSeparateModelsSetting === initialSettings.planActSeparateModelsSetting &&
enableCheckpointsSetting === initialSettings.enableCheckpointsSetting &&
mcpMarketplaceEnabled === initialSettings.mcpMarketplaceEnabled
setHasChanges(!currentSettingsMatchInitial)
}
}, [
apiConfiguration,
customInstructions,
telemetrySetting,
chatSettings,
planActSeparateModelsSetting,
enableCheckpointsSetting,
mcpMarketplaceEnabled,
initialSettings,
])
const handleSubmit = (withoutDone: boolean = false) => {
const apiValidationResult = validateApiConfiguration(apiConfiguration)
@@ -76,15 +120,52 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
apiConfigurationToSubmit = undefined
}
vscode.postMessage({
type: "updateSettings",
planActSeparateModelsSetting,
customInstructionsSetting: customInstructions,
telemetrySetting,
enableCheckpointsSetting,
mcpMarketplaceEnabled,
apiConfiguration: apiConfigurationToSubmit,
})
if (hasChanges) {
const payload: {
type: "updateSettings"
planActSeparateModelsSetting?: boolean
customInstructionsSetting?: string | undefined
telemetrySetting?: TelemetrySetting | undefined
enableCheckpointsSetting?: boolean | undefined
mcpMarketplaceEnabled?: boolean | undefined
apiConfiguration?: ApiConfiguration | undefined
} = { type: "updateSettings" }
if (initialSettings) {
if (planActSeparateModelsSetting !== initialSettings.planActSeparateModelsSetting) {
payload.planActSeparateModelsSetting = planActSeparateModelsSetting
}
if (customInstructions !== initialSettings.customInstructions) {
payload.customInstructionsSetting = customInstructions === null ? undefined : customInstructions
}
if (telemetrySetting !== initialSettings.telemetrySetting) {
payload.telemetrySetting = telemetrySetting
}
if (enableCheckpointsSetting !== initialSettings.enableCheckpointsSetting) {
payload.enableCheckpointsSetting = enableCheckpointsSetting
}
if (mcpMarketplaceEnabled !== initialSettings.mcpMarketplaceEnabled) {
payload.mcpMarketplaceEnabled = mcpMarketplaceEnabled
}
if (!isEqual(apiConfiguration, initialSettings.apiConfiguration)) {
payload.apiConfiguration = apiConfigurationToSubmit
}
} else {
// Fallback if initialSettings is null (should ideally not happen here)
// Send all current values as a precaution, though this path indicates an issue.
payload.planActSeparateModelsSetting = planActSeparateModelsSetting
payload.customInstructionsSetting = customInstructions
payload.telemetrySetting = telemetrySetting
payload.enableCheckpointsSetting = enableCheckpointsSetting
payload.mcpMarketplaceEnabled = mcpMarketplaceEnabled
payload.apiConfiguration = apiConfigurationToSubmit
}
// Only send if there's actually something to update besides the type
if (Object.keys(payload).length > 1) {
vscode.postMessage(payload)
}
}
if (!withoutDone) {
onDone()
@@ -171,7 +252,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
<div className="fixed top-0 left-0 right-0 bottom-0 pt-[10px] pr-0 pb-0 pl-5 flex flex-col overflow-hidden">
<div className="flex justify-between items-center mb-[13px] pr-[17px]">
<h3 className="text-[var(--vscode-foreground)] m-0">Settings</h3>
<VSCodeButton onClick={() => handleSubmit(false)}>Save</VSCodeButton>
<VSCodeButton onClick={() => handleSubmit(false)}>{hasChanges ? "Save" : "Done"}</VSCodeButton>
</div>
<div className="grow overflow-y-scroll pr-2 flex flex-col">
{/* Tabs container */}
@@ -241,7 +322,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
<div className="mb-[5px]">
<VSCodeCheckbox
className="mb-[5px]"
checked={telemetrySetting === "enabled"}
checked={telemetrySetting !== "disabled"}
onChange={(e: any) => {
const checked = e.target.checked === true
setTelemetrySetting(checked ? "enabled" : "disabled")
@@ -1,11 +1,12 @@
import React, { useState } from "react"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useExtensionState } from "@/store/extensionStore" // Changed import
import { StateServiceClient } from "@/services/grpc-client"
import { Int64, Int64Request } from "@shared/proto/common"
export const TerminalSettingsSection: React.FC = () => {
const { shellIntegrationTimeout, setShellIntegrationTimeout } = useExtensionState()
const shellIntegrationTimeout = useExtensionState((state) => state.shellIntegrationTimeout)
const setShellIntegrationTimeout = useExtensionState((state) => state.setShellIntegrationTimeout) // This will become an action from the store
const [inputValue, setInputValue] = useState((shellIntegrationTimeout / 1000).toString())
const [inputError, setInputError] = useState<string | null>(null)
@@ -1,34 +1,111 @@
import { render, screen, fireEvent } from "@testing-library/react"
import { describe, it, expect, vi } from "vitest"
import ApiOptions from "../ApiOptions"
import { ExtensionStateContextProvider, useExtensionState } from "@/context/ExtensionStateContext"
// Removed import for ExtensionStateProviderWrapper
import { useExtensionState, ExtensionStoreState } from "@/store/extensionStore" // Import store and its type
import { ApiConfiguration } from "@shared/api"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
import { DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
import { DEFAULT_PLATFORM } from "@shared/ExtensionMessage"
vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => {
const getDefaultMockState = (): Partial<ExtensionStoreState> => ({
// Provide sensible defaults for all required fields in ExtensionStoreState
// Based on the initial state in extensionStore.ts
version: "test-version",
vscMachineId: "test-machine-id",
clineMessages: [],
taskHistory: [],
shouldShowAnnouncement: false,
autoApprovalSettings: DEFAULT_AUTO_APPROVAL_SETTINGS,
browserSettings: DEFAULT_BROWSER_SETTINGS,
chatSettings: DEFAULT_CHAT_SETTINGS,
platform: DEFAULT_PLATFORM,
telemetrySetting: "unset",
planActSeparateModelsSetting: true,
enableCheckpointsSetting: true,
globalClineRulesToggles: {},
localClineRulesToggles: {},
localCursorRulesToggles: {},
localWindsurfRulesToggles: {},
workflowToggles: {},
shellIntegrationTimeout: 4000,
isNewUser: false,
apiConfiguration: {
apiProvider: "requesty",
requestyApiKey: "",
requestyModelId: "",
},
customInstructions: undefined,
mcpMarketplaceEnabled: undefined,
didHydrateState: true,
showWelcome: false,
theme: {},
openRouterModels: {},
openAiModels: [],
requestyModels: {},
mcpServers: [],
mcpMarketplaceCatalog: { items: [] },
filePaths: [],
totalTasksSize: null,
showMcp: false,
mcpTab: undefined,
showSettings: false,
showHistory: false,
showAccount: false,
showAnnouncementView: false,
uriScheme: "vscode", // Added from original mock
// Mock actions as vi.fn()
setApiConfiguration: vi.fn(),
setCustomInstructions: vi.fn(),
setTelemetrySetting: vi.fn(),
setShowAnnouncementView: vi.fn(),
setPlanActSeparateModelsSetting: vi.fn(),
setEnableCheckpointsSetting: vi.fn(),
setMcpMarketplaceEnabled: vi.fn(),
setShellIntegrationTimeout: vi.fn(),
setChatSettings: vi.fn(),
setStoreMcpServers: vi.fn(),
setGlobalClineRulesToggles: vi.fn(),
setLocalClineRulesToggles: vi.fn(),
setLocalCursorRulesToggles: vi.fn(),
setLocalWindsurfRulesToggles: vi.fn(),
setStoreMcpMarketplaceCatalog: vi.fn(),
navigateToMcp: vi.fn(),
navigateToSettings: vi.fn(),
navigateToHistory: vi.fn(),
navigateToAccount: vi.fn(),
navigateToChat: vi.fn(),
hideSettings: vi.fn(),
hideHistory: vi.fn(),
hideAccount: vi.fn(),
closeMcpView: vi.fn(),
processMessage: vi.fn(),
initializeStore: vi.fn(),
})
vi.mock("@/store/extensionStore", async (importOriginal) => {
const actual = await importOriginal()
return {
...(actual || {}),
// your mocked methods
useExtensionState: vi.fn(() => ({
apiConfiguration: {
apiProvider: "requesty",
requestyApiKey: "",
requestyModelId: "",
},
setApiConfiguration: vi.fn(),
uriScheme: "vscode",
requestyModels: {},
})),
useExtensionState: vi.fn((selector) => {
const mockState = getDefaultMockState()
return typeof selector === "function" ? selector(mockState) : mockState
}),
}
})
const mockExtensionState = (apiConfiguration: Partial<ApiConfiguration>) => {
vi.mocked(useExtensionState).mockReturnValue({
apiConfiguration,
setApiConfiguration: vi.fn(),
uriScheme: "vscode",
requestyModels: {},
} as any)
const mockExtensionStoreState = (apiConfig: Partial<ApiConfiguration>) => {
vi.mocked(useExtensionState).mockImplementation((selector) => {
const mockState: ExtensionStoreState = {
...(getDefaultMockState() as ExtensionStoreState), // Cast to ensure all defaults are there
apiConfiguration: {
...(getDefaultMockState().apiConfiguration as ApiConfiguration),
...apiConfig,
} as ApiConfiguration,
}
return typeof selector === "function" ? selector(mockState) : mockState
})
}
describe("ApiOptions Component", () => {
@@ -38,27 +115,20 @@ describe("ApiOptions Component", () => {
beforeEach(() => {
//@ts-expect-error - vscode is not defined in the global namespace in test environment
global.vscode = { postMessage: mockPostMessage }
mockExtensionState({
mockExtensionStoreState({
// Changed function name
apiProvider: "requesty",
})
})
it("renders Requesty API Key input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
</ExtensionStateContextProvider>,
)
render(<ApiOptions showModelOptions={true} />)
const apiKeyInput = screen.getByPlaceholderText("Enter API Key...")
expect(apiKeyInput).toBeInTheDocument()
})
it("renders Requesty Model ID input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
</ExtensionStateContextProvider>,
)
render(<ApiOptions showModelOptions={true} />)
const modelIdInput = screen.getByPlaceholderText("Search and select a model...")
expect(modelIdInput).toBeInTheDocument()
})
@@ -71,27 +141,20 @@ describe("ApiOptions Component", () => {
beforeEach(() => {
//@ts-expect-error - vscode is not defined in the global namespace in test environment
global.vscode = { postMessage: mockPostMessage }
mockExtensionState({
mockExtensionStoreState({
// Changed function name
apiProvider: "together",
})
})
it("renders Together API Key input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
</ExtensionStateContextProvider>,
)
render(<ApiOptions showModelOptions={true} />)
const apiKeyInput = screen.getByPlaceholderText("Enter API Key...")
expect(apiKeyInput).toBeInTheDocument()
})
it("renders Together Model ID input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
</ExtensionStateContextProvider>,
)
render(<ApiOptions showModelOptions={true} />)
const modelIdInput = screen.getByPlaceholderText("Enter Model ID...")
expect(modelIdInput).toBeInTheDocument()
})
@@ -105,7 +168,8 @@ describe("ApiOptions Component", () => {
//@ts-expect-error - vscode is not defined in the global namespace in test environment
global.vscode = { postMessage: mockPostMessage }
mockExtensionState({
mockExtensionStoreState({
// Changed function name
apiProvider: "fireworks",
fireworksApiKey: "",
fireworksModelId: "",
@@ -115,41 +179,25 @@ describe("ApiOptions Component", () => {
})
it("renders Fireworks API Key input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
</ExtensionStateContextProvider>,
)
render(<ApiOptions showModelOptions={true} />)
const apiKeyInput = screen.getByPlaceholderText("Enter API Key...")
expect(apiKeyInput).toBeInTheDocument()
})
it("renders Fireworks Model ID input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
</ExtensionStateContextProvider>,
)
render(<ApiOptions showModelOptions={true} />)
const modelIdInput = screen.getByPlaceholderText("Enter Model ID...")
expect(modelIdInput).toBeInTheDocument()
})
it("renders Fireworks Max Completion Tokens input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
</ExtensionStateContextProvider>,
)
render(<ApiOptions showModelOptions={true} />)
const maxCompletionTokensInput = screen.getByPlaceholderText("2000")
expect(maxCompletionTokensInput).toBeInTheDocument()
})
it("renders Fireworks Max Tokens input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
</ExtensionStateContextProvider>,
)
render(<ApiOptions showModelOptions={true} />)
const maxTokensInput = screen.getByPlaceholderText("4000")
expect(maxTokensInput).toBeInTheDocument()
})
@@ -162,39 +210,28 @@ describe("OpenApiInfoOptions", () => {
vi.clearAllMocks()
//@ts-expect-error - vscode is not defined in the global namespace in test environment
global.vscode = { postMessage: mockPostMessage }
mockExtensionState({
mockExtensionStoreState({
// Changed function name
apiProvider: "openai",
})
})
it("renders OpenAI Supports Images input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
</ExtensionStateContextProvider>,
)
render(<ApiOptions showModelOptions={true} />)
fireEvent.click(screen.getByText("Model Configuration"))
const apiKeyInput = screen.getByText("Supports Images")
expect(apiKeyInput).toBeInTheDocument()
})
it("renders OpenAI Context Window Size input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
</ExtensionStateContextProvider>,
)
render(<ApiOptions showModelOptions={true} />)
fireEvent.click(screen.getByText("Model Configuration"))
const orgIdInput = screen.getByText("Context Window Size")
expect(orgIdInput).toBeInTheDocument()
})
it("renders OpenAI Max Output Tokens input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
</ExtensionStateContextProvider>,
)
render(<ApiOptions showModelOptions={true} />)
fireEvent.click(screen.getByText("Model Configuration"))
const modelInput = screen.getByText("Max Output Tokens")
expect(modelInput).toBeInTheDocument()
@@ -1,6 +1,6 @@
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { useEffect, useState, memo } from "react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useExtensionState } from "@/store/extensionStore" // Changed import
import { validateApiConfiguration } from "@/utils/validate"
import { vscode } from "@/utils/vscode"
import ApiOptions from "@/components/settings/ApiOptions"
@@ -9,7 +9,7 @@ import { AccountServiceClient } from "@/services/grpc-client"
import { EmptyRequest } from "@shared/proto/common"
const WelcomeView = memo(() => {
const { apiConfiguration } = useExtensionState()
const apiConfiguration = useExtensionState((state) => state.apiConfiguration)
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const [showApiOptions, setShowApiOptions] = useState(false)
@@ -1,555 +0,0 @@
import React, { createContext, useCallback, useContext, useEffect, useState, useRef } from "react"
import { useEvent } from "react-use"
import { StateServiceClient } from "../services/grpc-client"
import { EmptyRequest } from "@shared/proto/common"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
import { ExtensionMessage, ExtensionState, DEFAULT_PLATFORM } from "@shared/ExtensionMessage"
import {
ApiConfiguration,
ModelInfo,
openRouterDefaultModelId,
openRouterDefaultModelInfo,
requestyDefaultModelId,
requestyDefaultModelInfo,
} from "../../../src/shared/api"
import { findLastIndex } from "@shared/array"
import { McpMarketplaceCatalog, McpServer, McpViewTab } from "../../../src/shared/mcp"
import { convertTextMateToHljs } from "../utils/textMateToHljs"
import { vscode } from "../utils/vscode"
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
import { ChatSettings, DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
import { TelemetrySetting } from "@shared/TelemetrySetting"
interface ExtensionStateContextType extends ExtensionState {
didHydrateState: boolean
showWelcome: boolean
theme: Record<string, string> | undefined
openRouterModels: Record<string, ModelInfo>
openAiModels: string[]
requestyModels: Record<string, ModelInfo>
mcpServers: McpServer[]
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
setCustomInstructions: (value?: string) => void
setTelemetrySetting: (value: TelemetrySetting) => void
setShowAnnouncement: (value: boolean) => void
setPlanActSeparateModelsSetting: (value: boolean) => void
setEnableCheckpointsSetting: (value: boolean) => void
setMcpMarketplaceEnabled: (value: boolean) => void
setShellIntegrationTimeout: (value: number) => void
setChatSettings: (value: ChatSettings) => void
setMcpServers: (value: McpServer[]) => void
setGlobalClineRulesToggles: (toggles: Record<string, boolean>) => void
setLocalClineRulesToggles: (toggles: Record<string, boolean>) => void
setLocalCursorRulesToggles: (toggles: Record<string, boolean>) => void
setLocalWindsurfRulesToggles: (toggles: Record<string, boolean>) => void
setMcpMarketplaceCatalog: (value: McpMarketplaceCatalog) => void
// 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)
export const ExtensionStateContextProvider: React.FC<{
children: React.ReactNode
}> = ({ children }) => {
// 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: "",
clineMessages: [],
taskHistory: [],
shouldShowAnnouncement: false,
autoApprovalSettings: DEFAULT_AUTO_APPROVAL_SETTINGS,
browserSettings: DEFAULT_BROWSER_SETTINGS,
chatSettings: DEFAULT_CHAT_SETTINGS,
platform: DEFAULT_PLATFORM,
telemetrySetting: "unset",
vscMachineId: "",
planActSeparateModelsSetting: true,
enableCheckpointsSetting: true,
globalClineRulesToggles: {},
localClineRulesToggles: {},
localCursorRulesToggles: {},
localWindsurfRulesToggles: {},
workflowToggles: {},
shellIntegrationTimeout: 4000, // default timeout for shell integration
isNewUser: false,
})
const [didHydrateState, setDidHydrateState] = useState(false)
const [showWelcome, setShowWelcome] = useState(false)
const [theme, setTheme] = useState<Record<string, string>>()
const [filePaths, setFilePaths] = useState<string[]>([])
const [openRouterModels, setOpenRouterModels] = useState<Record<string, ModelInfo>>({
[openRouterDefaultModelId]: openRouterDefaultModelInfo,
})
const [totalTasksSize, setTotalTasksSize] = useState<number | null>(null)
const [openAiModels, setOpenAiModels] = useState<string[]>([])
const [requestyModels, setRequestyModels] = useState<Record<string, ModelInfo>>({
[requestyDefaultModelId]: requestyDefaultModelInfo,
})
const [mcpServers, setMcpServers] = useState<McpServer[]>([])
const [mcpMarketplaceCatalog, setMcpMarketplaceCatalog] = useState<McpMarketplaceCatalog>({ items: [] })
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) {
const stateData = message.state as ExtensionState
console.log("[Webview Context Test Revert] Received direct 'state' message, updating state.")
setState((prevState) => {
// Versioning logic for autoApprovalSettings (copied from original onResponse)
const incomingVersion = stateData.autoApprovalSettings?.version ?? 1
const currentVersion = prevState.autoApprovalSettings?.version ?? 1
const shouldUpdateAutoApproval = incomingVersion > currentVersion
const newState = {
...stateData,
autoApprovalSettings: shouldUpdateAutoApproval
? stateData.autoApprovalSettings
: prevState.autoApprovalSettings,
}
// Update welcome screen state based on API configuration (copied from original onResponse)
const config = stateData.apiConfiguration
const hasKey = config
? [
config.apiKey,
config.openRouterApiKey,
config.awsRegion,
config.vertexProjectId,
config.openAiApiKey,
config.ollamaModelId,
config.lmStudioModelId,
config.liteLlmApiKey,
config.geminiApiKey,
config.openAiNativeApiKey,
config.deepSeekApiKey,
config.requestyApiKey,
config.togetherApiKey,
config.qwenApiKey,
config.doubaoApiKey,
config.mistralApiKey,
config.vsCodeLmModelSelector,
config.clineApiKey,
config.asksageApiKey,
config.xaiApiKey,
config.sambanovaApiKey,
].some((key) => key !== undefined)
: false
setShowWelcome(!hasKey)
setDidHydrateState(true)
return newState
})
}
break
}
case "theme": {
if (message.text) {
setTheme(convertTextMateToHljs(JSON.parse(message.text)))
}
break
}
case "workspaceUpdated": {
setFilePaths(message.filePaths ?? [])
break
}
case "partialMessage": {
const partialMessage = message.partialMessage!
setState((prevState) => {
// worth noting it will never be possible for a more up-to-date message to be sent here or in normal messages post since the presentAssistantContent function uses lock
const lastIndex = findLastIndex(prevState.clineMessages, (msg) => msg.ts === partialMessage.ts)
if (lastIndex !== -1) {
const newClineMessages = [...prevState.clineMessages]
newClineMessages[lastIndex] = partialMessage
return { ...prevState, clineMessages: newClineMessages }
}
return prevState
})
break
}
case "openRouterModels": {
const updatedModels = message.openRouterModels ?? {}
setOpenRouterModels({
[openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
...updatedModels,
})
break
}
case "openAiModels": {
const updatedModels = message.openAiModels ?? []
setOpenAiModels(updatedModels)
break
}
case "requestyModels": {
const updatedModels = message.requestyModels ?? {}
setRequestyModels({
[requestyDefaultModelId]: requestyDefaultModelInfo,
...updatedModels,
})
break
}
case "mcpServers": {
setMcpServers(message.mcpServers ?? [])
break
}
case "mcpMarketplaceCatalog": {
if (message.mcpMarketplaceCatalog) {
setMcpMarketplaceCatalog(message.mcpMarketplaceCatalog)
}
break
}
case "totalTasksSize": {
setTotalTasksSize(message.totalTasksSize ?? null)
break
}
}
}, [])
useEvent("message", handleMessage)
// Reference to store the state subscription cancellation function
const stateSubscriptionRef = useRef<(() => void) | null>(null)
// Subscribe to state updates using the new gRPC streaming API
/* // TEST REVERT: Commenting out gRPC state subscription
useEffect(() => {
// Set up state subscription
stateSubscriptionRef.current = StateServiceClient.subscribeToState(
{},
{
onResponse: (response) => {
console.log("[DEBUG] got state update via subscription", response);
if (response.stateJson) {
try {
const stateData = JSON.parse(response.stateJson) as ExtensionState;
console.log("[DEBUG] parsed state JSON, updating state");
setState((prevState) => {
// Versioning logic for autoApprovalSettings
const incomingVersion = stateData.autoApprovalSettings?.version ?? 1;
const currentVersion = prevState.autoApprovalSettings?.version ?? 1;
const shouldUpdateAutoApproval = incomingVersion > currentVersion;
const newState = {
...stateData,
autoApprovalSettings: shouldUpdateAutoApproval
? stateData.autoApprovalSettings
: prevState.autoApprovalSettings,
};
// Update welcome screen state based on API configuration
const config = stateData.apiConfiguration;
const hasKey = config
? [
config.apiKey,
config.openRouterApiKey,
config.awsRegion,
config.vertexProjectId,
config.openAiApiKey,
config.ollamaModelId,
config.lmStudioModelId,
config.liteLlmApiKey,
config.geminiApiKey,
config.openAiNativeApiKey,
config.deepSeekApiKey,
config.requestyApiKey,
config.togetherApiKey,
config.qwenApiKey,
config.doubaoApiKey,
config.mistralApiKey,
config.vsCodeLmModelSelector,
config.clineApiKey,
config.asksageApiKey,
config.xaiApiKey,
config.sambanovaApiKey,
].some((key) => key !== undefined)
: false;
setShowWelcome(!hasKey);
setDidHydrateState(true);
console.log("[DEBUG] returning new state in ESC");
return newState;
});
} catch (error) {
console.error("Error parsing state JSON:", error);
console.log("[DEBUG] ERR getting state", error);
}
}
console.log('[DEBUG] ended "got subscribed state"');
},
onError: (error) => {
console.error("Error in state subscription:", error);
},
onComplete: () => {
console.log("State subscription completed");
},
},
);
// Still send the webviewDidLaunch message for other initialization
vscode.postMessage({ type: "webviewDidLaunch" });
// Clean up subscription when component unmounts
return () => {
if (stateSubscriptionRef.current) {
stateSubscriptionRef.current();
stateSubscriptionRef.current = null;
}
};
}, []);
*/ // END TEST REVERT
// For the test revert, ensure webviewDidLaunch is still sent if not done by the above useEffect
useEffect(() => {
// This effect now only sends webviewDidLaunch if the gRPC subscription is commented out.
// If the gRPC subscription is active, it sends webviewDidLaunch.
// To avoid sending it twice if you uncomment the above, you might add a flag.
// For this specific test (gRPC sub commented out), this is fine.
console.log("[Webview Context Test Revert] Sending webviewDidLaunch from separate useEffect.")
vscode.postMessage({ type: "webviewDidLaunch" })
}, [])
const contextValue: ExtensionStateContextType = {
...state,
didHydrateState,
showWelcome,
theme,
openRouterModels,
openAiModels,
requestyModels,
mcpServers,
mcpMarketplaceCatalog,
filePaths,
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,
apiConfiguration: value,
})),
setCustomInstructions: (value) =>
setState((prevState) => ({
...prevState,
customInstructions: value,
})),
setTelemetrySetting: (value) =>
setState((prevState) => ({
...prevState,
telemetrySetting: value,
})),
setPlanActSeparateModelsSetting: (value) =>
setState((prevState) => ({
...prevState,
planActSeparateModelsSetting: value,
})),
setEnableCheckpointsSetting: (value) =>
setState((prevState) => ({
...prevState,
enableCheckpointsSetting: value,
})),
setMcpMarketplaceEnabled: (value) =>
setState((prevState) => ({
...prevState,
mcpMarketplaceEnabled: value,
})),
setShowAnnouncement: (value) =>
setState((prevState) => ({
...prevState,
shouldShowAnnouncement: value,
})),
setShellIntegrationTimeout: (value) =>
setState((prevState) => ({
...prevState,
shellIntegrationTimeout: value,
})),
setMcpServers: (mcpServers: McpServer[]) => setMcpServers(mcpServers),
setMcpMarketplaceCatalog: (catalog: McpMarketplaceCatalog) => setMcpMarketplaceCatalog(catalog),
setShowMcp,
closeMcpView,
setChatSettings: (value) => {
setState((prevState) => ({
...prevState,
chatSettings: value,
}))
vscode.postMessage({
type: "updateSettings",
chatSettings: value,
apiConfiguration: state.apiConfiguration,
customInstructionsSetting: state.customInstructions,
telemetrySetting: state.telemetrySetting,
planActSeparateModelsSetting: state.planActSeparateModelsSetting,
enableCheckpointsSetting: state.enableCheckpointsSetting,
mcpMarketplaceEnabled: state.mcpMarketplaceEnabled,
})
},
setGlobalClineRulesToggles: (toggles) =>
setState((prevState) => ({
...prevState,
globalClineRulesToggles: toggles,
})),
setLocalClineRulesToggles: (toggles) =>
setState((prevState) => ({
...prevState,
localClineRulesToggles: toggles,
})),
setLocalCursorRulesToggles: (toggles) =>
setState((prevState) => ({
...prevState,
localCursorRulesToggles: toggles,
})),
setLocalWindsurfRulesToggles: (toggles) =>
setState((prevState) => ({
...prevState,
localWindsurfRulesToggles: toggles,
})),
setMcpTab,
}
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
}
export const useExtensionState = () => {
const context = useContext(ExtensionStateContext)
if (context === undefined) {
throw new Error("useExtensionState must be used within an ExtensionStateContextProvider")
}
return context
}
@@ -1,11 +1,11 @@
import { useCallback } from "react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useExtensionState } from "@/store/extensionStore" // Changed import
import { vscode } from "@/utils/vscode"
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
import { ActionMetadata } from "@/components/chat/auto-approve-menu/types"
export function useAutoApproveActions() {
const { autoApprovalSettings } = useExtensionState()
const autoApprovalSettings = useExtensionState((state) => state.autoApprovalSettings) // Changed hook
// Check if action is enabled
const isChecked = useCallback(
-7
View File
@@ -1,9 +1,6 @@
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useFeatureFlagPayload } from "posthog-js/react"
export const useFeatureFlag = (flagName: string): boolean => {
const { telemetrySetting } = useExtensionState()
try {
const payload = useFeatureFlagPayload(flagName) as { enabled: boolean }
if (payload && typeof payload === "object") {
@@ -12,10 +9,6 @@ export const useFeatureFlag = (flagName: string): boolean => {
return payload.enabled
}
}
if (telemetrySetting === "enabled") {
console.warn(`Feature flag ${flagName} not found or missing enabled property.`)
}
} catch (error) {
console.error(`Error retrieving feature flag "${flagName}":`, error)
}
+4
View File
@@ -3,6 +3,10 @@ import { createRoot } from "react-dom/client"
import "./index.css"
import App from "./App.tsx"
import "../../node_modules/@vscode/codicons/dist/codicon.css"
import { useExtensionState } from "./store/extensionStore" // Import the store
// Initialize the store
useExtensionState.getState().initializeStore()
createRoot(document.getElementById("root")!).render(
<StrictMode>
+432
View File
@@ -0,0 +1,432 @@
import { create } from "zustand"
import { vscode } from "../utils/vscode"
import { logger } from "../utils/logger"
import { findLastIndex } from "@shared/array"
import { convertTextMateToHljs } from "../utils/textMateToHljs"
import {
ExtensionMessage,
ExtensionState,
DEFAULT_PLATFORM,
// ApiConfiguration as SharedApiConfiguration, // Will be imported from ../../../src/shared/api
} from "@shared/ExtensionMessage"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
import { DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
import {
ApiConfiguration,
ApiConfiguration as SharedApiConfiguration, // Added alias here
ModelInfo,
openRouterDefaultModelId,
openRouterDefaultModelInfo,
requestyDefaultModelId,
requestyDefaultModelInfo,
} from "../../../src/shared/api"
import { McpMarketplaceCatalog, McpServer, McpViewTab } from "../../../src/shared/mcp"
import { TelemetrySetting } from "@shared/TelemetrySetting"
import { ChatSettings } from "@shared/ChatSettings"
// Helper function to check if any API keys are configured
const hasConfiguredApiKeys = (config?: SharedApiConfiguration): boolean => {
if (!config) return false
return [
config.apiKey,
config.openRouterApiKey,
config.awsRegion,
config.vertexProjectId,
config.openAiApiKey,
config.ollamaModelId,
config.lmStudioModelId,
config.liteLlmApiKey,
config.geminiApiKey,
config.openAiNativeApiKey,
config.deepSeekApiKey,
config.requestyApiKey,
config.togetherApiKey,
config.qwenApiKey,
config.doubaoApiKey,
config.mistralApiKey,
config.vsCodeLmModelSelector,
config.clineApiKey,
config.asksageApiKey,
config.xaiApiKey,
config.sambanovaApiKey,
].some((key) => key !== undefined && key !== "")
}
// Helper function for deep equality check
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const areObjectsDeepEqual = (objA: any, objB: any): boolean => {
return JSON.stringify(objA) === JSON.stringify(objB)
}
// Generic helper to update state if it has changed (used internally by actions)
function updateStateIfChanged<T>(
currentValue: T,
newValue: T,
setter: (value: T) => void, // This would be a partial set from Zustand
valueName: string,
comparisonFn: (a: T, b: T) => boolean = (a, b) => !areObjectsDeepEqual(a, b),
): boolean {
if (comparisonFn(newValue, currentValue)) {
logger.debug(`[ExtensionStore] ${valueName} changed. Updating.`)
setter(newValue)
return true
}
logger.debug(`[ExtensionStore] ${valueName} unchanged. Skipping update.`)
return false
}
export interface ExtensionStoreState extends ExtensionState {
didHydrateState: boolean
showWelcome: boolean
theme: Record<string, string> | undefined
openRouterModels: Record<string, ModelInfo>
openAiModels: string[]
requestyModels: Record<string, ModelInfo>
mcpServers: McpServer[]
mcpMarketplaceCatalog: McpMarketplaceCatalog
filePaths: string[]
totalTasksSize: number | null
// View state
showMcp: boolean
mcpTab?: McpViewTab
showSettings: boolean
showHistory: boolean
showAccount: boolean
showAnnouncementView: boolean // Renamed from shouldShowAnnouncement to distinguish UI state
// Actions
setApiConfiguration: (config: ApiConfiguration) => void
setCustomInstructions: (value?: string) => void
setTelemetrySetting: (value: TelemetrySetting) => void
setShowAnnouncementView: (value: boolean) => void // For UI element
setPlanActSeparateModelsSetting: (value: boolean) => void
setEnableCheckpointsSetting: (value: boolean) => void
setMcpMarketplaceEnabled: (value: boolean) => void
setShellIntegrationTimeout: (value: number) => void
setChatSettings: (value: ChatSettings) => void
setStoreMcpServers: (value: McpServer[]) => void // Renamed to avoid conflict with state prop
setGlobalClineRulesToggles: (toggles: Record<string, boolean>) => void
setLocalClineRulesToggles: (toggles: Record<string, boolean>) => void
setLocalCursorRulesToggles: (toggles: Record<string, boolean>) => void
setLocalWindsurfRulesToggles: (toggles: Record<string, boolean>) => void
setStoreMcpMarketplaceCatalog: (value: McpMarketplaceCatalog) => void // Renamed
navigateToMcp: (tab?: McpViewTab) => void
navigateToSettings: () => void
navigateToHistory: () => void
navigateToAccount: () => void
navigateToChat: () => void
hideSettings: () => void
hideHistory: () => void
hideAccount: () => void
closeMcpView: () => void
processMessage: (message: ExtensionMessage) => void
initializeStore: () => void
}
export const useExtensionState = create<ExtensionStoreState>((set, get) => ({
// Initial State from ExtensionState defaults
version: "", // Initialized from __INITIAL_DATA__
vscMachineId: "", // Initialized from __INITIAL_DATA__
clineMessages: [],
taskHistory: [],
shouldShowAnnouncement: false, // This is from ExtensionState, controls if announcement *should* be shown
autoApprovalSettings: DEFAULT_AUTO_APPROVAL_SETTINGS,
browserSettings: DEFAULT_BROWSER_SETTINGS,
chatSettings: DEFAULT_CHAT_SETTINGS,
platform: DEFAULT_PLATFORM,
telemetrySetting: "unset",
planActSeparateModelsSetting: true,
enableCheckpointsSetting: true,
globalClineRulesToggles: {},
localClineRulesToggles: {},
localCursorRulesToggles: {},
localWindsurfRulesToggles: {},
workflowToggles: {},
shellIntegrationTimeout: 4000,
isNewUser: false,
apiConfiguration: undefined, // Explicitly undefined initially
customInstructions: undefined, // Explicitly undefined initially
mcpMarketplaceEnabled: undefined, // Explicitly undefined initially
// Initial State for additional properties in ExtensionStoreState
didHydrateState: false,
showWelcome: false, // Derived, but good to have an initial value
theme: undefined,
openRouterModels: { [openRouterDefaultModelId]: openRouterDefaultModelInfo },
openAiModels: [],
requestyModels: { [requestyDefaultModelId]: requestyDefaultModelInfo },
mcpServers: [],
mcpMarketplaceCatalog: { items: [] },
filePaths: [],
totalTasksSize: null,
// View state initial values
showMcp: false,
mcpTab: undefined,
showSettings: false,
showHistory: false,
showAccount: false,
showAnnouncementView: false, // UI state for whether announcement is *currently* shown
// Actions
initializeStore: () => {
const initialData = (window as any).__INITIAL_DATA__
if (initialData) {
logger.debug("[ExtensionStore] Initializing with __INITIAL_DATA__:", initialData)
set({
vscMachineId: initialData.vscMachineId,
version: initialData.extensionVersion,
})
} else {
logger.warn("[ExtensionStore] __INITIAL_DATA__ not found.")
}
// Post message to extension that webview is ready
vscode.postMessage({ type: "webviewDidLaunch" })
},
setApiConfiguration: (config) => set({ apiConfiguration: config }),
setCustomInstructions: (value) => set({ customInstructions: value }),
setTelemetrySetting: (value) => set({ telemetrySetting: value }),
setShowAnnouncementView: (value) => set({ showAnnouncementView: value }),
setPlanActSeparateModelsSetting: (value) => set({ planActSeparateModelsSetting: value }),
setEnableCheckpointsSetting: (value) => set({ enableCheckpointsSetting: value }),
setMcpMarketplaceEnabled: (value) => set({ mcpMarketplaceEnabled: value }),
setShellIntegrationTimeout: (value) => set({ shellIntegrationTimeout: value }),
setStoreMcpServers: (value) => set({ mcpServers: value }),
setGlobalClineRulesToggles: (toggles) => set({ globalClineRulesToggles: toggles }),
setLocalClineRulesToggles: (toggles) => set({ localClineRulesToggles: toggles }),
setLocalCursorRulesToggles: (toggles) => set({ localCursorRulesToggles: toggles }),
setLocalWindsurfRulesToggles: (toggles) => set({ localWindsurfRulesToggles: toggles }),
setStoreMcpMarketplaceCatalog: (value) => set({ mcpMarketplaceCatalog: value }),
navigateToMcp: (tab) =>
set({
showSettings: false,
showHistory: false,
showAccount: false,
mcpTab: tab,
showMcp: true,
showAnnouncementView: false,
}),
navigateToSettings: () =>
set({
showHistory: false,
showMcp: false,
mcpTab: undefined,
showAccount: false,
showSettings: true,
showAnnouncementView: false,
}),
navigateToHistory: () =>
set({
showSettings: false,
showMcp: false,
mcpTab: undefined,
showAccount: false,
showHistory: true,
showAnnouncementView: false,
}),
navigateToAccount: () =>
set({
showSettings: false,
showMcp: false,
mcpTab: undefined,
showHistory: false,
showAccount: true,
showAnnouncementView: false,
}),
navigateToChat: () =>
set({
showSettings: false,
showMcp: false,
mcpTab: undefined,
showHistory: false,
showAccount: false,
showAnnouncementView: false,
}),
hideSettings: () => set({ showSettings: false }),
hideHistory: () => set({ showHistory: false }),
hideAccount: () => set({ showAccount: false }),
// hideAnnouncement is covered by setShowAnnouncementView or navigating away
closeMcpView: () => set({ showMcp: false, mcpTab: undefined }),
setChatSettings: (value) => {
set({ chatSettings: value })
const s = get() // get current full state for other settings
vscode.postMessage({
type: "updateSettings",
chatSettings: value,
apiConfiguration: s.apiConfiguration,
customInstructionsSetting: s.customInstructions,
telemetrySetting: s.telemetrySetting,
planActSeparateModelsSetting: s.planActSeparateModelsSetting,
enableCheckpointsSetting: s.enableCheckpointsSetting,
mcpMarketplaceEnabled: s.mcpMarketplaceEnabled,
})
},
processMessage: (message: ExtensionMessage) => {
const currentState = get() // Get current state for comparisons and updates
switch (message.type) {
case "action": {
switch (message.action!) {
case "mcpButtonClicked":
get().navigateToMcp(message.tab)
break
case "settingsButtonClicked":
get().navigateToSettings()
break
case "historyButtonClicked":
get().navigateToHistory()
break
case "accountButtonClicked":
get().navigateToAccount()
break
case "chatButtonClicked":
get().navigateToChat()
break
}
break
}
case "state": {
if (message.state) {
const stateData = message.state as ExtensionState
logger.debug("[ExtensionStore] Processing 'state' message. Incoming version:", stateData.version)
set((prevState) => {
const newShowWelcomeValue = !hasConfiguredApiKeys(stateData.apiConfiguration)
const newDidHydrateStateValue = true
const incomingAutoApprovalVersion = stateData.autoApprovalSettings?.version ?? 1
const currentAutoApprovalVersion = prevState.autoApprovalSettings?.version ?? 1
const finalAutoApprovalSettings =
incomingAutoApprovalVersion >= currentAutoApprovalVersion // Use >= to ensure latest is always taken
? stateData.autoApprovalSettings
: prevState.autoApprovalSettings
// Preserve version and vscMachineId if they were set from __INITIAL_DATA__
// and are not part of the incoming stateData or are empty in stateData.
const versionToKeep = prevState.version || stateData.version
const vscMachineIdToKeep = prevState.vscMachineId || stateData.vscMachineId
return {
...prevState, // Start with previous state
...stateData, // Overlay with all incoming data
autoApprovalSettings: finalAutoApprovalSettings,
version: versionToKeep,
vscMachineId: vscMachineIdToKeep,
showWelcome: newShowWelcomeValue,
didHydrateState: newDidHydrateStateValue,
// Ensure UI view states are not accidentally overridden by a full 'state' message
// if they are meant to be controlled independently by navigation actions.
// However, if 'state' message is the source of truth for these, this is fine.
// For now, assume 'stateData' can overwrite them if it contains them.
}
})
}
break
}
case "theme": {
if (message.text) {
try {
const newThemeObject = convertTextMateToHljs(JSON.parse(message.text))
if (!areObjectsDeepEqual(currentState.theme, newThemeObject)) {
set({ theme: newThemeObject })
}
} catch (e) {
logger.error("[ExtensionStore] Error parsing theme message:", e, message.text)
}
}
break
}
case "workspaceUpdated": {
const newFilePaths = message.filePaths ?? []
if (!areObjectsDeepEqual(currentState.filePaths, newFilePaths)) {
set({ filePaths: newFilePaths })
}
break
}
case "partialMessage": {
const partialMessage = message.partialMessage!
logger.debug("[ExtensionStore] Processing 'partialMessage'. TS:", partialMessage.ts)
set((prevState) => {
const lastIndex = findLastIndex(prevState.clineMessages, (msg) => msg.ts === partialMessage.ts)
if (lastIndex !== -1) {
const oldMessage = prevState.clineMessages[lastIndex]
if (!areObjectsDeepEqual(oldMessage, partialMessage)) {
logger.debug("[ExtensionStore] Updating message at index:", lastIndex)
const newClineMessages = [...prevState.clineMessages]
newClineMessages[lastIndex] = partialMessage
return { ...prevState, clineMessages: newClineMessages }
}
logger.debug("[ExtensionStore] partialMessage content identical. Skipping update.")
} else {
logger.debug("[ExtensionStore] partialMessage no matching TS. Skipping update.")
}
return prevState // No change
})
break
}
case "openRouterModels": {
const updatedModels = message.openRouterModels ?? {}
const newOpenRouterModels = {
[openRouterDefaultModelId]: openRouterDefaultModelInfo,
...updatedModels,
}
if (!areObjectsDeepEqual(currentState.openRouterModels, newOpenRouterModels)) {
set({ openRouterModels: newOpenRouterModels })
}
break
}
case "openAiModels": {
const updatedModels = message.openAiModels ?? []
if (!areObjectsDeepEqual(currentState.openAiModels, updatedModels)) {
set({ openAiModels: updatedModels })
}
break
}
case "requestyModels": {
const updatedModels = message.requestyModels ?? {}
const newRequestyModels = {
[requestyDefaultModelId]: requestyDefaultModelInfo,
...updatedModels,
}
if (!areObjectsDeepEqual(currentState.requestyModels, newRequestyModels)) {
set({ requestyModels: newRequestyModels })
}
break
}
case "mcpServers": {
const newMcpServers = message.mcpServers ?? []
if (!areObjectsDeepEqual(currentState.mcpServers, newMcpServers)) {
set({ mcpServers: newMcpServers })
}
break
}
case "mcpMarketplaceCatalog": {
if (message.mcpMarketplaceCatalog) {
if (!areObjectsDeepEqual(currentState.mcpMarketplaceCatalog, message.mcpMarketplaceCatalog)) {
set({ mcpMarketplaceCatalog: message.mcpMarketplaceCatalog })
}
}
break
}
case "totalTasksSize": {
const newTotalTasksSize = message.totalTasksSize ?? null
if (currentState.totalTasksSize !== newTotalTasksSize) {
set({ totalTasksSize: newTotalTasksSize })
}
break
}
}
},
}))
// Optional: Log when the store is created (runs once)
logger.debug("[ExtensionStore] Zustand store created.")
+33
View File
@@ -0,0 +1,33 @@
const prefix = "[ClineWebview]"
export const logger = {
debug: (...args: any[]) => {
if (import.meta.env.DEV) {
console.debug(prefix, ...args)
}
},
info: (...args: any[]) => {
// In a production app, you might send these to a logging service
// or have a more sophisticated way to enable/disable them.
// For now, we'll log them if in DEV or if explicitly enabled via a global flag (not implemented here).
// As a simple default, let's make info logs also DEV only for now to keep prod console clean,
// unless a specific need arises to show them in prod.
if (import.meta.env.DEV) {
console.info(prefix, ...args)
}
},
warn: (...args: any[]) => {
console.warn(prefix, ...args)
},
error: (...args: any[]) => {
console.error(prefix, ...args)
},
}
// Example of a more specific logger for a component, if needed elsewhere:
// export const createComponentLogger = (componentName: string) => ({
// debug: (...args: any[]) => logger.debug(`[${componentName}]`, ...args),
// info: (...args: any[]) => logger.info(`[${componentName}]`, ...args),
// warn: (...args: any[]) => logger.warn(`[${componentName}]`, ...args),
// error: (...args: any[]) => logger.error(`[${componentName}]`, ...args),
// });