Compare commits

...

6 Commits

Author SHA1 Message Date
Dominic Cooney ca22ccf4a1 chore(vscode): remove /reportbug slash command
/reportbug survived the SDK migration in a broken, partial state: the
controller handler (reportBug.ts) just called
handleWebviewAskResponse("yesButtonClicked") unconditionally, with no
SDK-side tool ever emitting the ask:"report_bug" message it expects to
resolve, and no GitHub issue URL builder left in github-url-utils.ts
(createGitHubIssueUrl/createAndOpenGitHubIssue were dropped during the
migration). Selecting "Report bug" was effectively a no-op.

This mirrors an earlier full removal of the feature elsewhere in the
project's history (a232def05, "feat: remove /reportbug slash
command") for the same reasoning: it's a rarely-used utility that adds
system-prompt noise and slash-command clutter for a task users can
already do directly at github.com/cline/cline/issues. Rather than
rebuild the AI-driven flow (custom SDK tool + coordinator + ask/preview
+ restored URL builder) on top of the SDK architecture, remove it
entirely, consistent with that precedent.

Removed:
- Backend: reportBug.ts handler, the reportBug RPC (slash.proto) and
  REPORT_BUG value (ui.proto's ClineAsk enum, tools.ts's
  ClineDefaultTool enum), proto-conversion mapping entries.
- Webview: ReportBugPreview.tsx, the /reportbug entry in
  BASE_SLASH_COMMANDS, the report_bug button config and ask-routing
  cases (buttonConfig.ts, useMessageHandlers.ts, ChatRow.tsx), the
  App.stories.tsx story, and the rotating FeatureTip mentioning
  /reportbug.
- Docs: the /reportbug table row and section in using-commands.mdx,
  and the ide.mdx "Report issues" bullet, repointed at the GitHub
  issues URL directly.

Ran `bun run protos` to regenerate the ProtoBus/gRPC bindings
accordingly (git-ignored, not committed).
2026-07-08 19:17:51 +09:00
Dominic Cooney fe2ceeaf97 refactor(vscode): remove dead getAvailableSlashCommands RPC and cliCompatible
The getAvailableSlashCommands gRPC handler assembled base commands +
plugin commands + workflows into a SlashCommandsResponse, tagged with
a cliCompatible flag (originally meant to distinguish VS Code-only
commands like the since-removed /explain-changes). Nothing ever called
this RPC from the webview: ChatTextArea/SlashCommandMenu compute the
autocomplete list entirely through the separate slash-commands.ts
utility, driven by the ExtensionState pushed on every state update
rather than a pulled RPC response. slash-commands.ts's local
assembly is also strictly more complete (it additionally covers MCP
prompt commands, which the RPC never included).

With no consumer, cliCompatible was dead metadata: grepping the whole
repo (webview, generated hosts, IntelliJ plugin, CLI) turned up no
reads of the field outside the handler's own tests.

Remove the RPC (and its now-unused SlashCommandInfo/SlashCommandsResponse
messages) from slash.proto, delete the handler and its tests, and drop
cliCompatible from the shared SlashCommand type and BASE_SLASH_COMMANDS.
Ran `bun run protos` to regenerate the generated ProtoBus/gRPC files
accordingly (git-ignored, not committed).
2026-07-08 19:16:21 +09:00
Dominic Cooney 634cc8ac9a fix(vscode): fix compile errors in plugin slash command coordinator
CI caught two real TypeScript errors that had gone unnoticed locally
(the dev environment's @cline/core build artifacts were stale, masking
them):

1. sdk-plugin-commands.ts: createContributionRegistry() was called
   without type arguments, defaulting TMessage to `unknown`. This
   doesn't match AgentExtension's setup() signature, which expects
   Message[] (per @cline/shared), causing a type mismatch. Mirror the
   CLI's equivalent call in plugin-chat-commands.ts, which explicitly
   parameterizes <Extension, AgentTool, Message[]>.

2. SdkController.ts: emitSessionEvents(messages, event) requires two
   arguments, but the plugin-command reply path only passed one. Add
   the missing status event, matching the pattern used elsewhere in
   this file (e.g. the provider-failure error path) and in
   sdk-followup-coordinator.ts.
2026-07-08 19:15:50 +09:00
Dominic Cooney 70d2088306 fix(vscode): thread plugin slash commands into autocomplete menu and navigation
ChatTextArea/SlashCommandMenu destructure pluginSlashCommands from
ExtensionState and pass it to validateSlashCommand() for input
highlighting, but never pass it to getMatchingSlashCommands() for
arrow-key navigation, Enter/Tab selection, or the rendered
SlashCommandMenu itself. Plugin-registered commands (e.g. /goal) are
discovered correctly on the backend and shipped to the webview, but
never appear in the actual autocomplete dropdown.

Thread pluginSlashCommands through all three remaining call sites and
add it to the relevant useCallback/useLayoutEffect dependency arrays
so the menu updates once plugin discovery resolves asynchronously
after mount.
2026-07-08 14:44:38 +09:00
Dominic Cooney cf6dbdc779 feat(vscode): surface plugin commands in slash command autocomplete (CLINE-2584) 2026-07-08 14:41:31 +09:00
Dominic Cooney efc28486fd fix(vscode): bundle plugin sandbox bootstrap and surface plugin commands (CLINE-2584) 2026-07-08 14:41:30 +09:00
30 changed files with 354 additions and 571 deletions
+35
View File
@@ -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) => {
+2
View File
@@ -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",
-17
View File
@@ -10,23 +10,6 @@ option java_package = "bot.cline.proto";
// SlashService provides methods for managing slash commands
service SlashService {
// Sends button click message
rpc reportBug(StringRequest) returns (Empty);
rpc condense(StringRequest) returns (Empty);
// Get available slash commands for autocomplete (used by CLI)
rpc getAvailableSlashCommands(EmptyRequest) returns (SlashCommandsResponse);
}
// Slash command definition for autocomplete
message SlashCommandInfo {
string name = 1; // Command name without slash, e.g., "newtask", "smol"
string description = 2; // Human-readable description
string section = 3; // "default", "custom", or "cli"
bool cli_compatible = 4; // false for VS Code-only commands
}
// Response containing all available slash commands
message SlashCommandsResponse {
repeated SlashCommandInfo commands = 1;
}
-1
View File
@@ -30,7 +30,6 @@ enum ClineAsk {
USE_MCP_SERVER = 11;
NEW_TASK = 12;
CONDENSE = 13;
REPORT_BUG = 14;
SUMMARIZE_TASK = 15;
ACT_MODE_RESPOND = 16;
USE_SUBAGENTS = 17;
@@ -1,88 +0,0 @@
import { EmptyRequest } from "@shared/proto/cline/common"
import { SlashCommandInfo, SlashCommandsResponse } from "@shared/proto/cline/slash"
import { BASE_SLASH_COMMANDS } from "@/shared/slashCommands"
import { Controller } from ".."
/**
* Returns all available slash commands for autocomplete.
*/
export async function getAvailableSlashCommands(controller: Controller, _request: EmptyRequest): Promise<SlashCommandsResponse> {
const commands: SlashCommandInfo[] = []
// Add built-in commands
for (const cmd of [...BASE_SLASH_COMMANDS]) {
commands.push(
SlashCommandInfo.create({
name: cmd.name,
description: cmd.description,
section: "default",
cliCompatible: cmd.cliCompatible,
}),
)
}
// Get workflow toggles from state
const localWorkflowToggles = controller.stateManager.getWorkspaceStateKey("workflowToggles") ?? {}
const globalWorkflowToggles = controller.stateManager.getGlobalSettingsKey("globalWorkflowToggles") ?? {}
const remoteWorkflowToggles = controller.stateManager.getGlobalStateKey("remoteWorkflowToggles") ?? {}
const remoteConfigSettings = controller.stateManager.getRemoteConfigSettings()
const remoteWorkflows = remoteConfigSettings?.remoteGlobalWorkflows ?? []
// Track local workflow names to avoid duplicates from global
const localNames = new Set<string>()
// Add local workflows (enabled only)
for (const [path, enabled] of Object.entries(localWorkflowToggles)) {
if (enabled) {
const fileName = fullPathToFileName(path)
localNames.add(fileName)
commands.push(
SlashCommandInfo.create({
name: fileName,
description: `Custom workflow: ${fileName}`,
section: "custom",
cliCompatible: true,
}),
)
}
}
// Add global workflows (enabled only, skip if local exists with same name)
for (const [path, enabled] of Object.entries(globalWorkflowToggles)) {
if (enabled) {
const fileName = fullPathToFileName(path)
if (!localNames.has(fileName)) {
commands.push(
SlashCommandInfo.create({
name: fileName,
description: `Custom workflow: ${fileName}`,
section: "custom",
cliCompatible: true,
}),
)
}
}
}
// Add remote workflows that are enabled
for (const workflow of remoteWorkflows) {
const enabled = workflow.alwaysEnabled || remoteWorkflowToggles[workflow.name] !== false
if (enabled) {
commands.push(
SlashCommandInfo.create({
name: workflow.name,
description: `Remote workflow: ${workflow.name}`,
section: "custom",
cliCompatible: true,
}),
)
}
}
return SlashCommandsResponse.create({ commands })
}
function fullPathToFileName(path: string): string {
// e.g. replace /path/to/workflow.md with workflow.md
return path.replace(/^.*[/\\]/, "")
}
@@ -1,10 +0,0 @@
import { Empty, StringRequest } from "@shared/proto/cline/common"
import { Controller } from ".."
/**
* Report bug slash command logic
*/
export async function reportBug(controller: Controller, _request: StringRequest): Promise<Empty> {
await controller.task?.handleWebviewAskResponse("yesButtonClicked")
return Empty.create()
}
@@ -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?.() ?? [],
+52 -3
View File
@@ -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,7 @@ export class Controller {
},
postStateToWebview: () => this.postStateToWebview(),
})
this.pluginCommands = new SdkPluginCommandCoordinator()
this.taskStart = new SdkTaskStartCoordinator({
stateManager: this.stateManager,
sessions: this.sessions,
@@ -575,6 +578,16 @@ export class Controller {
this.providerCatalog.invalidateProviderListings()
}
/**
* Return plugin-registered slash commands for autocomplete. Surfaced to
* the webview as `pluginSlashCommands` in ExtensionState (see
* getStateToPostToWebview) so the chat input's slash-command menu can
* show them.
*/
getPluginSlashCommands(): Promise<PluginSlashCommand[]> {
return this.pluginCommands.getSlashCommands()
}
private handleProviderConfigChange(event: ProviderConfigChange): void {
this.scheduleProviderConfigStatePost()
@@ -677,6 +690,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()
@@ -737,14 +751,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)
@@ -1803,6 +1851,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)
+156
View File
@@ -0,0 +1,156 @@
// 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 class SdkPluginCommandCoordinator {
private loadedPromise: Promise<LoadedPlugins | undefined> | undefined
/**
* 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 {
loaded = await resolveAndLoadAgentPlugins({
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: cmd.name,
description: cmd.description,
}))
}
/**
* 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 = 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) => 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(() => {})
}
}
}
@@ -1583,7 +1583,7 @@ export class TelemetryService {
/**
* Records when slash commands or workflows are activated
* @param ulid Unique identifier for the task
* @param commandName The name of the command (e.g., "newtask", "reportbug", or custom workflow name)
* @param commandName The name of the command (e.g., "newtask", "newrule", or custom workflow name)
* @param commandType Whether it's a built-in command, custom workflow, or MCP prompt
*/
public captureSlashCommandUsed(ulid: string, commandName: string, commandType: "builtin" | "workflow" | "mcp_prompt") {
+3 -1
View File
@@ -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
@@ -211,7 +214,6 @@ export type ClineAsk =
| "new_task"
| "condense"
| "summarize_task"
| "report_bug"
| "use_subagents"
export type ClineSay =
@@ -24,7 +24,6 @@ function convertClineAskToProtoEnum(ask: AppClineAsk | undefined): ClineAsk | un
new_task: ClineAsk.NEW_TASK,
condense: ClineAsk.CONDENSE,
summarize_task: ClineAsk.SUMMARIZE_TASK,
report_bug: ClineAsk.REPORT_BUG,
use_subagents: ClineAsk.USE_SUBAGENTS,
}
@@ -57,7 +56,6 @@ function convertProtoEnumToClineAsk(ask: ClineAsk): AppClineAsk | undefined {
[ClineAsk.NEW_TASK]: "new_task",
[ClineAsk.CONDENSE]: "condense",
[ClineAsk.SUMMARIZE_TASK]: "summarize_task",
[ClineAsk.REPORT_BUG]: "report_bug",
[ClineAsk.USE_SUBAGENTS]: "use_subagents",
}
-11
View File
@@ -2,7 +2,6 @@ export interface SlashCommand {
name: string
description?: string
section?: "default" | "custom" | "mcp"
cliCompatible?: boolean
}
export const BASE_SLASH_COMMANDS: SlashCommand[] = [
@@ -10,31 +9,21 @@ export const BASE_SLASH_COMMANDS: SlashCommand[] = [
name: "newtask",
description: "Create a new task with context from the current task",
section: "default",
cliCompatible: true,
},
{
name: "deep-planning",
description: "Create a comprehensive implementation plan before coding",
section: "default",
cliCompatible: true,
},
{
name: "smol",
description: "Condenses your current context window",
section: "default",
cliCompatible: true,
},
{
name: "newrule",
description: "Create a new Cline rule based on your conversation",
section: "default",
cliCompatible: true,
},
{
name: "reportbug",
description: "Create a Github issue with Cline",
section: "default",
cliCompatible: true,
},
]
-1
View File
@@ -27,7 +27,6 @@ export enum ClineDefaultTool {
WEB_SEARCH = "web_search",
CONDENSE = "condense",
SUMMARIZE_TASK = "summarize_task",
REPORT_BUG = "report_bug",
NEW_RULE = "new_rule",
APPLY_PATCH = "apply_patch",
USE_SKILL = "use_skill",
@@ -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)
})
-285
View File
@@ -1,285 +0,0 @@
import { afterEach, beforeEach, describe, it } from "mocha"
import "should"
import * as sinon from "sinon"
import { Controller } from "../core/controller"
import { getAvailableSlashCommands } from "../core/controller/slash/getAvailableSlashCommands"
import { EmptyRequest } from "../shared/proto/cline/common"
import { BASE_SLASH_COMMANDS } from "../shared/slashCommands"
/**
* Unit tests for getAvailableSlashCommands RPC endpoint
* Tests the slash command discovery and filtering functionality
*/
describe("getAvailableSlashCommands", () => {
let mockController: Partial<Controller>
let mockStateManager: {
getWorkspaceStateKey: sinon.SinonStub
getGlobalSettingsKey: sinon.SinonStub
getGlobalStateKey: sinon.SinonStub
getRemoteConfigSettings: sinon.SinonStub
}
beforeEach(() => {
mockStateManager = {
getWorkspaceStateKey: sinon.stub(),
getGlobalSettingsKey: sinon.stub(),
getGlobalStateKey: sinon.stub(),
getRemoteConfigSettings: sinon.stub(),
}
// Default stubs return empty/null values
mockStateManager.getWorkspaceStateKey.returns(null)
mockStateManager.getGlobalSettingsKey.returns(null)
mockStateManager.getGlobalStateKey.returns(null)
mockStateManager.getRemoteConfigSettings.returns(null)
mockController = {
stateManager: mockStateManager as any,
}
})
afterEach(() => {
sinon.restore()
})
describe("Base Slash Commands", () => {
it("should return all base slash commands", async () => {
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
// Should have at least all base commands
response.commands.length.should.be.greaterThanOrEqual(BASE_SLASH_COMMANDS.length)
// Verify each base command is present
for (const baseCmd of BASE_SLASH_COMMANDS) {
const found = response.commands.find((cmd) => cmd.name === baseCmd.name)
found!.should.not.be.undefined()
found!.description.should.equal(baseCmd.description)
found!.section.should.equal("default")
found!.cliCompatible.should.equal(baseCmd.cliCompatible ?? false)
}
})
it("should not include the deprecated subagent slash command", async () => {
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
const deprecatedCommand = response.commands.find((cmd) => cmd.name === "subagent")
;(deprecatedCommand === undefined).should.be.true()
})
it("should mark base commands with section 'default'", async () => {
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
const baseCommandNames = BASE_SLASH_COMMANDS.map((cmd) => cmd.name)
for (const cmd of response.commands) {
if (baseCommandNames.includes(cmd.name)) {
cmd.section.should.equal("default")
}
}
})
})
describe("Local Workflow Toggles", () => {
it("should include enabled local workflows", async () => {
mockStateManager.getWorkspaceStateKey.withArgs("workflowToggles").returns({
"/path/to/my-workflow.md": true,
"/path/to/another-workflow.md": true,
})
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
const myWorkflow = response.commands.find((cmd) => cmd.name === "my-workflow.md")
myWorkflow!.should.not.be.undefined()
myWorkflow!.section.should.equal("custom")
myWorkflow!.cliCompatible.should.equal(true)
const anotherWorkflow = response.commands.find((cmd) => cmd.name === "another-workflow.md")
anotherWorkflow!.should.not.be.undefined()
})
it("should exclude disabled local workflows", async () => {
mockStateManager.getWorkspaceStateKey.withArgs("workflowToggles").returns({
"/path/to/enabled-workflow.md": true,
"/path/to/disabled-workflow.md": false,
})
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
const enabled = response.commands.find((cmd) => cmd.name === "enabled-workflow.md")
enabled!.should.not.be.undefined()
const disabled = response.commands.find((cmd) => cmd.name === "disabled-workflow.md")
;(disabled === undefined).should.be.true()
})
it("should extract filename from full path", async () => {
mockStateManager.getWorkspaceStateKey.withArgs("workflowToggles").returns({
"/Users/test/project/.clinerules/workflows/deep-analysis.md": true,
})
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
const workflow = response.commands.find((cmd) => cmd.name === "deep-analysis.md")
workflow!.should.not.be.undefined()
})
it("should handle Windows-style paths", async () => {
mockStateManager.getWorkspaceStateKey.withArgs("workflowToggles").returns({
"C:\\Users\\test\\project\\.clinerules\\workflows\\windows-workflow.md": true,
})
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
const workflow = response.commands.find((cmd) => cmd.name === "windows-workflow.md")
workflow!.should.not.be.undefined()
})
})
describe("Global Workflow Toggles", () => {
it("should include enabled global workflows", async () => {
mockStateManager.getGlobalSettingsKey.withArgs("globalWorkflowToggles").returns({
"/global/path/global-workflow.md": true,
})
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
const workflow = response.commands.find((cmd) => cmd.name === "global-workflow.md")
workflow!.should.not.be.undefined()
workflow!.section.should.equal("custom")
})
it("should exclude disabled global workflows", async () => {
mockStateManager.getGlobalSettingsKey.withArgs("globalWorkflowToggles").returns({
"/global/path/disabled-global.md": false,
})
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
const workflow = response.commands.find((cmd) => cmd.name === "disabled-global.md")
;(workflow === undefined).should.be.true()
})
})
describe("Workflow Deduplication", () => {
it("should prefer local workflows over global workflows with same name", async () => {
// Same filename in both local and global
mockStateManager.getWorkspaceStateKey.withArgs("workflowToggles").returns({
"/local/path/shared-workflow.md": true,
})
mockStateManager.getGlobalSettingsKey.withArgs("globalWorkflowToggles").returns({
"/global/path/shared-workflow.md": true,
})
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
// Should only appear once
const matches = response.commands.filter((cmd) => cmd.name === "shared-workflow.md")
matches.length.should.equal(1)
})
it("should include global workflow if local with same name is disabled", async () => {
mockStateManager.getWorkspaceStateKey.withArgs("workflowToggles").returns({
"/local/path/shared-workflow.md": false, // disabled locally
})
mockStateManager.getGlobalSettingsKey.withArgs("globalWorkflowToggles").returns({
"/global/path/shared-workflow.md": true, // enabled globally
})
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
// Global should appear since local is disabled
const workflow = response.commands.find((cmd) => cmd.name === "shared-workflow.md")
workflow!.should.not.be.undefined()
})
})
describe("Remote Workflows", () => {
it("should include alwaysEnabled remote workflows", async () => {
mockStateManager.getRemoteConfigSettings.returns({
remoteGlobalWorkflows: [{ name: "always-on-workflow", alwaysEnabled: true }],
})
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
const workflow = response.commands.find((cmd) => cmd.name === "always-on-workflow")
workflow!.should.not.be.undefined()
workflow!.section.should.equal("custom")
})
it("should include remote workflows enabled by toggle", async () => {
mockStateManager.getRemoteConfigSettings.returns({
remoteGlobalWorkflows: [{ name: "toggle-workflow", alwaysEnabled: false }],
})
mockStateManager.getGlobalStateKey.withArgs("remoteWorkflowToggles").returns({
"toggle-workflow": true, // not explicitly disabled
})
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
const workflow = response.commands.find((cmd) => cmd.name === "toggle-workflow")
workflow!.should.not.be.undefined()
})
it("should exclude remote workflows explicitly disabled by toggle", async () => {
mockStateManager.getRemoteConfigSettings.returns({
remoteGlobalWorkflows: [{ name: "disabled-remote", alwaysEnabled: false }],
})
mockStateManager.getGlobalStateKey.withArgs("remoteWorkflowToggles").returns({
"disabled-remote": false,
})
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
const workflow = response.commands.find((cmd) => cmd.name === "disabled-remote")
;(workflow === undefined).should.be.true()
})
it("should include remote workflows by default if not explicitly disabled", async () => {
mockStateManager.getRemoteConfigSettings.returns({
remoteGlobalWorkflows: [{ name: "default-enabled", alwaysEnabled: false }],
})
// No toggle entry for this workflow
mockStateManager.getGlobalStateKey.withArgs("remoteWorkflowToggles").returns({})
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
const workflow = response.commands.find((cmd) => cmd.name === "default-enabled")
workflow!.should.not.be.undefined()
})
})
describe("Edge Cases", () => {
it("should handle null/undefined state values gracefully", async () => {
mockStateManager.getWorkspaceStateKey.returns(null)
mockStateManager.getGlobalSettingsKey.returns(undefined)
mockStateManager.getGlobalStateKey.returns(null)
mockStateManager.getRemoteConfigSettings.returns(null)
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
// Should still return base commands
response.commands.length.should.be.greaterThanOrEqual(BASE_SLASH_COMMANDS.length)
})
it("should handle empty workflow toggle objects", async () => {
mockStateManager.getWorkspaceStateKey.withArgs("workflowToggles").returns({})
mockStateManager.getGlobalSettingsKey.withArgs("globalWorkflowToggles").returns({})
mockStateManager.getGlobalStateKey.withArgs("remoteWorkflowToggles").returns({})
mockStateManager.getRemoteConfigSettings.returns({
remoteGlobalWorkflows: [],
})
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
// Should only have base commands
response.commands.length.should.equal(BASE_SLASH_COMMANDS.length)
})
it("should handle remote config with no remoteGlobalWorkflows property", async () => {
mockStateManager.getRemoteConfigSettings.returns({})
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
// Should not throw, just return base commands
response.commands.length.should.be.greaterThanOrEqual(BASE_SLASH_COMMANDS.length)
})
})
})
@@ -780,15 +780,6 @@ export const CondenseConversation = quickStory(
"Would you like me to condense the conversation to improve performance?",
"Shows utility action to condense conversation for better performance.",
)
export const ReportBug = quickStory(
"Report Bug",
"report_bug",
JSON.stringify({
steps_to_reproduce: "1. Open Cline\n2. Start a new task\n3. Observe the error",
what_happened: "Cline crashes unexpectedly",
}),
"Shows utility action to report bugs to the GitHub repository.",
)
export const ResumeCompletedTask = quickStory(
"Resume Completed Task type",
"resume_completed_task",
@@ -55,7 +55,6 @@ import { MarkdownRow } from "./MarkdownRow"
import NewTaskPreview from "./NewTaskPreview"
import PlanCompletionOutputRow from "./PlanCompletionOutputRow"
import QuoteButton from "./QuoteButton"
import ReportBugPreview from "./ReportBugPreview"
import { RequestStartRow } from "./RequestStartRow"
import SearchResultsDisplay from "./SearchResultsDisplay"
import SubagentStatusRow from "./SubagentStatusRow"
@@ -1159,16 +1158,6 @@ export const ChatRowContent = memo(
<NewTaskPreview context={message.text || ""} />
</div>
)
case "report_bug":
return (
<div>
<div className={HEADER_CLASSNAMES}>
<FilePlus2Icon className="size-2" />
<span className="text-foreground font-bold">Cline wants to create a Github issue:</span>
</div>
<ReportBugPreview data={message.text || ""} />
</div>
)
case "plan_mode_respond": {
let response: string | undefined
let options: string[] | undefined
@@ -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()
@@ -1400,6 +1413,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
mcpServers={mcpServers}
onMouseDown={handleMenuMouseDown}
onSelect={handleSlashCommandsSelect}
pluginSlashCommands={pluginSlashCommands}
query={slashCommandsQuery}
remoteWorkflows={remoteConfigSettings?.remoteGlobalWorkflows}
remoteWorkflowToggles={remoteWorkflowToggles}
@@ -37,9 +37,6 @@ const FEATURE_TIPS: FeatureTipItem[] = [
{
text: "You can drag and drop images into the chat to share screenshots with Cline.",
},
{
text: "Use /reportbug to quickly file a GitHub issue with diagnostic context included.",
},
{
text: 'You can disable these tips in Settings → Features → "Feature Tips".',
},
@@ -1,84 +0,0 @@
import React from "react"
import MarkdownBlock from "../common/MarkdownBlock"
interface ReportBugPreviewProps {
data: string
}
const ReportBugPreview: React.FC<ReportBugPreviewProps> = ({ data }) => {
// Parse the JSON data from the context string
const bugData = React.useMemo(() => {
try {
return JSON.parse(data || "{}")
} catch (e) {
console.error("Failed to parse bug report data", e)
return {}
}
}, [data])
return (
<div className="bg-badge-background/50 text-badge-foreground rounded-xs p-3">
<h2 className="font-bold mb-3">{bugData.title || "Bug Report"}</h2>
<div className="space-y-3 text-sm">
{bugData.what_happened && (
<div>
<div className="font-semibold">What Happened?</div>
<MarkdownBlock markdown={bugData.what_happened} />
</div>
)}
{bugData.steps_to_reproduce && (
<div>
<div className="font-semibold">Steps to Reproduce</div>
<MarkdownBlock markdown={bugData.steps_to_reproduce} />
</div>
)}
{bugData.api_request_output && (
<div>
<div className="font-semibold">Relevant API Request Output</div>
<MarkdownBlock markdown={bugData.api_request_output} />
</div>
)}
{bugData.provider_and_model && (
<div>
<div className="font-semibold">Provider/Model</div>
<MarkdownBlock markdown={bugData.provider_and_model} />
</div>
)}
{bugData.operating_system && (
<div>
<div className="font-semibold">Operating System</div>
<MarkdownBlock markdown={bugData.operating_system} />
</div>
)}
{bugData.system_info && (
<div>
<div className="font-semibold">System Info</div>
<MarkdownBlock markdown={bugData.system_info} />
</div>
)}
{bugData.cline_version && (
<div>
<div className="font-semibold">Cline Version</div>
<MarkdownBlock markdown={bugData.cline_version} />
</div>
)}
{bugData.additional_context && (
<div>
<div className="font-semibold">Additional Context</div>
<MarkdownBlock markdown={bugData.additional_context} />
</div>
)}
</div>
</div>
)
}
export default ReportBugPreview
@@ -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")
@@ -17,7 +17,6 @@ vi.mock("@/services/grpc-client", () => ({
},
SlashServiceClient: {
condense: (req: unknown) => condense(req),
reportBug: vi.fn().mockResolvedValue(undefined),
},
UiServiceClient: {
trackIntent: (req: unknown) => trackIntent(req),
@@ -179,8 +179,7 @@ export function useMessageHandlers(messages: ClineMessage[], chatState: ChatStat
case "mistake_limit_reached":
case "api_req_failed":
case "new_task":
case "condense":
case "report_bug": {
case "condense": {
// Most askResponse sends need a temporary webview-only user bubble because the
// extension will not echo the user's message until later. Active follow-up
// questions are the exception: they are backed by the SDK's pending ask_question
@@ -411,11 +410,6 @@ export function useMessageHandlers(messages: ClineMessage[], chatState: ChatStat
console.error(err),
)
break
case "report_bug":
await SlashServiceClient.reportBug(StringRequest.create({ value: lastMessage?.text })).catch((err) =>
console.error(err),
)
break
}
break
}
@@ -112,7 +112,6 @@ describe("getButtonConfig", () => {
{ ask: "resume_completed_task", expectedConfig: "resume_completed_task" },
{ ask: "new_task", expectedConfig: "new_task" },
{ ask: "condense", expectedConfig: "condense" },
{ ask: "report_bug", expectedConfig: "report_bug" },
]
stateConfigs.forEach(({ ask, expectedConfig }) => {
@@ -10,7 +10,7 @@ export type ButtonActionType =
| "proceed" // Send messageResponse or yesButtonClicked
| "new_task" // Start a new task
| "cancel" // Cancel streaming
| "utility" // Execute utility function (condense, report_bug)
| "utility" // Execute utility function (condense)
| "retry" // Retry the last action
/**
@@ -169,15 +169,6 @@ export const BUTTON_CONFIGS: Record<string, ButtonConfig> = {
primaryAction: "utility",
secondaryAction: undefined,
},
report_bug: {
sendingDisabled: false,
enableButtons: true,
primaryText: "Report GitHub issue",
secondaryText: undefined,
primaryAction: "utility",
secondaryAction: undefined,
},
// Streaming/partial states - disable interaction during streaming
partial: {
sendingDisabled: true,
@@ -287,8 +278,6 @@ export function getButtonConfig(message: ClineMessage | undefined, _mode: Mode =
// Utility
case "condense":
return BUTTON_CONFIGS.condense
case "report_bug":
return BUTTON_CONFIGS.report_bug
default:
return BUTTON_CONFIGS.tool_approve
@@ -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())
-19
View File
@@ -19,8 +19,6 @@ Type `/` in the chat input to see available slash commands:
| `/newtask` | Start fresh task with distilled context from current conversation |
| `/smol` | Compress conversation history while preserving essential context |
| `/newrule` | Create a rule file to teach Cline your preferences |
| `/deep-planning` | Investigate codebase, plan thoroughly, then create implementation task |
| `/reportbug` | Report a bug with diagnostic info |
### /newtask
@@ -40,23 +38,6 @@ Use `/smol` when you're deep into a debugging session or brainstorming and need
Use `/newrule` when you find yourself repeating the same instructions across tasks. For more about rules, see [Cline Rules](/customization/cline-rules).
### /deep-planning
Transform Cline into a meticulous architect who investigates your codebase, asks clarifying questions, and creates a comprehensive implementation plan before writing any code. Deep planning follows a four-step process:
1. **Silent Investigation** - Cline explores your codebase structure and patterns
2. **Discussion** - Targeted questions about requirements and approach
3. **Plan Creation** - Generates `implementation_plan.md` with detailed specifications
4. **Task Creation** - Creates a new task with trackable implementation steps
Use `/deep-planning` for features touching multiple parts of your codebase, architectural changes, or complex integrations.
### /reportbug
`/reportbug` collects diagnostic information and helps you report issues with Cline. It gathers relevant context like your configuration, recent errors, and system details to make bug reports more useful for the development team.
Use `/reportbug` when you encounter unexpected behavior, crashes, or bugs you want to report.
## Skills via Slash Commands
In addition to built-in commands, you can trigger enabled skills directly from chat using slash commands.
+1 -1
View File
@@ -258,5 +258,5 @@ These same patterns work for any project, from simple scripts to full applicatio
## Need Help?
- **Start a fresh conversation**: Type `/new` in the chat input to begin a new task
- **Report issues**: Use `/reportbug` to help us improve
- **Report issues**: Open an issue at [github.com/cline/cline/issues](https://github.com/cline/cline/issues)
- **Get support**: Join our [Discord community](https://discord.gg/cline)