Compare commits

...

1 Commits

Author SHA1 Message Date
Trevor Hudson 8c6129782f Add optional telemetry 2025-02-24 14:23:00 -08:00
59 changed files with 786 additions and 3643 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fix: make AWS Bedrock authentication predictable
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Add translation to CODE_OF_CONDUCT, CONTRIBUTING and README to Arabic ar-sa.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
A new MCP Marketplace display setting has been added to VSCode settings. (It is defaulted to "true")
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Added timeout configuration for individual MCP servers.
+10
View File
@@ -0,0 +1,10 @@
---
"claude-dev": patch
---
Improve Requesty provider integration
- Adding Cline headers to API requests, to enable targeted optimizations
- Read o3 reasoning effort from Cline config, not model name
- Show token information in task header
- Get total cost from response when available
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
This version of cline adds optional telemetry. If you opt in, cline will use anonymous interaction data to improve our product.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Adjustment to MCP server installation prompt
+1 -1
View File
@@ -7,4 +7,4 @@ tmp
.DS_Store
pnpm-lock.yaml
pnpm-lock.yaml
+1 -9
View File
@@ -1,18 +1,10 @@
# Changelog
## [3.4.6]
- Add support for Claude 3.7 Sonnet
## [3.4.0]
- Introducing MCP Marketplace! You can now discover and install the best MCP servers right from within the extension, with new servers added regularly
- Add mermaid diagram support in Plan mode! You can now see visual representations of mermaid code blocks in chat, and click on them to see an expanded view
- Use more visual checkpoints indicators after editing files & running commands
- Create a checkpoint at the beginning of each task to easily revert to the initial state
- Send current textfield contents as additional feedback when toggling from Plan to Act Mode, or when hitting 'Approve' button
- Add 'Terminal' context mention to reference the active terminal's contents
- Add 'Git Commits' context mention to reference current working changes or specific commits (thanks @mrubens!)
- Send current textfield contents as additional feedback when toggling from Plan to Act Mode, or when hitting 'Approve' button
- Add advanced configuration options for OpenAI Compatible (context window, max output, pricing, etc.)
- Add Alibaba Qwen 2.5 coder models, VL models, and DeepSeek-R1/V3 support
- Improve support for AWS Bedrock Profiles
+2 -1
View File
@@ -9,6 +9,7 @@ Cline Bot Inc. ("Cline," "we," "our," and/or "us") values the privacy of individ
- Your data is only sent to your chosen AI provider (e.g., Anthropic, OpenAI) when you explicitly request assistance
- All processing happens locally on your machine
- API keys are stored securely in VS Code's built-in settings storage
- Telemetry is collected anonymously via PostHog if the user opts in
## Information We Process
@@ -43,7 +44,7 @@ Cline functions solely as a client-side VS Code extension that facilitates commu
- All operations happen on your local machine
- No central servers or data collection
- No telemetry or usage statistics gathered
- No telemetry or usage statistics gathered unless the user opts in
- No account creation required
2. **API Key Security**:
+1 -1
View File
@@ -28,7 +28,7 @@ Welcome to the Cline documentation - your comprehensive guide to using and exten
- **Interested in contributing?** We welcome your input:
- Feel free to submit a pull request
- [Contribution Guidelines](../CONTRIBUTING.md)
- [Contribution Guidelines](CONTRIBUTING.md)
## Additional Resources
@@ -1,153 +0,0 @@
# Cline's Memory Bank
I am Cline, an expert software engineer with a unique characteristic: my memory resets completely between sessions. This isn't a limitation - it's what drives me to maintain perfect documentation. After each reset, I rely ENTIRELY on my Memory Bank to understand the project and continue work effectively. I MUST read ALL memory bank files at the start of EVERY task - this is not optional.
## Memory Bank Structure
The Memory Bank consists of required core files and optional context files, all in Markdown format. Files build upon each other in a clear hierarchy:
```mermaid
flowchart TD
PB[projectbrief.md] --> PC[productContext.md]
PB --> SP[systemPatterns.md]
PB --> TC[techContext.md]
PC --> AC[activeContext.md]
SP --> AC
TC --> AC
AC --> P[progress.md]
```
### Core Files (Required)
1. `projectbrief.md`
- Foundation document that shapes all other files
- Created at project start if it doesn't exist
- Defines core requirements and goals
- Source of truth for project scope
2. `productContext.md`
- Why this project exists
- Problems it solves
- How it should work
- User experience goals
3. `activeContext.md`
- Current work focus
- Recent changes
- Next steps
- Active decisions and considerations
4. `systemPatterns.md`
- System architecture
- Key technical decisions
- Design patterns in use
- Component relationships
5. `techContext.md`
- Technologies used
- Development setup
- Technical constraints
- Dependencies
6. `progress.md`
- What works
- What's left to build
- Current status
- Known issues
### Additional Context
Create additional files/folders within memory-bank/ when they help organize:
- Complex feature documentation
- Integration specifications
- API documentation
- Testing strategies
- Deployment procedures
## Core Workflows
### Plan Mode
```mermaid
flowchart TD
Start[Start] --> ReadFiles[Read Memory Bank]
ReadFiles --> CheckFiles{Files Complete?}
CheckFiles -->|No| Plan[Create Plan]
Plan --> Document[Document in Chat]
CheckFiles -->|Yes| Verify[Verify Context]
Verify --> Strategy[Develop Strategy]
Strategy --> Present[Present Approach]
```
### Act Mode
```mermaid
flowchart TD
Start[Start] --> Context[Check Memory Bank]
Context --> Update[Update Documentation]
Update --> Rules[Update .clinerules if needed]
Rules --> Execute[Execute Task]
Execute --> Document[Document Changes]
```
## Documentation Updates
Memory Bank updates occur when:
1. Discovering new project patterns
2. After implementing significant changes
3. When user requests with **update memory bank** (MUST review ALL files)
4. When context needs clarification
```mermaid
flowchart TD
Start[Update Process]
subgraph Process
P1[Review ALL Files]
P2[Document Current State]
P3[Clarify Next Steps]
P4[Update .clinerules]
P1 --> P2 --> P3 --> P4
end
Start --> Process
```
Note: When triggered by **update memory bank**, I MUST review every memory bank file, even if some don't require updates. Focus particularly on activeContext.md and progress.md as they track current state.
## Project Intelligence (.clinerules)
The .clinerules file is my learning journal for each project. It captures important patterns, preferences, and project intelligence that help me work more effectively. As I work with you and the project, I'll discover and document key insights that aren't obvious from the code alone.
```mermaid
flowchart TD
Start{Discover New Pattern}
subgraph Learn [Learning Process]
D1[Identify Pattern]
D2[Validate with User]
D3[Document in .clinerules]
end
subgraph Apply [Usage]
A1[Read .clinerules]
A2[Apply Learned Patterns]
A3[Improve Future Work]
end
Start --> Learn
Learn --> Apply
```
### What to Capture
- Critical implementation paths
- User preferences and workflow
- Project-specific patterns
- Known challenges
- Evolution of project decisions
- Tool usage patterns
The format is flexible - focus on capturing valuable insights that help me work more effectively with you and the project. Think of .clinerules as a living document that grows smarter as we work together.
REMEMBER: After every memory reset, I begin completely fresh. The Memory Bank is my only link to previous work. It must be maintained with precision and clarity, as my effectiveness depends entirely on its accuracy.
-47
View File
@@ -1,47 +0,0 @@
# ميثاق المساهمين
## تعهدنا
نحن المساهمون والقائمون على هذا المشروع، نتعهد بتوفير بيئة مفتوحة ومرحبة، ونجعل المشاركة في مشروعنا ومجتمعنا تجربة خالية من التحرش للجميع، بغض النظر عن العمر، أو حجم الجسم، أو الإعاقة، أو العرق، أو الخصائص الجنسية، أو الهوية الجنسية والتعبير عنها، أو مستوى الخبرة، أو التعليم، أو الوضع الاجتماعي والاقتصادي، أو الجنسية، أو المظهر الشخصي، أو الدين، أو الهوية الجنسية والتوجه الجنسي.
## معاييرنا
أمثلة على السلوك الذي يساهم في خلق بيئة إيجابية تشمل:
- استخدام لغة ترحيبية وشاملة
- احترام وجهات النظر والخبرات المختلفة
- تقبل النقد البناء برحابة صدر
- التركيز على ما هو الأفضل للمجتمع
- إظهار التعاطف تجاه أعضاء المجتمع الآخرين
أمثلة على السلوك غير المقبول من قبل المشاركين تشمل:
- استخدام لغة أو صور جنسية والاهتمام الجنسي غير المرغوب فيه أو التحرش الجنسي
- التصيد، والتعليقات المهينة/المسيئة، والهجمات الشخصية أو السياسية
- التحرش العلني أو الخاص
- نشر معلومات الآخرين الخاصة، مثل العنوان الفعلي أو الإلكتروني، دون إذن صريح
- أي سلوك آخر يمكن اعتباره غير لائق في بيئة مهنية
## مسؤولياتنا
يتحمل القائمون على المشروع مسؤولية توضيح معايير السلوك المقبول، ومن المتوقع أن يتخذوا إجراءات تصحيحية مناسبة وعادلة استجابة لأي حالات سلوك غير مقبول.
يحق للقائمين على المشروع إزالة أو تعديل أو رفض التعليقات والالتزامات والتعليمات البرمجية وتعديلات wiki والمشكلات والمساهمات الأخرى التي لا تتماشى مع مدونة قواعد السلوك هذه، أو حظر أي مساهم بشكل مؤقت أو دائم بسبب سلوكيات أخرى يعتبرونها غير لائقة أو مهددة أو مسيئة أو ضارة، كما أنهم يتحملون مسؤولية ذلك.
## النطاق
تنطبق مدونة قواعد السلوك هذه داخل مساحات المشروع وفي الأماكن العامة عندما يمثل الفرد المشروع أو مجتمعه. تتضمن أمثلة تمثيل مشروع أو مجتمع استخدام عنوان بريد إلكتروني رسمي للمشروع، أو النشر عبر حساب رسمي على وسائل التواصل الاجتماعي، أو العمل كممثل معين في حدث عبر الإنترنت أو خارجه. يمكن للقائمين على المشروع تحديد وتوضيح تمثيل المشروع بشكل أكبر.
## التنفيذ
يمكن الإبلاغ عن حالات السلوك المسيء أو التحرش أو السلوك غير المقبول عن طريق الاتصال بفريق المشروع على hi@cline.bot. ستتم مراجعة جميع الشكاوى والتحقيق فيها وستؤدي إلى استجابة تعتبر ضرورية ومناسبة للظروف. يلتزم فريق المشروع بالحفاظ على السرية فيما يتعلق بالمبلغ عن الحادث. يمكن نشر مزيد من التفاصيل حول سياسات التنفيذ المحددة بشكل منفصل.
قد يواجه القائمون على المشروع الذين لا يتبعون أو يفرضون مدونة قواعد السلوك بحسن نية تداعيات مؤقتة أو دائمة على النحو الذي يحدده الأعضاء الآخرون في قيادة المشروع.
## الإسناد
تم اقتباس مدونة قواعد السلوك هذه من [تعهد المساهم][homepage]، الإصدار 1.4، متاح على https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
للحصول على إجابات للأسئلة الشائعة حول مدونة قواعد السلوك هذه، راجع https://www.contributor-covenant.org/faq
-93
View File
@@ -1,93 +0,0 @@
# المساهمة في Cline
نحن سعداء لاهتمامك بالمساهمة في Cline. سواء كنت تصلح خطأً أو تضيف ميزة أو تحسن الوثائق لدينا، فإن كل مساهمة تجعل Cline أذكى! للحفاظ على مجتمعنا نابضًا بالحياة وترحيبيًا، يجب على جميع الأعضاء الالتزام بـ [مدونة قواعد السلوك](CODE_OF_CONDUCT.md) لدينا.
## الإبلاغ عن الأخطاء أو المشكلات
تساعد تقارير الأخطاء على جعل Cline أفضل للجميع! قبل إنشاء مشكلة جديدة، يرجى [البحث عن المشكلات الموجودة](https://github.com/cline/cline/issues) لتجنب الازدواجية. عندما تكون جاهزًا للإبلاغ عن خطأ، انتقل إلى [صفحة المشكلات](https://github.com/cline/cline/issues/new/choose) حيث ستجد قالبًا لمساعدتك في ملء المعلومات ذات الصلة.
<blockquote class='warning-note'>
🔐 <b>مهم:</b> إذا اكتشفت ثغرة أمنية، فيرجى استخدام <a href="https://github.com/cline/cline/security/advisories/new">أداة الأمان على Github للإبلاغ عنها بشكل خاص</a>.
</blockquote>
## تحديد ما يجب العمل عليه
تبحث عن مساهمة أولى جيدة؟ تحقق من المشكلات المميزة بـ ["good first issue"](https://github.com/cline/cline/labels/good%20first%20issue) أو ["help wanted"](https://github.com/cline/cline/labels/help%20wanted). تم تحديد هذه المشكلات خصيصًا للمساهمين الجدد والمجالات التي نرحب فيها بالمساعدة!
نرحب أيضًا بالمساهمات في [الوثائق](https://github.com/cline/cline/tree/main/docs) لدينا! سواء كان تصحيح أخطاء إملائية، أو تحسين الأدلة الحالية، أو إنشاء محتوى تعليمي جديد - نود بناء مستودع موارد مدفوع من المجتمع يساعد الجميع على الاستفادة القصوى من Cline. يمكنك البدء بالغوص في `/docs` والبحث عن مجالات تحتاج إلى تحسين.
إذا كنت تخطط للعمل على ميزة أكبر، فيرجى إنشاء [طلب ميزة](https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop) أولاً حتى نتمكن من مناقشة ما إذا كان ذلك يتماشى مع رؤية Cline.
## إعداد التطوير
1. **إضافات VS Code**
- عند فتح المشروع، سيطالبك VS Code بتثبيت الإضافات الموصى بها
- هذه الإضافات مطلوبة للتطوير - يرجى قبول جميع مطالبات التثبيت
- إذا تجاهلت المطالبات، يمكنك تثبيتها يدويًا من لوحة الإضافات
2. **التطوير المحلي**
- قم بتشغيل `npm run install:all` لتثبيت التبعيات
- قم بتشغيل `npm run test` لتشغيل الاختبارات محليًا
- قبل تقديم طلب السحب، قم بتشغيل `npm run format:fix` لتنسيق التعليمات البرمجية الخاصة بك
## كتابة وتقديم التعليمات البرمجية
يمكن لأي شخص المساهمة بالتعليمات البرمجية في Cline، لكننا نطلب منك اتباع هذه الإرشادات لضمان دمج مساهماتك بسلاسة:
1. **احتفظ بطلبات السحب مركزة**
- قيد طلبات السحب بميزة واحدة أو إصلاح خطأ
- قسم التغييرات الأكبر إلى طلبات سحب أصغر ومتصلة
- قسم التغييرات إلى التزامات منطقية يمكن مراجعتها بشكل مستقل
2. **جودة التعليمات البرمجية**
- قم بتشغيل `npm run lint` للتحقق من نمط التعليمات البرمجية
- قم بتشغيل `npm run format` لتنسيق التعليمات البرمجية تلقائيًا
- يجب أن تجتاز جميع طلبات السحب عمليات التحقق المستمر التي تشمل كلاً من التنضيد والتنسيق
- تعامل مع أي تحذيرات أو أخطاء ESLint قبل التقديم
- اتبع أفضل ممارسات TypeScript والحفاظ على سلامة النوع
3. **الاختبار**
- أضف اختبارات للميزات الجديدة
- قم بتشغيل `npm test` للتأكد من اجتياز جميع الاختبارات
- قم بتحديث الاختبارات الحالية إذا كانت تغييراتك تؤثر عليها
- تضمين كل من اختبارات الوحدة واختبارات التكامل حيثما كان ذلك مناسبًا
4. **إدارة الإصدار مع Changesets**
- أنشئ changeset لأي تغييرات واجهة المستخدم باستخدام `npm run changeset`
- اختر زيادة الإصدار المناسبة:
- `major` للتغييرات الكبيرة (1.0.0 → 2.0.0)
- `minor` للميزات الجديدة (1.0.0 → 1.1.0)
- `patch` لإصلاحات الأخطاء (1.0.0 → 1.0.1)
- اكتب رسائل changeset واضحة ووصفية تشرح التأثير
- لا تتطلب التغييرات في الوثائق فقط changesets
5. **إرشادات الالتزام (Commit Guidelines)**
- اكتب رسائل التزام واضحة وواصفة
- استخدم تنسيق الالتزام التقليدي (مثل: "feat:", "fix:", "docs:")
- أشر إلى القضايا ذات الصلة في الالتزامات باستخدام #رقم-القضية
6. **قبل الإرسال**
- قم بإعادة دمج فرعك مع أحدث إصدار من الفرع الرئيسي
- تأكد من أن الفرع الخاص بك يُبنى بنجاح
- تحقق من اجتياز جميع الاختبارات
- راجع التغييرات الخاصة بك للتأكد من عدم وجود تعليمات تصحيح الأخطاء أو سجلات وحدة التحكم
7. **وصف طلب السحب (Pull Request Description)**
- صف بوضوح ما تقوم به التغييرات
- قم بتضمين خطوات لاختبار التغييرات
- أدرج أي تغييرات غير متوافقة
- أضف لقطات شاشة للتغييرات في واجهة المستخدم
## اتفاقية المساهمة
من خلال إرسال طلب سحب، فإنك توافق على أن مساهماتك سيتم ترخيصها بنفس ترخيص المشروع ([Apache 2.0](LICENSE)).
تذكر: المساهمة في Cline لا تقتصر فقط على كتابة الكود - إنها تتعلق بأن تكون جزءًا من مجتمع يُشكل مستقبل التطوير بمساعدة الذكاء الاصطناعي. لنبنِ شيئًا رائعًا معًا! 🚀
-189
View File
@@ -1,189 +0,0 @@
<div align="center"><sub>
العربية | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md" target="_blank">الإسبانية</a> | <a href="https://github.com/cline/cline/blob/main/locales/de/README.md" target="_blank">الألمانية</a> | <a href="https://github.com/cline/cline/blob/main/locales/ja/README.md" target="_blank">اليابانية</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-cn/README.md" target="_blank">الصينية المبسطة</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-tw/README.md" target="_blank">الصينية التقليدية</a> | <a href="https://github.com/cline/cline/blob/main/locales/pt-BR/README.md" target="_blank">البرتغالية</a>
</sub></div>
# Cline \#1 على OpenRouter
<p align="center">
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
</p>
<div align="center">
<table>
<tbody>
<td align="center">
<a href="https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev" target="_blank"><strong>تنزيل من متجر VS</strong></a>
</td>
<td align="center">
<a href="https://discord.gg/cline" target="_blank"><strong>Discord</strong></a>
</td>
<td align="center">
<a href="https://www.reddit.com/r/cline/" target="_blank"><strong>r/cline</strong></a>
</td>
<td align="center">
<a href="https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><strong>طلبات الميزات</strong></a>
</td>
<td align="center">
<a href="https://docs.cline.bot/getting-started/getting-started-new-coders" target="_blank"><strong>البدء</strong></a>
</td>
</tbody>
</table>
</div>
التقى Cline، مساعد الذكاء الاصطناعي الذي يمكنه استخدام **سطر الأوامر** و **محرر النصوص** الخاص بك.
بفضل [قدرات Claude 3.5 Sonnet على التعليمات البرمجية الوكيلة](https://www-cdn.anthropic.com/fed9cc193a14b84131812372d8d5857f8f304c52/Model_Card_Claude_3_Addendum.pdf)، يمكن لـ Cline التعامل مع مهام تطوير البرامج المعقدة خطوة بخطوة. مع الأدوات التي تسمح له بإنشاء وتعديل الملفات، واستكشاف المشاريع الكبيرة، واستخدام المتصفح، وتنفيذ أوامر الطرفية (بعد منحك الإذن)، يمكنه مساعدتك بطرق تتجاوز إكمال الكود أو الدعم الفني. يمكن لـ Cline أيضًا استخدام بروتوكول سياق النموذج (MCP) لإنشاء أدوات جديدة وتوسيع قدراته الخاصة. في حين تعمل النصوص البرمجية الآلية المستقلة تقليديًا في بيئات محاصرة، توفر هذه الإضافة واجهة رسومية لموافقة المستخدم على كل تغيير في الملف وأمر طرفية، مما يوفر طريقة آمنة وسهلة الاستخدام لاستكشاف إمكانات الذكاء الاصطناعي الوكيل.
1. أدخل مهمتك وأضف الصور لتحويل المحاكاة إلى تطبيقات وظيفية أو إصلاح الأخطاء مع لقطات الشاشة.
2. يبدأ Cline بتحليل هيكل الملفات الخاصة بك وشجرة التعريف المصدرية، وإجراء عمليات بحث regex، وقراءة الملفات ذات الصلة للاطلاع على المشاريع الحالية. من خلال إدارة المعلومات التي يتم إضافتها إلى السياق بعناية، يمكن لـ Cline تقديم مساعدة قيمة حتى للمشاريع الكبيرة والمعقدة دون إرهاق نافذة السياق.
3. بمجرد حصول Cline على المعلومات التي يحتاجها، يمكنه:
- إنشاء وتعديل الملفات + مراقبة أخطاء Linter/Compiler أثناء السير، مما يسمح له بإصلاح المشكلات مثل الواردات المفقودة وأخطاء البناء النحوي بمفرده.
- تنفيذ الأوامر مباشرة في الطرفية الخاصة بك ومراقبة إخراجها أثناء العمل، مما يسمح له على سبيل المثال بالاستجابة لمشكلات خادم التطوير بعد تعديل ملف.
- بالنسبة لمهام تطوير الويب، يمكن لـ Cline إطلاق الموقع في متصفح بلا رأس، والنقر، وكتابة النص، والتمرير، والتقاط لقطات الشاشة + سجلات وحدة التحكم، مما يسمح له بإصلاح أخطاء وقت التشغيل والأخطاء البصرية.
4. عند اكتمال المهمة، سيقدم Cline النتيجة لك مع أمر طرفية مثل `open -a "Google Chrome" index.html`، والذي تقوم بتشغيله بنقرة زر.
> [!TIP]
> استخدم اختصار `CMD/CTRL + Shift + P` لفتح لوحة الأوامر واكتب "Cline: Open In New Tab" لفتح الإضافة كعلامة تبويب في محرر النصوص الخاص بك. يتيح لك هذا استخدام Cline جنبًا إلى جنب مع مستكشف الملفات الخاص بك، ورؤية كيف يغير مساحة العمل الخاصة بك بوضوح أكبر.
---
<img align="right" width="340" src="https://github.com/user-attachments/assets/3cf21e04-7ce9-4d22-a7b9-ba2c595e88a4">
### استخدم أي واجهة برمجة تطبيقات ونموذج
يدعم Cline مقدمي واجهات برمجة التطبيقات مثل OpenRouter و Anthropic و OpenAI و Google Gemini و AWS Bedrock و Azure و GCP Vertex. يمكنك أيضًا تكوين أي واجهة برمجة تطبيقات متوافقة مع OpenAI، أو استخدام نموذج محلي من خلال LM Studio/Ollama. إذا كنت تستخدم OpenRouter، فستقوم الإضافة بجلب قائمة النماذج الأحدث الخاصة بهم، مما يسمح لك باستخدام أحدث النماذج بمجرد توفرها.
تتتبع الإضافة أيضًا إجمالي الرموز والاستخدام الخاص بواجهة برمجة التطبيقات لدورة المهمة بأكملها وطلبات فردية، مما يبقيك على اطلاع بالإنفاق في كل خطوة.
<!-- Transparent pixel to create line break after floating image -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="left" width="370" src="https://github.com/user-attachments/assets/81be79a8-1fdb-4028-9129-5fe055e01e76">
### تشغيل الأوامر في الطرفية
بفضل [تحديثات تكامل الشل الجديدة في VSCode v1.93](https://code.visualstudio.com/updates/v1_93#_terminal-shell-integration-api)، يمكن لـ Cline تنفيذ الأوامر مباشرة في الطرفية الخاصة بك وتلقي الإخراج. يسمح له هذا بأداء مجموعة واسعة من المهام، من تثبيت الحزم وتشغيل سكربتات البناء إلى نشر التطبيقات، وإدارة قواعد البيانات، وتنفيذ الاختبارات، وذلك بالتكيف مع بيئة التطوير الخاصة بك وسلسلة الأدوات للقيام بالعمل على النحو الصحيح.
بالنسبة للعمليات الطويلة المدى مثل خوادم التطوير، استخدم زر "المتابعة أثناء التشغيل" للسماح لـ Cline بالاستمرار في المهمة بينما يعمل الأمر في الخلفية. أثناء عمل Cline، سيتم إخباره بأي إخراج طرفية جديد على الطريق، مما يسمح له بالاستجابة للمشكلات التي قد تنشأ، مثل أخطاء وقت الإنشاء عند تعديل الملفات.
<!-- Transparent pixel to create line break after floating image -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="right" width="400" src="https://github.com/user-attachments/assets/c5977833-d9b8-491e-90f9-05f9cd38c588">
### إنشاء وتعديل الملفات
يمكن لـ Cline إنشاء وتعديل الملفات مباشرة في محرر النصوص الخاص بك، وعرض الاختلافات. يمكنك تعديل أو إلغاء تغييرات Cline مباشرة في محرر الاختلافات، أو تقديم ملاحظات في الدردشة حتى تكون راضيًا عن النتيجة. يراقب Cline أيضًا أخطاء Linter/Compiler (الواردات المفقودة، أخطاء البناء النحوي، إلخ) حتى يتمكن من إصلاح المشكلات التي تنشأ أثناء السير بمفرده.
يتم تسجيل جميع التغييرات التي أجراها Cline في جدول زمني للملف، مما يوفر طريقة سهلة لتتبع وإلغاء التعديلات إذا لزم الأمر.
<!-- Transparent pixel to create line break after floating image -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="left" width="370" src="https://github.com/user-attachments/assets/bc2e85ba-dfeb-4fe6-9942-7cfc4703cbe5">
### استخدم المتصفح
مع قدرة [استخدام الكمبيوتر](https://www.anthropic.com/news/3-5-models-and-computer-use) الجديدة لـ Claude 3.5 Sonnet، يمكن لـ Cline إطلاق متصفح، والنقر على العناصر، وكتابة النص، والتمرير، والتقاط لقطات الشاشة وسجلات وحدة التحكم في كل خطوة. يسمح له هذا بالتصحيح التفاعلي، واختبار نهاية إلى نهاية، وحتى الاستخدام العام للويب! يمنحه هذا الاستقلالية لإصلاح الأخطاء البصرية وأخطاء وقت التشغيل دون الحاجة إلى نسخ ولصق سجلات الأخطاء بنفسك.
حاول طلب من Cline "اختبار التطبيق"، وشاهده يشغل أمرًا مثل `npm run dev`، ويطلق خادم التطوير المحلي في متصفح، ويجري سلسلة من الاختبارات للتأكد من أن كل شيء يعمل. [شاهد عرضًا توضيحيًا هنا.](https://x.com/sdrzn/status/1850880547825823989)
<!-- Transparent pixel to create line break after floating image -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="right" width="350" src="https://github.com/user-attachments/assets/ac0efa14-5c1f-4c26-a42d-9d7c56f5fadd">
### "إضافة أداة التي..."
شكراً لـ [بروتوكول سياق النموذج](https://github.com/modelcontextprotocol)، يمكن لـ Cline توسيع قدراته من خلال الأدوات المخصصة. بينما يمكنك استخدام [الخوادم التي أنشأها المجتمع](https://github.com/modelcontextprotocol/servers)، يمكن لـ Cline بدلاً من ذلك إنشاء أدوات وتثبيتها مصممة خصيصًا لتناسب سير عملك. ما عليك سوى أن تطلب من Cline "إضافة أداة"، وسيتولى كل شيء، من إنشاء خادم MCP جديد إلى تثبيته في الامتداد. تصبح هذه الأدوات المخصصة بعد ذلك جزءًا من مجموعة أدوات Cline، جاهزة للاستخدام في المهام المستقبلية.
- **"أضف أداة تجلب تذاكر Jira"**: استرجع تذاكر AC وقم بتشغيل Cline
- **"أضف أداة تدير AWS EC2s"**: تحقق من مقاييس الخادم وقم بتوسيع أو تقليص عدد الحالات
- **"أضف أداة تجلب أحدث حوادث PagerDuty"**: استرجع التفاصيل واطلب من Cline إصلاح الأخطاء
<!-- بكسل شفاف لإنشاء فاصل سطر بعد الصورة العائمة -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="left" width="360" src="https://github.com/user-attachments/assets/7fdf41e6-281a-4b4b-ac19-020b838b6970">
### إضافة السياق
**`@url`**: الصق رابط URL ليقوم الامتداد بجلبه وتحويله إلى Markdown، مفيد عندما تريد تزويد Cline بأحدث الوثائق
**`@problems`**: أضف أخطاء وتحذيرات بيئة العمل ('لوحة المشكلات') ليتمكن Cline من إصلاحها
**`@file`**: يضيف محتويات ملف حتى لا تضطر إلى إهدار طلبات API بالموافقة على قراءة الملف (+ البحث في الملفات)
**`@folder`**: يضيف جميع ملفات المجلد دفعة واحدة لتسريع سير العمل بشكل أكبر
<!-- بكسل شفاف لإنشاء فاصل سطر بعد الصورة العائمة -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="right" width="350" src="https://github.com/user-attachments/assets/140c8606-d3bf-41b9-9a1f-4dbf0d4c90cb">
### نقاط التحقق: المقارنة والاستعادة
أثناء عمل Cline على مهمة، يأخذ الامتداد لقطة من بيئة العمل في كل خطوة. يمكنك استخدام زر "Compare" لرؤية الفرق بين اللقطة وبيئة العمل الحالية، وزر "Restore" للعودة إلى تلك النقطة.
على سبيل المثال، عند العمل مع خادم ويب محلي، يمكنك استخدام "استعادة بيئة العمل فقط" لاختبار إصدارات مختلفة من تطبيقك بسرعة، ثم استخدام "استعادة المهمة وبيئة العمل" عندما تجد الإصدار الذي تريد المتابعة منه. يتيح لك ذلك استكشاف أساليب مختلفة بأمان دون فقدان التقدم.
<!-- بكسل شفاف لإنشاء فاصل سطر بعد الصورة العائمة -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
## المساهمة
للمساهمة في المشروع، ابدأ بـ [دليل المساهمة](CONTRIBUTING.md) لتعلم الأساسيات. يمكنك أيضًا الانضمام إلى [خادم Discord](https://discord.gg/cline) للدردشة مع المساهمين الآخرين في قناة `#contributors`. إذا كنت تبحث عن عمل بدوام كامل، تحقق من الوظائف المتاحة على [صفحة التوظيف](https://cline.bot/join-us)!
<details>
<summary>تعليمات التطوير المحلي</summary>
1. استنساخ المستودع _(يتطلب [git-lfs](https://git-lfs.com/))_:
```bash
git clone https://github.com/cline/cline.git
```
2. افتح المشروع في VSCode:
```bash
code cline
```
3. قم بتثبيت التبعيات اللازمة للامتداد وواجهة الويب:
```bash
npm run install:all
```
4. قم بالتشغيل بالضغط على `F5` (أو من `Run` -> `Start Debugging`) لفتح نافذة VSCode جديدة مع تحميل الامتداد. (قد تحتاج إلى تثبيت [إضافة esbuild problem matchers](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers) إذا واجهت مشكلات في بناء المشروع.)
</details>
<details>
<summary>إنشاء طلب سحب (Pull Request)</summary>
1. قبل إنشاء PR، قم بإنشاء إدخال للتغييرات:
```bash
npm run changeset
```
سيطلب منك تحديد:
- نوع التغيير (رئيسي، ثانوي، إصلاح)
- `رئيسي` → تغييرات غير متوافقة (1.0.0 → 2.0.0)
- `ثانوي` → ميزات جديدة (1.0.0 → 1.1.0)
- `إصلاح` → إصلاحات للأخطاء (1.0.0 → 1.0.1)
- وصف التغييرات التي قمت بها
2. قم بحفظ التغييرات وملف `.changeset` الذي تم إنشاؤه
3. ادفع فرعك وأنشئ PR على GitHub. سيقوم CI بـ:
- تشغيل الاختبارات والفحوصات
- سيقوم Changesetbot بإنشاء تعليق يوضح تأثير الإصدار
- عند الدمج مع الفرع الرئيسي، سيقوم Changesetbot بإنشاء PR لحزم الإصدار
- عند دمج PR لحزم الإصدار، سيتم نشر إصدار جديد
</details>
## الرخصة
[Apache 2.0 © 2025 Cline Bot Inc.](./LICENSE)
+15 -2
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.4.3",
"version": "3.4.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.4.3",
"version": "3.4.0",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
@@ -40,6 +40,7 @@
"os-name": "^6.0.0",
"p-wait-for": "^5.0.2",
"pdf-parse": "^1.1.1",
"posthog-node": "^4.6.0",
"puppeteer-chromium-resolver": "^23.0.0",
"puppeteer-core": "^23.4.0",
"serialize-error": "^11.0.3",
@@ -11606,6 +11607,18 @@
"node": ">= 0.4"
}
},
"node_modules/posthog-node": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-4.6.0.tgz",
"integrity": "sha512-Dq6sBTeBXfK0Ez9OWAl+lhUcSInGUshxtoe0b26gFZtnV4byWTEZtwitKB6Xel9wYx3pNHixKOTSIHC/6a6CIA==",
"license": "MIT",
"dependencies": {
"axios": "^1.7.4"
},
"engines": {
"node": ">=15.0.0"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+6 -5
View File
@@ -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.4.6",
"version": "3.4.0",
"icon": "assets/icons/icon.png",
"galleryBanner": {
"color": "#617A91",
@@ -73,7 +73,7 @@
{
"command": "cline.mcpButtonClicked",
"title": "MCP Servers",
"icon": "$(extensions)"
"icon": "$(server)"
},
{
"command": "cline.historyButtonClicked",
@@ -182,10 +182,10 @@
"default": null,
"description": "Path to Chrome executable for browser use functionality. If not set, the extension will attempt to find or download it automatically."
},
"cline.mcpMarketplace.enabled": {
"cline.enableTelemetry": {
"type": "boolean",
"default": true,
"description": "Controls whether the MCP Marketplace is enabled."
"default": false,
"description": "Enables extension to post anonymous error tracking & usage data to help improve the product."
}
}
}
@@ -268,6 +268,7 @@
"os-name": "^6.0.0",
"p-wait-for": "^5.0.2",
"pdf-parse": "^1.1.1",
"posthog-node": "^4.6.0",
"puppeteer-chromium-resolver": "^23.0.0",
"puppeteer-core": "^23.4.0",
"serialize-error": "^11.0.3",
-2
View File
@@ -24,7 +24,6 @@ export class AnthropicHandler implements ApiHandler {
const modelId = model.id
switch (modelId) {
// 'latest' alias does not support cache_control
case "claude-3-7-sonnet-20250219":
case "claude-3-5-sonnet-20241022":
case "claude-3-5-haiku-20241022":
case "claude-3-opus-20240229":
@@ -89,7 +88,6 @@ export class AnthropicHandler implements ApiHandler {
// https://github.com/anthropics/anthropic-sdk-typescript?tab=readme-ov-file#default-headers
// https://github.com/anthropics/anthropic-sdk-typescript/commit/c920b77fc67bd839bfeb6716ceab9d7c9bbe7393
switch (modelId) {
case "claude-3-7-sonnet-20250219":
case "claude-3-5-sonnet-20241022":
case "claude-3-5-haiku-20241022":
case "claude-3-opus-20240229":
+61 -71
View File
@@ -3,25 +3,79 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { ApiHandler } from "../"
import { ApiHandlerOptions, bedrockDefaultModelId, BedrockModelId, bedrockModels, ModelInfo } from "../../shared/api"
import { ApiStream } from "../transform/stream"
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
import { fromIni } from "@aws-sdk/credential-providers"
// https://docs.anthropic.com/en/api/claude-on-amazon-bedrock
export class AwsBedrockHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: AnthropicBedrock | any
private initializationPromise: Promise<void>
constructor(options: ApiHandlerOptions) {
this.options = options
this.initializationPromise = this.initializeClient()
}
private async initializeClient() {
let clientConfig: any = {
awsRegion: this.options.awsRegion || "us-east-1",
}
try {
if (this.options.awsUseProfile) {
// Use profile-based credentials if enabled
// Use named profile, defaulting to 'default' if not specified
var credentials: any
if (this.options.awsProfile) {
credentials = await fromIni({
profile: this.options.awsProfile,
ignoreCache: true,
})()
} else {
credentials = await fromIni({
ignoreCache: true,
})()
}
clientConfig.awsAccessKey = credentials.accessKeyId
clientConfig.awsSecretKey = credentials.secretAccessKey
clientConfig.awsSessionToken = credentials.sessionToken
} else if (this.options.awsAccessKey && this.options.awsSecretKey) {
// Use direct credentials if provided
clientConfig.awsAccessKey = this.options.awsAccessKey
clientConfig.awsSecretKey = this.options.awsSecretKey
if (this.options.awsSessionToken) {
clientConfig.awsSessionToken = this.options.awsSessionToken
}
}
} catch (error) {
console.error("Failed to initialize Bedrock client:", error)
throw error
} finally {
this.client = new AnthropicBedrock(clientConfig)
}
}
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
// cross region inference requires prefixing the model id with the region
let modelId = await this.getModelId()
let modelId: string
if (this.options.awsUseCrossRegionInference) {
let regionPrefix = (this.options.awsRegion || "").slice(0, 3)
switch (regionPrefix) {
case "us-":
modelId = `us.${this.getModel().id}`
break
case "eu-":
modelId = `eu.${this.getModel().id}`
break
default:
// cross region inference is not supported in this region, falling back to default model
modelId = this.getModel().id
break
}
} else {
modelId = this.getModel().id
}
// create anthropic client, using sessions created or renewed after this handler's
// initialization, and allowing for session renewal if necessary as well
let client = await this.getClient()
const stream = await client.messages.create({
const stream = await this.client.messages.create({
model: modelId,
max_tokens: this.getModel().info.maxTokens || 8192,
temperature: 0,
@@ -88,68 +142,4 @@ export class AwsBedrockHandler implements ApiHandler {
info: bedrockModels[bedrockDefaultModelId],
}
}
private async getClient(): Promise<AnthropicBedrock> {
// Create AWS credentials by executing a an AWS provider chain exactly as the
// Anthropic SDK does it, by wrapping the default chain into a temporary process
// environment.
const providerChain = fromNodeProviderChain()
const credentials = await AwsBedrockHandler.withTempEnv(
() => {
AwsBedrockHandler.setEnv("AWS_REGION", this.options.awsRegion)
AwsBedrockHandler.setEnv("AWS_ACCESS_KEY_ID", this.options.awsAccessKey)
AwsBedrockHandler.setEnv("AWS_SECRET_ACCESS_KEY", this.options.awsSecretKey)
AwsBedrockHandler.setEnv("AWS_SESSION_TOKEN", this.options.awsSessionToken)
AwsBedrockHandler.setEnv("AWS_PROFILE", this.options.awsProfile)
},
() => providerChain(),
)
// Return an AnthropicBedrock client with the resolved/assumed credentials.
//
// When AnthropicBedrock creates its AWS client, the chain will execute very
// fast as the access/secret keys will already be already provided, and have
// a higher precedence than the profiles.
return new AnthropicBedrock({
awsAccessKey: credentials.accessKeyId,
awsSecretKey: credentials.secretAccessKey,
awsSessionToken: credentials.sessionToken,
awsRegion: this.options.awsRegion || "us-east-1",
})
}
private async getModelId(): Promise<string> {
if (this.options.awsUseCrossRegionInference) {
let regionPrefix = (this.options.awsRegion || "").slice(0, 3)
switch (regionPrefix) {
case "us-":
return `us.${this.getModel().id}`
case "eu-":
return `eu.${this.getModel().id}`
break
default:
// cross region inference is not supported in this region, falling back to default model
return this.getModel().id
break
}
}
return this.getModel().id
}
private static async withTempEnv<R>(updateEnv: () => void, fn: () => Promise<R>): Promise<R> {
const previousEnv = { ...process.env }
try {
updateEnv()
return await fn()
} finally {
process.env = previousEnv
}
}
private static async setEnv(key: string, value: string | undefined) {
if (key !== "" && value !== undefined) {
process.env[key] = value
}
}
}
+6 -7
View File
@@ -38,8 +38,6 @@ export class OpenRouterHandler implements ApiHandler {
// prompt caching: https://openrouter.ai/docs/prompt-caching
// this is specifically for claude models (some models may 'support prompt caching' automatically without this)
switch (model.id) {
case "anthropic/claude-3-7-sonnet":
case "anthropic/claude-3-7-sonnet:beta":
case "anthropic/claude-3.5-sonnet":
case "anthropic/claude-3.5-sonnet:beta":
case "anthropic/claude-3.5-sonnet-20240620":
@@ -91,8 +89,6 @@ export class OpenRouterHandler implements ApiHandler {
// (models usually default to max tokens allowed)
let maxTokens: number | undefined
switch (model.id) {
case "anthropic/claude-3-7-sonnet":
case "anthropic/claude-3-7-sonnet:beta":
case "anthropic/claude-3.5-sonnet":
case "anthropic/claude-3.5-sonnet:beta":
case "anthropic/claude-3.5-sonnet-20240620":
@@ -107,11 +103,14 @@ export class OpenRouterHandler implements ApiHandler {
let temperature = 0
let topP: number | undefined = undefined
// Handle models based on deepseek-r1
if (this.getModel().id.startsWith("deepseek/deepseek-r1") || this.getModel().id === "perplexity/sonar-reasoning") {
// Recommended values from DeepSeek
temperature = 0.7
topP = 0.95
// Recommended temperature for DeepSeek reasoning models
temperature = 0.6
// DeepSeek highly recommends using user instead of system role
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
// Some provider support topP and 0.95 is value that Deepseek used in their benchmarks
topP = 0.95
}
// Removes messages in the middle when close to context window limit. Should not be applied to models that support prompt caching since it would continuously break the cache.
+27 -21
View File
@@ -1,24 +1,30 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
type ContentPartText = OpenAI.Chat.ChatCompletionContentPartText
type ContentPartImage = OpenAI.Chat.ChatCompletionContentPartImage
type UserMessage = OpenAI.Chat.ChatCompletionUserMessageParam
type AssistantMessage = OpenAI.Chat.ChatCompletionAssistantMessageParam
type Message = OpenAI.Chat.ChatCompletionMessageParam
type AnthropicMessage = Anthropic.Messages.MessageParam
/**
* Converts Anthropic messages to OpenAI format and merges consecutive messages with the same role.
* Converts Anthropic messages to OpenAI format while merging consecutive messages with the same role.
* This is required for DeepSeek Reasoner which does not support successive messages with the same role.
* DeepSeek highly recommends using 'user' role instead of 'system' role for optimal performance.
*
* @param messages Array of Anthropic messages
* @returns Array of OpenAI messages where consecutive messages with the same role are merged together
* @returns Array of OpenAI messages where consecutive messages with the same role are combined
*/
export function convertToR1Format(messages: Anthropic.Messages.MessageParam[]): OpenAI.Chat.ChatCompletionMessageParam[] {
return messages.reduce<OpenAI.Chat.ChatCompletionMessageParam[]>((merged, message) => {
export function convertToR1Format(messages: AnthropicMessage[]): Message[] {
return messages.reduce<Message[]>((merged, message) => {
const lastMessage = merged[merged.length - 1]
let messageContent: string | (OpenAI.Chat.ChatCompletionContentPartText | OpenAI.Chat.ChatCompletionContentPartImage)[] =
""
let messageContent: string | (ContentPartText | ContentPartImage)[] = ""
let hasImages = false
// Convert content to appropriate format
if (Array.isArray(message.content)) {
const textParts: string[] = []
const imageParts: OpenAI.Chat.ChatCompletionContentPartImage[] = []
const imageParts: ContentPartImage[] = []
message.content.forEach((part) => {
if (part.type === "text") {
@@ -34,7 +40,7 @@ export function convertToR1Format(messages: Anthropic.Messages.MessageParam[]):
})
if (hasImages) {
const parts: (OpenAI.Chat.ChatCompletionContentPartText | OpenAI.Chat.ChatCompletionContentPartImage)[] = []
const parts: (ContentPartText | ContentPartImage)[] = []
if (textParts.length > 0) {
parts.push({ type: "text", text: textParts.join("\n") })
}
@@ -47,11 +53,13 @@ export function convertToR1Format(messages: Anthropic.Messages.MessageParam[]):
messageContent = message.content
}
// If the last message has the same role, merge the content
// If last message has same role, merge the content
if (lastMessage?.role === message.role) {
if (typeof lastMessage.content === "string" && typeof messageContent === "string") {
lastMessage.content += `\n${messageContent}`
} else {
}
// If either has image content, convert both to array format
else {
const lastContent = Array.isArray(lastMessage.content)
? lastMessage.content
: [{ type: "text" as const, text: lastMessage.content || "" }]
@@ -61,32 +69,30 @@ export function convertToR1Format(messages: Anthropic.Messages.MessageParam[]):
: [{ type: "text" as const, text: messageContent }]
if (message.role === "assistant") {
const mergedContent = [
...lastContent,
...newContent,
] as OpenAI.Chat.ChatCompletionAssistantMessageParam["content"]
const mergedContent = [...lastContent, ...newContent] as AssistantMessage["content"]
lastMessage.content = mergedContent
} else {
const mergedContent = [...lastContent, ...newContent] as OpenAI.Chat.ChatCompletionUserMessageParam["content"]
const mergedContent = [...lastContent, ...newContent] as UserMessage["content"]
lastMessage.content = mergedContent
}
}
} else {
// Adds new message with the correct type based on role
// Add as new message with the correct type based on role
if (message.role === "assistant") {
const newMessage: OpenAI.Chat.ChatCompletionAssistantMessageParam = {
const newMessage: AssistantMessage = {
role: "assistant",
content: messageContent as OpenAI.Chat.ChatCompletionAssistantMessageParam["content"],
content: messageContent as AssistantMessage["content"],
}
merged.push(newMessage)
} else {
const newMessage: OpenAI.Chat.ChatCompletionUserMessageParam = {
const newMessage: UserMessage = {
role: "user",
content: messageContent as OpenAI.Chat.ChatCompletionUserMessageParam["content"],
content: messageContent as UserMessage["content"],
}
merged.push(newMessage)
}
}
return merged
}, [])
}
+41 -10
View File
@@ -59,6 +59,7 @@ import { formatResponse } from "./prompts/responses"
import { addUserInstructions, SYSTEM_PROMPT } from "./prompts/system"
import { getNextTruncationRange, getTruncatedMessages } from "./sliding-window"
import { ClineProvider, GlobalFileNames } from "./webview/ClineProvider"
import posthog from "../services/analytics/PostHogClient"
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
@@ -148,6 +149,16 @@ export class Cline {
} else {
throw new Error("Either historyItem or task/images must be provided")
}
// capture start of thread with the state at the beginning
posthog.capture({
event: "cline created",
properties: {
taskId: this.taskId,
isHistory: !!historyItem,
chatMode: this.chatSettings.mode,
hasImages: !!images,
},
})
}
updateBrowserSettings(browserSettings: BrowserSettings) {
@@ -353,17 +364,15 @@ export class Cline {
break
}
if (restoreType !== "task") {
// Set isCheckpointCheckedOut flag on the message
// Find all checkpoint messages before this one
const checkpointMessages = this.clineMessages.filter((m) => m.say === "checkpoint_created")
const currentMessageIndex = checkpointMessages.findIndex((m) => m.ts === messageTs)
// Set isCheckpointCheckedOut flag on the message
// Find all checkpoint messages before this one
const checkpointMessages = this.clineMessages.filter((m) => m.say === "checkpoint_created")
const currentMessageIndex = checkpointMessages.findIndex((m) => m.ts === messageTs)
// Set isCheckpointCheckedOut to false for all checkpoint messages
checkpointMessages.forEach((m, i) => {
m.isCheckpointCheckedOut = i === currentMessageIndex
})
}
// Set isCheckpointCheckedOut to false for all checkpoint messages
checkpointMessages.forEach((m, i) => {
m.isCheckpointCheckedOut = i === currentMessageIndex
})
await this.saveClineMessages()
@@ -3139,6 +3148,7 @@ export class Cline {
try {
for await (const chunk of stream) {
if (!chunk) {
// Sometimes chunk is undefined, no idea that can cause it, but this workaround seems to fix it
continue
}
switch (chunk.type) {
@@ -3265,6 +3275,27 @@ export class Cline {
this.consecutiveMistakeCount++
}
posthog.capture({
event: "message sent",
properties: {
taskId: this.taskId,
chatMode: this.chatSettings.mode,
apiConversationHistoryCount: this.apiConversationHistory.length,
userMessageCount: this.apiConversationHistory.filter((m) => m.role === "user").length,
assistantMessageCount: this.apiConversationHistory.filter((m) => m.role === "assistant").length,
clineMessageCount: this.clineMessages.length,
textMessageCount: this.clineMessages.filter((m) => !!m.text).length,
askMessageCount: this.clineMessages.filter((m) => !!m.ask).length,
sayMessageCount: this.clineMessages.filter((m) => !!m.say).length,
reasoningMessageCount: this.clineMessages.filter((m) => !!m.reasoning).length,
partialMessageCount: this.clineMessages.filter((m) => !!m.partial).length,
consecutiveMistakeCount: this.consecutiveMistakeCount,
consecutiveAutoApprovedRequestsCount: this.consecutiveAutoApprovedRequestsCount,
toolUseCount: this.clineMessages.filter((m) => m.say === "tool").length,
checkpointsCount: this.clineMessages.filter((m) => m.say === "checkpoint_created").length,
},
})
const recDidEndLoop = await this.recursivelyMakeClineRequests(this.userMessageContent)
didEndLoop = recDidEndLoop
} else {
+6 -25
View File
@@ -331,24 +331,7 @@ ${
<access_mcp_resource>
<server_name>weather-server</server_name>
<uri>weather://san-francisco/current</uri>
</access_mcp_resource>
## Example 6: Another example of using an MCP tool (where the server name is a unique identifier such as a URL)
<use_mcp_tool>
<server_name>github.com/modelcontextprotocol/servers/tree/main/src/github</server_name>
<tool_name>create_issue</tool_name>
<arguments>
{
"owner": "octocat",
"repo": "hello-world",
"title": "Found a bug",
"body": "I'm having a problem with this.",
"labels": ["bug", "help wanted"],
"assignees": ["octocat"]
}
</arguments>
</use_mcp_tool>`
</access_mcp_resource>`
: ""
}
@@ -759,16 +742,15 @@ IMPORTANT: Regardless of what else you see in the MCP settings file, you must de
"args": ["/path/to/weather-server/build/index.js"],
"env": {
"OPENWEATHER_API_KEY": "user-provided-api-key"
},
"timeout": 60, // 60 seconds should be set as the default timeout for all MCP servers
}
}
},
}
}
\`\`\`
(Note: the user may also ask you to install the MCP server to the Claude desktop app, in which case you would read then modify \`~/Library/Application\ Support/Claude/claude_desktop_config.json\` on macOS for example. It follows the same format of a top level \`mcpServers\` object.)
6. After you have edited the MCP settings configuration file, the system will automatically run all the servers and expose the available tools and resources in the 'Connected MCP Servers' section. (Note: If you encounter a 'not connected' error when testing a newly installed mcp server, a common cause is an incorrect build path in your MCP settings configuration. Since compiled JavaScript files are commonly output to either 'dist/' or 'build/' directories, double-check that the build path in your MCP settings matches where your files are actually being compiled. E.g. If you assumed 'build' as the folder, check tsconfig.json to see if it's using 'dist' instead.)
6. After you have edited the MCP settings configuration file, the system will automatically run all the servers and expose the available tools and resources in the 'Connected MCP Servers' section.
7. Now that you have access to these new tools and resources, you may suggest ways the user can command you to invoke them - for example, with this new weather tool now available, you can invite the user to ask "what's the weather in San Francisco?"
@@ -883,10 +865,9 @@ In each user message, the environment_details will specify the current mode. The
## What is PLAN MODE?
- While you are usually in ACT MODE, the user may switch to PLAN MODE in order to have a back and forth with you to plan how to best accomplish the task.
- When starting in PLAN MODE, depending on the user's request, you may need to do some information gathering e.g. using read_file or search_files to get more context about the task. You may also ask the user clarifying questions to get a better understanding of the task. You may return mermaid diagrams to visually display your understanding.
- Once you've gained more context about the user's request, you should architect a detailed plan for how you will accomplish the task. Returning mermaid diagrams may be helpful here as well.
- When starting in PLAN MODE, depending on the user's request, you may need to do some information gathering e.g. using read_file or search_files to get more context about the task. You may also ask the user clarifying questions to get a better understanding of the task.
- Once you've gained more context about the user's request, you should architect a detailed plan for how you will accomplish the task.
- Then you might ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and plan the best way to accomplish it.
- If at any point a mermaid diagram would make your plan clearer to help the user quickly see the structure, you are encouraged to include a Mermaid code block in the response. (Note: if you use colors in your mermaid diagrams, be sure to use high contrast colors so the text is readable.)
- Finally once it seems like you've reached a good plan, ask the user to switch you back to ACT MODE to implement the solution.
====
+115 -391
View File
@@ -14,7 +14,6 @@ import { selectImages } from "../../integrations/misc/process-images"
import { getTheme } from "../../integrations/theme/getTheme"
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
import { McpHub } from "../../services/mcp/McpHub"
import { McpDownloadResponse, McpMarketplaceCatalog, McpMarketplaceItem, McpServer } from "../../shared/mcp"
import { FirebaseAuthManager, UserInfo } from "../../services/auth/FirebaseAuthManager"
import { ApiProvider, ModelInfo } from "../../shared/api"
import { findLast } from "../../shared/array"
@@ -29,10 +28,7 @@ import { getUri } from "./getUri"
import { AutoApprovalSettings, DEFAULT_AUTO_APPROVAL_SETTINGS } from "../../shared/AutoApprovalSettings"
import { BrowserSettings, DEFAULT_BROWSER_SETTINGS } from "../../shared/BrowserSettings"
import { ChatSettings, DEFAULT_CHAT_SETTINGS } from "../../shared/ChatSettings"
import { DIFF_VIEW_URI_SCHEME } from "../../integrations/editor/DiffViewProvider"
import { searchCommits } from "../../utils/git"
import { ChatContent } from "../../shared/ChatContent"
import { getShell } from "../../utils/shell"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -93,8 +89,7 @@ type GlobalStateKey =
| "qwenApiLine"
| "requestyModelId"
| "togetherModelId"
| "mcpMarketplaceCatalog"
| "hideTelemetryOptIn"
export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
uiMessages: "ui_messages.json",
@@ -113,7 +108,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
workspaceTracker?: WorkspaceTracker
mcpHub?: McpHub
private authManager: FirebaseAuthManager
private latestAnnouncementId = "feb-19-2025" // update to some unique identifier when we add a new announcement
private latestAnnouncementId = "jan-20-2025" // update to some unique identifier when we add a new announcement
constructor(
readonly context: vscode.ExtensionContext,
@@ -241,7 +236,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.disposables,
)
// Listen for configuration changes
// Listen for when color changes
vscode.workspace.onDidChangeConfiguration(
async (e) => {
if (e && e.affectsConfiguration("workbench.colorTheme")) {
@@ -251,10 +246,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
text: JSON.stringify(await getTheme()),
})
}
if (e && e.affectsConfiguration("cline.mcpMarketplace.enabled")) {
// Update state when marketplace tab setting changes
await this.postStateToWebview()
}
},
null,
this.disposables,
@@ -365,7 +356,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource}; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} https: data:; script-src 'nonce-${nonce}';">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; connect-src https://*.posthog.com; font-src ${webview.cspSource}; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} https: data:; script-src 'nonce-${nonce}' https://*.posthog.com;">
<link rel="stylesheet" type="text/css" href="${stylesUri}">
<link href="${codiconsUri}" rel="stylesheet" />
<title>Cline</title>
@@ -410,17 +401,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// 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
// (see normalizeApiConfiguration > openrouter)
// Prefetch marketplace and OpenRouter models
this.getGlobalState("mcpMarketplaceCatalog").then((mcpMarketplaceCatalog) => {
if (mcpMarketplaceCatalog) {
this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
mcpMarketplaceCatalog: mcpMarketplaceCatalog as McpMarketplaceCatalog,
})
}
})
this.silentlyRefreshMcpMarketplace()
this.refreshOpenRouterModels().then(async (openRouterModels) => {
if (openRouterModels) {
// update model info in state (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there)
@@ -434,7 +414,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
}
})
break
case "newTask":
// Code that should run in response to the hello message command
@@ -556,9 +535,105 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.postStateToWebview()
}
break
case "togglePlanActMode":
case "chatSettings":
if (message.chatSettings) {
await this.togglePlanActModeWithChatSettings(message.chatSettings, message.chatContent)
const didSwitchToActMode = message.chatSettings.mode === "act"
// Get previous model info that we will revert to after saving current mode api info
const {
apiConfiguration,
previousModeApiProvider: newApiProvider,
previousModeModelId: newModelId,
previousModeModelInfo: newModelInfo,
} = await this.getState()
// Save the last model used in this mode
await this.updateGlobalState("previousModeApiProvider", apiConfiguration.apiProvider)
switch (apiConfiguration.apiProvider) {
case "anthropic":
case "bedrock":
case "vertex":
case "gemini":
await this.updateGlobalState("previousModeModelId", apiConfiguration.apiModelId)
break
case "openrouter":
await this.updateGlobalState("previousModeModelId", apiConfiguration.openRouterModelId)
await this.updateGlobalState("previousModeModelInfo", apiConfiguration.openRouterModelInfo)
break
case "vscode-lm":
await this.updateGlobalState("previousModeModelId", apiConfiguration.vsCodeLmModelSelector)
break
case "openai":
await this.updateGlobalState("previousModeModelId", apiConfiguration.openAiModelId)
await this.updateGlobalState("previousModeModelInfo", apiConfiguration.openAiModelInfo)
break
case "ollama":
await this.updateGlobalState("previousModeModelId", apiConfiguration.ollamaModelId)
break
case "lmstudio":
await this.updateGlobalState("previousModeModelId", apiConfiguration.lmStudioModelId)
break
case "litellm":
await this.updateGlobalState("previousModeModelId", apiConfiguration.liteLlmModelId)
break
}
// Restore the model used in previous mode
if (newApiProvider && newModelId) {
await this.updateGlobalState("apiProvider", newApiProvider)
switch (newApiProvider) {
case "anthropic":
case "bedrock":
case "vertex":
case "gemini":
await this.updateGlobalState("apiModelId", newModelId)
break
case "openrouter":
await this.updateGlobalState("openRouterModelId", newModelId)
await this.updateGlobalState("openRouterModelInfo", newModelInfo)
break
case "vscode-lm":
await this.updateGlobalState("vsCodeLmModelSelector", newModelId)
break
case "openai":
await this.updateGlobalState("openAiModelId", newModelId)
await this.updateGlobalState("openAiModelInfo", newModelInfo)
break
case "ollama":
await this.updateGlobalState("ollamaModelId", newModelId)
break
case "lmstudio":
await this.updateGlobalState("lmStudioModelId", newModelId)
break
case "litellm":
await this.updateGlobalState("liteLlmModelId", newModelId)
break
}
if (this.cline) {
const { apiConfiguration: updatedApiConfiguration } = await this.getState()
this.cline.api = buildApiHandler(updatedApiConfiguration)
}
}
await this.updateGlobalState("chatSettings", message.chatSettings)
await this.postStateToWebview()
// console.log("chatSettings", message.chatSettings)
if (this.cline) {
this.cline.updateChatSettings(message.chatSettings)
if (this.cline.isAwaitingPlanResponse && didSwitchToActMode) {
this.cline.didRespondToPlanAskBySwitchingMode = true
// this is necessary for the webview to update accordingly, but Cline instance will not send text back as feedback message
await this.postMessageToWebview({
type: "invoke",
invoke: "sendMessage",
text: message.chatContent?.message || "PLAN_MODE_TOGGLE_RESPONSE",
images: message.chatContent?.images,
})
} else {
this.cancelTask()
}
}
}
break
// case "relaunchChromeDebugMode":
@@ -698,10 +773,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.handleSignOut()
break
}
case "showMcpView": {
await this.postMessageToWebview({ type: "action", action: "mcpButtonClicked" })
break
}
case "openMcpSettings": {
const mcpSettingsFilePath = await this.mcpHub?.getMcpSettingsFilePath()
if (mcpSettingsFilePath) {
@@ -709,69 +780,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
break
}
case "fetchMcpMarketplace": {
await this.fetchMcpMarketplace(message.bool)
break
}
case "downloadMcp": {
if (message.mcpId) {
// 1. Toggle to act mode if we are in plan mode
const { chatSettings } = await this.getStateToPostToWebview()
if (chatSettings.mode === "plan") {
await this.togglePlanActModeWithChatSettings({ mode: "act" })
}
// 2. Enable MCP settings if disabled
// Enable MCP mode if disabled
const mcpConfig = vscode.workspace.getConfiguration("cline.mcp")
if (mcpConfig.get<string>("mode") !== "full") {
await mcpConfig.update("mode", "full", true)
}
// 3. download MCP
await this.downloadMcp(message.mcpId)
}
break
}
case "silentlyRefreshMcpMarketplace": {
await this.silentlyRefreshMcpMarketplace()
break
}
// case "openMcpMarketplaceServerDetails": {
// if (message.text) {
// const response = await fetch(`https://api.cline.bot/v1/mcp/marketplace/item?mcpId=${message.mcpId}`)
// const details: McpDownloadResponse = await response.json()
// if (details.readmeContent) {
// // Disable markdown preview markers
// const config = vscode.workspace.getConfiguration("markdown")
// await config.update("preview.markEditorSelection", false, true)
// // Create URI with base64 encoded markdown content
// const uri = vscode.Uri.parse(
// `${DIFF_VIEW_URI_SCHEME}:${details.name} README?${Buffer.from(details.readmeContent).toString("base64")}`,
// )
// // close existing
// const tabs = vscode.window.tabGroups.all
// .flatMap((tg) => tg.tabs)
// .filter((tab) => tab.label && tab.label.includes("README") && tab.label.includes("Preview"))
// for (const tab of tabs) {
// await vscode.window.tabGroups.close(tab)
// }
// // Show only the preview
// await vscode.commands.executeCommand("markdown.showPreview", uri, {
// sideBySide: true,
// preserveFocus: true,
// })
// }
// }
// this.postMessageToWebview({ type: "relinquishControl" })
// break
// }
case "toggleMcpServer": {
try {
await this.mcpHub?.toggleServerDisabled(message.serverName!, message.disabled!)
@@ -796,16 +804,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
break
}
case "deleteMcpServer": {
if (message.serverName) {
this.mcpHub?.deleteServer(message.serverName)
}
break
}
case "fetchLatestMcpServersFromHub": {
this.mcpHub?.sendLatestMcpServers()
break
}
case "searchCommits": {
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
if (cwd) {
@@ -821,16 +819,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
break
}
case "updateMcpTimeout": {
try {
if (message.serverName && message.timeout) {
await this.mcpHub?.updateServerTimeout(message.serverName, message.timeout)
}
} catch (error) {
console.error(`Failed to update timeout for server ${message.serverName}:`, error)
}
break
}
case "openExtensionSettings": {
const settingsFilter = message.text || ""
await vscode.commands.executeCommand(
@@ -839,6 +827,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
)
break
}
case "toggleTelemetryOptin": {
await vscode.workspace.getConfiguration().update("cline.enableTelemetry", true, true)
await this.updateGlobalState("hideTelemetryOptIn", true)
await this.postStateToWebview()
break
}
// Add more switch case statements here as more webview message commands
// are created within the webview context (i.e. inside media/main.js)
}
@@ -848,106 +842,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
)
}
async togglePlanActModeWithChatSettings(chatSettings: ChatSettings, chatContent?: ChatContent) {
const didSwitchToActMode = chatSettings.mode === "act"
// Get previous model info that we will revert to after saving current mode api info
const {
apiConfiguration,
previousModeApiProvider: newApiProvider,
previousModeModelId: newModelId,
previousModeModelInfo: newModelInfo,
} = await this.getState()
// Save the last model used in this mode
await this.updateGlobalState("previousModeApiProvider", apiConfiguration.apiProvider)
switch (apiConfiguration.apiProvider) {
case "anthropic":
case "bedrock":
case "vertex":
case "gemini":
await this.updateGlobalState("previousModeModelId", apiConfiguration.apiModelId)
break
case "openrouter":
await this.updateGlobalState("previousModeModelId", apiConfiguration.openRouterModelId)
await this.updateGlobalState("previousModeModelInfo", apiConfiguration.openRouterModelInfo)
break
case "vscode-lm":
await this.updateGlobalState("previousModeModelId", apiConfiguration.vsCodeLmModelSelector)
break
case "openai":
await this.updateGlobalState("previousModeModelId", apiConfiguration.openAiModelId)
await this.updateGlobalState("previousModeModelInfo", apiConfiguration.openAiModelInfo)
break
case "ollama":
await this.updateGlobalState("previousModeModelId", apiConfiguration.ollamaModelId)
break
case "lmstudio":
await this.updateGlobalState("previousModeModelId", apiConfiguration.lmStudioModelId)
break
case "litellm":
await this.updateGlobalState("previousModeModelId", apiConfiguration.liteLlmModelId)
break
}
// Restore the model used in previous mode
if (newApiProvider && newModelId) {
await this.updateGlobalState("apiProvider", newApiProvider)
switch (newApiProvider) {
case "anthropic":
case "bedrock":
case "vertex":
case "gemini":
await this.updateGlobalState("apiModelId", newModelId)
break
case "openrouter":
await this.updateGlobalState("openRouterModelId", newModelId)
await this.updateGlobalState("openRouterModelInfo", newModelInfo)
break
case "vscode-lm":
await this.updateGlobalState("vsCodeLmModelSelector", newModelId)
break
case "openai":
await this.updateGlobalState("openAiModelId", newModelId)
await this.updateGlobalState("openAiModelInfo", newModelInfo)
break
case "ollama":
await this.updateGlobalState("ollamaModelId", newModelId)
break
case "lmstudio":
await this.updateGlobalState("lmStudioModelId", newModelId)
break
case "litellm":
await this.updateGlobalState("liteLlmModelId", newModelId)
break
}
if (this.cline) {
const { apiConfiguration: updatedApiConfiguration } = await this.getState()
this.cline.api = buildApiHandler(updatedApiConfiguration)
}
}
await this.updateGlobalState("chatSettings", chatSettings)
await this.postStateToWebview()
// console.log("chatSettings", message.chatSettings)
if (this.cline) {
this.cline.updateChatSettings(chatSettings)
if (this.cline.isAwaitingPlanResponse && didSwitchToActMode) {
this.cline.didRespondToPlanAskBySwitchingMode = true
// this is necessary for the webview to update accordingly, but Cline instance will not send text back as feedback message
await this.postMessageToWebview({
type: "invoke",
invoke: "sendMessage",
text: chatContent?.message || "PLAN_MODE_TOGGLE_RESPONSE",
images: chatContent?.images,
})
} else {
this.cancelTask()
}
}
}
async subscribeEmail(email?: string) {
if (!email) {
return
@@ -1130,176 +1024,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
}
// MCP Marketplace
private async fetchMcpMarketplaceFromApi(silent: boolean = false): Promise<McpMarketplaceCatalog | undefined> {
try {
const response = await axios.get("https://api.cline.bot/v1/mcp/marketplace", {
headers: {
"Content-Type": "application/json",
},
})
if (!response.data) {
throw new Error("Invalid response from MCP marketplace API")
}
const catalog: McpMarketplaceCatalog = {
items: (response.data || []).map((item: any) => ({
...item,
githubStars: item.githubStars ?? 0,
downloadCount: item.downloadCount ?? 0,
tags: item.tags ?? [],
})),
}
// Store in global state
await this.updateGlobalState("mcpMarketplaceCatalog", catalog)
return catalog
} catch (error) {
console.error("Failed to fetch MCP marketplace:", error)
if (!silent) {
const errorMessage = error instanceof Error ? error.message : "Failed to fetch MCP marketplace"
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
error: errorMessage,
})
vscode.window.showErrorMessage(errorMessage)
}
return undefined
}
}
async silentlyRefreshMcpMarketplace() {
try {
const catalog = await this.fetchMcpMarketplaceFromApi(true)
if (catalog) {
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
mcpMarketplaceCatalog: catalog,
})
}
} catch (error) {
console.error("Failed to silently refresh MCP marketplace:", error)
}
}
private async fetchMcpMarketplace(forceRefresh: boolean = false) {
try {
// Check if we have cached data
const cachedCatalog = (await this.getGlobalState("mcpMarketplaceCatalog")) as McpMarketplaceCatalog | undefined
if (!forceRefresh && cachedCatalog?.items) {
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
mcpMarketplaceCatalog: cachedCatalog,
})
return
}
const catalog = await this.fetchMcpMarketplaceFromApi(false)
if (catalog) {
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
mcpMarketplaceCatalog: catalog,
})
}
} catch (error) {
console.error("Failed to handle cached MCP marketplace:", error)
const errorMessage = error instanceof Error ? error.message : "Failed to handle cached MCP marketplace"
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
error: errorMessage,
})
vscode.window.showErrorMessage(errorMessage)
}
}
private async downloadMcp(mcpId: string) {
try {
// First check if we already have this MCP server installed
const servers = this.mcpHub?.getServers() || []
const isInstalled = servers.some((server: McpServer) => server.name === mcpId)
if (isInstalled) {
throw new Error("This MCP server is already installed")
}
// Fetch server details from marketplace
const response = await axios.post<McpDownloadResponse>(
"https://api.cline.bot/v1/mcp/download",
{ mcpId },
{
headers: { "Content-Type": "application/json" },
timeout: 10000,
},
)
if (!response.data) {
throw new Error("Invalid response from MCP marketplace API")
}
console.log("[downloadMcp] Response from download API", { response })
const mcpDetails = response.data
// Validate required fields
if (!mcpDetails.githubUrl) {
throw new Error("Missing GitHub URL in MCP download response")
}
if (!mcpDetails.readmeContent) {
throw new Error("Missing README content in MCP download response")
}
// Send details to webview
await this.postMessageToWebview({
type: "mcpDownloadDetails",
mcpDownloadDetails: mcpDetails,
})
// Create task with context from README and added guidelines for MCP server installation
const task = `Set up the MCP server from ${mcpDetails.githubUrl} while adhering to these MCP server installation rules:
- Use "${mcpDetails.mcpId}" as the server name in cline_mcp_settings.json.
- Use commands aligned with the user's shell and operating system best practices. The user's shell is: ${getShell()}.
- Create the directory for the new MCP server before starting installation.
- Follow the MCP servers README exactly—only deviate if it clearly conflicts with the user's OS, in which case proceed thoughtfully.
- Ensure any steps requiring the use of pip, npm, or any other package manager, are followed as required.
- After running each command, read its output carefully and adjust subsequent steps as needed based on that information.
- Once installed, demonstrate the server's capabilities by using one of its tools.
Here is the project's README to help you get started:\n\n${mcpDetails.readmeContent}\n${mcpDetails.llmsInstallationContent}`
// Initialize task and show chat view
await this.initClineWithTask(task)
await this.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
} catch (error) {
console.error("Failed to download MCP:", error)
let errorMessage = "Failed to download MCP"
if (axios.isAxiosError(error)) {
if (error.code === "ECONNABORTED") {
errorMessage = "Request timed out. Please try again."
} else if (error.response?.status === 404) {
errorMessage = "MCP server not found in marketplace."
} else if (error.response?.status === 500) {
errorMessage = "Internal server error. Please try again later."
} else if (!error.response && error.request) {
errorMessage = "Network error. Please check your internet connection."
}
} else if (error instanceof Error) {
errorMessage = error.message
}
// Show error in both notification and marketplace UI
vscode.window.showErrorMessage(errorMessage)
await this.postMessageToWebview({
type: "mcpDownloadDetails",
error: errorMessage,
})
}
}
// OpenAi
async getOpenAiModels(baseUrl?: string, apiKey?: string) {
@@ -1423,8 +1147,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
}
switch (rawModel.id) {
case "anthropic/claude-3-7-sonnet":
case "anthropic/claude-3-7-sonnet:beta":
case "anthropic/claude-3.5-sonnet":
case "anthropic/claude-3.5-sonnet:beta":
// NOTE: this needs to be synced with api.ts/openrouter default model info
@@ -1603,7 +1325,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
chatSettings,
userInfo,
authToken,
mcpMarketplaceEnabled,
hideTelemetryOptIn,
} = await this.getState()
return {
@@ -1621,8 +1343,10 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
browserSettings,
chatSettings,
isLoggedIn: !!authToken,
advancedSettings: vscode.workspace.getConfiguration("cline"),
vscMachineId: vscode.env.machineId,
userInfo,
mcpMarketplaceEnabled,
hideTelemetryOptIn: hideTelemetryOptIn || false,
}
}
@@ -1729,6 +1453,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
previousModeModelInfo,
qwenApiLine,
liteLlmApiKey,
hideTelemetryOptIn,
] = await Promise.all([
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
this.getGlobalState("apiModelId") as Promise<string | undefined>,
@@ -1780,6 +1505,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
this.getGlobalState("previousModeModelInfo") as Promise<ModelInfo | undefined>,
this.getGlobalState("qwenApiLine") as Promise<string | undefined>,
this.getSecret("liteLlmApiKey") as Promise<string | undefined>,
this.getGlobalState("hideTelemetryOptIn") as Promise<boolean | undefined>,
])
let apiProvider: ApiProvider
@@ -1800,8 +1526,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
.getConfiguration("cline.modelSettings.o3Mini")
.get("reasoningEffort", "medium")
const mcpMarketplaceEnabled = vscode.workspace.getConfiguration("cline").get<boolean>("mcpMarketplace.enabled", true)
return {
apiConfiguration: {
apiProvider,
@@ -1856,7 +1580,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
previousModeApiProvider,
previousModeModelId,
previousModeModelInfo,
mcpMarketplaceEnabled,
hideTelemetryOptIn,
}
}
+19 -1
View File
@@ -7,6 +7,7 @@ import { Logger } from "./services/logging/Logger"
import { createClineAPI } from "./exports"
import "./utils/path" // necessary to have access to String.prototype.toPosix
import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider"
import posthog from "./services/analytics/PostHogClient"
/*
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
@@ -57,6 +58,22 @@ export function activate(context: vscode.ExtensionContext) {
}),
)
context.subscriptions.push(
vscode.workspace.onDidChangeConfiguration(async (e) => {
if (e.affectsConfiguration("cline")) {
Logger.log("Configuration changed")
await sidebarProvider.postStateToWebview()
const config = vscode.workspace.getConfiguration("cline")
// we use optIn and optOut because we want to keep posthog active for feature flags
if (config.get("enableTelemetry")) {
posthog.optIn()
} else {
posthog.optOut()
}
}
}),
)
const openClineInNewTab = async () => {
Logger.log("Opening Cline in new tab")
// (this example uses webviewProvider activation event which is necessary to deserialize cached webview, but since we use retainContextWhenHidden, we don't need to use that event)
@@ -187,6 +204,7 @@ export function activate(context: vscode.ExtensionContext) {
}
// This method is called when your extension is deactivated
export function deactivate() {
export async function deactivate() {
Logger.log("Cline extension deactivated")
await posthog.shutdown()
}
+42
View File
@@ -0,0 +1,42 @@
import { PostHog } from "posthog-node"
import * as vscode from "vscode"
class PostHogClient {
private static instance: PostHogClient
private client: PostHog
private distinctId: string = vscode.env.machineId
private constructor() {
this.client = new PostHog("phc_5WnLHpYyC30Bsb7VSJ6DzcPXZ34JSF08DJLyM7svZ15", {
host: "https://us.i.posthog.com",
enableExceptionAutocapture: true,
})
}
public static getInstance(): PostHogClient {
if (!PostHogClient.instance) {
PostHogClient.instance = new PostHogClient()
}
return PostHogClient.instance
}
public optIn(): void {
this.client.identify({ distinctId: this.distinctId })
this.client.optIn()
}
public optOut(): void {
this.client.optOut()
}
public capture(event: { event: string; properties?: any }): void {
this.client.capture({ distinctId: this.distinctId, event: event.event, properties: event.properties })
}
public async shutdown(): Promise<void> {
await this.client.shutdown()
}
}
// Export a single instance
export default PostHogClient.getInstance()
+27 -90
View File
@@ -16,7 +16,6 @@ import * as vscode from "vscode"
import { z } from "zod"
import { ClineProvider, GlobalFileNames } from "../../core/webview/ClineProvider"
import {
DEFAULT_MCP_TIMEOUT_SECONDS,
McpMode,
McpResource,
McpResourceResponse,
@@ -27,7 +26,7 @@ import {
} from "../../shared/mcp"
import { fileExistsAtPath } from "../../utils/fs"
import { arePathsEqual } from "../../utils/path"
import { secondsToMs } from "../../utils/time"
export type McpConnection = {
server: McpServer
client: Client
@@ -43,7 +42,6 @@ const StdioConfigSchema = z.object({
env: z.record(z.string()).optional(),
autoApprove: AutoApproveSchema.optional(),
disabled: z.boolean().optional(),
timeout: z.number().min(1).max(3600).optional().default(DEFAULT_MCP_TIMEOUT_SECONDS),
})
const McpSettingsSchema = z.object({
@@ -244,6 +242,28 @@ export class McpHub {
}
transport.start = async () => {} // No-op now, .connect() won't fail
// // Set up notification handlers
// client.setNotificationHandler(
// // @ts-ignore-next-line
// { method: "notifications/tools/list_changed" },
// async () => {
// console.log(`Tools changed for server: ${name}`)
// connection.server.tools = await this.fetchTools(name)
// await this.notifyWebviewOfServerChanges()
// },
// )
// client.setNotificationHandler(
// // @ts-ignore-next-line
// { method: "notifications/resources/list_changed" },
// async () => {
// console.log(`Resources changed for server: ${name}`)
// connection.server.resources = await this.fetchResources(name)
// connection.server.resourceTemplates = await this.fetchResourceTemplates(name)
// await this.notifyWebviewOfServerChanges()
// },
// )
// Connect
await client.connect(transport)
connection.server.status = "connected"
@@ -323,6 +343,10 @@ export class McpHub {
const connection = this.connections.find((conn) => conn.server.name === name)
if (connection) {
try {
// connection.client.removeNotificationHandler("notifications/tools/list_changed")
// connection.client.removeNotificationHandler("notifications/resources/list_changed")
// connection.client.removeNotificationHandler("notifications/stderr")
// connection.client.removeNotificationHandler("notifications/stderr")
await connection.transport.close()
await connection.client.close()
} catch (error) {
@@ -448,10 +472,6 @@ export class McpHub {
})
}
async sendLatestMcpServers() {
await this.notifyWebviewOfServerChanges()
}
// Using server
// Public methods for server management
@@ -539,7 +559,6 @@ export class McpHub {
if (connection.server.disabled) {
throw new Error(`Server "${serverName}" is disabled`)
}
return await connection.client.request(
{
method: "resources/read",
@@ -563,17 +582,6 @@ export class McpHub {
throw new Error(`Server "${serverName}" is disabled and cannot be used`)
}
let timeout = secondsToMs(DEFAULT_MCP_TIMEOUT_SECONDS) // sdk expects ms
try {
const config = JSON.parse(connection.server.config)
const parsedConfig = StdioConfigSchema.parse(config)
timeout = secondsToMs(parsedConfig.timeout)
} catch (error) {
console.error(`Failed to parse timeout configuration for server ${serverName}: ${error}`)
// Continue with default timeout
}
return await connection.client.request(
{
method: "tools/call",
@@ -583,9 +591,6 @@ export class McpHub {
},
},
CallToolResultSchema,
{
timeout,
},
)
}
@@ -627,74 +632,6 @@ export class McpHub {
}
}
public async deleteServer(serverName: string) {
try {
const settingsPath = await this.getMcpSettingsFilePath()
const content = await fs.readFile(settingsPath, "utf-8")
const config = JSON.parse(content)
if (!config.mcpServers || typeof config.mcpServers !== "object") {
config.mcpServers = {}
}
if (config.mcpServers[serverName]) {
delete config.mcpServers[serverName]
const updatedConfig = {
mcpServers: config.mcpServers,
}
await fs.writeFile(settingsPath, JSON.stringify(updatedConfig, null, 2))
await this.updateServerConnections(config.mcpServers)
vscode.window.showInformationMessage(`Deleted ${serverName} MCP server`)
} else {
vscode.window.showWarningMessage(`${serverName} not found in MCP configuration`)
}
} catch (error) {
vscode.window.showErrorMessage(
`Failed to delete MCP server: ${error instanceof Error ? error.message : String(error)}`,
)
throw error
}
}
public async updateServerTimeout(serverName: string, timeout: number): Promise<void> {
try {
// Validate timeout against schema
const setConfigResult = StdioConfigSchema.shape.timeout.safeParse(timeout)
if (!setConfigResult.success) {
throw new Error(`Invalid timeout value: ${timeout}. Must be between 1 and 3600 seconds.`)
}
const settingsPath = await this.getMcpSettingsFilePath()
const content = await fs.readFile(settingsPath, "utf-8")
const config = JSON.parse(content)
if (!config.mcpServers?.[serverName]) {
throw new Error(`Server "${serverName}" not found in settings`)
}
// Update the timeout in the config
config.mcpServers[serverName] = {
...config.mcpServers[serverName],
timeout,
}
// Write updated config back to file
await fs.writeFile(settingsPath, JSON.stringify(config, null, 2))
// Update server connections to apply the new timeout
await this.updateServerConnections(config.mcpServers)
vscode.window.showInformationMessage(`Updated timeout to ${timeout} seconds`)
} catch (error) {
console.error("Failed to update server timeout:", error)
if (error instanceof Error) {
console.error("Error details:", error.message, error.stack)
}
vscode.window.showErrorMessage(
`Failed to update server timeout: ${error instanceof Error ? error.message : String(error)}`,
)
throw error
}
}
async dispose(): Promise<void> {
this.removeAllFileWatchers()
for (const connection of this.connections) {
+19
View File
@@ -0,0 +1,19 @@
export interface ClineConfiguration {
vsCodeLmModelSelector?: {
vendor?: string
family?: string
}
mcp: {
mode: "full" | "server-use-only" | "off"
}
enableCheckpoints: boolean
enableTelemetry: boolean
}
export const DEFAULT_ADVANCED_SETTINGS: ClineConfiguration = {
mcp: {
mode: "full",
},
enableCheckpoints: true,
enableTelemetry: false,
}
+6 -7
View File
@@ -1,12 +1,14 @@
// type that represents json data that is sent from extension to webview, called ExtensionMessage and has 'type' enum which can be 'plusButtonClicked' or 'settingsButtonClicked' or 'hello'
import { GitCommit } from "../utils/git"
import { ClineConfiguration } from "./AdvancedSettings"
import { ApiConfiguration, ModelInfo } from "./api"
import { AutoApprovalSettings } from "./AutoApprovalSettings"
import { BrowserSettings } from "./BrowserSettings"
import { ChatSettings } from "./ChatSettings"
import { HistoryItem } from "./HistoryItem"
import { McpServer, McpMarketplaceCatalog, McpMarketplaceItem, McpDownloadResponse } from "./mcp"
import { McpServer } from "./mcp"
import { WorkspaceConfiguration } from "vscode"
// webview will hold state
export interface ExtensionMessage {
@@ -27,8 +29,6 @@ export interface ExtensionMessage {
| "vsCodeLmModels"
| "requestVsCodeLmModels"
| "emailSubscribed"
| "mcpMarketplaceCatalog"
| "mcpDownloadDetails"
| "commitSearchResults"
text?: string
action?:
@@ -50,9 +50,6 @@ export interface ExtensionMessage {
openRouterModels?: Record<string, ModelInfo>
openAiModels?: string[]
mcpServers?: McpServer[]
mcpMarketplaceCatalog?: McpMarketplaceCatalog
error?: string
mcpDownloadDetails?: McpDownloadResponse
commits?: GitCommit[]
}
@@ -75,12 +72,14 @@ export interface ExtensionState {
chatSettings: ChatSettings
isLoggedIn: boolean
platform: Platform
advancedSettings: WorkspaceConfiguration | ClineConfiguration
vscMachineId: string
hideTelemetryOptIn: boolean
userInfo?: {
displayName: string | null
email: string | null
photoURL: string | null
}
mcpMarketplaceEnabled?: boolean
}
export interface ClineMessage {
+2 -10
View File
@@ -29,10 +29,9 @@ export interface WebviewMessage {
| "refreshOpenAiModels"
| "openMcpSettings"
| "restartMcpServer"
| "deleteMcpServer"
| "autoApprovalSettings"
| "browserSettings"
| "togglePlanActMode"
| "chatSettings"
| "checkpointDiff"
| "checkpointRestore"
| "taskCompletionViewChanges"
@@ -44,13 +43,8 @@ export interface WebviewMessage {
| "accountLoginClicked"
| "accountLogoutClicked"
| "subscribeEmail"
| "fetchMcpMarketplace"
| "downloadMcp"
| "silentlyRefreshMcpMarketplace"
| "searchCommits"
| "showMcpView"
| "fetchLatestMcpServersFromHub"
| "updateMcpTimeout"
| "toggleTelemetryOptin"
// | "relaunchChromeDebugMode"
text?: string
disabled?: boolean
@@ -63,8 +57,6 @@ export interface WebviewMessage {
browserSettings?: BrowserSettings
chatSettings?: ChatSettings
chatContent?: ChatContent
mcpId?: string
timeout?: number // For updateMcpTimeout
// For toggleToolAutoApprove
serverName?: string
+1 -32
View File
@@ -80,19 +80,8 @@ export interface ModelInfo {
// Anthropic
// https://docs.anthropic.com/en/docs/about-claude/models // prices updated 2025-01-02
export type AnthropicModelId = keyof typeof anthropicModels
export const anthropicDefaultModelId: AnthropicModelId = "claude-3-7-sonnet-20250219"
export const anthropicDefaultModelId: AnthropicModelId = "claude-3-5-sonnet-20241022"
export const anthropicModels = {
"claude-3-7-sonnet-20250219": {
maxTokens: 8192,
contextWindow: 200_000,
supportsImages: true,
supportsComputerUse: true,
supportsPromptCache: true,
inputPrice: 3.0,
outputPrice: 15.0,
cacheWritesPrice: 3.75,
cacheReadsPrice: 0.3,
},
"claude-3-5-sonnet-20241022": {
maxTokens: 8192,
contextWindow: 200_000,
@@ -141,17 +130,6 @@ export const anthropicModels = {
export type BedrockModelId = keyof typeof bedrockModels
export const bedrockDefaultModelId: BedrockModelId = "anthropic.claude-3-5-sonnet-20241022-v2:0"
export const bedrockModels = {
"anthropic.claude-3-7-sonnet-20250219-v1:0": {
maxTokens: 8192,
contextWindow: 200_000,
supportsImages: true,
supportsComputerUse: true,
supportsPromptCache: true,
inputPrice: 3.0,
outputPrice: 15.0,
cacheWritesPrice: 3.75,
cacheReadsPrice: 0.3,
},
"anthropic.claude-3-5-sonnet-20241022-v2:0": {
maxTokens: 8192,
contextWindow: 200_000,
@@ -225,15 +203,6 @@ export const openRouterDefaultModelInfo: ModelInfo = {
export type VertexModelId = keyof typeof vertexModels
export const vertexDefaultModelId: VertexModelId = "claude-3-5-sonnet-v2@20241022"
export const vertexModels = {
"claude-3-7-sonnet@20250219": {
maxTokens: 8192,
contextWindow: 200_000,
supportsImages: true,
supportsComputerUse: true,
supportsPromptCache: false,
inputPrice: 3.0,
outputPrice: 15.0,
},
"claude-3-5-sonnet-v2@20241022": {
maxTokens: 8192,
contextWindow: 200_000,
-39
View File
@@ -1,5 +1,3 @@
export const DEFAULT_MCP_TIMEOUT_SECONDS = 60 // matches Anthropic's default timeout in their MCP SDK
export type McpMode = "full" | "server-use-only" | "off"
export type McpServer = {
@@ -11,7 +9,6 @@ export type McpServer = {
resources?: McpResource[]
resourceTemplates?: McpResourceTemplate[]
disabled?: boolean
timeout?: number
}
export type McpTool = {
@@ -69,39 +66,3 @@ export type McpToolCallResponse = {
>
isError?: boolean
}
export interface McpMarketplaceItem {
mcpId: string
githubUrl: string
name: string
author: string
description: string
codiconIcon: string
logoUrl: string
category: string
tags: string[]
requiresApiKey: boolean
readmeContent?: string
llmsInstallationContent?: string
isRecommended: boolean
githubStars: number
downloadCount: number
createdAt: string
updatedAt: string
lastGithubSync: string
}
export interface McpMarketplaceCatalog {
items: McpMarketplaceItem[]
}
export interface McpDownloadResponse {
mcpId: string
githubUrl: string
name: string
author: string
description: string
readmeContent: string
llmsInstallationContent: string
requiresApiKey: boolean
}
+3 -3
View File
@@ -29,7 +29,7 @@ describe("Chat Integration Tests", () => {
break;
case 'toggleMode':
vscode.postMessage({
type: 'togglePlanActMode',
type: 'chatSettings',
chatSettings: { mode: 'act' },
chatContent: {
message: "message test",
@@ -84,7 +84,7 @@ describe("Chat Integration Tests", () => {
// Set up state change listener
const stateChangePromise = new Promise<any>((resolve) => {
panel.webview.onDidReceiveMessage((message) => {
if (message.type === "togglePlanActMode") {
if (message.type === "chatSettings") {
resolve(message)
}
})
@@ -102,7 +102,7 @@ describe("Chat Integration Tests", () => {
// Set up state change listener
const stateChangePromise = new Promise<any>((resolve) => {
panel.webview.onDidReceiveMessage((message) => {
if (message.type === "togglePlanActMode") {
if (message.type === "chatSettings") {
resolve(message)
}
})
-3
View File
@@ -1,3 +0,0 @@
export function secondsToMs(seconds: number): number {
return seconds * 1000
}
+59 -1124
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -9,7 +9,7 @@
"fast-deep-equal": "^3.1.3",
"fuse.js": "^7.0.0",
"fzf": "^0.5.2",
"mermaid": "^11.4.1",
"posthog-js": "^1.215.3",
"pretty-bytes": "^6.1.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -60,6 +60,7 @@
"@types/node": "^20.x",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/vscode": "^1.84.0",
"@types/vscode-webview": "^1.57.5",
"jsdom": "^25.0.1",
"vitest": "^2.1.9"
+4 -3
View File
@@ -12,6 +12,9 @@
const rewire = require("rewire")
const defaults = rewire("react-scripts/scripts/build.js")
const config = defaults.__get__("config")
const ModuleScopePlugin = require("react-dev-utils/ModuleScopePlugin")
const path = require("path")
const fs = require("fs")
/* Modifying Webpack Configuration for 'shared' dir
This section uses Rewire to modify Create React App's webpack configuration without ejecting. Rewire allows us to inject and alter the internal build scripts of CRA at runtime. This allows us to maintain a flexible project structure that keeps shared code outside the webview-ui/src directory, while still adhering to CRA's security model that typically restricts imports to within src/.
@@ -21,9 +24,7 @@ Before, we would just import types from shared dir and specifying include (and a
- Imports from the shared directory must use full paths relative to the src directory, without file extensions.
- Example: import { someFunction } from '../../src/shared/utils/helpers'
*/
const ModuleScopePlugin = require("react-dev-utils/ModuleScopePlugin")
const path = require("path")
const fs = require("fs")
// Get all files in the shared directory
const sharedDir = path.resolve(__dirname, "..", "..", "src", "shared")
+14 -1
View File
@@ -9,14 +9,17 @@ import AccountView from "./components/account/AccountView"
import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext"
import { vscode } from "./utils/vscode"
import McpView from "./components/mcp/McpView"
import posthog from "posthog-js"
const AppContent = () => {
const { didHydrateState, showWelcome, shouldShowAnnouncement } = useExtensionState()
const { didHydrateState, showWelcome, shouldShowAnnouncement, advancedSettings, vscMachineId, hideTelemetryOptIn } =
useExtensionState()
const [showSettings, setShowSettings] = useState(false)
const [showHistory, setShowHistory] = useState(false)
const [showMcp, setShowMcp] = useState(false)
const [showAccount, setShowAccount] = useState(false)
const [showAnnouncement, setShowAnnouncement] = useState(false)
const telemetryEnabled = advancedSettings.enableTelemetry
const handleMessage = useCallback((e: MessageEvent) => {
const message: ExtensionMessage = e.data
@@ -60,6 +63,15 @@ const AppContent = () => {
useEvent("message", handleMessage)
useEffect(() => {
if (telemetryEnabled) {
posthog.identify(vscMachineId)
posthog.opt_in_capturing()
} else {
posthog.opt_out_capturing()
}
}, [telemetryEnabled, vscMachineId])
useEffect(() => {
if (shouldShowAnnouncement) {
setShowAnnouncement(true)
@@ -93,6 +105,7 @@ const AppContent = () => {
hideAnnouncement={() => {
setShowAnnouncement(false)
}}
hideTelemetryOptIn={hideTelemetryOptIn}
/>
</>
)}
+16 -25
View File
@@ -1,7 +1,6 @@
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { memo } from "react"
import { getAsVar, VSC_DESCRIPTION_FOREGROUND, VSC_INACTIVE_SELECTION_BACKGROUND } from "../../utils/vscStyles"
import { vscode } from "../../utils/vscode"
interface AnnouncementProps {
version: string
@@ -31,34 +30,29 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
</h3>
<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
<li>
<b>Introducing MCP Marketplace:</b> Discover and install the best MCP servers right from the extension, with
new servers added regularly! Get started by going to the{" "}
<span className="codicon codicon-extensions" style={{ marginRight: "4px", fontSize: 10 }}></span>
<VSCodeLink
onClick={() => {
vscode.postMessage({ type: "showMcpView" })
}}>
MCP Servers tab
<b>Plan/Act mode toggle:</b> Plan mode turns Cline into an architect that gathers information, asks clarifying
questions, and designs a solution. Switch back to Act mode to let him execute the plan!{" "}
<VSCodeLink href="https://x.com/sdrzn/status/1881761978986934582" style={{ display: "inline" }}>
See a demo here.
</VSCodeLink>
.
</li>
<li>
<b>Mermaid diagrams in Plan mode!</b> Cline can now visualize his plans using flowcharts, sequences,
entity-relationships, and more. When he explains his approach using mermaid, you'll see a diagram right in
chat that you can click to expand.
<b>Quick API/model switching</b> with a new popup menu under the chat field
</li>
<li>
Use <code>@terminal</code> to reference terminal contents, and <code>@git</code> to reference working changes
and commits!
<b>VS Code LM API</b> lets you use models from other extensions like GitHub Copilot
</li>
<li>
New visual indicator for checkpoints after edits & commands, and automatic checkpoint at the start of each
task.
<b>MCP server improvements:</b> On/off toggle to disable servers when not in use, and Auto-approve option for
individual tools
</li>
<li>
In case you missed it, Cline now supports Checkpoints!{" "}
<VSCodeLink href="https://x.com/sdrzn/status/1876378124126236949" style={{ display: "inline" }}>
See it in action here.
</VSCodeLink>
</li>
</ul>
<VSCodeLink href="https://x.com/sdrzn/status/1892262424881090721" style={{ display: "inline" }}>
See a demo of the changes here!
</VSCodeLink>
{/*<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
<li>
OpenRouter now supports prompt caching! They also have much higher rate limits than other providers,
@@ -115,12 +109,9 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
}}
/>
<p style={{ margin: "0" }}>
Join us on{" "}
<VSCodeLink style={{ display: "inline" }} href="https://x.com/cline">
X,
</VSCodeLink>{" "}
Join our{" "}
<VSCodeLink style={{ display: "inline" }} href="https://discord.gg/cline">
discord,
discord
</VSCodeLink>{" "}
or{" "}
<VSCodeLink style={{ display: "inline" }} href="https://www.reddit.com/r/cline/">
+4 -7
View File
@@ -13,7 +13,7 @@ import {
} from "../../../../src/shared/ExtensionMessage"
import { COMMAND_OUTPUT_STRING, COMMAND_REQ_APP_STRING } from "../../../../src/shared/combineCommandSequences"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { findMatchingResourceOrTemplate, getMcpServerDisplayName } from "../../utils/mcp"
import { findMatchingResourceOrTemplate } from "../../utils/mcp"
import { vscode } from "../../utils/vscode"
import { CheckpointControls, CheckpointOverlay } from "../common/CheckpointControls"
import CodeAccordian, { cleanPathPrefix } from "../common/CodeAccordian"
@@ -100,7 +100,7 @@ const ChatRow = memo(
export default ChatRow
export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifiedMessage, isLast }: ChatRowContentProps) => {
const { mcpServers, mcpMarketplaceCatalog } = useExtensionState()
const { mcpServers } = useExtensionState()
const [seeNewChangesDisabled, setSeeNewChangesDisabled] = useState(false)
@@ -201,12 +201,9 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
marginBottom: "-1.5px",
}}></span>
),
<span style={{ color: normalColor, fontWeight: "bold", wordBreak: "break-word" }}>
<span style={{ color: normalColor, fontWeight: "bold" }}>
Cline wants to {mcpServerUse.type === "use_mcp_tool" ? "use a tool" : "access a resource"} on the{" "}
<code style={{ wordBreak: "break-all" }}>
{getMcpServerDisplayName(mcpServerUse.serverName, mcpMarketplaceCatalog)}
</code>{" "}
MCP server:
<code>{mcpServerUse.serverName}</code> MCP server:
</span>,
]
case "completion_result":
@@ -22,7 +22,6 @@ import Tooltip from "../common/Tooltip"
import ApiOptions, { normalizeApiConfiguration } from "../settings/ApiOptions"
import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
import ContextMenu from "./ContextMenu"
import { ChatSettings } from "../../../../src/shared/ChatSettings"
interface ChatTextAreaProps {
inputValue: string
@@ -237,7 +236,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const buttonRef = useRef<HTMLDivElement>(null)
const [arrowPosition, setArrowPosition] = useState(0)
const [menuPosition, setMenuPosition] = useState(0)
const [shownTooltipMode, setShownTooltipMode] = useState<ChatSettings["mode"] | null>(null)
const [, metaKeyChar] = useMetaKeyDetection(platform)
@@ -655,7 +653,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
setTimeout(() => {
const newMode = chatSettings.mode === "plan" ? "act" : "plan"
vscode.postMessage({
type: "togglePlanActMode",
type: "chatSettings",
chatSettings: {
mode: newMode,
},
@@ -943,6 +941,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
/>
<DynamicTextArea
data-testid="chat-input"
className="ph-no-capture"
ref={(el) => {
if (typeof ref === "function") {
ref(el)
@@ -1132,23 +1131,12 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
</ModelContainer>
</ButtonGroup>
<Tooltip
visible={shownTooltipMode !== null}
tipText={`In ${shownTooltipMode === "act" ? "Act" : "Plan"} mode, Cline will ${shownTooltipMode === "act" ? "complete the task immediately" : "gather information to architect a plan"}`}
tipText={`In ${chatSettings.mode === "act" ? "Act" : "Plan"} mode, Cline will ${chatSettings.mode === "act" ? "complete the task immediately" : "gather information to architect a plan"}`}
hintText={`Toggle w/ ${metaKeyChar}+Shift+A`}>
<SwitchContainer data-testid="mode-switch" disabled={false} onClick={onModeToggle}>
<Slider isAct={chatSettings.mode === "act"} isPlan={chatSettings.mode === "plan"} />
<SwitchOption
isActive={chatSettings.mode === "plan"}
onMouseOver={() => setShownTooltipMode("plan")}
onMouseLeave={() => setShownTooltipMode(null)}>
Plan
</SwitchOption>
<SwitchOption
isActive={chatSettings.mode === "act"}
onMouseOver={() => setShownTooltipMode("act")}
onMouseLeave={() => setShownTooltipMode(null)}>
Act
</SwitchOption>
<SwitchOption isActive={chatSettings.mode === "plan"}>Plan</SwitchOption>
<SwitchOption isActive={chatSettings.mode === "act"}>Act</SwitchOption>
</SwitchContainer>
</Tooltip>
</ControlsContainer>
+14 -6
View File
@@ -26,17 +26,19 @@ import BrowserSessionRow from "./BrowserSessionRow"
import ChatRow from "./ChatRow"
import ChatTextArea from "./ChatTextArea"
import TaskHeader from "./TaskHeader"
import TelemetryOptin from "./TelemetryOptin"
interface ChatViewProps {
isHidden: boolean
showAnnouncement: boolean
hideAnnouncement: () => void
showHistoryView: () => void
hideTelemetryOptIn: boolean
}
export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryView }: ChatViewProps) => {
const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryView, hideTelemetryOptIn }: ChatViewProps) => {
const { version, clineMessages: messages, taskHistory, apiConfiguration } = useExtensionState()
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined
@@ -789,15 +791,21 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
flexDirection: "column",
paddingBottom: "10px",
}}>
{!hideTelemetryOptIn && <TelemetryOptin />}
{showAnnouncement && <Announcement version={version} hideAnnouncement={hideAnnouncement} />}
<div style={{ padding: "0 20px", flexShrink: 0 }}>
<h2>What can I do for you?</h2>
<p>
Thanks to Claude 3.7 Sonnet's agentic coding capabilities, I can handle complex software development
tasks step-by-step. With tools that let me create & edit files, explore complex projects, use the
browser, and execute terminal commands (after you grant permission), I can assist you in ways that go
beyond code completion or tech support. I can even use MCP to create new tools and extend my own
capabilities.
Thanks to{" "}
<VSCodeLink
href="https://www-cdn.anthropic.com/fed9cc193a14b84131812372d8d5857f8f304c52/Model_Card_Claude_3_Addendum.pdf"
style={{ display: "inline" }}>
Claude 3.5 Sonnet's agentic coding capabilities,
</VSCodeLink>{" "}
I can handle complex software development tasks step-by-step. With tools that let me create & edit
files, explore complex projects, use the browser, and execute terminal commands (after you grant
permission), I can assist you in ways that go beyond code completion or tech support. I can even use
MCP to create new tools and extend my own capabilities.
</p>
</div>
{taskHistory.length > 0 && <HistoryPreview showHistoryView={showHistoryView} />}
@@ -0,0 +1,86 @@
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { memo, useState, useEffect } from "react"
import { getAsVar, VSC_INACTIVE_SELECTION_BACKGROUND } from "../../utils/vscStyles"
import { vscode } from "../../utils/vscode"
const boxStyles = {
backgroundColor: getAsVar(VSC_INACTIVE_SELECTION_BACKGROUND),
borderRadius: "3px",
padding: "12px 16px",
margin: "5px 15px 5px 15px",
position: "relative" as const,
flexShrink: 0,
minHeight: "120px",
display: "flex",
flexDirection: "column" as const,
justifyContent: "center" as const,
}
const TelemetryOptin = () => {
const [showThankYou, setShowThankYou] = useState(false)
const [isVisible, setIsVisible] = useState(true)
useEffect(() => {
if (showThankYou) {
const fadeTimer = setTimeout(() => {
setIsVisible(false)
}, 1500)
const closeTimer = setTimeout(() => {
vscode.postMessage({ type: "toggleTelemetryOptin", bool: true })
}, 2000)
return () => {
clearTimeout(fadeTimer)
clearTimeout(closeTimer)
}
}
}, [showThankYou])
const handleOptIn = () => {
setShowThankYou(true)
}
const handleCancel = () => {
vscode.postMessage({ type: "toggleTelemetryOptin", bool: false })
}
if (showThankYou) {
return (
<div
style={{
...boxStyles,
opacity: isVisible ? 1 : 0,
transition: "opacity 0.5s ease-out",
textAlign: "center",
}}>
<h3 style={{ margin: "0" }}>Thank you for helping improve Cline!</h3>
</div>
)
}
return (
<div style={boxStyles}>
<h3 style={{ margin: "0 0 8px" }}>Help Improve Cline</h3>
<p style={{ margin: "0 0 12px" }}>
Would you like to help make Cline better by sending anonymous error reports and usage data? No personal or project
information will be collected. You can change this setting anytime in{" "}
<VSCodeLink onClick={() => vscode.postMessage({ type: "openExtensionSettings" })}>VS Code preferences</VSCodeLink>
.{" "}
<VSCodeLink href="https://github.com/cline/cline/blob/main/docs/PRIVACY.md" style={{ display: "inline" }}>
Learn more
</VSCodeLink>
</p>
<div style={{ display: "flex", gap: "8px" }}>
<VSCodeButton appearance="primary" onClick={handleOptIn}>
Opt In
</VSCodeButton>
<VSCodeButton appearance="secondary" onClick={handleCancel}>
Cancel
</VSCodeButton>
</div>
</div>
)
}
export default memo(TelemetryOptin)
@@ -180,7 +180,7 @@ export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut }: Checkmar
what will be reverted)
</p>
</RestoreOption>
<RestoreOption>
{/* <RestoreOption>
<VSCodeButton
onClick={handleRestoreTask}
disabled={restoreTaskDisabled}
@@ -192,7 +192,7 @@ export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut }: Checkmar
Restore Task Only
</VSCodeButton>
<p>Deletes messages after this point (does not affect workspace files)</p>
</RestoreOption>
</RestoreOption> */}
<RestoreOption>
<VSCodeButton
onClick={handleRestoreBoth}
@@ -1,30 +0,0 @@
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import styled from "styled-components"
const StyledButton = styled(VSCodeButton)`
--danger-button-bg: #c42b2b;
--danger-button-hover: #a82424;
--danger-button-active: #8f1f1f;
background-color: var(--danger-button-bg) !important;
border-color: var(--danger-button-bg) !important;
color: #ffffff !important;
&:hover {
background-color: var(--danger-button-hover) !important;
border-color: var(--danger-button-hover) !important;
}
&:active {
background-color: var(--danger-button-active) !important;
border-color: var(--danger-button-active) !important;
}
`
interface DangerButtonProps extends React.ComponentProps<typeof VSCodeButton> {}
const DangerButton: React.FC<DangerButtonProps> = (props) => {
return <StyledButton {...props} />
}
export default DangerButton
@@ -1,11 +1,10 @@
import React, { memo, useEffect } from "react"
import { memo, useEffect } from "react"
import { useRemark } from "react-remark"
import rehypeHighlight, { Options } from "rehype-highlight"
import styled from "styled-components"
import { visit } from "unist-util-visit"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { CODE_BLOCK_BG_COLOR } from "./CodeBlock"
import MermaidBlock from "./MermaidBlock"
interface MarkdownBlockProps {
markdown?: string
@@ -221,27 +220,7 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
],
rehypeReactOptions: {
components: {
pre: ({ node, children, ...preProps }: any) => {
if (Array.isArray(children) && children.length === 1 && React.isValidElement(children[0])) {
const child = children[0] as React.ReactElement<{ className?: string }>
if (child.props?.className?.includes("language-mermaid")) {
return child
}
}
return (
<StyledPre {...preProps} theme={theme}>
{children}
</StyledPre>
)
},
code: (props: any) => {
const className = props.className || ""
if (className.includes("language-mermaid")) {
const codeText = String(props.children || "")
return <MermaidBlock code={codeText} />
}
return <code {...props} />
},
pre: ({ node, ...preProps }: any) => <StyledPre {...preProps} theme={theme} />,
},
},
})
@@ -1,227 +0,0 @@
import { useEffect, useRef, useState } from "react"
import mermaid from "mermaid"
import { useDebounceEffect } from "../../utils/useDebounceEffect"
import styled from "styled-components"
import { vscode } from "../../utils/vscode"
const MERMAID_THEME = {
background: "#1e1e1e", // VS Code dark theme background
textColor: "#ffffff", // Main text color
mainBkg: "#2d2d2d", // Background for nodes
nodeBorder: "#888888", // Border color for nodes
lineColor: "#cccccc", // Lines connecting nodes
primaryColor: "#3c3c3c", // Primary color for highlights
primaryTextColor: "#ffffff", // Text in primary colored elements
primaryBorderColor: "#888888",
secondaryColor: "#2d2d2d", // Secondary color for alternate elements
tertiaryColor: "#454545", // Third color for special elements
// Class diagram specific
classText: "#ffffff",
// State diagram specific
labelColor: "#ffffff",
// Sequence diagram specific
actorLineColor: "#cccccc",
actorBkg: "#2d2d2d",
actorBorder: "#888888",
actorTextColor: "#ffffff",
// Flow diagram specific
fillType0: "#2d2d2d",
fillType1: "#3c3c3c",
fillType2: "#454545",
}
mermaid.initialize({
startOnLoad: false,
securityLevel: "loose",
theme: "dark",
themeVariables: {
...MERMAID_THEME,
fontSize: "16px",
fontFamily: "var(--vscode-font-family, 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif)",
// Additional styling
noteTextColor: "#ffffff",
noteBkgColor: "#454545",
noteBorderColor: "#888888",
// Improve contrast for special elements
critBorderColor: "#ff9580",
critBkgColor: "#803d36",
// Task diagram specific
taskTextColor: "#ffffff",
taskTextOutsideColor: "#ffffff",
taskTextLightColor: "#ffffff",
// Numbers/sections
sectionBkgColor: "#2d2d2d",
sectionBkgColor2: "#3c3c3c",
// Alt sections in sequence diagrams
altBackground: "#2d2d2d",
// Links
linkColor: "#6cb6ff",
// Borders and lines
compositeBackground: "#2d2d2d",
compositeBorder: "#888888",
titleColor: "#ffffff",
},
})
interface MermaidBlockProps {
code: string
}
export default function MermaidBlock({ code }: MermaidBlockProps) {
const containerRef = useRef<HTMLDivElement>(null)
const [isLoading, setIsLoading] = useState(false)
// 1) Whenever `code` changes, mark that we need to re-render a new chart
useEffect(() => {
setIsLoading(true)
}, [code])
// 2) Debounce the actual parse/render
useDebounceEffect(
() => {
if (containerRef.current) {
containerRef.current.innerHTML = ""
}
mermaid
.parse(code, { suppressErrors: true })
.then((isValid) => {
if (!isValid) {
throw new Error("Invalid or incomplete Mermaid code")
}
const id = `mermaid-${Math.random().toString(36).substring(2)}`
return mermaid.render(id, code)
})
.then(({ svg }) => {
if (containerRef.current) {
containerRef.current.innerHTML = svg
}
})
.catch((err) => {
console.warn("Mermaid parse/render failed:", err)
containerRef.current!.innerHTML = code.replace(/</g, "&lt;").replace(/>/g, "&gt;")
})
.finally(() => {
setIsLoading(false)
})
},
500, // Delay 500ms
[code], // Dependencies for scheduling
)
/**
* Called when user clicks the rendered diagram.
* Converts the <svg> to a PNG and sends it to the extension.
*/
const handleClick = async () => {
if (!containerRef.current) return
const svgEl = containerRef.current.querySelector("svg")
if (!svgEl) return
try {
const pngDataUrl = await svgToPng(svgEl)
vscode.postMessage({
type: "openImage",
text: pngDataUrl,
})
} catch (err) {
console.error("Error converting SVG to PNG:", err)
}
}
return (
<MermaidBlockContainer>
{isLoading && <LoadingMessage>Generating mermaid diagram...</LoadingMessage>}
{/* The container for the final <svg> or raw code. */}
<SvgContainer onClick={handleClick} ref={containerRef} $isLoading={isLoading} />
</MermaidBlockContainer>
)
}
async function svgToPng(svgEl: SVGElement): Promise<string> {
console.log("svgToPng function called")
// Clone the SVG to avoid modifying the original
const svgClone = svgEl.cloneNode(true) as SVGElement
// Get the original viewBox
const viewBox = svgClone.getAttribute("viewBox")?.split(" ").map(Number) || []
const originalWidth = viewBox[2] || svgClone.clientWidth
const originalHeight = viewBox[3] || svgClone.clientHeight
// Calculate the scale factor to fit editor width while maintaining aspect ratio
// Unless we can find a way to get the actual editor window dimensions through the VS Code API (which might be possible but would require changes to the extension side),
// the fixed width seems like a reliable approach.
const editorWidth = 3_600
const scale = editorWidth / originalWidth
const scaledHeight = originalHeight * scale
// Update SVG dimensions
svgClone.setAttribute("width", `${editorWidth}`)
svgClone.setAttribute("height", `${scaledHeight}`)
const serializer = new XMLSerializer()
const svgString = serializer.serializeToString(svgClone)
const svgDataUrl = "data:image/svg+xml;base64," + btoa(decodeURIComponent(encodeURIComponent(svgString)))
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = () => {
const canvas = document.createElement("canvas")
canvas.width = editorWidth
canvas.height = scaledHeight
const ctx = canvas.getContext("2d")
if (!ctx) return reject("Canvas context not available")
// Fill background with Mermaid's dark theme background color
ctx.fillStyle = MERMAID_THEME.background
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.imageSmoothingEnabled = true
ctx.imageSmoothingQuality = "high"
ctx.drawImage(img, 0, 0, editorWidth, scaledHeight)
resolve(canvas.toDataURL("image/png", 1.0))
}
img.onerror = reject
img.src = svgDataUrl
})
}
const MermaidBlockContainer = styled.div`
position: relative;
margin: 8px 0;
`
const LoadingMessage = styled.div`
padding: 8px 0;
color: var(--vscode-descriptionForeground);
font-style: italic;
font-size: 0.9em;
`
interface SvgContainerProps {
$isLoading: boolean
}
const SvgContainer = styled.div<SvgContainerProps>`
opacity: ${(props) => (props.$isLoading ? 0.3 : 1)};
min-height: 20px;
transition: opacity 0.2s ease;
cursor: pointer;
display: flex;
justify-content: center;
`
+7 -3
View File
@@ -9,7 +9,6 @@ import {
} from "../../utils/vscStyles"
interface TooltipProps {
visible: boolean
hintText: string
tipText: string
children: React.ReactNode
@@ -39,9 +38,14 @@ const Hint = styled.div`
margin-top: 2px;
`
const Tooltip: React.FC<TooltipProps> = ({ visible, tipText, hintText, children }) => {
const Tooltip: React.FC<TooltipProps> = ({ tipText, hintText, children }) => {
const [visible, setVisible] = useState(false)
const showTooltip = () => setVisible(true)
const hideTooltip = () => setVisible(false)
return (
<div style={{ position: "relative", display: "inline-block" }}>
<div style={{ position: "relative", display: "inline-block" }} onMouseEnter={showTooltip} onMouseLeave={hideTooltip}>
{children}
{visible && (
<TooltipBody>
+126 -225
View File
@@ -1,48 +1,78 @@
import {
VSCodeButton,
VSCodeLink,
VSCodePanels,
VSCodePanelTab,
VSCodePanelView,
VSCodeDropdown,
VSCodeOption,
} from "@vscode/webview-ui-toolkit/react"
import { useEffect, useState } from "react"
import styled from "styled-components"
import { DEFAULT_MCP_TIMEOUT_SECONDS, McpServer } from "../../../../src/shared/mcp"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { getMcpServerDisplayName } from "../../utils/mcp"
import { VSCodeButton, VSCodeLink, VSCodePanels, VSCodePanelTab, VSCodePanelView } from "@vscode/webview-ui-toolkit/react"
import { useState } from "react"
import { vscode } from "../../utils/vscode"
import DangerButton from "../common/DangerButton"
import McpMarketplaceView from "./marketplace/McpMarketplaceView"
import McpResourceRow from "./McpResourceRow"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { McpServer } from "../../../../src/shared/mcp"
import McpToolRow from "./McpToolRow"
import McpResourceRow from "./McpResourceRow"
type McpViewProps = {
onDone: () => void
}
const McpView = ({ onDone }: McpViewProps) => {
const { mcpServers: servers, mcpMarketplaceEnabled } = useExtensionState()
const [activeTab, setActiveTab] = useState(mcpMarketplaceEnabled ? "marketplace" : "installed")
const { mcpServers: servers } = useExtensionState()
const handleTabChange = (tab: string) => {
setActiveTab(tab)
}
useEffect(() => {
if (!mcpMarketplaceEnabled && activeTab === "marketplace") {
// If marketplace is disabled and we're on marketplace tab, switch to installed
setActiveTab("installed")
}
}, [mcpMarketplaceEnabled, activeTab])
useEffect(() => {
if (mcpMarketplaceEnabled) {
vscode.postMessage({ type: "silentlyRefreshMcpMarketplace" })
vscode.postMessage({ type: "fetchLatestMcpServersFromHub" })
}
}, [mcpMarketplaceEnabled])
// const [servers, setServers] = useState<McpServer[]>([
// // Add some mock servers for testing
// {
// name: "local-tools",
// config: JSON.stringify({
// mcpServers: {
// "local-tools": {
// command: "npx",
// args: ["-y", "@modelcontextprotocol/server-tools"],
// },
// },
// }),
// status: "connected",
// tools: [
// {
// name: "execute_command",
// description: "Run a shell command on the local system",
// },
// {
// name: "read_file",
// description: "Read contents of a file from the filesystem",
// },
// ],
// },
// {
// name: "postgres-db",
// config: JSON.stringify({
// mcpServers: {
// "postgres-db": {
// command: "npx",
// args: ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"],
// },
// },
// }),
// status: "disconnected",
// error: "Failed to connect to database: Connection refused",
// },
// {
// name: "github-tools",
// config: JSON.stringify({
// mcpServers: {
// "github-tools": {
// command: "npx",
// args: ["-y", "@modelcontextprotocol/server-github"],
// },
// },
// }),
// status: "connecting",
// resources: [
// {
// uri: "github://repo/issues",
// name: "Repository Issues",
// },
// {
// uri: "github://repo/pulls",
// name: "Pull Requests",
// },
// ],
// },
// ])
return (
<div
@@ -60,146 +90,86 @@ const McpView = ({ onDone }: McpViewProps) => {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "10px 17px 5px 20px",
padding: "10px 17px 10px 20px",
}}>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>MCP Servers</h3>
<VSCodeButton onClick={onDone}>Done</VSCodeButton>
</div>
<div style={{ flex: 1, overflow: "auto" }}>
{/* Tabs container */}
<div style={{ flex: 1, overflow: "auto", padding: "0 20px" }}>
<div
style={{
display: "flex",
gap: "1px",
padding: "0 20px 0 20px",
borderBottom: "1px solid var(--vscode-panel-border)",
color: "var(--vscode-foreground)",
fontSize: "13px",
marginBottom: "16px",
marginTop: "5px",
}}>
{mcpMarketplaceEnabled && (
<TabButton isActive={activeTab === "marketplace"} onClick={() => handleTabChange("marketplace")}>
Marketplace
</TabButton>
)}
<TabButton isActive={activeTab === "installed"} onClick={() => handleTabChange("installed")}>
Installed
</TabButton>
The{" "}
<VSCodeLink href="https://github.com/modelcontextprotocol" style={{ display: "inline" }}>
Model Context Protocol
</VSCodeLink>{" "}
enables communication with locally running MCP servers that provide additional tools and resources to extend
Cline's capabilities. You can use{" "}
<VSCodeLink href="https://github.com/modelcontextprotocol/servers" style={{ display: "inline" }}>
community-made servers
</VSCodeLink>{" "}
or ask Cline to create new tools specific to your workflow (e.g., "add a tool that gets the latest npm docs").{" "}
<VSCodeLink href="https://x.com/sdrzn/status/1867271665086074969" style={{ display: "inline" }}>
See a demo here.
</VSCodeLink>
</div>
{/* Content container */}
<div style={{ width: "100%" }}>
{mcpMarketplaceEnabled && activeTab === "marketplace" && <McpMarketplaceView />}
{activeTab === "installed" && (
<div style={{ padding: "16px 20px" }}>
<div
style={{
color: "var(--vscode-foreground)",
fontSize: "13px",
marginBottom: "16px",
marginTop: "5px",
}}>
The{" "}
<VSCodeLink href="https://github.com/modelcontextprotocol" style={{ display: "inline" }}>
Model Context Protocol
</VSCodeLink>{" "}
enables communication with locally running MCP servers that provide additional tools and resources
to extend Cline's capabilities. You can use{" "}
<VSCodeLink href="https://github.com/modelcontextprotocol/servers" style={{ display: "inline" }}>
community-made servers
</VSCodeLink>{" "}
or ask Cline to create new tools specific to your workflow (e.g., "add a tool that gets the latest
npm docs").{" "}
<VSCodeLink href="https://x.com/sdrzn/status/1867271665086074969" style={{ display: "inline" }}>
See a demo here.
</VSCodeLink>
</div>
{servers.length > 0 && (
<div
style={{
display: "flex",
flexDirection: "column",
gap: "10px",
}}>
{servers.map((server) => (
<ServerRow key={server.name} server={server} />
))}
</div>
)}
{servers.length > 0 ? (
<div
style={{
display: "flex",
flexDirection: "column",
gap: "10px",
}}>
{servers.map((server) => (
<ServerRow key={server.name} server={server} />
))}
</div>
) : (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "12px",
marginTop: 20,
marginBottom: 20,
color: "var(--vscode-descriptionForeground)",
}}>
No MCP servers installed
</div>
)}
{/* Server Configuration Button */}
{/* Settings Section */}
<div style={{ marginBottom: "20px", marginTop: 10 }}>
<VSCodeButton
appearance="secondary"
style={{ width: "100%", marginBottom: "5px" }}
onClick={() => {
vscode.postMessage({ type: "openMcpSettings" })
}}>
<span className="codicon codicon-server" style={{ marginRight: "6px" }}></span>
Configure MCP Servers
</VSCodeButton>
<div style={{ textAlign: "center" }}>
<VSCodeLink
onClick={() => {
vscode.postMessage({
type: "openExtensionSettings",
text: "cline.mcp",
})
}}
style={{ fontSize: "12px" }}>
Advanced MCP Settings
</VSCodeLink>
</div>
</div>
</div>
)}
<div style={{ marginTop: "10px", width: "100%" }}>
<VSCodeButton
appearance="secondary"
style={{ width: "100%" }}
onClick={() => {
vscode.postMessage({ type: "openMcpSettings" })
}}>
<span className="codicon codicon-server" style={{ marginRight: "6px" }}></span>
Configure MCP Servers
</VSCodeButton>
</div>
{/* Advanced Settings Link */}
<div style={{ textAlign: "center", marginTop: "5px" }}>
<VSCodeLink
onClick={() => {
vscode.postMessage({
type: "openExtensionSettings",
text: "cline.mcp",
})
}}
style={{ fontSize: "12px" }}>
Advanced MCP Settings
</VSCodeLink>
</div>
{/* Bottom padding */}
<div style={{ height: "20px" }} />
</div>
</div>
)
}
const StyledTabButton = styled.button<{ isActive: boolean }>`
background: none;
border: none;
border-bottom: 2px solid ${(props) => (props.isActive ? "var(--vscode-foreground)" : "transparent")};
color: ${(props) => (props.isActive ? "var(--vscode-foreground)" : "var(--vscode-descriptionForeground)")};
padding: 8px 16px;
cursor: pointer;
font-size: 13px;
margin-bottom: -1px;
font-family: inherit;
&:hover {
color: var(--vscode-foreground);
}
`
const TabButton = ({ children, isActive, onClick }: { children: React.ReactNode; isActive: boolean; onClick: () => void }) => (
<StyledTabButton isActive={isActive} onClick={onClick}>
{children}
</StyledTabButton>
)
// Server Row Component
const ServerRow = ({ server }: { server: McpServer }) => {
const { mcpMarketplaceCatalog } = useExtensionState()
const [isExpanded, setIsExpanded] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
const getStatusColor = () => {
switch (server.status) {
@@ -218,36 +188,6 @@ const ServerRow = ({ server }: { server: McpServer }) => {
}
}
const [timeout, setTimeout] = useState<string>(() => {
try {
const config = JSON.parse(server.config)
return config.timeout?.toString() || DEFAULT_MCP_TIMEOUT_SECONDS.toString()
} catch {
return DEFAULT_MCP_TIMEOUT_SECONDS.toString()
}
})
const timeoutOptions = [
{ value: "30", label: "30 seconds" },
{ value: "60", label: "1 minute" },
{ value: "300", label: "5 minutes" },
{ value: "600", label: "10 minutes" },
{ value: "1800", label: "30 minutes" },
{ value: "3600", label: "1 hour" },
]
const handleTimeoutChange = (e: any) => {
const select = e.target as HTMLSelectElement
const value = select.value
const num = parseInt(value)
setTimeout(value)
vscode.postMessage({
type: "updateMcpTimeout",
serverName: server.name,
timeout: num,
})
}
const handleRestart = () => {
vscode.postMessage({
type: "restartMcpServer",
@@ -255,14 +195,6 @@ const ServerRow = ({ server }: { server: McpServer }) => {
})
}
const handleDelete = () => {
setIsDeleting(true)
vscode.postMessage({
type: "deleteMcpServer",
serverName: server.name,
})
}
return (
<div style={{ marginBottom: "10px" }}>
<div
@@ -279,18 +211,7 @@ const ServerRow = ({ server }: { server: McpServer }) => {
{!server.error && (
<span className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`} style={{ marginRight: "8px" }} />
)}
<span
style={{
flex: 1,
overflow: "hidden",
wordBreak: "break-all",
whiteSpace: "normal",
display: "flex",
alignItems: "center",
marginRight: "4px",
}}>
{getMcpServerDisplayName(server.name, mcpMarketplaceCatalog)}
</span>
<span style={{ flex: 1 }}>{server.name}</span>
<div style={{ display: "flex", alignItems: "center", marginRight: "8px" }} onClick={(e) => e.stopPropagation()}>
<div
role="switch"
@@ -448,16 +369,6 @@ const ServerRow = ({ server }: { server: McpServer }) => {
</VSCodePanelView>
</VSCodePanels>
<div style={{ margin: "10px 7px" }}>
<label style={{ display: "block", marginBottom: "4px", fontSize: "13px" }}>Request Timeout</label>
<VSCodeDropdown style={{ width: "100%" }} value={timeout} onChange={handleTimeoutChange}>
{timeoutOptions.map((option) => (
<VSCodeOption key={option.value} value={option.value}>
{option.label}
</VSCodeOption>
))}
</VSCodeDropdown>
</div>
<VSCodeButton
appearance="secondary"
onClick={handleRestart}
@@ -468,16 +379,6 @@ const ServerRow = ({ server }: { server: McpServer }) => {
}}>
{server.status === "connecting" ? "Restarting..." : "Restart Server"}
</VSCodeButton>
<DangerButton
onClick={handleDelete}
disabled={isDeleting}
style={{
width: "calc(100% - 14px)",
margin: "5px 7px 3px 7px",
}}>
{isDeleting ? "Deleting..." : "Delete Server"}
</DangerButton>
</div>
)
)}
@@ -1,302 +0,0 @@
import { useCallback, useState, useRef, useMemo } from "react"
import styled from "styled-components"
import { McpMarketplaceItem, McpServer } from "../../../../../src/shared/mcp"
import { vscode } from "../../../utils/vscode"
import { useEvent } from "react-use"
interface McpMarketplaceCardProps {
item: McpMarketplaceItem
installedServers: McpServer[]
}
const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps) => {
const isInstalled = installedServers.some((server) => server.name === item.mcpId)
const [isDownloading, setIsDownloading] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const githubLinkRef = useRef<HTMLDivElement>(null)
const handleMessage = useCallback((event: MessageEvent) => {
const message = event.data
switch (message.type) {
case "mcpDownloadDetails":
setIsDownloading(false)
break
case "relinquishControl":
setIsLoading(false)
break
}
}, [])
useEvent("message", handleMessage)
const githubAuthorUrl = useMemo(() => {
const url = new URL(item.githubUrl)
const pathParts = url.pathname.split("/")
if (pathParts.length >= 2) {
return `${url.origin}/${pathParts[1]}`
}
return item.githubUrl
}, [item.githubUrl])
return (
<>
<style>
{`
.mcp-card {
cursor: pointer;
outline: none !important;
}
.mcp-card:hover {
background-color: var(--vscode-list-hoverBackground);
}
.mcp-card:focus {
outline: none !important;
}
`}
</style>
<a
href={item.githubUrl}
className="mcp-card"
style={{
padding: "14px 16px",
display: "flex",
flexDirection: "column",
gap: 12,
cursor: isLoading ? "wait" : "pointer",
textDecoration: "none",
color: "inherit",
}}>
{/* Main container with logo and content */}
<div style={{ display: "flex", gap: "12px" }}>
{/* Logo */}
{item.logoUrl && (
<img
src={item.logoUrl}
alt={`${item.name} logo`}
style={{
width: 42,
height: 42,
borderRadius: 4,
}}
/>
)}
{/* Content section */}
<div
style={{
flex: 1,
minWidth: 0,
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
}}>
{/* First row: name and install button */}
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: "16px",
}}>
<h3
style={{
margin: 0,
fontSize: "13px",
fontWeight: 600,
}}>
{item.name}
</h3>
<div
onClick={(e) => {
e.preventDefault() // Prevent card click when clicking install
e.stopPropagation() // Stop event from bubbling up to parent link
if (!isInstalled && !isDownloading) {
setIsDownloading(true)
vscode.postMessage({
type: "downloadMcp",
mcpId: item.mcpId,
})
}
}}
style={{}}>
<StyledInstallButton disabled={isInstalled || isDownloading} $isInstalled={isInstalled}>
{isInstalled ? "Installed" : isDownloading ? "Installing..." : "Install"}
</StyledInstallButton>
</div>
</div>
{/* Second row: metadata */}
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
flexWrap: "wrap",
minWidth: 0,
rowGap: 0,
}}>
<a
href={githubAuthorUrl}
style={{
display: "flex",
alignItems: "center",
color: "var(--vscode-foreground)",
minWidth: 0,
opacity: 0.7,
textDecoration: "none",
border: "none !important",
}}
className="github-link"
onMouseEnter={(e) => {
e.currentTarget.style.opacity = "1"
e.currentTarget.style.color = "var(--link-active-foreground)"
}}
onMouseLeave={(e) => {
e.currentTarget.style.opacity = "0.7"
e.currentTarget.style.color = "var(--vscode-foreground)"
}}>
<div style={{ display: "flex", gap: "4px", alignItems: "center" }} ref={githubLinkRef}>
<span className="codicon codicon-github" style={{ fontSize: "14px" }} />
<span
style={{
overflow: "hidden",
textOverflow: "ellipsis",
wordBreak: "break-all",
minWidth: 0,
}}>
{item.author}
</span>
</div>
</a>
<div
style={{
display: "flex",
alignItems: "center",
gap: "4px",
minWidth: 0,
flexShrink: 0,
}}>
<span className="codicon codicon-star-full" />
<span style={{ wordBreak: "break-all" }}>{item.githubStars?.toLocaleString() ?? 0}</span>
</div>
{/* <div
style={{
display: "flex",
alignItems: "center",
gap: "4px",
minWidth: 0,
flexShrink: 0,
}}>
<span className="codicon codicon-cloud-download" />
<span style={{ wordBreak: "break-all" }}>{item.downloadCount?.toLocaleString() ?? 0}</span>
</div> */}
{item.requiresApiKey && (
<span className="codicon codicon-key" title="Requires API key" style={{ flexShrink: 0 }} />
)}
</div>
</div>
</div>
{/* Description and tags */}
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
{/* {!item.isRecommended && (
<div
style={{
display: "flex",
alignItems: "center",
gap: "4px",
fontSize: "12px",
color: "var(--vscode-notificationsWarningIcon-foreground)",
marginTop: -3,
marginBottom: -3,
}}>
<span className="codicon codicon-warning" style={{ fontSize: "14px" }} />
<span>Community Made (use at your own risk)</span>
</div>
)} */}
<p style={{ fontSize: "13px", margin: 0 }}>{item.description}</p>
<div
style={{
display: "flex",
gap: "6px",
flexWrap: "nowrap",
overflow: "hidden",
position: "relative",
}}>
<span
style={{
fontSize: "10px",
padding: "1px 4px",
borderRadius: "3px",
border: "1px solid color-mix(in srgb, var(--vscode-descriptionForeground) 50%, transparent)",
color: "var(--vscode-descriptionForeground)",
whiteSpace: "nowrap",
}}>
{item.category}
</span>
{item.tags.map((tag, index) => (
<span
key={tag}
style={{
fontSize: "10px",
padding: "1px 4px",
borderRadius: "3px",
border: "1px solid color-mix(in srgb, var(--vscode-descriptionForeground) 50%, transparent)",
color: "var(--vscode-descriptionForeground)",
whiteSpace: "nowrap",
display: "inline-flex",
}}>
{tag}
{index === item.tags.length - 1 ? "" : ""}
</span>
))}
<div
style={{
position: "absolute",
right: 0,
top: 0,
bottom: 0,
width: "32px",
background: "linear-gradient(to right, transparent, var(--vscode-sideBar-background))",
pointerEvents: "none",
}}
/>
</div>
</div>
</a>
</>
)
}
const StyledInstallButton = styled.button<{ $isInstalled?: boolean }>`
font-size: 12px;
font-weight: 500;
padding: 2px 6px;
border-radius: 2px;
border: none;
cursor: pointer;
background: ${(props) =>
props.$isInstalled ? "var(--vscode-button-secondaryBackground)" : "var(--vscode-button-background)"};
color: var(--vscode-button-foreground);
&:hover:not(:disabled) {
background: ${(props) =>
props.$isInstalled ? "var(--vscode-button-secondaryHoverBackground)" : "var(--vscode-button-hoverBackground)"};
}
&:active:not(:disabled) {
background: ${(props) =>
props.$isInstalled ? "var(--vscode-button-secondaryBackground)" : "var(--vscode-button-background)"};
opacity: 0.7;
}
&:disabled {
opacity: 0.5;
cursor: default;
}
`
export default McpMarketplaceCard
@@ -1,286 +0,0 @@
import { useEffect, useMemo, useState } from "react"
import {
VSCodeButton,
VSCodeProgressRing,
VSCodeRadioGroup,
VSCodeRadio,
VSCodeDropdown,
VSCodeOption,
VSCodeTextField,
} from "@vscode/webview-ui-toolkit/react"
import { McpMarketplaceItem } from "../../../../../src/shared/mcp"
import { useExtensionState } from "../../../context/ExtensionStateContext"
import { vscode } from "../../../utils/vscode"
import McpMarketplaceCard from "./McpMarketplaceCard"
import McpSubmitCard from "./McpSubmitCard"
const McpMarketplaceView = () => {
const { mcpServers } = useExtensionState()
const [items, setItems] = useState<McpMarketplaceItem[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [isRefreshing, setIsRefreshing] = useState(false)
const [searchQuery, setSearchQuery] = useState("")
const [selectedCategory, setSelectedCategory] = useState<string | null>(null)
const [sortBy, setSortBy] = useState<"newest" | "stars" | "name">("newest")
const categories = useMemo(() => {
const uniqueCategories = new Set(items.map((item) => item.category))
return Array.from(uniqueCategories).sort()
}, [items])
const filteredItems = useMemo(() => {
return items
.filter((item) => {
const matchesSearch =
searchQuery === "" ||
item.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.tags.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase()))
const matchesCategory = !selectedCategory || item.category === selectedCategory
return matchesSearch && matchesCategory
})
.sort((a, b) => {
switch (sortBy) {
// case "downloadCount":
// return b.downloadCount - a.downloadCount
case "stars":
return b.githubStars - a.githubStars
case "name":
return a.name.localeCompare(b.name)
case "newest":
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
default:
return 0
}
})
}, [items, searchQuery, selectedCategory, sortBy])
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
const message = event.data
if (message.type === "mcpMarketplaceCatalog") {
if (message.error) {
setError(message.error)
} else {
setItems(message.mcpMarketplaceCatalog?.items || [])
setError(null)
}
setIsLoading(false)
setIsRefreshing(false)
} else if (message.type === "mcpDownloadDetails") {
if (message.error) {
setError(message.error)
}
}
}
window.addEventListener("message", handleMessage)
// Fetch marketplace catalog
fetchMarketplace()
return () => {
window.removeEventListener("message", handleMessage)
}
}, [])
const fetchMarketplace = (forceRefresh: boolean = false) => {
if (forceRefresh) {
setIsRefreshing(true)
} else {
setIsLoading(true)
}
setError(null)
vscode.postMessage({ type: "fetchMcpMarketplace", bool: forceRefresh })
}
if (isLoading || isRefreshing) {
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
padding: "20px",
}}>
<VSCodeProgressRing />
</div>
)
}
if (error) {
return (
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
height: "100%",
padding: "20px",
gap: "12px",
}}>
<div style={{ color: "var(--vscode-errorForeground)" }}>{error}</div>
<VSCodeButton appearance="secondary" onClick={() => fetchMarketplace(true)}>
<span className="codicon codicon-refresh" style={{ marginRight: "6px" }} />
Retry
</VSCodeButton>
</div>
)
}
return (
<div
style={{
display: "flex",
flexDirection: "column",
width: "100%",
}}>
<div style={{ padding: "20px 20px 5px", display: "flex", flexDirection: "column", gap: "16px" }}>
{/* Search row */}
<VSCodeTextField
style={{ width: "100%" }}
placeholder="Search MCPs..."
value={searchQuery}
onInput={(e) => setSearchQuery((e.target as HTMLInputElement).value)}>
<div
slot="start"
className="codicon codicon-search"
style={{
fontSize: 13,
opacity: 0.8,
}}
/>
{searchQuery && (
<div
className="codicon codicon-close"
aria-label="Clear search"
onClick={() => setSearchQuery("")}
slot="end"
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
cursor: "pointer",
}}
/>
)}
</VSCodeTextField>
{/* Filter row */}
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
}}>
<span
style={{
fontSize: "11px",
color: "var(--vscode-descriptionForeground)",
textTransform: "uppercase",
fontWeight: 500,
flexShrink: 0,
}}>
Filter:
</span>
<div
style={{
position: "relative",
zIndex: 2,
flex: 1,
}}>
<VSCodeDropdown
style={{
width: "100%",
}}
value={selectedCategory || ""}
onChange={(e) => setSelectedCategory((e.target as HTMLSelectElement).value || null)}>
<VSCodeOption value="">All Categories</VSCodeOption>
{categories.map((category) => (
<VSCodeOption key={category} value={category}>
{category}
</VSCodeOption>
))}
</VSCodeDropdown>
</div>
</div>
{/* Sort row */}
<div
style={{
display: "flex",
gap: "8px",
}}>
<span
style={{
fontSize: "11px",
color: "var(--vscode-descriptionForeground)",
textTransform: "uppercase",
fontWeight: 500,
marginTop: "3px",
}}>
Sort:
</span>
<VSCodeRadioGroup
style={{
display: "flex",
flexWrap: "wrap",
marginTop: "-2.5px",
}}
value={sortBy}
onChange={(e) => setSortBy((e.target as HTMLInputElement).value as typeof sortBy)}>
{/* <VSCodeRadio value="downloadCount">Most Installs</VSCodeRadio> */}
<VSCodeRadio value="newest">Newest</VSCodeRadio>
<VSCodeRadio value="stars">GitHub Stars</VSCodeRadio>
<VSCodeRadio value="name">Name</VSCodeRadio>
</VSCodeRadioGroup>
</div>
</div>
<style>
{`
.mcp-search-input,
.mcp-select {
box-sizing: border-box;
}
.mcp-search-input {
min-width: 140px;
}
.mcp-search-input:focus,
.mcp-select:focus {
border-color: var(--vscode-focusBorder) !important;
}
.mcp-search-input:hover,
.mcp-select:hover {
opacity: 0.9;
}
`}
</style>
<div style={{ display: "flex", flexDirection: "column" }}>
{filteredItems.length === 0 ? (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
padding: "20px",
color: "var(--vscode-descriptionForeground)",
}}>
{searchQuery || selectedCategory
? "No matching MCP servers found"
: "No MCP servers found in the marketplace"}
</div>
) : (
filteredItems.map((item) => <McpMarketplaceCard key={item.mcpId} item={item} installedServers={mcpServers} />)
)}
<McpSubmitCard />
</div>
</div>
)
}
export default McpMarketplaceView
@@ -1,45 +0,0 @@
const McpSubmitCard = () => {
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "12px",
padding: "15px",
margin: "20px",
backgroundColor: "var(--vscode-textBlockQuote-background)",
borderRadius: "6px",
}}>
{/* Icon */}
<i className="codicon codicon-add" style={{ fontSize: "18px" }} />
{/* Content */}
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "4px",
textAlign: "center",
maxWidth: "480px",
}}>
<h3
style={{
margin: 0,
fontSize: "14px",
fontWeight: 600,
color: "var(--vscode-foreground)",
}}>
Submit MCP Server
</h3>
<p style={{ fontSize: "13px", margin: 0, color: "var(--vscode-descriptionForeground)" }}>
Help others discover great MCP servers by submitting an issue to{" "}
<a href="https://github.com/cline/mcp-marketplace">github.com/cline/mcp-marketplace</a>
</p>
</div>
</div>
)
}
export default McpSubmitCard
@@ -201,6 +201,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
{selectedProvider === "anthropic" && (
<div>
<VSCodeTextField
className="ph-no-capture"
value={apiConfiguration?.apiKey || ""}
style={{ width: "100%" }}
type="password"
@@ -938,7 +939,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
}}>
The VS Code Language Model API allows you to run models provided by other VS Code extensions
(including but not limited to GitHub Copilot). The easiest way to get started is to install the
Copilot extension from the VS Marketplace and enabling Claude 3.7 Sonnet.
Copilot extension from the VS Marketplace and enabling Claude 3.5 Sonnet.
</p>
)}
@@ -228,8 +228,8 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
If you're unsure which model to choose, Cline works best with{" "}
<VSCodeLink
style={{ display: "inline", fontSize: "inherit" }}
onClick={() => handleModelChange("anthropic/claude-3-7-sonnet")}>
anthropic/claude-3-7-sonnet.
onClick={() => handleModelChange("anthropic/claude-3.5-sonnet")}>
anthropic/claude-3.5-sonnet.
</VSCodeLink>
You can also try searching "free" for no-cost options currently available.
</>
@@ -1,11 +1,11 @@
import { VSCodeButton, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { useCallback, useEffect, useState } from "react"
import { useEvent } from "react-use"
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
import { VSCodeButton, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { useEffect, useState, useCallback } from "react"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { validateApiConfiguration } from "../../utils/validate"
import { vscode } from "../../utils/vscode"
import ApiOptions from "../settings/ApiOptions"
import { useEvent } from "react-use"
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
const WelcomeView = () => {
const { apiConfiguration } = useExtensionState()
@@ -58,13 +58,18 @@ const WelcomeView = () => {
}}>
<h2>Hi, I'm Cline</h2>
<p>
I can do all kinds of tasks thanks to breakthroughs in Claude 3.7 Sonnet's agentic coding capabilities and
access to tools that let me create & edit files, explore complex projects, use the browser, and execute
I can do all kinds of tasks thanks to the latest breakthroughs in{" "}
<VSCodeLink
href="https://www-cdn.anthropic.com/fed9cc193a14b84131812372d8d5857f8f304c52/Model_Card_Claude_3_Addendum.pdf"
style={{ display: "inline" }}>
Claude 3.5 Sonnet's agentic coding capabilities
</VSCodeLink>{" "}
and access to tools that let me create & edit files, explore complex projects, use the browser, and execute
terminal commands (with your permission, of course). I can even use MCP to create new tools and extend my own
capabilities.
</p>
<b>To get started, this extension needs an API provider for Claude 3.7 Sonnet.</b>
<b>To get started, this extension needs an API provider for Claude 3.5 Sonnet.</b>
<div
style={{
@@ -4,11 +4,12 @@ import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "../../../src/shared/AutoApproval
import { ExtensionMessage, ExtensionState, DEFAULT_PLATFORM } from "../../../src/shared/ExtensionMessage"
import { ApiConfiguration, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "../../../src/shared/api"
import { findLastIndex } from "../../../src/shared/array"
import { McpMarketplaceCatalog, McpServer } from "../../../src/shared/mcp"
import { McpServer } from "../../../src/shared/mcp"
import { convertTextMateToHljs } from "../utils/textMateToHljs"
import { vscode } from "../utils/vscode"
import { DEFAULT_BROWSER_SETTINGS } from "../../../src/shared/BrowserSettings"
import { DEFAULT_CHAT_SETTINGS } from "../../../src/shared/ChatSettings"
import { DEFAULT_ADVANCED_SETTINGS } from "../../../src/shared/AdvancedSettings"
interface ExtensionStateContextType extends ExtensionState {
didHydrateState: boolean
@@ -17,7 +18,6 @@ interface ExtensionStateContextType extends ExtensionState {
openRouterModels: Record<string, ModelInfo>
openAiModels: string[]
mcpServers: McpServer[]
mcpMarketplaceCatalog: McpMarketplaceCatalog
filePaths: string[]
setApiConfiguration: (config: ApiConfiguration) => void
setCustomInstructions: (value?: string) => void
@@ -39,6 +39,9 @@ export const ExtensionStateContextProvider: React.FC<{
chatSettings: DEFAULT_CHAT_SETTINGS,
isLoggedIn: false,
platform: DEFAULT_PLATFORM,
vscMachineId: "",
advancedSettings: DEFAULT_ADVANCED_SETTINGS,
hideTelemetryOptIn: false,
})
const [didHydrateState, setDidHydrateState] = useState(false)
const [showWelcome, setShowWelcome] = useState(false)
@@ -50,7 +53,7 @@ export const ExtensionStateContextProvider: React.FC<{
const [openAiModels, setOpenAiModels] = useState<string[]>([])
const [mcpServers, setMcpServers] = useState<McpServer[]>([])
const [mcpMarketplaceCatalog, setMcpMarketplaceCatalog] = useState<McpMarketplaceCatalog>({ items: [] })
const handleMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
switch (message.type) {
@@ -122,12 +125,6 @@ export const ExtensionStateContextProvider: React.FC<{
setMcpServers(message.mcpServers ?? [])
break
}
case "mcpMarketplaceCatalog": {
if (message.mcpMarketplaceCatalog) {
setMcpMarketplaceCatalog(message.mcpMarketplaceCatalog)
}
break
}
}
}, [])
@@ -145,7 +142,6 @@ export const ExtensionStateContextProvider: React.FC<{
openRouterModels,
openAiModels,
mcpServers,
mcpMarketplaceCatalog,
filePaths,
setApiConfiguration: (value) =>
setState((prevState) => ({
+6 -1
View File
@@ -4,11 +4,16 @@ import "./index.css"
import App from "./App"
import reportWebVitals from "./reportWebVitals"
import "../../node_modules/@vscode/codicons/dist/codicon.css"
import { PostHogProvider } from "posthog-js/react"
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement)
root.render(
<React.StrictMode>
<App />
<PostHogProvider
apiKey="phc_5WnLHpYyC30Bsb7VSJ6DzcPXZ34JSF08DJLyM7svZ15"
options={{ api_host: "https://us.i.posthog.com", capture_pageview: false }}>
<App />
</PostHogProvider>
</React.StrictMode>,
)
+1 -15
View File
@@ -1,4 +1,4 @@
import { McpMarketplaceCatalog, McpResource, McpResourceTemplate } from "../../../src/shared/mcp"
import { McpResource, McpResourceTemplate } from "../../../src/shared/mcp"
/**
* Matches a URI against an array of URI templates and returns the matching template
@@ -40,17 +40,3 @@ export function findMatchingResourceOrTemplate(
// If no exact match, try to find a matching template
return findMatchingTemplate(uri, templates)
}
/**
* Attempts to convert an MCP server name to its display name using the marketplace catalog
* @param serverName The server name/ID to look up
* @param mcpMarketplaceCatalog The marketplace catalog containing server metadata
* @returns The display name if found in catalog, otherwise returns the original server name
*/
export function getMcpServerDisplayName(serverName: string, mcpMarketplaceCatalog: McpMarketplaceCatalog): string {
// Find matching item in marketplace catalog
const catalogItem = mcpMarketplaceCatalog.items.find((item) => item.mcpId === serverName)
// Return display name if found, otherwise return original server name
return catalogItem?.name || serverName
}
-42
View File
@@ -1,42 +0,0 @@
import { useEffect, useRef } from "react"
type VoidFn = () => void
/**
* Runs `effectRef.current()` after `delay` ms whenever any of the `deps` change,
* but cancels/re-schedules if they change again before the delay.
*/
export function useDebounceEffect(effect: VoidFn, delay: number, deps: any[]) {
const callbackRef = useRef<VoidFn>(effect)
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
// Keep callbackRef current
useEffect(() => {
callbackRef.current = effect
}, [effect])
useEffect(() => {
// Clear any queued call
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
// Schedule a new call
timeoutRef.current = setTimeout(() => {
// always call the *latest* version of effect
callbackRef.current()
}, delay)
// Cleanup on unmount or next effect
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
}
// We want to reschedule if any item in `deps` changed,
// or if `delay` changed.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [delay, ...deps])
}