mirror of
https://github.com/cline/cline.git
synced 2026-09-01 23:19:18 +08:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d1636145e | |||
| 20ee6dca29 | |||
| b2ffaf1026 |
@@ -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.
|
||||
@@ -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`.
|
||||
Generated
+2
-2
@@ -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",
|
||||
|
||||
@@ -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 || {},
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+44
-3
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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[]>([])
|
||||
|
||||
@@ -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" }}>
|
||||
|
||||
+3
-3
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.")
|
||||
@@ -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),
|
||||
// });
|
||||
Reference in New Issue
Block a user