mirror of
https://github.com/cline/cline.git
synced 2026-09-05 05:02:27 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 18e2bdf194 | |||
| b83c9ec240 | |||
| 711a8c6ec0 | |||
| d68badae38 | |||
| d67c403392 | |||
| 02f405f6e2 |
@@ -1,5 +1,6 @@
|
||||
import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
import { createRequire } from "node:module"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import * as esbuild from "esbuild"
|
||||
|
||||
@@ -178,6 +179,39 @@ const e2eBuildConfig = {
|
||||
plugins: [aliasResolverPlugin, esbuildProblemMatcherPlugin],
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the plugin sandbox bootstrap from the built @cline/core package into
|
||||
* the extension's dist directory. The bootstrap runs in an isolated child
|
||||
* process spawned by SubprocessSandbox and must be a separate file — it cannot
|
||||
* be inlined into the main bundle. resolveBootstrap() (bundled into
|
||||
* extension.js) searches for it at dist/extensions/plugin-sandbox-bootstrap.js.
|
||||
*
|
||||
* The bootstrap has external runtime dependencies (jiti for TypeScript
|
||||
* transpilation, @cline/shared) that it resolves via Node's standard module
|
||||
* resolution from its on-disk location. Both must be direct dependencies of
|
||||
* the extension so they are present in node_modules and resolvable from
|
||||
* dist/extensions/. The CLI build performs the same copy in apps/cli/bun.mts.
|
||||
*/
|
||||
function copyPluginSandboxBootstrap() {
|
||||
if (e2eBuild) return
|
||||
const projectRequire = createRequire(import.meta.url)
|
||||
let corePackageDir
|
||||
try {
|
||||
corePackageDir = path.dirname(projectRequire.resolve("@cline/core/package.json"))
|
||||
} catch {
|
||||
console.warn("[esbuild] @cline/core not found — skipping plugin sandbox bootstrap copy")
|
||||
return
|
||||
}
|
||||
const bootstrapSrc = path.join(corePackageDir, "dist", "extensions", "plugin-sandbox-bootstrap.js")
|
||||
if (!fs.existsSync(bootstrapSrc)) {
|
||||
console.warn(`[esbuild] plugin-sandbox-bootstrap.js not found at ${bootstrapSrc} — build @cline/core first`)
|
||||
return
|
||||
}
|
||||
const bootstrapDest = path.join(__dirname, destDir, "extensions", "plugin-sandbox-bootstrap.js")
|
||||
fs.mkdirSync(path.dirname(bootstrapDest), { recursive: true })
|
||||
fs.copyFileSync(bootstrapSrc, bootstrapDest)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const config = standalone ? standaloneConfig : e2eBuild ? e2eBuildConfig : extensionConfig
|
||||
const extensionCtx = await esbuild.context(config)
|
||||
@@ -187,6 +221,7 @@ async function main() {
|
||||
await extensionCtx.rebuild()
|
||||
await extensionCtx.dispose()
|
||||
}
|
||||
copyPluginSandboxBootstrap()
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
|
||||
@@ -467,6 +467,7 @@
|
||||
"@cline/agents": "workspace:*",
|
||||
"@cline/core": "workspace:*",
|
||||
"@cline/llms": "workspace:*",
|
||||
"@cline/sdk": "workspace:*",
|
||||
"@cline/shared": "workspace:*",
|
||||
"@google/genai": "^1.30.0",
|
||||
"@grpc/grpc-js": "^1.9.15",
|
||||
@@ -519,6 +520,7 @@
|
||||
"ignore": "^7.0.3",
|
||||
"image-size": "^2.0.2",
|
||||
"isbinaryfile": "^5.0.2",
|
||||
"jiti": "^2.7.0",
|
||||
"js-yaml": "^4.1.1",
|
||||
"jschardet": "^3.1.4",
|
||||
"json5": "^2.2.3",
|
||||
|
||||
@@ -21,6 +21,23 @@ export async function getAvailableSlashCommands(controller: Controller, _request
|
||||
)
|
||||
}
|
||||
|
||||
// Add plugin-registered commands
|
||||
try {
|
||||
const pluginCommands = await controller.getPluginSlashCommands()
|
||||
for (const cmd of pluginCommands) {
|
||||
commands.push(
|
||||
SlashCommandInfo.create({
|
||||
name: cmd.name,
|
||||
description: cmd.description ?? `Plugin command: ${cmd.name}`,
|
||||
section: "custom",
|
||||
cliCompatible: true,
|
||||
}),
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
// Plugin command discovery is best-effort; don't fail the whole list.
|
||||
}
|
||||
|
||||
// Get workflow toggles from state
|
||||
const localWorkflowToggles = controller.stateManager.getWorkspaceStateKey("workflowToggles") ?? {}
|
||||
const globalWorkflowToggles = controller.stateManager.getGlobalSettingsKey("globalWorkflowToggles") ?? {}
|
||||
|
||||
@@ -27,6 +27,7 @@ export async function getStateToPostToWebview(controller: {
|
||||
backgroundCommandTaskId?: string
|
||||
workspaceManager?: any
|
||||
checkpointRestoreInput?: ExtensionState["checkpointRestoreInput"]
|
||||
getPluginSlashCommands?: () => Promise<{ name: string; description?: string }[]>
|
||||
}): Promise<ExtensionState> {
|
||||
const stateManager = controller.stateManager
|
||||
|
||||
@@ -108,6 +109,15 @@ export async function getStateToPostToWebview(controller: {
|
||||
// Codex OAuth not available
|
||||
}
|
||||
|
||||
// Plugin slash commands are fetched best-effort so autocomplete failures
|
||||
// don't block state posting.
|
||||
let pluginSlashCommands: { name: string; description?: string }[] = []
|
||||
try {
|
||||
pluginSlashCommands = (await controller.getPluginSlashCommands?.()) ?? []
|
||||
} catch {
|
||||
// Plugin command discovery is best-effort.
|
||||
}
|
||||
|
||||
return {
|
||||
version,
|
||||
apiConfiguration,
|
||||
@@ -155,6 +165,7 @@ export async function getStateToPostToWebview(controller: {
|
||||
taskHistory: processedTaskHistory,
|
||||
shouldShowAnnouncement,
|
||||
favoritedModelIds,
|
||||
pluginSlashCommands,
|
||||
backgroundCommandRunning: controller.backgroundCommandRunning ?? false,
|
||||
backgroundCommandTaskId: controller.backgroundCommandTaskId,
|
||||
workspaceRoots: controller.workspaceManager?.getRoots?.() ?? [],
|
||||
|
||||
@@ -71,6 +71,7 @@ import { SdkInteractionCoordinator } from "./sdk-interaction-coordinator"
|
||||
import { SdkMcpCoordinator } from "./sdk-mcp-coordinator"
|
||||
import { SdkMessageCoordinator, type SessionEventListener } from "./sdk-message-coordinator"
|
||||
import { SdkModeCoordinator } from "./sdk-mode-coordinator"
|
||||
import { type PluginSlashCommand, SdkPluginCommandCoordinator } from "./sdk-plugin-commands"
|
||||
import { SdkProviderChangeCoordinator } from "./sdk-provider-change-coordinator"
|
||||
import { SdkSessionConfigBuilder } from "./sdk-session-config-builder"
|
||||
import { SdkSessionEventCoordinator } from "./sdk-session-event-coordinator"
|
||||
@@ -166,6 +167,7 @@ export class Controller {
|
||||
private compaction: SdkCompactionCoordinator
|
||||
private sessionEvents: SdkSessionEventCoordinator
|
||||
private sessionHistory: SdkSessionHistoryLoader
|
||||
private pluginCommands: SdkPluginCommandCoordinator
|
||||
private readonly sdkTelemetry: VscodeSdkTelemetryHandle
|
||||
private readonly providerFailureTelemetryTurnGate = new ProviderFailureTelemetryTurnGate()
|
||||
private readonly providerConfigStore: ProviderConfigStore
|
||||
@@ -480,6 +482,9 @@ export class Controller {
|
||||
},
|
||||
postStateToWebview: () => this.postStateToWebview(),
|
||||
})
|
||||
this.pluginCommands = new SdkPluginCommandCoordinator({
|
||||
getWorkspaceRoot: () => this.getWorkspaceRoot(),
|
||||
})
|
||||
this.taskStart = new SdkTaskStartCoordinator({
|
||||
stateManager: this.stateManager,
|
||||
sessions: this.sessions,
|
||||
@@ -572,6 +577,15 @@ export class Controller {
|
||||
this.providerCatalog.invalidateProviderListings()
|
||||
}
|
||||
|
||||
/**
|
||||
* Return plugin-registered slash commands for autocomplete. Used by the
|
||||
* getAvailableSlashCommands gRPC handler to surface plugin commands in the
|
||||
* webview's slash command picker.
|
||||
*/
|
||||
getPluginSlashCommands(): Promise<PluginSlashCommand[]> {
|
||||
return this.pluginCommands.getSlashCommands()
|
||||
}
|
||||
|
||||
private handleProviderConfigChange(event: ProviderConfigChange): void {
|
||||
this.scheduleProviderConfigStatePost()
|
||||
|
||||
@@ -674,6 +688,7 @@ export class Controller {
|
||||
// are disposed below — see StatePostDebouncer.dispose().
|
||||
await this.statePostDebouncer.dispose()
|
||||
await this.invalidateUserInstructionService()
|
||||
await this.pluginCommands.dispose()
|
||||
this.messages.cancelPendingSave()
|
||||
// Clear MCP tool list change callback before disposing McpHub
|
||||
this.mcpHub?.clearToolListChangeCallback()
|
||||
@@ -734,14 +749,48 @@ export class Controller {
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand a leading `/workflow` or `/skill` slash command into its instruction
|
||||
* body. Mirrors the CLI's `buildUserInputMessage`. Returns the input unchanged
|
||||
* if it is not a known command or expansion fails.
|
||||
* Expand a leading slash command. First checks plugin-registered commands
|
||||
* (e.g. `/goal`), then falls back to workflow/skill expansion via the
|
||||
* user-instruction service. For plugin commands:
|
||||
* - If the handler returns `submitPrompt`, that becomes the prompt text.
|
||||
* - If the handler returns `reply`, it is emitted as a say message.
|
||||
* - If only `reply` is returned (no `submitPrompt`), returns empty string
|
||||
* so the agent turn is suppressed (the reply was already shown).
|
||||
* Returns the input unchanged if it is not a known command.
|
||||
*/
|
||||
private async resolveSlashCommands(text: string): Promise<string> {
|
||||
if (this.isDisposed) {
|
||||
return text
|
||||
}
|
||||
|
||||
// Check plugin commands first — they take precedence over
|
||||
// workflow/skill expansion so plugin names cannot be shadowed.
|
||||
try {
|
||||
const result = await this.pluginCommands.resolveCommand(text)
|
||||
if (result) {
|
||||
if (result.reply) {
|
||||
this.messages.emitSessionEvents(
|
||||
[
|
||||
{
|
||||
ts: Date.now(),
|
||||
type: "say",
|
||||
say: "text",
|
||||
text: result.reply,
|
||||
partial: false,
|
||||
},
|
||||
],
|
||||
{
|
||||
type: "status",
|
||||
payload: { sessionId: this.sessions.getActiveSession()?.sessionId ?? "", status: "running" },
|
||||
},
|
||||
)
|
||||
}
|
||||
return result.submitPrompt ?? ""
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.warn("[SdkController] Plugin command resolution failed, falling through:", error)
|
||||
}
|
||||
|
||||
try {
|
||||
const workspaceRoot = await this.getWorkspaceRoot()
|
||||
const service = await this.ensureUserInstructionService(workspaceRoot)
|
||||
@@ -1800,6 +1849,7 @@ export class Controller {
|
||||
mcpHub: this.mcpHub,
|
||||
backgroundCommandRunning: this.backgroundCommandRunning,
|
||||
backgroundCommandTaskId: this.backgroundCommandTaskId,
|
||||
getPluginSlashCommands: () => this.pluginCommands.getSlashCommands(),
|
||||
})
|
||||
const sdkTaskHistory = (await this.taskHistory.listHistory({ limit: 100, hydrate: false }))
|
||||
.map(sessionHistoryRecordToHistoryItem)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { normalizePluginCommandName, SdkPluginCommandCoordinator } from "./sdk-plugin-commands"
|
||||
|
||||
describe("SdkPluginCommandCoordinator", () => {
|
||||
it("loads plugins from the active workspace", async () => {
|
||||
const loadPlugins = vi.fn(async () => ({
|
||||
extensions: [],
|
||||
pluginPaths: [],
|
||||
failures: [],
|
||||
warnings: [],
|
||||
}))
|
||||
const coordinator = new SdkPluginCommandCoordinator({
|
||||
getWorkspaceRoot: async () => "/workspace/project",
|
||||
loadPlugins,
|
||||
})
|
||||
|
||||
await coordinator.getSlashCommands()
|
||||
|
||||
expect(loadPlugins).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
cwd: "/workspace/project",
|
||||
workspacePath: "/workspace/project",
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("normalizes plugin command names like the CLI", () => {
|
||||
expect(normalizePluginCommandName(" /Goal ")).toBe("goal")
|
||||
expect(normalizePluginCommandName("GOAL")).toBe("goal")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,174 @@
|
||||
// SdkPluginCommandCoordinator — discovers and executes plugin-registered
|
||||
// slash commands, mirroring the CLI's createWorkspaceChatCommandHost.
|
||||
//
|
||||
// Plugins register commands via `api.registerCommand({ name, handler })` in
|
||||
// their setup(). The ContributionRegistry runs setup() and collects the
|
||||
// registered commands. This coordinator:
|
||||
// 1. Lazily loads plugins via resolveAndLoadAgentPlugins (sandbox mode)
|
||||
// 2. Initializes a ContributionRegistry to run setup() and gather commands
|
||||
// 3. Exposes getSlashCommands() for autocomplete
|
||||
// 4. Exposes resolveCommand(text) to execute a /command and return its result
|
||||
|
||||
import {
|
||||
type AgentExtensionCommand,
|
||||
type AgentExtensionCommandResult,
|
||||
createContributionRegistry,
|
||||
noopBasicLogger,
|
||||
resolveAndLoadAgentPlugins,
|
||||
} from "@cline/core"
|
||||
import type { AgentTool, Message } from "@cline/shared"
|
||||
import { Logger } from "@shared/services/Logger"
|
||||
|
||||
export interface PluginSlashCommand {
|
||||
name: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface PluginCommandResult {
|
||||
reply?: string
|
||||
submitPrompt?: string
|
||||
}
|
||||
|
||||
interface LoadedPlugins {
|
||||
commands: AgentExtensionCommand[]
|
||||
shutdown: () => Promise<void>
|
||||
}
|
||||
|
||||
export interface SdkPluginCommandCoordinatorOptions {
|
||||
getWorkspaceRoot: () => Promise<string>
|
||||
loadPlugins?: typeof resolveAndLoadAgentPlugins
|
||||
}
|
||||
|
||||
export function normalizePluginCommandName(name: string): string {
|
||||
const trimmed = name.trim()
|
||||
return (trimmed.startsWith("/") ? trimmed.slice(1) : trimmed).toLowerCase()
|
||||
}
|
||||
|
||||
export class SdkPluginCommandCoordinator {
|
||||
private loadedPromise: Promise<LoadedPlugins | undefined> | undefined
|
||||
|
||||
constructor(private readonly options: SdkPluginCommandCoordinatorOptions) {}
|
||||
|
||||
/**
|
||||
* Lazily load plugins and initialize the contribution registry. The result
|
||||
* is cached so subsequent calls reuse the same sandbox process. Returns
|
||||
* undefined if no plugins are installed or loading fails.
|
||||
*/
|
||||
private ensureLoaded(): Promise<LoadedPlugins | undefined> {
|
||||
if (this.loadedPromise) {
|
||||
return this.loadedPromise
|
||||
}
|
||||
this.loadedPromise = (async () => {
|
||||
let loaded: Awaited<ReturnType<typeof resolveAndLoadAgentPlugins>>
|
||||
try {
|
||||
const workspaceRoot = await this.options.getWorkspaceRoot()
|
||||
loaded = await (this.options.loadPlugins ?? resolveAndLoadAgentPlugins)({
|
||||
cwd: workspaceRoot,
|
||||
workspacePath: workspaceRoot,
|
||||
logger: noopBasicLogger,
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
Logger.warn(`[PluginCommands] Plugin loading failed; continuing without plugin commands (${message})`)
|
||||
return undefined
|
||||
}
|
||||
if (!loaded.extensions.length) {
|
||||
await loaded.shutdown?.().catch(() => {})
|
||||
return undefined
|
||||
}
|
||||
|
||||
const registry = createContributionRegistry<(typeof loaded.extensions)[number], AgentTool, Message[]>({
|
||||
extensions: loaded.extensions,
|
||||
})
|
||||
try {
|
||||
await registry.initialize()
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
Logger.warn(`[PluginCommands] Contribution registry initialization failed (${message})`)
|
||||
await loaded.shutdown?.().catch(() => {})
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
commands: registry.getRegistrySnapshot().commands,
|
||||
shutdown: async () => {
|
||||
await loaded.shutdown?.().catch(() => {})
|
||||
},
|
||||
}
|
||||
})()
|
||||
return this.loadedPromise
|
||||
}
|
||||
|
||||
/**
|
||||
* Return plugin-registered slash commands for autocomplete. Returns an
|
||||
* empty array if no plugins are installed or loading fails.
|
||||
*/
|
||||
async getSlashCommands(): Promise<PluginSlashCommand[]> {
|
||||
const loaded = await this.ensureLoaded()
|
||||
if (!loaded) {
|
||||
return []
|
||||
}
|
||||
return loaded.commands
|
||||
.filter((cmd) => typeof cmd.handler === "function")
|
||||
.map((cmd) => ({
|
||||
name: normalizePluginCommandName(cmd.name),
|
||||
description: cmd.description,
|
||||
}))
|
||||
.filter((cmd) => cmd.name.length > 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a leading /command from a plugin. Returns null if the text does
|
||||
* not match a plugin command. Returns { reply?, submitPrompt? } from the
|
||||
* command handler.
|
||||
*/
|
||||
async resolveCommand(text: string): Promise<PluginCommandResult | null> {
|
||||
if (!text.startsWith("/") || text.length < 2) {
|
||||
return null
|
||||
}
|
||||
const match = text.match(/^\/(\S+)/)
|
||||
if (!match?.[1]) {
|
||||
return null
|
||||
}
|
||||
const name = normalizePluginCommandName(match[1])
|
||||
const remainder = text.slice(name.length + 1).trim()
|
||||
|
||||
const loaded = await this.ensureLoaded()
|
||||
if (!loaded) {
|
||||
return null
|
||||
}
|
||||
const command = loaded.commands.find(
|
||||
(cmd) => normalizePluginCommandName(cmd.name) === name && typeof cmd.handler === "function",
|
||||
)
|
||||
if (!command?.handler) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const result: AgentExtensionCommandResult = await command.handler(remainder)
|
||||
if (typeof result === "string") {
|
||||
return { reply: result }
|
||||
}
|
||||
return {
|
||||
reply: result.reply,
|
||||
submitPrompt: result.submitPrompt,
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
Logger.warn(`[PluginCommands] Command "/${name}" failed: ${message}`)
|
||||
return { reply: `Command /${name} failed: ${message}` }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shut down the plugin sandbox process. Called on extension disposal.
|
||||
*/
|
||||
async dispose(): Promise<void> {
|
||||
const promise = this.loadedPromise
|
||||
this.loadedPromise = undefined
|
||||
if (promise) {
|
||||
const loaded = await promise.catch(() => undefined)
|
||||
await loaded?.shutdown().catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import { OnboardingModelGroup } from "./proto/cline/state"
|
||||
import { Mode } from "./storage/types"
|
||||
import { TelemetrySetting } from "./TelemetrySetting"
|
||||
import { UserInfo } from "./UserInfo"
|
||||
import type { SlashCommand } from "./slashCommands"
|
||||
// webview will hold state
|
||||
export interface ExtensionMessage {
|
||||
type: "grpc_response" // New type for gRPC responses
|
||||
@@ -114,6 +115,8 @@ export interface ExtensionState {
|
||||
worktreesEnabled?: ClineFeatureSetting
|
||||
customPrompt?: string
|
||||
favoritedModelIds: string[]
|
||||
/** Plugin-registered slash commands surfaced for autocomplete. */
|
||||
pluginSlashCommands?: SlashCommand[]
|
||||
// NEW: Add workspace information
|
||||
workspaceRoots: WorkspaceRoot[]
|
||||
primaryRootIndex: number
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { existsSync } from "node:fs"
|
||||
import { readFile } from "node:fs/promises"
|
||||
import { createRequire } from "node:module"
|
||||
import { join } from "node:path"
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { $ } from "bun"
|
||||
|
||||
/**
|
||||
* Integration test for CLINE-2584: the plugin sandbox bootstrap
|
||||
* (`plugin-sandbox-bootstrap.js`) must be shipped with the VS Code extension.
|
||||
*
|
||||
* The bootstrap runs in an isolated child process spawned by
|
||||
* `SubprocessSandbox` — it cannot be inlined into `extension.js` because the
|
||||
* sandbox spawns it via `node <bootstrapFile>`. The CLI build copies this
|
||||
* file (`apps/cli/bun.mts`); the extension build (`esbuild.mjs`) must do the
|
||||
* same.
|
||||
*
|
||||
* The bootstrap also has external runtime dependencies that must be resolvable
|
||||
* from its on-disk location via Node's standard module resolution:
|
||||
* - jiti (TypeScript transpilation of .ts plugins)
|
||||
* - @cline/shared, @cline/sdk (host-provided SDK packages that plugins import)
|
||||
*
|
||||
* This test runs the real `bun esbuild.mjs` build and checks the real
|
||||
* `dist/` output, exercising the same build pipeline CI uses.
|
||||
*/
|
||||
|
||||
const projectRoot = join(import.meta.dir, "..", "..")
|
||||
const distDir = join(projectRoot, "dist")
|
||||
const bootstrapPath = join(distDir, "extensions", "plugin-sandbox-bootstrap.js")
|
||||
|
||||
describe("plugin-sandbox bootstrap build artifact (CLINE-2584)", () => {
|
||||
it("esbuild.mjs emits plugin-sandbox-bootstrap.js into dist/", async () => {
|
||||
const result = await $`bun esbuild.mjs`.cwd(projectRoot).quiet()
|
||||
expect(result.exitCode).toBe(0)
|
||||
|
||||
expect(existsSync(join(distDir, "extension.js"))).toBe(true)
|
||||
expect(existsSync(bootstrapPath)).toBe(true)
|
||||
}, 60_000)
|
||||
|
||||
it("the bootstrap is a real executable script with IPC handling", async () => {
|
||||
expect(existsSync(bootstrapPath)).toBe(true)
|
||||
|
||||
const content = await readFile(bootstrapPath, "utf8")
|
||||
expect(content.length).toBeGreaterThan(1000)
|
||||
expect(content).toMatch(/process\.on\(.process\.message|process\.send|type:\s*["']response["']/)
|
||||
}, 60_000)
|
||||
|
||||
it("the bootstrap's runtime dependencies resolve from dist/", () => {
|
||||
expect(existsSync(bootstrapPath)).toBe(true)
|
||||
|
||||
// The bootstrap is spawned as a standalone Node child process. It
|
||||
// imports jiti (for TypeScript transpilation) and @cline/shared as
|
||||
// external modules, and plugins import @cline/sdk. Node resolves
|
||||
// these by walking up from the bootstrap's directory. All must be
|
||||
// direct dependencies of the extension so they appear in
|
||||
// node_modules and are resolvable.
|
||||
const requireFromBootstrap = createRequire(bootstrapPath)
|
||||
expect(() => requireFromBootstrap.resolve("jiti")).not.toThrow()
|
||||
expect(() => requireFromBootstrap.resolve("@cline/shared")).not.toThrow()
|
||||
// @cline/sdk is a host-provided SDK specifier that plugins import.
|
||||
// The bootstrap's findHostPackageRoot walks up from dist/extensions/
|
||||
// looking for node_modules/@cline/sdk/package.json.
|
||||
expect(
|
||||
existsSync(join(projectRoot, "node_modules", "@cline", "sdk", "package.json")),
|
||||
).toBe(true)
|
||||
}, 60_000)
|
||||
})
|
||||
@@ -224,6 +224,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
remoteConfigSettings,
|
||||
navigateToSettingsModelPicker,
|
||||
mcpServers,
|
||||
pluginSlashCommands,
|
||||
} = useExtensionState()
|
||||
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
|
||||
const [isDraggingOver, setIsDraggingOver] = useState(false)
|
||||
@@ -489,6 +490,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
remoteWorkflowToggles,
|
||||
remoteConfigSettings?.remoteGlobalWorkflows,
|
||||
mcpServers,
|
||||
pluginSlashCommands,
|
||||
)
|
||||
|
||||
if (allCommands.length === 0) {
|
||||
@@ -514,6 +516,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
remoteWorkflowToggles,
|
||||
remoteConfigSettings?.remoteGlobalWorkflows,
|
||||
mcpServers,
|
||||
pluginSlashCommands,
|
||||
)
|
||||
if (commands.length > 0) {
|
||||
handleSlashCommandsSelect(commands[selectedSlashCommandsIndex])
|
||||
@@ -673,6 +676,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
slashCommandsQuery,
|
||||
handleSlashCommandsSelect,
|
||||
sendingDisabled,
|
||||
pluginSlashCommands,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -984,6 +988,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
globalWorkflowToggles,
|
||||
remoteWorkflowToggles,
|
||||
remoteConfigSettings?.remoteGlobalWorkflows,
|
||||
mcpServers,
|
||||
pluginSlashCommands,
|
||||
)
|
||||
|
||||
if (isValidCommand) {
|
||||
@@ -997,7 +1003,14 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
highlightLayerRef.current.innerHTML = processedText
|
||||
highlightLayerRef.current.scrollTop = textAreaRef.current.scrollTop
|
||||
highlightLayerRef.current.scrollLeft = textAreaRef.current.scrollLeft
|
||||
}, [localWorkflowToggles, globalWorkflowToggles, remoteWorkflowToggles, remoteConfigSettings])
|
||||
}, [
|
||||
localWorkflowToggles,
|
||||
globalWorkflowToggles,
|
||||
remoteWorkflowToggles,
|
||||
remoteConfigSettings,
|
||||
mcpServers,
|
||||
pluginSlashCommands,
|
||||
])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
updateHighlights()
|
||||
@@ -1406,6 +1419,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
mcpServers={mcpServers}
|
||||
onMouseDown={handleMenuMouseDown}
|
||||
onSelect={handleSlashCommandsSelect}
|
||||
pluginSlashCommands={pluginSlashCommands}
|
||||
query={slashCommandsQuery}
|
||||
remoteWorkflows={remoteConfigSettings?.remoteGlobalWorkflows}
|
||||
remoteWorkflowToggles={remoteWorkflowToggles}
|
||||
|
||||
@@ -16,6 +16,7 @@ interface SlashCommandMenuProps {
|
||||
remoteWorkflowToggles?: Record<string, boolean>
|
||||
remoteWorkflows?: any[]
|
||||
mcpServers?: McpServer[]
|
||||
pluginSlashCommands?: SlashCommand[]
|
||||
}
|
||||
|
||||
const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({
|
||||
@@ -29,6 +30,7 @@ const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({
|
||||
remoteWorkflowToggles,
|
||||
remoteWorkflows,
|
||||
mcpServers = [],
|
||||
pluginSlashCommands = [],
|
||||
}) => {
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -40,6 +42,7 @@ const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({
|
||||
remoteWorkflowToggles,
|
||||
remoteWorkflows,
|
||||
mcpServers,
|
||||
pluginSlashCommands,
|
||||
)
|
||||
const defaultCommands = filteredCommands.filter((cmd) => cmd.section === "default" || !cmd.section)
|
||||
const workflowCommands = filteredCommands.filter((cmd) => cmd.section === "custom")
|
||||
|
||||
@@ -289,6 +289,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
localAgentsRulesToggles: {},
|
||||
localWorkflowToggles: {},
|
||||
globalWorkflowToggles: {},
|
||||
pluginSlashCommands: [],
|
||||
shellIntegrationTimeout: 4000,
|
||||
terminalReuseEnabled: true,
|
||||
vscodeTerminalExecutionMode: "vscodeTerminal",
|
||||
@@ -906,6 +907,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
localAgentsRulesToggles: state.localAgentsRulesToggles || {},
|
||||
localWorkflowToggles: state.localWorkflowToggles || {},
|
||||
globalWorkflowToggles: state.globalWorkflowToggles || {},
|
||||
pluginSlashCommands: state.pluginSlashCommands || [],
|
||||
remoteRulesToggles: state.remoteRulesToggles || {},
|
||||
remoteWorkflowToggles: state.remoteWorkflowToggles || {},
|
||||
enableCheckpointsSetting: state.enableCheckpointsSetting,
|
||||
|
||||
@@ -181,6 +181,7 @@ export function getMatchingSlashCommands(
|
||||
remoteWorkflowToggles?: Record<string, boolean>,
|
||||
remoteWorkflows?: any[],
|
||||
mcpServers: McpServer[] = [],
|
||||
pluginSlashCommands: SlashCommand[] = [],
|
||||
): SlashCommand[] {
|
||||
const workflowCommands = getWorkflowCommands(
|
||||
localWorkflowToggles,
|
||||
@@ -189,7 +190,7 @@ export function getMatchingSlashCommands(
|
||||
remoteWorkflows,
|
||||
)
|
||||
const mcpPromptCommands = getMcpPromptCommands(mcpServers)
|
||||
const allCommands = [...DEFAULT_SLASH_COMMANDS, ...workflowCommands, ...mcpPromptCommands]
|
||||
const allCommands = [...DEFAULT_SLASH_COMMANDS, ...workflowCommands, ...mcpPromptCommands, ...pluginSlashCommands]
|
||||
|
||||
if (!query) {
|
||||
return allCommands
|
||||
@@ -233,6 +234,7 @@ export function validateSlashCommand(
|
||||
remoteWorkflowToggles?: Record<string, boolean>,
|
||||
remoteWorkflows?: any[],
|
||||
mcpServers: McpServer[] = [],
|
||||
pluginSlashCommands: SlashCommand[] = [],
|
||||
): "full" | "partial" | null {
|
||||
if (!command) {
|
||||
return null
|
||||
@@ -245,7 +247,7 @@ export function validateSlashCommand(
|
||||
remoteWorkflows,
|
||||
)
|
||||
const mcpPromptCommands = getMcpPromptCommands(mcpServers)
|
||||
const allCommands = [...DEFAULT_SLASH_COMMANDS, ...workflowCommands, ...mcpPromptCommands]
|
||||
const allCommands = [...DEFAULT_SLASH_COMMANDS, ...workflowCommands, ...mcpPromptCommands, ...pluginSlashCommands]
|
||||
|
||||
// case insensitive matching
|
||||
const exactMatch = allCommands.some((cmd) => cmd.name.toLowerCase() === command.toLowerCase())
|
||||
|
||||
@@ -354,6 +354,7 @@
|
||||
"@cline/agents": "workspace:*",
|
||||
"@cline/core": "workspace:*",
|
||||
"@cline/llms": "workspace:*",
|
||||
"@cline/sdk": "workspace:*",
|
||||
"@cline/shared": "workspace:*",
|
||||
"@google/genai": "^1.30.0",
|
||||
"@grpc/grpc-js": "^1.9.15",
|
||||
@@ -406,6 +407,7 @@
|
||||
"ignore": "^7.0.3",
|
||||
"image-size": "^2.0.2",
|
||||
"isbinaryfile": "^5.0.2",
|
||||
"jiti": "^2.7.0",
|
||||
"js-yaml": "^4.1.1",
|
||||
"jschardet": "^3.1.4",
|
||||
"json5": "^2.2.3",
|
||||
|
||||
Reference in New Issue
Block a user