Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"name": "roo-cline",
"displayName": "Roo Code (prev. Roo Cline)",
"description": "A VS Code plugin that enhances coding with AI-powered automation, multi-model support, and experimental features.",
"publisher": "RooVeterinaryInc",
"name": "pearai-roo-cline",
"displayName": "PearAI Roo Code / Cline",
"description": "PearAI's integration of Roo Code / Cline, a coding agent.",
"publisher": "PearAI",
"version": "3.3.6",
"icon": "assets/icons/rocket.png",
"galleryBanner": {
Expand All @@ -13,7 +13,7 @@
"vscode": "^1.84.0"
},
"author": {
"name": "Roo Vet"
"name": "PearAI"
},
"repository": {
"type": "git",
Expand Down
3 changes: 3 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { DeepSeekHandler } from "./providers/deepseek"
import { MistralHandler } from "./providers/mistral"
import { VsCodeLmHandler } from "./providers/vscode-lm"
import { ApiStream } from "./transform/stream"
import { PearAiHandler } from "./providers/pearai"
import { UnboundHandler } from "./providers/unbound"

export interface SingleCompletionHandler {
Expand Down Expand Up @@ -54,6 +55,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
return new VsCodeLmHandler(options)
case "mistral":
return new MistralHandler(options)
case "pearai":
return new PearAiHandler(options)
case "unbound":
return new UnboundHandler(options)
default:
Expand Down
5 changes: 4 additions & 1 deletion src/api/providers/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
case "claude-3-opus-20240229":
case "claude-3-haiku-20240307":
return {
headers: { "anthropic-beta": "prompt-caching-2024-07-31" },
headers: {
"anthropic-beta": "prompt-caching-2024-07-31",
"authorization": `Bearer ${this.options.apiKey}`,
},
}
default:
return undefined
Expand Down
31 changes: 31 additions & 0 deletions src/api/providers/pearai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { OpenAiHandler } from "./openai"
import * as vscode from "vscode"
import { ApiHandlerOptions, PEARAI_URL } from "../../shared/api"
import { AnthropicHandler } from "./anthropic"

export class PearAiHandler extends AnthropicHandler {
constructor(options: ApiHandlerOptions) {
if (!options.pearaiApiKey) {
vscode.window.showErrorMessage("PearAI API key not found.", "Login to PearAI").then(async (selection) => {
if (selection === "Login to PearAI") {
const extensionUrl = `${vscode.env.uriScheme}://pearai.pearai/auth`
const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(extensionUrl))

vscode.env.openExternal(
await vscode.env.asExternalUri(
vscode.Uri.parse(
`https://trypear.ai/signin?callback=${callbackUri.toString()}`, // Change to localhost if running locally
),
),
)
}
})
throw new Error("PearAI API key not found. Please login to PearAI.")
}
super({
...options,
apiKey: options.pearaiApiKey,
anthropicBaseUrl: PEARAI_URL,
})
}
}
43 changes: 41 additions & 2 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { getTheme } from "../../integrations/theme/getTheme"
import { getDiffStrategy } from "../diff/DiffStrategy"
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
import { McpHub } from "../../services/mcp/McpHub"
import { ApiConfiguration, ApiProvider, ModelInfo } from "../../shared/api"
import { ApiConfiguration, ApiProvider, ModelInfo, PEARAI_URL } from "../../shared/api"
import { findLast } from "../../shared/array"
import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage"
import { HistoryItem } from "../../shared/HistoryItem"
Expand Down Expand Up @@ -63,6 +63,8 @@ type SecretKey =
| "openAiNativeApiKey"
| "deepSeekApiKey"
| "mistralApiKey"
| "pearai-token"
| "pearai-refresh" // Array of custom modes
| "unboundApiKey"
type GlobalStateKey =
| "apiProvider"
Expand Down Expand Up @@ -98,6 +100,10 @@ type GlobalStateKey =
| "openRouterModelId"
| "openRouterModelInfo"
| "openRouterBaseUrl"
| "pearaiModelId"
| "pearaiModelInfo"
| "pearaiBaseUrl"
| "pearaiApiKey"
| "openRouterUseMiddleOutTransform"
| "allowedCommands"
| "soundEnabled"
Expand Down Expand Up @@ -1373,6 +1379,19 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("mode", defaultModeSlug)
await this.postStateToWebview()
}
break
case "openPearAiAuth":
const extensionUrl = `${vscode.env.uriScheme}://pearai.pearai/auth`
const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(extensionUrl))

await vscode.env.openExternal(
await vscode.env.asExternalUri(
vscode.Uri.parse(
`https://trypear.ai/signin?callback=${callbackUri.toString()}`, // Change to localhost if running locally
),
),
)
break
}
},
null,
Expand Down Expand Up @@ -1422,7 +1441,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// Update mode's default config
const { mode } = await this.getState()
if (mode) {
const currentApiConfigName = await this.getGlobalState("currentApiConfigName")
const currentApiConfigName = (await this.getGlobalState("currentApiConfigName")) ?? "default"
const listApiConfig = await this.configManager.listConfig()
const config = listApiConfig?.find((c) => c.name === currentApiConfigName)
if (config?.id) {
Expand Down Expand Up @@ -1468,6 +1487,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
openRouterUseMiddleOutTransform,
vsCodeLmModelSelector,
mistralApiKey,
pearaiBaseUrl,
pearaiModelId,
pearaiModelInfo,
unboundApiKey,
unboundModelId,
} = apiConfiguration
Expand Down Expand Up @@ -1508,6 +1530,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("openRouterUseMiddleOutTransform", openRouterUseMiddleOutTransform)
await this.updateGlobalState("vsCodeLmModelSelector", vsCodeLmModelSelector)
await this.storeSecret("mistralApiKey", mistralApiKey)
await this.updateGlobalState("pearaiBaseUrl", PEARAI_URL)
await this.updateGlobalState("pearaiModelId", pearaiModelId)
await this.updateGlobalState("pearaiModelInfo", pearaiModelInfo)
await this.storeSecret("unboundApiKey", unboundApiKey)
await this.updateGlobalState("unboundModelId", unboundModelId)
if (this.cline) {
Expand Down Expand Up @@ -2140,6 +2165,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
openAiNativeApiKey,
deepSeekApiKey,
mistralApiKey,
pearaiApiKey,
pearaiRefreshKey,
pearaiBaseUrl,
pearaiModelId,
pearaiModelInfo,
azureApiVersion,
openAiStreamingEnabled,
openRouterModelId,
Expand Down Expand Up @@ -2213,6 +2243,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getSecret("openAiNativeApiKey") as Promise<string | undefined>,
this.getSecret("deepSeekApiKey") as Promise<string | undefined>,
this.getSecret("mistralApiKey") as Promise<string | undefined>,
this.getSecret("pearai-token") as Promise<string | undefined>,
this.getSecret("pearai-refresh") as Promise<string | undefined>,
this.getGlobalState("pearaiBaseUrl") as Promise<string | undefined>,
this.getGlobalState("pearaiModelId") as Promise<string | undefined>,
this.getGlobalState("pearaiModelInfo") as Promise<ModelInfo | undefined>,
this.getGlobalState("azureApiVersion") as Promise<string | undefined>,
this.getGlobalState("openAiStreamingEnabled") as Promise<boolean | undefined>,
this.getGlobalState("openRouterModelId") as Promise<string | undefined>,
Expand Down Expand Up @@ -2303,6 +2338,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
openAiNativeApiKey,
deepSeekApiKey,
mistralApiKey,
pearaiApiKey,
pearaiBaseUrl,
pearaiModelId,
pearaiModelInfo,
azureApiVersion,
openAiStreamingEnabled,
openRouterModelId,
Expand Down
18 changes: 18 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,24 @@ export function activate(context: vscode.ExtensionContext) {
}),
)

context.subscriptions.push(
vscode.commands.registerCommand("pearai-roo-cline.pearaiLogin", async (data) => {
console.dir("Logged in to PearAI:")
console.dir(data)
context.secrets.store("pearai-token", data.accessToken)
context.secrets.store("pearai-refresh", data.refreshToken)
vscode.commands.executeCommand("roo-cline.plusButtonClicked")
}),
)

context.subscriptions.push(
vscode.commands.registerCommand("pearai-roo-cline.pearaiLogout", async () => {
console.dir("Logged out of PearAI:")
context.secrets.delete("pearai-token")
context.secrets.delete("pearai-refresh")
}),
)

context.subscriptions.push(
vscode.commands.registerCommand("roo-cline.mcpButtonClicked", () => {
sidebarProvider.postMessageToWebview({ type: "action", action: "mcpButtonClicked" })
Expand Down
1 change: 1 addition & 0 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export interface WebviewMessage {
| "deleteCustomMode"
| "setopenAiCustomModelInfo"
| "openCustomModesSettings"
| "openPearAiAuth"
text?: string
disabled?: boolean
askResponse?: ClineAskResponse
Expand Down
11 changes: 11 additions & 0 deletions src/shared/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type ApiProvider =
| "deepseek"
| "vscode-lm"
| "mistral"
| "pearai"
| "unbound"

export interface ApiHandlerOptions {
Expand Down Expand Up @@ -58,6 +59,10 @@ export interface ApiHandlerOptions {
deepSeekBaseUrl?: string
deepSeekApiKey?: string
includeMaxTokens?: boolean
pearaiApiKey?: string
pearaiBaseUrl?: string
pearaiModelId?: string
pearaiModelInfo?: ModelInfo
unboundApiKey?: string
unboundModelId?: string
}
Expand Down Expand Up @@ -615,3 +620,9 @@ export const unboundModels = {
"deepseek/deepseek-reasoner": deepSeekModels["deepseek-reasoner"],
"mistral/codestral-latest": mistralModels["codestral-latest"],
} as const satisfies Record<string, ModelInfo>

// CHANGE AS NEEDED FOR TESTING
// PROD:
// export const PEARAI_URL = "https://stingray-app-gb2an.ondigitalocean.app/pearai-server-api2/integrations/cline"
// DEV:
export const PEARAI_URL = "http://localhost:8000/integrations/cline"
1 change: 1 addition & 0 deletions src/shared/checkExistApiConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export function checkExistKey(config: ApiConfiguration | undefined) {
config.deepSeekApiKey,
config.mistralApiKey,
config.vsCodeLmModelSelector,
config.pearaiBaseUrl,
].some((key) => key !== undefined)
: false
}
9 changes: 3 additions & 6 deletions webview-ui/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@
return true
} else {
const lastApiReqStarted = findLast(modifiedMessages, (message) => message.say === "api_req_started")
if (lastApiReqStarted && lastApiReqStarted.text != null && lastApiReqStarted.say === "api_req_started") {

Check warning on line 278 in webview-ui/src/components/chat/ChatView.tsx

View workflow job for this annotation

GitHub Actions / unit-test

Expected '!==' and instead saw '!='

Check warning on line 278 in webview-ui/src/components/chat/ChatView.tsx

View workflow job for this annotation

GitHub Actions / compile

Expected '!==' and instead saw '!='
const cost = JSON.parse(lastApiReqStarted.text).cost
if (cost === undefined) {
// api request has not finished yet
Expand Down Expand Up @@ -718,7 +718,7 @@
if (message.say === "api_req_started") {
// get last api_req_started in currentGroup to check if it's cancelled. If it is then this api req is not part of the current browser session
const lastApiReqStarted = [...currentGroup].reverse().find((m) => m.say === "api_req_started")
if (lastApiReqStarted?.text != null) {

Check warning on line 721 in webview-ui/src/components/chat/ChatView.tsx

View workflow job for this annotation

GitHub Actions / unit-test

Expected '!==' and instead saw '!='

Check warning on line 721 in webview-ui/src/components/chat/ChatView.tsx

View workflow job for this annotation

GitHub Actions / compile

Expected '!==' and instead saw '!='
const info = JSON.parse(lastApiReqStarted.text)
const isCancelled = info.cancelReason != null
if (isCancelled) {
Expand Down Expand Up @@ -994,13 +994,10 @@
}}>
{showAnnouncement && <Announcement version={version} hideAnnouncement={hideAnnouncement} />}
<div style={{ padding: "0 20px", flexShrink: 0 }}>
<h2>What can I do for you?</h2>
<h2>PearAI Coding Agent (Powered by Roo Code / Cline)</h2>
<p>
Thanks to the latest breakthroughs in agentic coding capabilities, I can handle complex
software development tasks step-by-step. With tools that let me create & edit files, explore
complex projects, use the browser, and execute terminal commands (after you grant
permission), I can assist you in ways that go beyond code completion or tech support. I can
even use MCP to create new tools and extend my own capabilities.
Ask me to create a new feature, fix a bug, anything else. I can create & edit files, explore
complex projects, use the browser, and execute terminal commands!
</p>
</div>
{taskHistory.length > 0 && <HistoryPreview showHistoryView={showHistoryView} />}
Expand Down
52 changes: 50 additions & 2 deletions webview-ui/src/components/settings/ApiOptions.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { Checkbox, Dropdown, Pane } from "vscrui"
import type { DropdownOption } from "vscrui"
import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import {
VSCodeButton,
VSCodeLink,
VSCodeRadio,
VSCodeRadioGroup,
VSCodeTextField,
} from "@vscode/webview-ui-toolkit/react"
import { Fragment, memo, useCallback, useEffect, useMemo, useState } from "react"
import { useEvent, useInterval } from "react-use"
import {
Expand Down Expand Up @@ -136,6 +142,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
}}
style={{ minWidth: 130, position: "relative", zIndex: OPENROUTER_MODEL_PICKER_Z_INDEX + 1 }}
options={[
{ value: "pearai", label: "PearAI" },
{ value: "openrouter", label: "OpenRouter" },
{ value: "anthropic", label: "Anthropic" },
{ value: "gemini", label: "Google Gemini" },
Expand All @@ -154,6 +161,40 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
/>
</div>

{selectedProvider === "pearai" && (
<div>
{!apiConfiguration?.pearaiApiKey ? (
<>
<VSCodeButton
onClick={() => {
vscode.postMessage({
type: "openPearAiAuth",
})
}}>
Login to PearAI
</VSCodeButton>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Connect your PearAI account to use servers.
</p>
</>
) : (
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
User already logged in to PearAI. Click 'Done' to proceed!
</p>
)}
</div>
)}

{selectedProvider === "anthropic" && (
<div>
<VSCodeTextField
Expand Down Expand Up @@ -1334,7 +1375,8 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
selectedProvider !== "openrouter" &&
selectedProvider !== "openai" &&
selectedProvider !== "ollama" &&
selectedProvider !== "lmstudio" && (
selectedProvider !== "lmstudio" &&
selectedProvider !== "pearai" && (
<>
<div className="dropdown-container">
<label htmlFor="model-id">
Expand Down Expand Up @@ -1585,6 +1627,12 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
supportsImages: false, // VSCode LM API currently doesn't support images
},
}
case "pearai":
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.pearaiModelId || "pearai_model",
selectedModelInfo: apiConfiguration?.pearaiModelInfo || openAiModelInfoSaneDefaults,
}
case "unbound":
return getProviderData(unboundModels, unboundDefaultModelId)
default:
Expand Down
Loading
Loading