mirror of
https://github.com/cline/cline.git
synced 2026-09-06 12:28:08 +08:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f463197a2b | |||
| e62fbf6b0c | |||
| 64254fc97a | |||
| c312c4aef6 | |||
| fab49e810b | |||
| 2a20523e16 | |||
| 06585821d1 | |||
| 9e802b11da | |||
| afb77c5a8d | |||
| 0a4811222f | |||
| b4ce378e4b | |||
| 2f60a898af |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix: do not retry request automatically on auth failure.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add support for sonar and sonar-pro models to SAP AI Core Provider
|
||||
@@ -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.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Remove native tool calling feature flag
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix task timeline display height.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Ensure tool arguments are streamed during file operations when native tool calling is enabled.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
feat(context): add context window error detection for vercel ai gateway
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Added thinking level setting for Gemini 3.0 Pro
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Feat: add thought signature support for Gemini SDK
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Adding Grok 4.1 and Grok code in Cline.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Feat: Enable native tool calling for Baseten and Kimi K2 models
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fixed SAP AI Core models running in deployment mode
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Cerebras models updated as of 2025-11-23
|
||||
@@ -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"
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Replaces generic robot icon with Cline logo across VS Code UI
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add Kimi K2 Thinking to Baseten Provider
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Remind models of new_task tool parameters when deep_planning is invoked
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
McpHub, Ollama and link-preview support HTTP proxies.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Skip MCP tool with invalid name (e.g. name too long) when native tool calling is enabled.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix Anthropic provider missing signature param when thinking is enabled.
|
||||
@@ -1,5 +1,18 @@
|
||||
# Changelog
|
||||
|
||||
## [3.38.3]
|
||||
|
||||
- Task export feature now opens the task directory, allowing easy access to the full task files
|
||||
- Added Grok 4.1 and Grok Code to XAI provider
|
||||
- Enabled native tool calling for Baseten and Kimi K2 models
|
||||
- Added thinking level to Gemini 3.0 Pro preview
|
||||
- Expanded Hooks functionality
|
||||
- Removed Task Timeline from Task Header
|
||||
- Bug fix for slash commands
|
||||
- Bug fixes for Vertex provider
|
||||
- Bug fixes for thinking/reasoning issues across multiple providers when using native tool calling
|
||||
- Bug fixes for terminal usage on Windows devices
|
||||
|
||||
## [3.38.2]
|
||||
|
||||
- Add Claude Opus 4.5
|
||||
|
||||
@@ -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":
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.38.2",
|
||||
"version": "3.38.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.38.2",
|
||||
"version": "3.38.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "claude-dev",
|
||||
"displayName": "Cline",
|
||||
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
|
||||
"version": "3.38.2",
|
||||
"version": "3.38.3",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
|
||||
@@ -225,7 +225,7 @@ message ToggleWorkflowRequest {
|
||||
message HookInfo {
|
||||
string name = 1;
|
||||
bool enabled = 2;
|
||||
string absolutePath = 3;
|
||||
string absolute_path = 3;
|
||||
}
|
||||
|
||||
message WorkspaceHooks {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 +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
|
||||
|
||||
@@ -53,18 +53,21 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
<XIcon />
|
||||
</Button>
|
||||
<h4 style={h4TitleStyle}>
|
||||
🎉{" "}New in v{minorVersion}
|
||||
🎉{" "}New in v{version}
|
||||
</h4>
|
||||
<ul style={ulStyle}>
|
||||
<li>
|
||||
<strong>MiniMax-M2</strong> is currently free to use in Cline!
|
||||
</li>
|
||||
<li>
|
||||
<strong>Gemini 3 Pro Preview</strong> is now available with SOTA reasoning and coding capabilities.
|
||||
<strong>Gemini 3 Pro Preview</strong> and <strong>Claude Opus 4.5</strong> are available with SOTA reasoning
|
||||
and coding capabilities.
|
||||
</li>
|
||||
<li>
|
||||
<strong>AquaVoice's Avalon</strong> model scores 97.3% accuracy on AISpeak and now powers voice-to-text
|
||||
dictation.
|
||||
<strong>Bug fixes,</strong> expanded features, and more! Check out the{" "}
|
||||
<VSCodeLink href="https://github.com/cline/cline/blob/main/CHANGELOG.md" style={linkStyle}>
|
||||
changelog
|
||||
</VSCodeLink>
|
||||
</li>
|
||||
</ul>
|
||||
<div style={hrStyle} />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user