diff --git a/src/activate/index.ts b/src/activate/index.ts index 658bf467f7a..2fff46a5f04 100644 --- a/src/activate/index.ts +++ b/src/activate/index.ts @@ -2,3 +2,4 @@ export { handleUri } from "./handleUri" export { registerCommands } from "./registerCommands" export { registerCodeActions } from "./registerCodeActions" export { registerTerminalActions } from "./registerTerminalActions" +export { registerPearListener } from "./registerPearListener" diff --git a/src/activate/registerPearListener.ts b/src/activate/registerPearListener.ts new file mode 100644 index 00000000000..003bab9ceaf --- /dev/null +++ b/src/activate/registerPearListener.ts @@ -0,0 +1,85 @@ +import * as vscode from "vscode" +import { ClineProvider } from "../core/webview/ClineProvider" +import { assert } from "../utils/util" + +export const getPearaiExtension = async () => { + const pearAiExtension = vscode.extensions.getExtension("pearai.pearai") + + assert(!!pearAiExtension, "PearAI extension not found") + + if (!pearAiExtension.isActive) { + await pearAiExtension.activate() + } + + return pearAiExtension +} + +export const registerPearListener = async () => { + // Getting the pear ai extension instance + const pearAiExtension = await getPearaiExtension() + + // Access the API directly from exports + if (pearAiExtension.exports) { + pearAiExtension.exports.pearAPI.creatorMode.onDidRequestExecutePlan(async (msg: any) => { + console.dir(`onDidRequestNewTask triggered with: ${JSON.stringify(msg)}`) + + // Get the sidebar provider + const sidebarProvider = ClineProvider.getSidebarInstance() + + if (sidebarProvider) { + // Focus the sidebar first + await vscode.commands.executeCommand("pearai-roo-cline.SidebarProvider.focus") + + // Wait for the view to be ready using a helper function + await ensureViewIsReady(sidebarProvider) + + if (msg.creatorModeConfig?.creatorMode) { + // Switch to creator mode + await sidebarProvider.handleModeSwitch("creator") + await sidebarProvider.postStateToWebview() + } + // Navigate to chat view + await sidebarProvider.postMessageToWebview({ type: "action", action: "chatButtonClicked" }) + + // Wait a brief moment for UI to update + await new Promise((resolve) => setTimeout(resolve, 300)) + + let creatorModeConfig = { + creatorMode: msg.creatorMode, + newProjectType: msg.newProjectType, + newProjectPath: msg.newProjectPath, + } + + // Initialize with task + await sidebarProvider.initClineWithTask(msg.plan, undefined, undefined, undefined, creatorModeConfig) + } + }) + } else { + console.error("⚠️⚠️ PearAI API not available in exports ⚠️⚠️") + } +} + +// TODO: decide if this is needed +// Helper function to ensure the webview is ready +async function ensureViewIsReady(provider: ClineProvider): Promise { + // If the view is already launched, we're good to go + if (provider.viewLaunched) { + return + } + + // Otherwise, we need to wait for it to initialize + return new Promise((resolve) => { + // Set up a one-time listener for when the view is ready + const disposable = provider.on("clineCreated", () => { + // Clean up the listener + disposable.dispose() + resolve() + }) + + // Set a timeout just in case + setTimeout(() => { + disposable.dispose() + resolve() + }, 5000) + }) +} diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts index f80fde03439..b65e89e0bb3 100644 --- a/src/api/providers/anthropic.ts +++ b/src/api/providers/anthropic.ts @@ -105,9 +105,18 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa case "claude-3-opus-20240229": case "claude-3-haiku-20240307": betas.push("prompt-caching-2024-07-31") + // Include prompt_key if newProjectType is set return { - headers: { "anthropic-beta": betas.join(",") }, - authorization: `Bearer ${this.options.apiKey}`, + headers: { + "anthropic-beta": betas.join(","), + prompt_key: this.options.creatorModeConfig?.newProjectType + ? String(this.options.creatorModeConfig.newProjectType) + : undefined, + project_path: this.options.creatorModeConfig?.newProjectPath + ? String(this.options.creatorModeConfig.newProjectPath) + : undefined, + authorization: `Bearer ${this.options.apiKey}`, + }, } default: return undefined diff --git a/src/core/Cline.ts b/src/core/Cline.ts index d32581be9fa..d4caf37c26a 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -89,6 +89,7 @@ import { RooIgnoreController } from "./ignore/RooIgnoreController" import { type AssistantMessageContent, parseAssistantMessage } from "./assistant-message" import { truncateConversationIfNeeded } from "./sliding-window" import { ClineProvider } from "./webview/ClineProvider" +import { creatorModeConfig } from "../shared/pearaiApi" import { validateToolUse } from "./mode-validator" import { MultiSearchReplaceDiffStrategy } from "./diff/strategies/multi-search-replace" import { readApiMessages, saveApiMessages, readTaskMessages, saveTaskMessages, taskMetadata } from "./task-persistence" @@ -125,6 +126,7 @@ export type ClineOptions = { rootTask?: Cline parentTask?: Cline taskNumber?: number + creatorModeConfig?: creatorModeConfig onCreated?: (cline: Cline) => void pearaiModels?: Record } @@ -142,6 +144,7 @@ export class Cline extends EventEmitter { pausedModeSlug: string = defaultModeSlug private pauseInterval: NodeJS.Timeout | undefined + public creatorModeConfig: creatorModeConfig readonly apiConfiguration: ApiConfiguration api: ApiHandler private promptCacheKey: string @@ -218,6 +221,7 @@ export class Cline extends EventEmitter { startTask = true, rootTask, parentTask, + creatorModeConfig, taskNumber = -1, onCreated, }: ClineOptions) { @@ -257,6 +261,8 @@ export class Cline extends EventEmitter { this.diffViewProvider = new DiffViewProvider(this.cwd) this.enableCheckpoints = enableCheckpoints + this.creatorModeConfig = creatorModeConfig ?? { creatorMode: false } + this.rootTask = rootTask this.parentTask = parentTask this.taskNumber = taskNumber diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 268b7050f8f..72efe865b79 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -48,9 +48,9 @@ import { getUri } from "./getUri" import { getSystemPromptFilePath } from "../prompts/sections/custom-system-prompt" import { telemetryService } from "../../services/telemetry/TelemetryService" import { getWorkspacePath } from "../../utils/path" +import { PEARAI_URL, creatorModeConfig } from "../../shared/pearaiApi" import { webviewMessageHandler } from "./webviewMessageHandler" import { WebviewMessage } from "../../shared/WebviewMessage" -import { PEARAI_URL } from "../../shared/pearaiApi" import { PearAIAgentModelsConfig } from "../../api/providers/pearai/pearai" /** @@ -86,6 +86,7 @@ export class ClineProvider extends EventEmitter implements private readonly outputChannel: vscode.OutputChannel, private readonly renderContext: "sidebar" | "editor" = "sidebar", public readonly contextProxy: ContextProxy, + private readonly isCreatorView: boolean = false, ) { super() @@ -115,6 +116,16 @@ export class ClineProvider extends EventEmitter implements }) } + public static getSidebarInstance(): ClineProvider | undefined { + const sidebar = Array.from(this.activeInstances).find((instance) => !instance.isCreatorView) + + if (!sidebar?.view?.visible) { + vscode.commands.executeCommand("pearai-roo-cline.SidebarProvider.focus") + } + + return sidebar + } + // Adds a new Cline instance to clineStack, marking the start of a new task. // The instance is pushed to the top of the stack (LIFO order). // When the task is completed, the top instance is removed, reactivating the previous task. @@ -454,7 +465,7 @@ export class ClineProvider extends EventEmitter implements } // When initializing a new task, (not from history but from a tool command - // new_task) there is no need to remove the previouse task since the new + // new_task) there is no need to remove the previous task since the new // task is a subtask of the previous one, and when it finishes it is removed // from the stack and the caller is resumed in this way we can have a chain // of tasks, each one being a sub task of the previous one until the main @@ -474,6 +485,7 @@ export class ClineProvider extends EventEmitter implements | "experiments" > > = {}, + creatorModeConfig?: creatorModeConfig, ) { const { apiConfiguration, @@ -486,6 +498,15 @@ export class ClineProvider extends EventEmitter implements experiments, } = await this.getState() + // Update API configuration with creator mode + await this.updateApiConfiguration({ + ...apiConfiguration, + creatorModeConfig, + }) + + // Post updated state to webview immediately + await this.postStateToWebview() + const modePrompt = customModePrompts?.[mode] as PromptComponent const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n") @@ -493,7 +514,11 @@ export class ClineProvider extends EventEmitter implements const cline = new Cline({ provider: this, - apiConfiguration: { ...apiConfiguration, pearaiAgentModels: pearaiAgentModels }, + apiConfiguration: { + ...apiConfiguration, + creatorModeConfig, + pearaiAgentModels, + }, customInstructions: effectiveInstructions, enableDiff, enableCheckpoints, @@ -504,6 +529,7 @@ export class ClineProvider extends EventEmitter implements rootTask: this.clineStack.length > 0 ? this.clineStack[0] : undefined, parentTask, taskNumber: this.clineStack.length + 1, + creatorModeConfig, onCreated: (cline) => this.emit("clineCreated", cline), ...options, }) @@ -824,6 +850,14 @@ export class ClineProvider extends EventEmitter implements async updateApiConfiguration(providerSettings: ProviderSettings) { // Update mode's default config. const { mode } = await this.getState() + const currentCline = this.getCurrentCline() + + // Preserve creator mode when updating configuration + const updatedConfig: ProviderSettings = { + ...providerSettings, + creatorModeConfig: currentCline?.creatorModeConfig, + } + if (mode) { const currentApiConfigName = this.getGlobalState("currentApiConfigName") @@ -835,6 +869,7 @@ export class ClineProvider extends EventEmitter implements } } + await this.updateApiConfiguration(updatedConfig) await this.contextProxy.setProviderSettings(providerSettings) if (this.getCurrentCline()) { @@ -1150,8 +1185,10 @@ export class ClineProvider extends EventEmitter implements } async getStateToPostToWebview() { + const currentCline = this.getCurrentCline() + // Get base state const { - apiConfiguration, + apiConfiguration: baseApiConfiguration, lastShownAnnouncementId, customInstructions, alwaysAllowReadOnly, @@ -1210,6 +1247,12 @@ export class ClineProvider extends EventEmitter implements historyPreviewCollapsed, } = await this.getState() + // Construct API configuration with creator mode + const apiConfiguration = { + ...baseApiConfiguration, + creatorModeConfig: currentCline?.creatorModeConfig, + } + const telemetryKey = process.env.POSTHOG_API_KEY const machineId = vscode.env.machineId const allowedCommands = vscode.workspace.getConfiguration("roo-cline").get("allowedCommands") || [] diff --git a/src/exports/roo-code.d.ts b/src/exports/roo-code.d.ts index 4eef2426dab..c6069a5d225 100644 --- a/src/exports/roo-code.d.ts +++ b/src/exports/roo-code.d.ts @@ -202,6 +202,13 @@ type ProviderSettings = { defaultModelId?: string | undefined } | undefined + creatorModeConfig?: + | { + creatorMode?: boolean | undefined + newProjectType?: string | undefined + newProjectPath?: string | undefined + } + | undefined } type GlobalSettings = { diff --git a/src/exports/types.ts b/src/exports/types.ts index 0b557dd0baf..2795442a125 100644 --- a/src/exports/types.ts +++ b/src/exports/types.ts @@ -203,6 +203,13 @@ type ProviderSettings = { defaultModelId?: string | undefined } | undefined + creatorModeConfig?: + | { + creatorMode?: boolean | undefined + newProjectType?: string | undefined + newProjectPath?: string | undefined + } + | undefined } export type { ProviderSettings } diff --git a/src/extension.ts b/src/extension.ts index c391aaf0cd2..d4dfe7e7fcc 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -26,7 +26,13 @@ import { TerminalRegistry } from "./integrations/terminal/TerminalRegistry" import { API } from "./exports/api" import { migrateSettings } from "./utils/migrateSettings" -import { handleUri, registerCommands, registerCodeActions, registerTerminalActions } from "./activate" +import { + handleUri, + registerCommands, + registerCodeActions, + registerTerminalActions, + registerPearListener, +} from "./activate" import { formatLanguage } from "./shared/language" /** @@ -202,6 +208,7 @@ export async function activate(context: vscode.ExtensionContext) { registerCodeActions(context) registerTerminalActions(context) + registerPearListener() context.subscriptions.push( vscode.commands.registerCommand("roo-cline.focus", async (...args: any[]) => { diff --git a/src/schemas/index.ts b/src/schemas/index.ts index db61b52a2e2..a658e552e93 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -339,6 +339,13 @@ export type Experiments = z.infer type _AssertExperiments = AssertEqual>> + +export const creatorModeConfig = z.object({ + creatorMode: z.boolean().optional(), + newProjectType: z.string().optional(), + newProjectPath: z.string().optional(), +}).optional() + /** * ProviderSettings */ @@ -449,6 +456,8 @@ export const providerSettingsSchema = z.object({ defaultModelId: z.string().optional(), }) .optional(), + creatorModeConfig: creatorModeConfig, + }) export type ProviderSettings = z.infer @@ -546,6 +555,7 @@ const providerSettingsRecord: ProviderSettingsRecord = { pearaiApiKey: undefined, pearaiModelInfo: undefined, pearaiAgentModels: undefined, + creatorModeConfig: undefined, // X.AI (Grok) xaiApiKey: undefined, } diff --git a/src/shared/creatorMode.ts b/src/shared/creatorMode.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/shared/modes.ts b/src/shared/modes.ts index b1a0038fd83..dbb335ca5de 100644 --- a/src/shared/modes.ts +++ b/src/shared/modes.ts @@ -52,6 +52,13 @@ export function getToolsForMode(groups: readonly GroupEntry[]): string[] { // Main modes configuration as an ordered array export const modes: readonly ModeConfig[] = [ + { + slug: "creator", + name: "Creator", + roleDefinition: + "You are PearAI Agent (Powered by Roo Code / Cline), a creative and systematic software architect focused on turning high-level ideas into actionable plans. Your primary goal is to help users transform their ideas into structured action plans.", + groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], "browser", "mcp"], + }, { slug: "code", name: "💻 Code", diff --git a/src/shared/pearaiApi.ts b/src/shared/pearaiApi.ts index 0d110218aa3..b56cca24785 100644 --- a/src/shared/pearaiApi.ts +++ b/src/shared/pearaiApi.ts @@ -130,3 +130,9 @@ export const allModels: { [key: string]: ModelInfo } = { // Unbound models (single default model) [`unbound/${unboundDefaultModelId}`]: unboundDefaultModelInfo, } as const satisfies Record + +export interface creatorModeConfig { + creatorMode?: boolean // Defaults to false when not set + newProjectType?: string + newProjectPath?: string +} diff --git a/src/utils/util.ts b/src/utils/util.ts new file mode 100644 index 00000000000..54e5cbc8f04 --- /dev/null +++ b/src/utils/util.ts @@ -0,0 +1,19 @@ +class AssertionError extends Error { + constructor(message: string) { + super(message) + // Adding the stack info to error. + // Inspired by: https://blog.dennisokeeffe.com/blog/2020-08-07-error-tracing-with-sentry-and-es6-classes + if (Error.captureStackTrace) { + Error.captureStackTrace(this, AssertionError) + } else { + this.stack = new Error(message).stack + } + this.name = "AssertionError" + } +} + +export function assert(condition: boolean, message: string): asserts condition { + if (!condition) { + throw new AssertionError(message) + } +} diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 33eb1916aa3..f2272f52d11 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -49,6 +49,7 @@ import { vscInputBorder, vscSidebarBorder, } from "../ui" +import { CreatorModeBar } from "./CreatorModeBar" import SystemPromptWarning from "./SystemPromptWarning" import { usePearAIModels } from "@/hooks/usePearAIModels" @@ -159,182 +160,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - // if last message is an ask, show user ask UI - // if user finished a task, then start a new task with a new conversation history since in this moment that the extension is waiting for user response, the user could close the extension and the conversation history would be lost. - // basically as long as a task is active, the conversation history will be persisted - if (lastMessage) { - switch (lastMessage.type) { - case "ask": - const isPartial = lastMessage.partial === true - switch (lastMessage.ask) { - case "api_req_failed": - playSound("progress_loop") - setTextAreaDisabled(true) - setClineAsk("api_req_failed") - setEnableButtons(true) - setPrimaryButtonText(t("chat:retry.title")) - setSecondaryButtonText(t("chat:startNewTask.title")) - break - case "mistake_limit_reached": - playSound("progress_loop") - setTextAreaDisabled(false) - setClineAsk("mistake_limit_reached") - setEnableButtons(true) - setPrimaryButtonText(t("chat:proceedAnyways.title")) - setSecondaryButtonText(t("chat:startNewTask.title")) - break - case "followup": - if (!isPartial) { - playSound("notification") - } - setTextAreaDisabled(isPartial) - setClineAsk("followup") - // setting enable buttons to `false` would trigger a focus grab when - // the text area is enabled which is undesirable. - // We have no buttons for this tool, so no problem having them "enabled" - // to workaround this issue. See #1358. - setEnableButtons(true) - setPrimaryButtonText(undefined) - setSecondaryButtonText(undefined) - break - case "tool": - if (!isAutoApproved(lastMessage) && !isPartial) { - playSound("notification") - } - setTextAreaDisabled(isPartial) - setClineAsk("tool") - setEnableButtons(!isPartial) - const tool = JSON.parse(lastMessage.text || "{}") as ClineSayTool - switch (tool.tool) { - case "editedExistingFile": - case "appliedDiff": - case "newFileCreated": - case "insertContent": - setPrimaryButtonText(t("chat:save.title")) - setSecondaryButtonText(t("chat:reject.title")) - break - case "finishTask": - setPrimaryButtonText(t("chat:completeSubtaskAndReturn")) - setSecondaryButtonText(undefined) - break - default: - setPrimaryButtonText(t("chat:approve.title")) - setSecondaryButtonText(t("chat:reject.title")) - break - } - break - case "browser_action_launch": - if (!isAutoApproved(lastMessage) && !isPartial) { - playSound("notification") - } - setTextAreaDisabled(isPartial) - setClineAsk("browser_action_launch") - setEnableButtons(!isPartial) - setPrimaryButtonText(t("chat:approve.title")) - setSecondaryButtonText(t("chat:reject.title")) - break - case "command": - if (!isAutoApproved(lastMessage) && !isPartial) { - playSound("notification") - } - setTextAreaDisabled(isPartial) - setClineAsk("command") - setEnableButtons(!isPartial) - setPrimaryButtonText(t("chat:runCommand.title")) - setSecondaryButtonText(t("chat:reject.title")) - break - case "command_output": - setTextAreaDisabled(false) - setClineAsk("command_output") - setEnableButtons(true) - setPrimaryButtonText(t("chat:proceedWhileRunning.title")) - setSecondaryButtonText(t("chat:killCommand.title")) - break - case "use_mcp_server": - if (!isAutoApproved(lastMessage) && !isPartial) { - playSound("notification") - } - setTextAreaDisabled(isPartial) - setClineAsk("use_mcp_server") - setEnableButtons(!isPartial) - setPrimaryButtonText(t("chat:approve.title")) - setSecondaryButtonText(t("chat:reject.title")) - break - case "completion_result": - // extension waiting for feedback. but we can just present a new task button - if (!isPartial) { - playSound("celebration") - } - setTextAreaDisabled(isPartial) - setClineAsk("completion_result") - setEnableButtons(!isPartial) - setPrimaryButtonText(t("chat:startNewTask.title")) - setSecondaryButtonText(undefined) - break - case "resume_task": - if (!isAutoApproved(lastMessage) && !isPartial) { - playSound("notification") - } - setTextAreaDisabled(false) - setClineAsk("resume_task") - setEnableButtons(true) - setPrimaryButtonText(t("chat:resumeTask.title")) - setSecondaryButtonText(t("chat:terminate.title")) - setDidClickCancel(false) // special case where we reset the cancel button state - break - case "resume_completed_task": - if (!isPartial) { - playSound("celebration") - } - setTextAreaDisabled(false) - setClineAsk("resume_completed_task") - setEnableButtons(true) - setPrimaryButtonText(t("chat:startNewTask.title")) - setSecondaryButtonText(undefined) - setDidClickCancel(false) - break - } - break - case "say": - // Don't want to reset since there could be a "say" after - // an "ask" while ask is waiting for response. - switch (lastMessage.say) { - case "api_req_retry_delayed": - setTextAreaDisabled(true) - break - case "api_req_started": - if (secondLastMessage?.ask === "command_output") { - // If the last ask is a command_output, and we - // receive an api_req_started, then that means - // the command has finished and we don't need - // input from the user anymore (in every other - // case, the user has to interact with input - // field or buttons to continue, which does the - // following automatically). - setInputValue("") - setTextAreaDisabled(true) - setSelectedImages([]) - setClineAsk(undefined) - setEnableButtons(false) - } - break - case "api_req_finished": - case "error": - case "text": - case "browser_action": - case "browser_action_result": - case "command_output": - case "mcp_server_request_started": - case "mcp_server_response": - case "completion_result": - break - } - break - } - } - }, [lastMessage, secondLastMessage]) - useEffect(() => { if (messages.length === 0) { setTextAreaDisabled(false) @@ -849,6 +674,182 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + // if last message is an ask, show user ask UI + // if user finished a task, then start a new task with a new conversation history since in this moment that the extension is waiting for user response, the user could close the extension and the conversation history would be lost. + // basically as long as a task is active, the conversation history will be persisted + if (lastMessage) { + switch (lastMessage.type) { + case "ask": + const isPartial = lastMessage.partial === true + switch (lastMessage.ask) { + case "api_req_failed": + playSound("progress_loop") + setTextAreaDisabled(true) + setClineAsk("api_req_failed") + setEnableButtons(true) + setPrimaryButtonText(t("chat:retry.title")) + setSecondaryButtonText(t("chat:startNewTask.title")) + break + case "mistake_limit_reached": + playSound("progress_loop") + setTextAreaDisabled(false) + setClineAsk("mistake_limit_reached") + setEnableButtons(true) + setPrimaryButtonText(t("chat:proceedAnyways.title")) + setSecondaryButtonText(t("chat:startNewTask.title")) + break + case "followup": + if (!isPartial) { + playSound("notification") + } + setTextAreaDisabled(isPartial) + setClineAsk("followup") + // setting enable buttons to `false` would trigger a focus grab when + // the text area is enabled which is undesirable. + // We have no buttons for this tool, so no problem having them "enabled" + // to workaround this issue. See #1358. + setEnableButtons(true) + setPrimaryButtonText(undefined) + setSecondaryButtonText(undefined) + break + case "tool": + if (!isAutoApproved(lastMessage) && !isPartial) { + playSound("notification") + } + setTextAreaDisabled(isPartial) + setClineAsk("tool") + setEnableButtons(!isPartial) + const tool = JSON.parse(lastMessage.text || "{}") as ClineSayTool + switch (tool.tool) { + case "editedExistingFile": + case "appliedDiff": + case "newFileCreated": + case "insertContent": + setPrimaryButtonText(t("chat:save.title")) + setSecondaryButtonText(t("chat:reject.title")) + break + case "finishTask": + setPrimaryButtonText(t("chat:completeSubtaskAndReturn")) + setSecondaryButtonText(undefined) + break + default: + setPrimaryButtonText(t("chat:approve.title")) + setSecondaryButtonText(t("chat:reject.title")) + break + } + break + case "browser_action_launch": + if (!isAutoApproved(lastMessage) && !isPartial) { + playSound("notification") + } + setTextAreaDisabled(isPartial) + setClineAsk("browser_action_launch") + setEnableButtons(!isPartial) + setPrimaryButtonText(t("chat:approve.title")) + setSecondaryButtonText(t("chat:reject.title")) + break + case "command": + if (!isAutoApproved(lastMessage) && !isPartial) { + playSound("notification") + } + setTextAreaDisabled(isPartial) + setClineAsk("command") + setEnableButtons(!isPartial) + setPrimaryButtonText(t("chat:runCommand.title")) + setSecondaryButtonText(t("chat:reject.title")) + break + case "command_output": + setTextAreaDisabled(false) + setClineAsk("command_output") + setEnableButtons(true) + setPrimaryButtonText(t("chat:proceedWhileRunning.title")) + setSecondaryButtonText(t("chat:killCommand.title")) + break + case "use_mcp_server": + if (!isAutoApproved(lastMessage) && !isPartial) { + playSound("notification") + } + setTextAreaDisabled(isPartial) + setClineAsk("use_mcp_server") + setEnableButtons(!isPartial) + setPrimaryButtonText(t("chat:approve.title")) + setSecondaryButtonText(t("chat:reject.title")) + break + case "completion_result": + // extension waiting for feedback. but we can just present a new task button + if (!isPartial) { + playSound("celebration") + } + setTextAreaDisabled(isPartial) + setClineAsk("completion_result") + setEnableButtons(!isPartial) + setPrimaryButtonText(t("chat:startNewTask.title")) + setSecondaryButtonText(undefined) + break + case "resume_task": + if (!isAutoApproved(lastMessage) && !isPartial) { + playSound("notification") + } + setTextAreaDisabled(false) + setClineAsk("resume_task") + setEnableButtons(true) + setPrimaryButtonText(t("chat:resumeTask.title")) + setSecondaryButtonText(t("chat:terminate.title")) + setDidClickCancel(false) // special case where we reset the cancel button state + break + case "resume_completed_task": + if (!isPartial) { + playSound("celebration") + } + setTextAreaDisabled(false) + setClineAsk("resume_completed_task") + setEnableButtons(true) + setPrimaryButtonText(t("chat:startNewTask.title")) + setSecondaryButtonText(undefined) + setDidClickCancel(false) + break + } + break + case "say": + // Don't want to reset since there could be a "say" after + // an "ask" while ask is waiting for response. + switch (lastMessage.say) { + case "api_req_retry_delayed": + setTextAreaDisabled(true) + break + case "api_req_started": + if (secondLastMessage?.ask === "command_output") { + // If the last ask is a command_output, and we + // receive an api_req_started, then that means + // the command has finished and we don't need + // input from the user anymore (in every other + // case, the user has to interact with input + // field or buttons to continue, which does the + // following automatically). + setInputValue("") + setTextAreaDisabled(true) + setSelectedImages([]) + setClineAsk(undefined) + setEnableButtons(false) + } + break + case "api_req_finished": + case "error": + case "text": + case "browser_action": + case "browser_action_result": + case "command_output": + case "mcp_server_request_started": + case "mcp_server_response": + case "completion_result": + break + } + break + } + } + }, [lastMessage, secondLastMessage, isAutoApproved, t]) + useEffect(() => { // This ensures the first message is not read, future user messages are // labeled as `user_feedback`. @@ -1237,6 +1238,9 @@ const ChatViewComponent: React.ForwardRefRenderFunction + {apiConfiguration?.creatorModeConfig?.creatorMode === true && ( + + )} {task ? ( <> void + nextCallback?: () => void + className?: string +} +// from: https://vscode.dev/github/trypear/pearai-submodule/blob/acorn/253-submodule-api-fixed/gui/src/pages/creator/ui/planningBar.tsx#L15-L50 +// TODO: UI LIBRARY COMPONENT SHARING SHIZZ HERE! + +export const CreatorModeBar: FC = ({ + isGenerating, + requestedPlan, + playCallback, + nextCallback, + className, +}) => { + return ( +
+ {isGenerating &&
} +
+
+
+
+
Planning
+
+
{requestedPlan}
+
+
+ +
+
+ + + +
+ + {/* */} +
+
+ ) +} diff --git a/webview-ui/src/components/chat/button/index.tsx b/webview-ui/src/components/chat/button/index.tsx new file mode 100644 index 00000000000..a5e043aa6e8 --- /dev/null +++ b/webview-ui/src/components/chat/button/index.tsx @@ -0,0 +1,116 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" +// FROM: https://vscode.dev/github/trypear/pearai-submodule/blob/acorn/253-submodule-api-fixed/gui/src/pages/creator/ui/button/index.tsx#L1-L121 +// TODO: UI LIBRARY COMPONENT SHARING SHIZZ HERE! +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 border-none whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[#a1a1aa] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-[#18181b] text-[#fafafa] shadow hover:bg-[#27272a]", + destructive: "bg-[#ef4444] text-[#fafafa] shadow-sm hover:bg-[#dc2626]", + outline: "border border-[#e4e4e7] bg-[#ffffff] shadow-sm hover:bg-[#f4f4f5] hover:text-[#18181b]", + secondary: "bg-[#f4f4f5] text-[#18181b] hover:bg-[#e4e4e7]", + ghost: "hover:bg-[#f4f4f5] hover:text-[#18181b]", + link: "text-[#18181b] underline-offset-4 hover:underline", + }, + size: { + // default: "h-9 px-4 py-2", + default: "h-7 rounded-md px-2 text-md", + sm: "h-6 rounded-md px-2 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + toggled: { + true: "", + }, + }, + compoundVariants: [ + { + variant: "default", + toggled: true, + className: " bg-[#3030ad] text-[#0B84FF] hover:bg-[#3a3ad2]", // bg-[#27272a] text-[#fafafa] + }, + { + variant: "destructive", + toggled: true, + className: "bg-[#dc2626] text-[#fafafa]", + }, + { + variant: "outline", + toggled: true, + className: "bg-[#f4f4f5] text-[#18181b] border-[#a1a1aa]", + }, + { + variant: "secondary", + toggled: true, + // className: "bg-[#e4e4e7] text-[#18181b]" + className: "bg-[#E3EFFF] text-[#4388F8] hover:bg-[#D1E3FF]", + }, + { + variant: "ghost", + toggled: true, + className: "bg-[#f4f4f5] text-[#18181b]", + }, + { + variant: "link", + toggled: true, + className: "text-[#18181b] underline", + }, + ], + defaultVariants: { + variant: "default", + size: "default", + toggled: false, + }, + }, +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean + onToggle?: (toggled: boolean) => void +} + +const Button = React.forwardRef( + ( + { className, variant, size, toggled: initialToggled = false, asChild = false, onToggle, onClick, ...props }, + ref, + ) => { + const Comp = asChild ? Slot : "button" + const [toggled, setToggled] = React.useState(initialToggled) + + const handleClick = (event: React.MouseEvent) => { + if (onToggle) { + const newToggled = !toggled + setToggled(newToggled) + onToggle(newToggled) + } + + onClick?.(event) + } + + return ( + + ) + }, +) +Button.displayName = "Button" + +export { Button, buttonVariants }