Compare commits

...

10 Commits

Author SHA1 Message Date
NightTrek f463197a2b feat(cli): Add hooks_enabled support to CLI settings
- Add hooks_enabled field to Settings proto message (field 134)
- Add hooks_enabled parsing to CLI settings parser
- Enables users to toggle hooks via -s hooks_enabled=true/false flag

Fixes missing CLI support for hooks that was available in the VSCode extension
2025-11-29 01:13:14 -08:00
DL Techy e62fbf6b0c Add shell option for cmd.exe to prevent double quote escaping (#7630)
* fix(terminal): Add shell option for cmd.exe to prevent double quote escaping

Added shell: true option specifically for cmd.exe to prevent double quotes
from being over escaped during command execution. This resolves Windows-specific
issues with terminal command handling while maintaining compatibility with
other shells.

* chore: Add changeset for terminal command execution fix
2025-11-28 13:02:47 -08:00
Saoud Rizwan 64254fc97a Fix API request badge causing text to wrap when hidden (#7739)
The cost badge was using opacity:0 to hide itself when there's no cost,
but still rendered "$0.0000" which took up horizontal space. This caused
the "API Request..." label to wrap to a second line unnecessarily.

Now the badge renders empty content when hidden, taking up no width
while still maintaining its height contribution to the row layout.
2025-11-28 08:05:09 -08:00
Luna c312c4aef6 Asksage usage fetch models (#7329)
* Add flagship models

* Add model fetching

* Add usage handling, tool result handling

* Update AskSageProvider.tsx

* Create eight-pants-explode.md

---------

Co-authored-by: alex-mcgraw-askSage <alex.mcgraw@asksage.ai>
2025-11-27 12:35:00 -06:00
celestial-vault fab49e810b Add fixed header to ClineRulesToggleModal (#7729)
- Add flex-shrink-0 to header section containing tabs and description text
- Keep tabs and description visible when content area scrolls
2025-11-27 12:32:17 -06:00
celestial-vault 2a20523e16 View remote rules and workflows in the editor (#7702)
* allow the user to view remote rules and workflows in the editor by creating a temp file

* add await
2025-11-27 11:37:47 -06:00
celestial-vault 06585821d1 conditionally fetch litellm models based on presence of api key and baseUrl (#7713) 2025-11-27 11:37:13 -06:00
Saoud Rizwan 9e802b11da Revert "Add Claude Code GitHub Workflow (#7717)"
This reverts commit afb77c5a8d.
2025-11-27 01:45:05 -08:00
Saoud Rizwan afb77c5a8d Add Claude Code GitHub Workflow (#7717)
* "Claude PR Assistant workflow"

* "Claude Code Review workflow"
2025-11-26 20:01:50 -08:00
Saoud Rizwan 0a4811222f fix: unblock opening a task when using cline account (#7715) 2025-11-26 18:40:51 -08:00
18 changed files with 552 additions and 291 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
This minor change adds new models and image support for rleated models, adds fetching of model info from API, updates tool handling, and adds retrieval usage stats for individual messages and a user's monthly token usage.
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fixed a bug where terminal commands with double quotes are broken when "Terminal Execution Mode" is set to "Background Exec"
+6
View File
@@ -290,6 +290,12 @@ func setSimpleField(settings *cline.Settings, key, value string) error {
return err
}
settings.ActModeAwsBedrockCustomSelected = boolPtr(val)
case "hooks_enabled":
val, err := parseBool(value)
if err != nil {
return err
}
settings.HooksEnabled = boolPtr(val)
// Integer fields
case "request_timeout_ms":
+1
View File
@@ -225,6 +225,7 @@ message Settings {
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 131;
optional string act_mode_aihubmix_model_id = 132;
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 133;
optional bool hooks_enabled = 134;
}
message DictationSettings {
+81 -2
View File
@@ -19,6 +19,16 @@ type AskSageRequest = {
}[]
model: string
dataset: "none"
usage: boolean
}
type AskSageUsage = {
model_tokens: {
completion_tokens: number
prompt_tokens: number
total_tokens: number
}
asksage_tokens: number
}
type AskSageResponse = {
@@ -28,6 +38,18 @@ type AskSageResponse = {
response: string
// Generated response message
message: string
// whether embedding & vector systems are down
embedding_down: boolean
vectors_down: boolean
// references if dataset is not none
references: string
type: string
added_obj: any
tool_calls: any
// usage metrics
usage: AskSageUsage | null
tool_responses: any[]
tool_calls_unified: any[]
}
export class AskSageHandler implements ApiHandler {
@@ -50,7 +72,6 @@ export class AskSageHandler implements ApiHandler {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
try {
const model = this.getModel()
// Transform messages into AskSageRequest format
const formattedMessages = messages.map((msg) => {
const content = Array.isArray(msg.content)
@@ -68,6 +89,7 @@ export class AskSageHandler implements ApiHandler {
message: formattedMessages,
model: model.id,
dataset: "none",
usage: true,
}
// Make request to AskSage API
@@ -91,15 +113,72 @@ export class AskSageHandler implements ApiHandler {
throw new Error("No content in AskSage response")
}
// Return entire response as a single chunk since streaming is not supported
// Yield tool responses if they exist
if (result.tool_responses && result.tool_responses.length > 0) {
for (const toolResponse of result.tool_responses) {
yield {
type: "text",
text: `[Tool Response: ${JSON.stringify(toolResponse)}]\n`,
}
}
}
// Yield the main response text
yield {
type: "text",
text: result.message,
}
// Yield usage information if available
if (result.usage) {
yield {
type: "usage",
inputTokens: result.usage.model_tokens.prompt_tokens,
outputTokens: result.usage.model_tokens.completion_tokens,
cacheReadTokens: 0,
cacheWriteTokens: 0,
totalCost: result.usage.asksage_tokens, // Cost = Consumed AskSage tokens
}
}
} catch (error) {
if (error instanceof Error) {
throw new Error(`AskSage request failed: ${error.message}`)
}
throw error
}
}
async getApiStreamUsage() {
if (!this.apiKey) {
return undefined
}
try {
const response = await fetch(`${this.apiUrl}/count-monthly-tokens`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-access-tokens": this.apiKey,
},
body: JSON.stringify({ app_name: "asksage" }),
})
if (!response.ok) {
console.error("Failed to fetch AskSage usage", await response.text())
return undefined
}
const data = await response.json()
const usedTokens = data.response as number
return {
type: "usage" as const,
inputTokens: usedTokens,
outputTokens: 0,
}
} catch (error) {
console.error("Error fetching AskSage usage:", error)
return undefined
}
}
+50 -2
View File
@@ -1,16 +1,64 @@
import { StateManager } from "@core/storage/StateManager"
import { openFile as openFileIntegration } from "@integrations/misc/open-file"
import { Empty, StringRequest } from "@shared/proto/cline/common"
import { REMOTE_URI_SCHEME } from "@shared/remote-config/constants"
import { writeFile } from "@utils/fs"
import * as os from "os"
import * as path from "path"
import { Controller } from ".."
/**
* Opens a file in the editor
* @param controller The controller instance
* @param request The request message containing the file path in the 'value' field
* @param request The request message containing the file path in the 'value' field.
* Supports special URI format for remote rules/workflows:
* - remote://rule/{ruleName}
* - remote://workflow/{workflowName}
* @returns Empty response
*/
export async function openFile(_controller: Controller, request: StringRequest): Promise<Empty> {
if (request.value) {
openFileIntegration(request.value)
// Check for remote:// prefix for remote rules/workflows
if (request.value.startsWith(REMOTE_URI_SCHEME)) {
await openRemoteFile(request.value)
} else {
await openFileIntegration(request.value)
}
}
return Empty.create()
}
/**
* Opens a remote rule or workflow file by creating a temp file with its contents
* @param uri The remote URI in format: remote://rule/{name} or remote://workflow/{name}
*/
async function openRemoteFile(uri: string): Promise<void> {
// Parse: remote://rule/{name} or remote://workflow/{name}
const match = uri.match(/^remote:\/\/(rule|workflow)\/(.+)$/)
if (!match) {
throw new Error(`Invalid remote file URI: ${uri}`)
}
const [, type, name] = match
const remoteConfig = StateManager.get().getRemoteConfigSettings()
// Look up content based on type
const items = type === "rule" ? remoteConfig.remoteGlobalRules : remoteConfig.remoteGlobalWorkflows
const item = items?.find((r) => r.name === name)
if (!item?.contents) {
throw new Error(`Remote ${type} not found: ${name}`)
}
// Create temp file with read-only header comment
const typeLabel = type === "rule" ? "rule" : "workflow"
const header = `# ⚠️ READ-ONLY: This ${typeLabel} is managed by your organization.\n# Changes made here will not be saved.\n\n`
const content = header + item.contents
// Sanitize the name for use in filename (replace invalid characters)
const sanitizedName = name.replace(/[<>:"/\\|?*]/g, "_")
const tempPath = path.join(os.tmpdir(), `cline-remote-${type}-${sanitizedName}.md`)
await writeFile(tempPath, content)
await openFileIntegration(tempPath)
}
+8 -1
View File
@@ -251,7 +251,14 @@ export class Controller {
historyItem?: HistoryItem,
taskSettings?: Partial<Settings>,
) {
await fetchRemoteConfig(this)
// Fire-and-forget: We intentionally don't await fetchRemoteConfig here.
// Remote config is already fetched in startRemoteConfigTimer() which runs in the constructor,
// so enterprise policies (yoloModeAllowed, allowedMCPServers, etc.) are already applied.
// This call just ensures we have the latest state, but we shouldn't block the UI for it.
// getGlobalSettingsKey() reads from remoteConfigCache on each call, so any updates
// will apply as soon as this fetch completes. The function also calls postStateToWebview()
// when done and catches all errors internally.
fetchRemoteConfig(this)
await this.clearTask() // ensures that an existing task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
+5 -2
View File
@@ -195,8 +195,11 @@ export async function initializeWebview(controller: Controller, _request: EmptyR
}
})
// State update event sent within the refreshLiteLlmModels function
refreshLiteLlmModels()
const liteLlmBaseUrl = controller.stateManager.getGlobalSettingsKey("liteLlmBaseUrl")
const liteLlmApiKey = controller.stateManager.getSecretKey("liteLlmApiKey")
if (liteLlmBaseUrl && liteLlmApiKey) {
await refreshLiteLlmModels()
}
// GUI relies on model info to be up-to-date to provide the most accurate pricing, so we need to fetch the latest details on launch.
// We do this for all users since many users switch between api providers and if they were to switch back to openrouter it would be showing outdated model info if we hadn't retrieved the latest at this point
+40
View File
@@ -2664,6 +2664,46 @@ export const askSageModels = {
"google-gemini-2.5-pro": {
maxTokens: 65536,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"google-claude-45-sonnet": {
maxTokens: 64000,
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"google-claude-4-opus": {
maxTokens: 32000,
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gpt-5": {
maxTokens: 65536,
contextWindow: 2_097_152,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gpt-5-mini": {
maxTokens: 32768,
contextWindow: 1_048_576,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gpt-5-nano": {
maxTokens: 16384,
contextWindow: 262_144,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
+5
View File
@@ -0,0 +1,5 @@
/**
* URI scheme for opening remote rules/workflows in the editor.
* Used to construct URIs like: remote://rule/{name} or remote://workflow/{name}
*/
export const REMOTE_URI_SCHEME = "remote://"
-1
View File
@@ -1 +0,0 @@
export * from "./schema"
@@ -32,8 +32,8 @@ class StandaloneTerminalProcess extends EventEmitter {
const shellArgs = this.getShellArgs(shell, command)
try {
// Spawn the process
this.childProcess = spawn(shell, shellArgs, {
// Create shell options
const shellOptions = {
cwd: cwd,
stdio: ["ignore", "pipe", "pipe"], // Disable STDIN to prevent interactivity
env: {
@@ -45,7 +45,18 @@ class StandaloneTerminalProcess extends EventEmitter {
SYSTEMD_PAGER: "", // Disable systemd pager
MANPAGER: "cat", // Disable man pager
},
})
}
// Enable the shell option for "cmd.exe" to prevent double quotes from being over escaped
if (shell.toLowerCase().includes("cmd")) {
shellOptions.shell = true
// Spawn the process with special handling for "cmd.exe"
this.childProcess = spawn("cmd.exe", shellArgs, shellOptions)
} else {
// Spawn the process
this.childProcess = spawn(shell, shellArgs, shellOptions)
}
// Track process state
let didEmitEmptyLine = false
+1 -1
View File
@@ -1275,7 +1275,7 @@ export const ChatRowContent = memo(
style={{
opacity: cost != null && cost > 0 ? 1 : 0,
}}>
${Number(cost || 0)?.toFixed(4)}
{cost != null && Number(cost || 0) > 0 ? `$${Number(cost || 0).toFixed(4)}` : ""}
</VSCodeBadge>
</div>
<span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
@@ -378,7 +378,7 @@ const ClineRulesToggleModal: React.FC = () => {
{isVisible && (
<div
className="fixed left-[15px] right-[15px] border border-editor-group-border pb-3 px-2 rounded z-1000 overflow-y-auto"
className="fixed left-[15px] right-[15px] border border-editor-group-border rounded z-1000 flex flex-col"
style={{
bottom: `calc(100vh - ${menuPosition}px + 6px)`,
background: CODE_BLOCK_BG_COLOR,
@@ -394,310 +394,322 @@ const ClineRulesToggleModal: React.FC = () => {
}}
/>
{/* Tabs container */}
<div
style={{
display: "flex",
justifyContent: "space-between",
marginBottom: "10px",
}}>
{/* Fixed header section - tabs and description */}
<div className="flex-shrink-0 px-2 pt-0">
{/* Tabs container */}
<div
style={{
display: "flex",
gap: "1px",
borderBottom: "1px solid var(--vscode-panel-border)",
justifyContent: "space-between",
marginBottom: "10px",
}}>
<TabButton isActive={currentView === "rules"} onClick={() => setCurrentView("rules")}>
Rules
</TabButton>
<TabButton isActive={currentView === "workflows"} onClick={() => setCurrentView("workflows")}>
Workflows
</TabButton>
{hooksEnabled?.user && (
<TabButton isActive={currentView === "hooks"} onClick={() => setCurrentView("hooks")}>
Hooks
<div
style={{
display: "flex",
gap: "1px",
borderBottom: "1px solid var(--vscode-panel-border)",
}}>
<TabButton isActive={currentView === "rules"} onClick={() => setCurrentView("rules")}>
Rules
</TabButton>
)}
<TabButton isActive={currentView === "workflows"} onClick={() => setCurrentView("workflows")}>
Workflows
</TabButton>
{hooksEnabled?.user && (
<TabButton isActive={currentView === "hooks"} onClick={() => setCurrentView("hooks")}>
Hooks
</TabButton>
)}
</div>
</div>
</div>
{/* Remote config banner */}
{(currentView === "rules" && hasRemoteRules) || (currentView === "workflows" && hasRemoteWorkflows) ? (
<div className="flex items-center gap-2 px-5 py-3 mb-4 bg-vscode-textBlockQuote-background border-l-[3px] border-vscode-textLink-foreground">
<i className="codicon codicon-lock text-sm" />
<span className="text-base">
{currentView === "rules"
? "Your organization manages some rules"
: "Your organization manages some workflows"}
</span>
</div>
) : null}
{/* Description text */}
<div className="text-xs text-description mb-4">
{currentView === "rules" ? (
<p>
Rules allow you to provide Cline with system-level guidance. Think of them as a persistent way to
include context and preferences for your projects or globally for every conversation.{" "}
<VSCodeLink
className="text-xs"
href="https://docs.cline.bot/features/cline-rules"
style={{ display: "inline", fontSize: "inherit" }}>
Docs
</VSCodeLink>
</p>
) : currentView === "workflows" ? (
<p>
Workflows allow you to define a series of steps to guide Cline through a repetitive set of tasks,
such as deploying a service or submitting a PR. To invoke a workflow, type{" "}
<span className="text-foreground font-bold">/workflow-name</span> in the chat.{" "}
<VSCodeLink
className="text-xs inline"
href="https://docs.cline.bot/features/slash-commands/workflows">
Docs
</VSCodeLink>
</p>
) : (
<p>
Hooks allow you to execute custom scripts at specific points in Cline's execution lifecycle,
enabling automation and integration with external tools.
</p>
)}
</div>
{currentView === "rules" ? (
<>
{/* Remote Rules Section */}
{hasRemoteRules && (
<div className="mb-3">
<div className="text-sm font-normal mb-2">Enterprise Rules</div>
<div className="flex flex-col gap-0">
{remoteGlobalRules.map((rule) => {
const enabled = rule.alwaysEnabled || remoteRulesToggles[rule.name] === true
return (
<RuleRow
alwaysEnabled={rule.alwaysEnabled}
enabled={enabled}
isGlobal={false}
isRemote={true}
key={rule.name}
rulePath={rule.name}
ruleType="cline"
toggleRule={toggleRemoteRule}
/>
)
})}
</div>
</div>
)}
{/* Global Rules Section */}
<div className="mb-3">
<div className="text-sm font-normal mb-2">Global Rules</div>
{/* File-based Global Rules */}
<RulesToggleList
isGlobal={true}
listGap="small"
rules={globalRules}
ruleType={"cline"}
showNewRule={true}
showNoRules={false}
toggleRule={(rulePath, enabled) => toggleRule(true, rulePath, enabled)}
/>
{/* Remote config banner */}
{(currentView === "rules" && hasRemoteRules) || (currentView === "workflows" && hasRemoteWorkflows) ? (
<div className="flex items-center gap-2 px-5 py-3 mb-4 bg-vscode-textBlockQuote-background border-l-[3px] border-vscode-textLink-foreground">
<i className="codicon codicon-lock text-sm" />
<span className="text-base">
{currentView === "rules"
? "Your organization manages some rules"
: "Your organization manages some workflows"}
</span>
</div>
) : null}
{/* Local Rules Section */}
<div style={{ marginBottom: -10 }}>
<div className="text-sm font-normal mb-2">Workspace Rules</div>
<RulesToggleList
isGlobal={false}
listGap="small"
rules={localRules}
ruleType={"cline"}
showNewRule={false}
showNoRules={false}
toggleRule={(rulePath, enabled) => toggleRule(false, rulePath, enabled)}
/>
<RulesToggleList
isGlobal={false}
listGap="small"
rules={cursorRules}
ruleType={"cursor"}
showNewRule={false}
showNoRules={false}
toggleRule={toggleCursorRule}
/>
<RulesToggleList
isGlobal={false}
listGap="small"
rules={windsurfRules}
ruleType={"windsurf"}
showNewRule={false}
showNoRules={false}
toggleRule={toggleWindsurfRule}
/>
<RulesToggleList
isGlobal={false}
listGap="small"
rules={agentsRules}
ruleType={"agents"}
showNewRule={true}
showNoRules={false}
toggleRule={toggleAgentsRule}
/>
</div>
</>
) : currentView === "workflows" ? (
<>
{/* Remote Workflows Section */}
{hasRemoteWorkflows && (
<div className="mb-3">
<div className="text-sm font-normal mb-2">Enterprise Workflows</div>
<div className="flex flex-col gap-0">
{remoteGlobalWorkflows.map((workflow) => {
const enabled =
workflow.alwaysEnabled || remoteWorkflowToggles[workflow.name] === true
return (
<RuleRow
alwaysEnabled={workflow.alwaysEnabled}
enabled={enabled}
isGlobal={false}
isRemote={true}
key={workflow.name}
rulePath={workflow.name}
ruleType="workflow"
toggleRule={toggleRemoteWorkflow}
/>
)
})}
</div>
</div>
)}
{/* Global Workflows Section */}
<div className="mb-3">
<div className="text-sm font-normal mb-2">Global Workflows</div>
{/* File-based Global Workflows */}
<RulesToggleList
isGlobal={true}
listGap="small"
rules={globalWorkflows}
ruleType={"workflow"}
showNewRule={true}
showNoRules={false}
toggleRule={(rulePath, enabled) => toggleWorkflow(true, rulePath, enabled)}
/>
</div>
{/* Local Workflows Section */}
<div style={{ marginBottom: -10 }}>
<div className="text-sm font-normal mb-2">Workspace Workflows</div>
<RulesToggleList
isGlobal={false}
listGap="small"
rules={localWorkflows}
ruleType={"workflow"}
showNewRule={true}
showNoRules={false}
toggleRule={(rulePath, enabled) => toggleWorkflow(false, rulePath, enabled)}
/>
</div>
</>
) : (
<>
<div className="text-xs text-description mb-4">
{/* Description text */}
<div className="text-xs text-description mb-4">
{currentView === "rules" ? (
<p>
Toggle to enable/disable (chmod +x/-x).{" "}
Rules allow you to provide Cline with system-level guidance. Think of them as a persistent way
to include context and preferences for your projects or globally for every conversation.{" "}
<VSCodeLink
className="text-xs"
href="https://docs.cline.bot/features/hooks"
href="https://docs.cline.bot/features/cline-rules"
style={{ display: "inline", fontSize: "inherit" }}>
Docs
</VSCodeLink>
</p>
</div>
{/* Hooks Tab */}
{/* Windows warning banner */}
{isWindows && (
<div className="flex items-center gap-2 px-5 py-3 mb-4 bg-vscode-inputValidation-warningBackground border-l-[3px] border-vscode-inputValidation-warningBorder">
<i className="codicon codicon-warning text-sm" />
<span className="text-base">
Hook toggling is not supported on Windows. Hooks can be created, edited, and deleted, but
cannot be enabled/disabled and will not execute.
</span>
</div>
) : currentView === "workflows" ? (
<p>
Workflows allow you to define a series of steps to guide Cline through a repetitive set of
tasks, such as deploying a service or submitting a PR. To invoke a workflow, type{" "}
<span className="text-foreground font-bold">/workflow-name</span> in the chat.{" "}
<VSCodeLink
className="text-xs inline"
href="https://docs.cline.bot/features/slash-commands/workflows">
Docs
</VSCodeLink>
</p>
) : (
<p>
Hooks allow you to execute custom scripts at specific points in Cline's execution lifecycle,
enabling automation and integration with external tools.
</p>
)}
</div>
</div>
{/* Global Hooks */}
<div className="mb-3">
<div className="text-sm font-normal mb-2">Global Hooks</div>
<div className="flex flex-col gap-0">
{globalHooks
.sort((a, b) => a.name.localeCompare(b.name))
.map((hook) => (
<HookRow
absolutePath={hook.absolutePath}
enabled={hook.enabled}
hookName={hook.name}
isGlobal={true}
isWindows={isWindows}
key={hook.name}
onDelete={(hooksToggles) => {
// Use response data directly, no need to refresh
setGlobalHooks(hooksToggles.globalHooks || [])
setWorkspaceHooks(hooksToggles.workspaceHooks || [])
}}
onToggle={(name: string, newEnabled: boolean) =>
toggleHook(true, name, newEnabled)
}
/>
))}
<NewRuleRow existingHooks={globalHooks.map((h) => h.name)} isGlobal={true} ruleType="hook" />
{/* Scrollable content area */}
<div className="flex-1 overflow-y-auto px-2 pb-3" style={{ minHeight: 0 }}>
{currentView === "rules" ? (
<>
{/* Remote Rules Section */}
{hasRemoteRules && (
<div className="mb-3">
<div className="text-sm font-normal mb-2">Enterprise Rules</div>
<div className="flex flex-col gap-0">
{remoteGlobalRules.map((rule) => {
const enabled = rule.alwaysEnabled || remoteRulesToggles[rule.name] === true
return (
<RuleRow
alwaysEnabled={rule.alwaysEnabled}
enabled={enabled}
isGlobal={false}
isRemote={true}
key={rule.name}
rulePath={rule.name}
ruleType="cline"
toggleRule={toggleRemoteRule}
/>
)
})}
</div>
</div>
)}
{/* Global Rules Section */}
<div className="mb-3">
<div className="text-sm font-normal mb-2">Global Rules</div>
{/* File-based Global Rules */}
<RulesToggleList
isGlobal={true}
listGap="small"
rules={globalRules}
ruleType={"cline"}
showNewRule={true}
showNoRules={false}
toggleRule={(rulePath, enabled) => toggleRule(true, rulePath, enabled)}
/>
</div>
</div>
{/* Workspace Hooks - one section per workspace */}
{workspaceHooks.map((workspace, index) => (
<div
key={workspace.workspaceName}
style={{ marginBottom: index === workspaceHooks.length - 1 ? -10 : 12 }}>
<div className="text-sm font-normal mb-2">{workspace.workspaceName}/.clinerules/hooks/</div>
{/* Local Rules Section */}
<div style={{ marginBottom: -10 }}>
<div className="text-sm font-normal mb-2">Workspace Rules</div>
<RulesToggleList
isGlobal={false}
listGap="small"
rules={localRules}
ruleType={"cline"}
showNewRule={false}
showNoRules={false}
toggleRule={(rulePath, enabled) => toggleRule(false, rulePath, enabled)}
/>
<RulesToggleList
isGlobal={false}
listGap="small"
rules={cursorRules}
ruleType={"cursor"}
showNewRule={false}
showNoRules={false}
toggleRule={toggleCursorRule}
/>
<RulesToggleList
isGlobal={false}
listGap="small"
rules={windsurfRules}
ruleType={"windsurf"}
showNewRule={false}
showNoRules={false}
toggleRule={toggleWindsurfRule}
/>
<RulesToggleList
isGlobal={false}
listGap="small"
rules={agentsRules}
ruleType={"agents"}
showNewRule={true}
showNoRules={false}
toggleRule={toggleAgentsRule}
/>
</div>
</>
) : currentView === "workflows" ? (
<>
{/* Remote Workflows Section */}
{hasRemoteWorkflows && (
<div className="mb-3">
<div className="text-sm font-normal mb-2">Enterprise Workflows</div>
<div className="flex flex-col gap-0">
{remoteGlobalWorkflows.map((workflow) => {
const enabled =
workflow.alwaysEnabled || remoteWorkflowToggles[workflow.name] === true
return (
<RuleRow
alwaysEnabled={workflow.alwaysEnabled}
enabled={enabled}
isGlobal={false}
isRemote={true}
key={workflow.name}
rulePath={workflow.name}
ruleType="workflow"
toggleRule={toggleRemoteWorkflow}
/>
)
})}
</div>
</div>
)}
{/* Global Workflows Section */}
<div className="mb-3">
<div className="text-sm font-normal mb-2">Global Workflows</div>
{/* File-based Global Workflows */}
<RulesToggleList
isGlobal={true}
listGap="small"
rules={globalWorkflows}
ruleType={"workflow"}
showNewRule={true}
showNoRules={false}
toggleRule={(rulePath, enabled) => toggleWorkflow(true, rulePath, enabled)}
/>
</div>
{/* Local Workflows Section */}
<div style={{ marginBottom: -10 }}>
<div className="text-sm font-normal mb-2">Workspace Workflows</div>
<RulesToggleList
isGlobal={false}
listGap="small"
rules={localWorkflows}
ruleType={"workflow"}
showNewRule={true}
showNoRules={false}
toggleRule={(rulePath, enabled) => toggleWorkflow(false, rulePath, enabled)}
/>
</div>
</>
) : (
<>
<div className="text-xs text-description mb-4">
<p>
Toggle to enable/disable (chmod +x/-x).{" "}
<VSCodeLink
className="text-xs"
href="https://docs.cline.bot/features/hooks"
style={{ display: "inline", fontSize: "inherit" }}>
Docs
</VSCodeLink>
</p>
</div>
{/* Hooks Tab */}
{/* Windows warning banner */}
{isWindows && (
<div className="flex items-center gap-2 px-5 py-3 mb-4 bg-vscode-inputValidation-warningBackground border-l-[3px] border-vscode-inputValidation-warningBorder">
<i className="codicon codicon-warning text-sm" />
<span className="text-base">
Hook toggling is not supported on Windows. Hooks can be created, edited, and deleted,
but cannot be enabled/disabled and will not execute.
</span>
</div>
)}
{/* Global Hooks */}
<div className="mb-3">
<div className="text-sm font-normal mb-2">Global Hooks</div>
<div className="flex flex-col gap-0">
{workspace.hooks
{globalHooks
.sort((a, b) => a.name.localeCompare(b.name))
.map((hook) => (
<HookRow
absolutePath={hook.absolutePath}
enabled={hook.enabled}
hookName={hook.name}
isGlobal={false}
isGlobal={true}
isWindows={isWindows}
key={hook.absolutePath}
key={hook.name}
onDelete={(hooksToggles) => {
// Use response data directly, no need to refresh
setGlobalHooks(hooksToggles.globalHooks || [])
setWorkspaceHooks(hooksToggles.workspaceHooks || [])
}}
onToggle={(name: string, newEnabled: boolean) =>
toggleHook(false, name, newEnabled, workspace.workspaceName)
toggleHook(true, name, newEnabled)
}
workspaceName={workspace.workspaceName}
/>
))}
<NewRuleRow
existingHooks={workspace.hooks.map((h) => h.name)}
isGlobal={false}
existingHooks={globalHooks.map((h) => h.name)}
isGlobal={true}
ruleType="hook"
workspaceName={workspace.workspaceName}
/>
</div>
</div>
))}
</>
)}
{/* Workspace Hooks - one section per workspace */}
{workspaceHooks.map((workspace, index) => (
<div
key={workspace.workspaceName}
style={{ marginBottom: index === workspaceHooks.length - 1 ? -10 : 12 }}>
<div className="text-sm font-normal mb-2">
{workspace.workspaceName}/.clinerules/hooks/
</div>
<div className="flex flex-col gap-0">
{workspace.hooks
.sort((a, b) => a.name.localeCompare(b.name))
.map((hook) => (
<HookRow
absolutePath={hook.absolutePath}
enabled={hook.enabled}
hookName={hook.name}
isGlobal={false}
isWindows={isWindows}
key={hook.absolutePath}
onDelete={(hooksToggles) => {
// Use response data directly, no need to refresh
setGlobalHooks(hooksToggles.globalHooks || [])
setWorkspaceHooks(hooksToggles.workspaceHooks || [])
}}
onToggle={(name: string, newEnabled: boolean) =>
toggleHook(false, name, newEnabled, workspace.workspaceName)
}
workspaceName={workspace.workspaceName}
/>
))}
<NewRuleRow
existingHooks={workspace.hooks.map((h) => h.name)}
isGlobal={false}
ruleType="hook"
workspaceName={workspace.workspaceName}
/>
</div>
</div>
))}
</>
)}
</div>
</div>
)}
</div>
@@ -1,10 +1,10 @@
import { StringRequest } from "@shared/proto/cline/common"
import { RuleFileRequest } from "@shared/proto/index.cline"
import { InfoIcon, PenIcon, Trash2Icon } from "lucide-react"
import { REMOTE_URI_SCHEME } from "@shared/remote-config/constants"
import { EyeIcon, InfoIcon, PenIcon, Trash2Icon } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Switch } from "@/components/ui/switch"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
import { cn } from "@/lib/utils"
import { FileServiceClient } from "@/services/grpc-client"
const RuleRow: React.FC<{
@@ -81,7 +81,9 @@ const RuleRow: React.FC<{
}
const handleEditClick = () => {
FileServiceClient.openFile(StringRequest.create({ value: rulePath })).catch((err) =>
// For remote rules, use the special remote:// URI format
const filePath = isRemote ? `${REMOTE_URI_SCHEME}${ruleType === "workflow" ? "workflow" : "rule"}/${rulePath}` : rulePath
FileServiceClient.openFile(StringRequest.create({ value: filePath })).catch((err) =>
console.error("Failed to open file:", err),
)
}
@@ -98,10 +100,7 @@ const RuleRow: React.FC<{
return (
<div className="mb-2.5">
<div
className={cn("flex items-center px-2 py-4 rounded bg-text-block-background max-h-4", {
"opacity-60": isDisabled,
})}>
<div className="flex items-center px-2 py-4 rounded bg-text-block-background max-h-4">
<span className="flex-1 overflow-hidden break-all whitespace-normal flex items-center mr-1" title={rulePath}>
{getRuleTypeIcon() && <span className="mr-1.5">{getRuleTypeIcon()}</span>}
<span className="ph-no-capture">{finalDisplayName}</span>
@@ -128,13 +127,12 @@ const RuleRow: React.FC<{
title={isDisabled ? "This rule is required and cannot be disabled" : undefined}
/>
<Button
aria-label="Edit rule file"
disabled={isRemote}
aria-label={isRemote ? "View rule file" : "Edit rule file"}
onClick={handleEditClick}
size="xs"
title="Edit rule file"
title={isRemote ? "View rule file (read-only)" : "Edit rule file"}
variant="icon">
<PenIcon />
{isRemote ? <EyeIcon /> : <PenIcon />}
</Button>
<Button
aria-label="Delete rule file"
@@ -1,5 +1,6 @@
import { askSageDefaultURL, askSageModels } from "@shared/api"
import { askSageDefaultURL, askSageModels, ModelInfo } from "@shared/api"
import { Mode } from "@shared/storage/types"
import { useEffect, useState } from "react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ApiKeyField } from "../common/ApiKeyField"
import { DebouncedTextField } from "../common/DebouncedTextField"
@@ -23,10 +24,49 @@ interface AskSageProviderProps {
export const AskSageProvider = ({ showModelOptions, isPopup, currentMode }: AskSageProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
const [availableModels, setAvailableModels] = useState<Record<string, ModelInfo>>(askSageModels)
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
useEffect(() => {
const fetchModels = async () => {
try {
const apiUrl = apiConfiguration?.asksageApiUrl || askSageDefaultURL
const response = await fetch(`${apiUrl}/get-models`)
if (!response.ok) {
console.error("Failed to fetch AskSage models, falling back to default list.")
setAvailableModels(askSageModels)
return
}
const data = await response.json()
const modelIds = data.response as string[]
if (Array.isArray(modelIds) && modelIds.length > 0) {
const filteredModels = Object.entries(askSageModels)
.filter(([id]) => modelIds.includes(id))
.reduce(
(acc, [id, info]) => {
acc[id] = info
return acc
},
{} as Record<string, ModelInfo>,
)
setAvailableModels(Object.keys(filteredModels).length > 0 ? filteredModels : askSageModels)
} else {
setAvailableModels(askSageModels)
}
} catch (error) {
console.error("Error fetching AskSage models:", error)
setAvailableModels(askSageModels)
}
}
fetchModels()
}, [apiConfiguration?.asksageApiUrl])
return (
<div>
<ApiKeyField
@@ -49,7 +89,7 @@ export const AskSageProvider = ({ showModelOptions, isPopup, currentMode }: AskS
<>
<ModelSelector
label="Model"
models={askSageModels}
models={availableModels}
onChange={(e) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
@@ -27,9 +27,11 @@ export const LiteLlmProvider = ({ showModelOptions, isPopup, currentMode }: Lite
// Get the normalized configuration with model info
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode, liteLlmModels)
// Refresh models on mount
// Refresh models when both base URL and API key are configured
useEffect(() => {
refreshLiteLlmModels()
if (apiConfiguration?.liteLlmBaseUrl && apiConfiguration?.liteLlmApiKey) {
refreshLiteLlmModels()
}
}, [refreshLiteLlmModels, apiConfiguration?.liteLlmApiKey, apiConfiguration?.liteLlmBaseUrl])
// Handle model change
+1 -1
View File
@@ -4,7 +4,7 @@ import * as React from "react"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xs focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 cursor-pointer [&_svg]:size-2",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xs focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 cursor-pointer [&_svg]:size-2",
{
variants: {
variant: {