mirror of
https://github.com/cline/cline.git
synced 2026-09-16 21:01:52 +08:00
reconciling merge conflicts with main
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add translation to CODE_OF_CONDUCT, CONTRIBUTING and README to Arabic ar-sa.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Adding .clineignore guide
|
||||
+18
-2
@@ -1,9 +1,25 @@
|
||||
# Changelog
|
||||
|
||||
## [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
|
||||
- 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
|
||||
- Fix Mistral provider support for non-codestral models
|
||||
- Add advanced setting to disable browser tool
|
||||
- Add advanced setting to set chromium executable path for browser tool
|
||||
|
||||
## [3.3.2]
|
||||
|
||||
- Fix bug where OpenRouter requests would periodically not return cost/token stats, leading to context window limit errors
|
||||
- Make checkpoints more visible and keep track of restored checkpoints
|
||||
- Make checkpoints more visible and keep track of restored checkpoints
|
||||
|
||||
## [3.3.0]
|
||||
|
||||
@@ -34,7 +50,7 @@
|
||||
|
||||
## [3.2.10]
|
||||
|
||||
- Improve support for DeepSeek-R1 (deepseek-reasoner) model for OpenRouter, OpenAI-compatible, and DeepSeek direct
|
||||
- Improve support for DeepSeek-R1 (deepseek-reasoner) model for OpenRouter, OpenAI-compatible, and DeepSeek direct (thanks @Szpadel!)
|
||||
- Show Reasoning tokens for models that support it
|
||||
- Fix issues with switching models between Plan/Act modes
|
||||
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,47 @@
|
||||
# ميثاق المساهمين
|
||||
|
||||
## تعهدنا
|
||||
|
||||
نحن المساهمون والقائمون على هذا المشروع، نتعهد بتوفير بيئة مفتوحة ومرحبة، ونجعل المشاركة في مشروعنا ومجتمعنا تجربة خالية من التحرش للجميع، بغض النظر عن العمر، أو حجم الجسم، أو الإعاقة، أو العرق، أو الخصائص الجنسية، أو الهوية الجنسية والتعبير عنها، أو مستوى الخبرة، أو التعليم، أو الوضع الاجتماعي والاقتصادي، أو الجنسية، أو المظهر الشخصي، أو الدين، أو الهوية الجنسية والتوجه الجنسي.
|
||||
|
||||
## معاييرنا
|
||||
|
||||
أمثلة على السلوك الذي يساهم في خلق بيئة إيجابية تشمل:
|
||||
|
||||
- استخدام لغة ترحيبية وشاملة
|
||||
- احترام وجهات النظر والخبرات المختلفة
|
||||
- تقبل النقد البناء برحابة صدر
|
||||
- التركيز على ما هو الأفضل للمجتمع
|
||||
- إظهار التعاطف تجاه أعضاء المجتمع الآخرين
|
||||
|
||||
أمثلة على السلوك غير المقبول من قبل المشاركين تشمل:
|
||||
|
||||
- استخدام لغة أو صور جنسية والاهتمام الجنسي غير المرغوب فيه أو التحرش الجنسي
|
||||
- التصيد، والتعليقات المهينة/المسيئة، والهجمات الشخصية أو السياسية
|
||||
- التحرش العلني أو الخاص
|
||||
- نشر معلومات الآخرين الخاصة، مثل العنوان الفعلي أو الإلكتروني، دون إذن صريح
|
||||
- أي سلوك آخر يمكن اعتباره غير لائق في بيئة مهنية
|
||||
|
||||
## مسؤولياتنا
|
||||
|
||||
يتحمل القائمون على المشروع مسؤولية توضيح معايير السلوك المقبول، ومن المتوقع أن يتخذوا إجراءات تصحيحية مناسبة وعادلة استجابة لأي حالات سلوك غير مقبول.
|
||||
|
||||
يحق للقائمين على المشروع إزالة أو تعديل أو رفض التعليقات والالتزامات والتعليمات البرمجية وتعديلات 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
|
||||
@@ -0,0 +1,93 @@
|
||||
# المساهمة في 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 لا تقتصر فقط على كتابة الكود - إنها تتعلق بأن تكون جزءًا من مجتمع يُشكل مستقبل التطوير بمساعدة الذكاء الاصطناعي. لنبنِ شيئًا رائعًا معًا! 🚀
|
||||
@@ -0,0 +1,189 @@
|
||||
<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)
|
||||
@@ -0,0 +1,51 @@
|
||||
# Código de Conduta para Contribuidores
|
||||
|
||||
## Nosso Compromisso
|
||||
|
||||
|
||||
Com o objetivo de promover um ambiente aberto e acolhedor, nós, como contribuidores e mantenedores, nos comprometemos a tornar a participação em nosso projeto e comunidade uma experiência livre de assédio para todos, independentemente de idade, tamanho corporal, deficiência, etnia, características sexuais, identidade e expressão de gênero, nível de experiência, educação, status socioeconômico, nacionalidade, aparência pessoal, raça, religião ou orientação sexual.
|
||||
|
||||
## Nossos Padrões
|
||||
|
||||
Exemplos de comportamentos que contribuem para criar um ambiente positivo incluem:
|
||||
|
||||
- Uso de linguagem acolhedora e inclusiva
|
||||
- Respeito por diferentes pontos de vista e experiências
|
||||
- Aceitar críticas de maneira construtiva
|
||||
- Foco no que é melhor para a comunidade
|
||||
- Ser empático com outros membros da comunidade
|
||||
|
||||
|
||||
Exemplos de comportamentos inaceitáveis por parte dos participantes incluem:
|
||||
|
||||
- Uso de linguagem ou imagens sexualizadas e atenção ou avanços sexuais indesejados
|
||||
- Trollar, insultar, fazer comentários depreciativos, ataques pessoais ou políticos
|
||||
- Assédio público ou privado
|
||||
- Divulgar informações privadas sem autorização, como endereços físicos ou eletrônicos, sem permissão explícita
|
||||
- Outras condutas que poderiam ser consideradas inadequadas em um ambiente profissional
|
||||
|
||||
## Nossas Responsabilidades
|
||||
|
||||
Os mantenedores do projeto são responsáveis por esclarecer os padrões de comportamento aceitáveis e devem tomar ações corretivas apropriadas e justas em resposta a qualquer instância de comportamento inaceitável.
|
||||
|
||||
Os mantenedores têm o direito e a responsabilidade de remover, editar ou rejeitar comentários, commits, códigos, edições no wiki, issues e outras contribuições que não estejam alinhadas com este Código de Conduta. Também podem banir temporária ou permanentemente qualquer colaborador cujo comportamento seja considerado inapropriado, ameaçador, ofensivo ou prejudicial.
|
||||
|
||||
## Escopo
|
||||
|
||||
Este Código de Conduta se aplica tanto aos espaços do projeto quanto aos espaços públicos
|
||||
quando uma pessoa representa o projeto ou sua comunidade. Exemplos de
|
||||
representação de um projeto ou comunidade incluem o uso de um endereço de e-mail oficial do projeto,
|
||||
publicar em uma conta oficial de mídia social ou atuar como representante designado
|
||||
em um evento online ou offline. A representação de um projeto pode
|
||||
ser mais especificamente definido e esclarecido pelos mantenedores do projeto.
|
||||
|
||||
## Aplicação
|
||||
|
||||
Casos de comportamento abusivo, assediador ou inaceitáveis podem ser reportados entrando em contato com a equipe do projeto pelo email hi@cline.bot. Todas as queixas serão revisadas e investigadas confidencialmente. Mais detalhes sobre políticas específicas podem ser publicados separadamente.
|
||||
|
||||
Os mantenedores que não seguirem ou aplicarem este Código de Conduta de boa fé podem enfrentar repercussões temporárias ou permanentes determinadas por outros membros da liderança do projeto.
|
||||
|
||||
## Atribuição
|
||||
|
||||
Este Código de Conduta é adaptado do [Contributor Covenant](https://www.contributor-covenant.org), versão 1.4, disponível em https://www.contributor-covenant.org/version/1/4/code-of-conduct.html.
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# Contribuir para o Cline
|
||||
|
||||
Estamos felizes por você estar interessado em contribuir com o Cline. Seja corrigindo um erro, adicionando uma funcionalidade ou melhorando nossa documentação, cada contribuição torna o Cline mais inteligente! Para manter nossa comunidade viva e acolhedora, todos os membros devem cumprir nosso Código de Conduta [Código de Conduta](CODE_OF_CONDUCT.md).
|
||||
|
||||
## Relatar erros ou problemas
|
||||
|
||||
Relatar erros ajuda a melhorar o Cline para todos! Antes de criar um novo issue, revise as [issues existentes](https://github.com/cline/cline/issues) para evitar duplicações. Quando estiver pronto para relatar um erro, vá até nossa [página de Issues](https://github.com/cline/cline/issues/new/choose), onde você encontrará um modelo que ajudará a preencher as informações relevantes.
|
||||
|
||||
<blockquote class='warning-note'>
|
||||
🔐 <b>Importante:</b> Se você descobrir uma vulnerabilidade de segurança, utilize a <a href="https://github.com/cline/cline/security/advisories/new">ferramenta de segurança do GitHub</a> para relatá-la de forma privada.
|
||||
</blockquote>
|
||||
|
||||
## Escolher no que trabalhar
|
||||
|
||||
Procurando uma boa primeira contribuição? Consulte os problemas marcados com ["good first issue"](https://github.com/cline/cline/labels/good%20first%20issue) ou ["help wanted"](https://github.com/cline/cline/labels/help%20wanted). Estes foram especialmente selecionados para novos colaboradores e são áreas em que adoraríamos receber ajuda!
|
||||
|
||||
Também damos boas-vindas a contribuições para nossa [documentação](https://github.com/cline/cline/tree/main/docs). Seja corrigindo erros de digitação, melhorando guias existentes ou criando novos conteúdos educativos, queremos construir um repositório de recursos gerido pela comunidade que ajude todos a tirar o máximo proveito do Cline. Você pode começar explorando `/docs` e procurando áreas que precisam de melhorias.
|
||||
|
||||
Se planeja trabalhar em uma funcionalidade maior, crie primeiro uma [solicitação de funcionalidade](https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop) para que possamos discutir se ela se alinha à visão do Cline.
|
||||
|
||||
## Configurar o ambiente de desenvolvimento
|
||||
|
||||
1. **Extensões do VS Code**
|
||||
|
||||
- Ao abrir o projeto, o VS Code solicitará que você instale as extensões recomendadas.
|
||||
- Essas extensões são necessárias para o desenvolvimento – aceite todas as solicitações de instalação.
|
||||
- Caso tenha rejeitado as solicitações, você pode instalá-las manualmente na seção de extensões.
|
||||
|
||||
2. **Desenvolvimento local**
|
||||
- Execute `npm run install:all` para instalar as dependências.
|
||||
- Execute `npm run test` para rodar os testes localmente.
|
||||
- Antes de enviar um PR, execute `npm run format:fix` para formatar seu código.
|
||||
|
||||
## Escrever e enviar código
|
||||
|
||||
Qualquer pessoa pode contribuir com código para o Cline, mas pedimos que siga estas diretrizes para garantir que suas contribuições sejam integradas sem problemas:
|
||||
|
||||
1. **Mantenha os Pull Requests focados**
|
||||
|
||||
- Limite os PRs a uma única funcionalidade ou correção de erro.
|
||||
- Divida alterações maiores em PRs menores e coerentes.
|
||||
- Divida as alterações em commits lógicos que possam ser revisados independentemente.
|
||||
|
||||
2. **Qualidade do código**
|
||||
|
||||
- Execute `npm run lint` para verificar o estilo do código.
|
||||
- Execute `npm run format` para formatar automaticamente o código.
|
||||
- Todos os PRs devem passar nas verificações do CI, que incluem linting e formatação.
|
||||
- Resolva todos os avisos ou erros do ESLint antes de enviar.
|
||||
- Siga as melhores práticas para TypeScript e mantenha a segurança dos tipos.
|
||||
|
||||
3. **Testes**
|
||||
|
||||
- Adicione testes para novas funcionalidades.
|
||||
- Execute `npm test` para garantir que todos os testes passem.
|
||||
- Atualize testes existentes caso suas alterações os afetem.
|
||||
- Inclua tanto testes unitários quanto de integração onde for apropriado.
|
||||
|
||||
4. **Diretrizes de commits**
|
||||
|
||||
- Escreva mensagens de commit claras e descritivas.
|
||||
- Use o formato convencional (por exemplo, "feat:", "fix:", "docs:").
|
||||
- Faça referência aos issues relevantes nos commits usando #número-do-issue.
|
||||
|
||||
5. **Antes de enviar**
|
||||
|
||||
- Faça rebase com sua branch com a última versão da branch principal (main).
|
||||
- Certifique-se de que sua branch seja construída corretamente.
|
||||
- Verifique se todos os testes passam.
|
||||
- Revise suas alterações para remover qualquer código de depuração ou logs desnecessários.
|
||||
|
||||
6. **Descrição do Pull Request**
|
||||
- Descreva claramente o que suas alterações fazem.
|
||||
- Inclua passos para testar as alterações.
|
||||
- Liste quaisquer mudanças importantes.
|
||||
- Adicione capturas de tela para mudanças na interface do usuário.
|
||||
|
||||
## Acordo de contribuição
|
||||
|
||||
Ao enviar um Pull Request, você concorda que suas contribuições serão licenciadas sob a mesma licença do projeto ([Apache 2.0](LICENSE)).
|
||||
|
||||
Lembre-se: Contribuir com o Cline não é apenas escrever código – é fazer parte de uma comunidade que está moldando o futuro do desenvolvimento assistido por IA. Vamos criar algo incrível juntos! 🚀
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
# Cline – #1 no 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>Baixar no VS Marketplace</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>Solicitação de Funcionalidades</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://cline.bot/join-us" target="_blank"><strong>Estamos Contratando!</strong></a>
|
||||
</td>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Conheça o Cline: um assistente de IA que pode usar seu **CLI** e **Editor**.
|
||||
|
||||
Graças às [habilidades avançadas do Claude 3.5 Sonnet](https://www-cdn.anthropic.com/fed9cc193a14b84131812372d8d5857f8f304c52/Model_Card_Claude_3_Addendum.pdf), o Cline pode lidar com tarefas complexas de desenvolvimento de software passo a passo. Com ferramentas que permitem criar e editar arquivos, explorar grandes projetos, usar o navegador e executar comandos no terminal (com sua aprovação), ele pode ajudar você de maneiras que vão além da inclusão de código ou suporte técnico. O Cline pode é capaz inclusive de usar o Model Context Protocol (MCP) para criar novas ferramentas e expandir seus próprios recursos. Embora os scripts de IA autônomas tradicionalmente sejam executados em ambientes isolados, esta extensão oferece uma GUI com um humano no circuito para aprovar cada alteração de arquivo e comando de terminal, fornecendo uma maneira segura e acessível de explorar todo o potencial da IA.
|
||||
|
||||
1. Insira sua tarefa e adicione imagens para transformar mockups em aplicativos funcionais ou corrigir erros através de capturas de tela.
|
||||
|
||||
2. O Cline começará analisando a estrutura do seu arquivo e os ASTs do código-fonte, fazendo pesquisas com Regex e lendo arquivos relevantes para se orientar em projetos existentes. Ao gerenciar cuidadosamente as informações agregadas, o Cline pode fornecer assistência valiosa mesmo em projetos grandes e complexos, sem sobrecarregar a janela de contexto.
|
||||
3. Assim que ele tiver as informações necessárias, o Cline poderá:
|
||||
- Criar e editar arquivos + monitorar erros de Linter/Compilador, para que você possa corrigir proativamente problemas como importações ausentes e erros de sintaxe.
|
||||
- Executar comandos diretamente no terminal e monitorar o resultado, para que você possa responder a problemas do servidor de desenvolvimento após editar um arquivo.
|
||||
- Para tarefas de desenvolvimento web, o Cline pode iniciar o site em um navegador headless, clicar, digitar, fazer scroll e capturar capturas de tela + registros de console, para que você possa corrigir erros em tempo de execução e erros visuais.
|
||||
|
||||
> [!TIP]
|
||||
> Use o atalho de teclado `CMD/CTRL + Shift + P` para abrir a lista de comandos possiveis e digite "Cline: Abrir em nova aba" para abrir a extensão como uma aba no seu editor. Dessa forma, você pode usar o Cline junto com seu explorador de arquivos e ver mais claramente como seu espaço de trabalho muda.
|
||||
|
||||
---
|
||||
|
||||
<img align="right" width="340" src="https://github.com/user-attachments/assets/3cf21e04-7ce9-4d22-a7b9-ba2c595e88a4">
|
||||
|
||||
### Use qualquer API ou modelo
|
||||
|
||||
O Cline oferece suporte a provedores de API como OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure e GCP Vertex. Você também pode configurar qualquer API compatível com OpenAI ou usar um modelo local via LM Studio/Ollama. Se você usar o OpenRouter, a extensão recuperará sua lista de modelos mais recentes, para que você possa usar os modelos mais novos assim que estiverem disponíveis.
|
||||
|
||||
A extensão também rastreia o uso total de tokens e os custos da API para todo o ciclo de tarefas e solicitações individuais, para que você seja informado sobre as despesas em cada etapa.
|
||||
|
||||
<!-- Pixel transparente para criar uma quebra de linha após a imagem flutuante -->
|
||||
|
||||
<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">
|
||||
|
||||
### Executar comandos no terminal
|
||||
|
||||
Graças às novas [atualizações de integração do Shell no VSCode v1.93](https://code.visualstudio.com/updates/v1_93#_terminal-shell-integration-api), o Cline pode executar comandos diretamente no seu terminal e receber o resultado. Isso permite que você execute uma variedade de tarefas, desde instalar pacotes e executar build scripts para fazer deploy de aplicações, gerenciar bancos de dados e executar testes, adaptando-se ao seu ambiente de desenvolvimento e ferramentas para fazer o trabalho corretamente.
|
||||
|
||||
Para processos de longa duração, como servidores de desenvolvimento, use o botão "Continuar durante a execução" para permitir que o Cline continue a tarefa enquanto o comando é executado em segundo plano. Enquanto Cline trabalha, você será notificado sobre novas saídas do terminal, para que possa responder a problemas que possam surgir, como erros de compilação ao editar arquivos.
|
||||
|
||||
<!-- Pixel transparente para criar uma quebra de linha após a imagem flutuante -->
|
||||
|
||||
<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">
|
||||
|
||||
### Criar e editar arquivos
|
||||
|
||||
Cline pode criar e editar arquivos diretamente no seu editor, apresentando um diff com as alterações. Você pode editar ou reverter as alterações do Cline diretamente no editor de diff ou fornecer feedback no chat até ficar satisfeito com o resultado. Cline também monitora erros de linter/compilador (importações ausentes, erros de sintaxe, etc.) para que possa corrigir problemas que surgem ao longo do caminho por conta própria.
|
||||
|
||||
Todas as alterações feitas pelo Cline são registradas na Linha do tempo do arquivo, fornecendo uma maneira fácil de rastrear e reverter modificações, caso seja necessário.
|
||||
|
||||
<!-- Pixel transparente para criar uma quebra de linha após a imagem flutuante -->
|
||||
|
||||
<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">
|
||||
|
||||
### Uso do navegador
|
||||
|
||||
Com a nova habilidade de [uso de computador](https://www.anthropic.com/news/3-5-models-and-computer-use) do Claude Sonnet 3.5, Cline pode abrir um navegador, clicar em elementos, digitar texto e rolar, capturando a tela e logs de console. Isso permite depurar de maneira interativa, testes end-to-end e até mesmo uso geral da web. Isso lhe dá autonomia para solucionar erros visuais e problemas em tempo de execução sem precisar copiar e colar logs dos erros.
|
||||
|
||||
Tente pedir a Cline para "testar o aplicativo" e observe enquanto o Cline executa um comando como `npm run dev`, inicia seu servidor de desenvolvimento local em um navegador e executa uma série de testes para confirmar se tudo funciona. [Veja uma demonstração aqui.](https://x.com/sdrzn/status/1850880547825823989)
|
||||
|
||||
<!-- Pixel transparente para crear un salto de línea después de la imagen flotante -->
|
||||
|
||||
<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">
|
||||
|
||||
### "adicione uma ferramenta que..."
|
||||
|
||||
Graças ao [Model Context Protocol](https://github.com/modelcontextprotocol), o Cline pode expandir seus recursos por meio de ferramentas personalizadas. Embora você possa usar [servidores criados pela comunidade](https://github.com/modelcontextprotocol/servers), Cline pode criar e instalar ferramentas especificamente para seu fluxo de trabalho. Basta pedir ao Cline para "adicionar uma ferramenta" e ele cuidará de tudo, desde a criação de um novo servidor MCP até a instalação na extensão. Essas ferramentas personalizadas se tornam parte do conjunto de ferramentas da Cline e estão prontas para serem usadas em tarefas futuras.
|
||||
|
||||
- "adicione uma ferramenta que recupere tickets do Jira": Recupere ACs de tickets e coloque Cline para trabalhar
|
||||
- "adicione uma ferramenta que gerencie AWS EC2s": verifique as métricas do servidor e aumente ou diminua as instâncias
|
||||
- "adicione uma ferramenta para recuperar os últimos incidentes do PagerDuty": Recupere detalhes e peça ao Cline para corrigir erros
|
||||
|
||||
<!-- Pixel transparente para crear un salto de línea después de la imagen flotante -->
|
||||
|
||||
<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">
|
||||
|
||||
### Adicione contexto
|
||||
|
||||
**`@url`:** Insira uma URL para a extensão recuperar e converter para Markdown, que é útil quando você deseja fornecer ao Cline documentos mais recentes
|
||||
|
||||
**`@problems`:** Adicionar erros e avisos do espaço de trabalho (painel 'Problemas') que o Cline deve corrigir
|
||||
|
||||
**`@file`:** Adicione o conteúdo de um arquivo para que você não precise desperdiçar solicitações de API para aprovar a leitura do arquivo (+ para pesquisar arquivos)
|
||||
|
||||
**`@folder`:** Adicione arquivos de uma pasta por vez para acelerar ainda mais seu fluxo de trabalho
|
||||
|
||||
<!-- Pixel transparente para criar uma quebra de linha após a imagem flutuante -->
|
||||
|
||||
<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">
|
||||
|
||||
### Checkpoints: Comparar e Restaurar
|
||||
|
||||
Enquanto Cline trabalha em uma tarefa, a extensão cria um instantâneo de seu espaço de trabalho em cada etapa. Você pode usar o botão "Comparar" para ver a diferença entre o instantâneo e seu espaço de trabalho atual, e o botão "Restaurar" para retornar a esse ponto.
|
||||
|
||||
Por exemplo, se estiver trabalhando com um servidor web local, você pode usar 'Restaurar somente o espaço de trabalho' para testar rapidamente diferentes versões do seu aplicativo e, em seguida, 'Restaurar tarefa e espaço de trabalho' quando encontrar a versão na qual deseja continuar trabalhando. Isso permite que você explore diferentes abordagens com segurança sem perder o progresso.
|
||||
|
||||
<!-- Pixel transparente para crear un salto de línea después de la imagen flotante -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
## Contribuições
|
||||
|
||||
Para contribuir com o projeto, comece com nosso [Guia de Contribuição](CONTRIBUTING.md) para aprender o básico. Você também pode entrar no nosso [Discord](https://discord.gg/cline) para bater papo com outros colaboradores no canal `#contributors`. Se você está procurando um emprego de período integral, confira nossas vagas em aberto na nossa [página de carreiras](https://cline.bot/join-us).
|
||||
|
||||
<details>
|
||||
<summary>Instruções para desenvolvimento local</summary>
|
||||
|
||||
1. Clone o repositório _(Necessário [git-lfs](https://git-lfs.com/))_:
|
||||
```bash
|
||||
git clone https://github.com/cline/cline.git
|
||||
```
|
||||
2. Abra o projeto no VSCode:
|
||||
```bash
|
||||
code cline
|
||||
```
|
||||
3. Instale as dependências necessárias para a extensão e webview-gui:
|
||||
```bash
|
||||
npm run install:all
|
||||
```
|
||||
4. Inicie pressionando `F5` (ou `Executar`->`Iniciar Depuração`) para abrir uma nova janela do VSCode com a extensão carregada. (Pode ser necessário instalar a [extensão esbuild problem matchers](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers) se você encontrar problemas ao compilar seu projeto.)
|
||||
|
||||
</details>
|
||||
|
||||
## Licença
|
||||
|
||||
[Apache 2.0 © 2024 Cline Bot Inc.](./LICENSE)
|
||||
Generated
+473
-42
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.3.1",
|
||||
"version": "3.4.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.3.1",
|
||||
"version": "3.4.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/bedrock-sdk": "^0.10.2",
|
||||
"@anthropic-ai/sdk": "^0.26.0",
|
||||
"@anthropic-ai/vertex-sdk": "^0.4.1",
|
||||
"@google/generative-ai": "^0.18.0",
|
||||
"@mistralai/mistralai": "^1.3.6",
|
||||
"@mistralai/mistralai": "^1.5.0",
|
||||
"@modelcontextprotocol/sdk": "^1.0.1",
|
||||
"@types/clone-deep": "^4.0.4",
|
||||
"@types/get-folder-size": "^3.0.4",
|
||||
@@ -48,7 +48,7 @@
|
||||
"tree-sitter-wasms": "^0.1.11",
|
||||
"turndown": "^7.2.0",
|
||||
"web-tree-sitter": "^0.22.6",
|
||||
"zod": "^3.23.8"
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.27.12",
|
||||
@@ -63,7 +63,7 @@
|
||||
"@vscode/test-cli": "^0.0.9",
|
||||
"@vscode/test-electron": "^2.4.0",
|
||||
"chai": "^4.3.10",
|
||||
"esbuild": "^0.21.5",
|
||||
"esbuild": "^0.25.0",
|
||||
"eslint": "^8.57.0",
|
||||
"husky": "^9.1.7",
|
||||
"npm-run-all": "^4.1.5",
|
||||
@@ -2533,10 +2533,78 @@
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz",
|
||||
"integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz",
|
||||
"integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2547,7 +2615,347 @@
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz",
|
||||
"integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz",
|
||||
"integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz",
|
||||
"integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz",
|
||||
"integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz",
|
||||
"integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz",
|
||||
"integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz",
|
||||
"integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz",
|
||||
"integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
@@ -3690,13 +4098,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mistralai/mistralai": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.4.0.tgz",
|
||||
"integrity": "sha512-xA3DAtIDh4Qgr1EoSuiGVE+2ABNrxpcTeC0kSXYbkDNUGdthalLAH7DgbG0fkKZ7TN8xdWXQq2WiIghp/O96Eg==",
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.5.0.tgz",
|
||||
"integrity": "sha512-AIn8pwAwA/fDvEUvmkt+40zH1ZmfaG3Q7oUWl17GUEC1tU7ZPwYz8Cv9P59lyS1SisHdDSu81oknO7f1ywkz8Q==",
|
||||
"dependencies": {
|
||||
"zod-to-json-schema": "^3.24.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": ">= 3"
|
||||
}
|
||||
},
|
||||
"node_modules/@mistralai/mistralai/node_modules/zod-to-json-schema": {
|
||||
"version": "3.24.1",
|
||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz",
|
||||
"integrity": "sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.24.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@mixmark-io/domino": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz",
|
||||
@@ -6650,6 +7070,15 @@
|
||||
"devtools-protocol": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/chromium-bidi/node_modules/zod": {
|
||||
"version": "3.23.8",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
|
||||
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/ci-info": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
|
||||
@@ -7472,9 +7901,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
|
||||
"integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -7482,32 +7911,34 @@
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.21.5",
|
||||
"@esbuild/android-arm": "0.21.5",
|
||||
"@esbuild/android-arm64": "0.21.5",
|
||||
"@esbuild/android-x64": "0.21.5",
|
||||
"@esbuild/darwin-arm64": "0.21.5",
|
||||
"@esbuild/darwin-x64": "0.21.5",
|
||||
"@esbuild/freebsd-arm64": "0.21.5",
|
||||
"@esbuild/freebsd-x64": "0.21.5",
|
||||
"@esbuild/linux-arm": "0.21.5",
|
||||
"@esbuild/linux-arm64": "0.21.5",
|
||||
"@esbuild/linux-ia32": "0.21.5",
|
||||
"@esbuild/linux-loong64": "0.21.5",
|
||||
"@esbuild/linux-mips64el": "0.21.5",
|
||||
"@esbuild/linux-ppc64": "0.21.5",
|
||||
"@esbuild/linux-riscv64": "0.21.5",
|
||||
"@esbuild/linux-s390x": "0.21.5",
|
||||
"@esbuild/linux-x64": "0.21.5",
|
||||
"@esbuild/netbsd-x64": "0.21.5",
|
||||
"@esbuild/openbsd-x64": "0.21.5",
|
||||
"@esbuild/sunos-x64": "0.21.5",
|
||||
"@esbuild/win32-arm64": "0.21.5",
|
||||
"@esbuild/win32-ia32": "0.21.5",
|
||||
"@esbuild/win32-x64": "0.21.5"
|
||||
"@esbuild/aix-ppc64": "0.25.0",
|
||||
"@esbuild/android-arm": "0.25.0",
|
||||
"@esbuild/android-arm64": "0.25.0",
|
||||
"@esbuild/android-x64": "0.25.0",
|
||||
"@esbuild/darwin-arm64": "0.25.0",
|
||||
"@esbuild/darwin-x64": "0.25.0",
|
||||
"@esbuild/freebsd-arm64": "0.25.0",
|
||||
"@esbuild/freebsd-x64": "0.25.0",
|
||||
"@esbuild/linux-arm": "0.25.0",
|
||||
"@esbuild/linux-arm64": "0.25.0",
|
||||
"@esbuild/linux-ia32": "0.25.0",
|
||||
"@esbuild/linux-loong64": "0.25.0",
|
||||
"@esbuild/linux-mips64el": "0.25.0",
|
||||
"@esbuild/linux-ppc64": "0.25.0",
|
||||
"@esbuild/linux-riscv64": "0.25.0",
|
||||
"@esbuild/linux-s390x": "0.25.0",
|
||||
"@esbuild/linux-x64": "0.25.0",
|
||||
"@esbuild/netbsd-arm64": "0.25.0",
|
||||
"@esbuild/netbsd-x64": "0.25.0",
|
||||
"@esbuild/openbsd-arm64": "0.25.0",
|
||||
"@esbuild/openbsd-x64": "0.25.0",
|
||||
"@esbuild/sunos-x64": "0.25.0",
|
||||
"@esbuild/win32-arm64": "0.25.0",
|
||||
"@esbuild/win32-ia32": "0.25.0",
|
||||
"@esbuild/win32-x64": "0.25.0"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
@@ -13388,9 +13819,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.23.8",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
|
||||
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
|
||||
"version": "3.24.2",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
|
||||
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
|
||||
+15
-5
@@ -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.3.2",
|
||||
"version": "3.4.4",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"galleryBanner": {
|
||||
"color": "#617A91",
|
||||
@@ -73,7 +73,7 @@
|
||||
{
|
||||
"command": "cline.mcpButtonClicked",
|
||||
"title": "MCP Servers",
|
||||
"icon": "$(server)"
|
||||
"icon": "$(extensions)"
|
||||
},
|
||||
{
|
||||
"command": "cline.historyButtonClicked",
|
||||
@@ -172,6 +172,11 @@
|
||||
"default": true,
|
||||
"description": "Enables extension to save checkpoints of workspace throughout the task."
|
||||
},
|
||||
"cline.disableBrowserTool": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Disables extension from spawning browser session."
|
||||
},
|
||||
"cline.modelSettings.o3Mini.reasoningEffort": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -181,6 +186,11 @@
|
||||
],
|
||||
"default": "medium",
|
||||
"description": "Controls the reasoning effort when using the o3-mini model. Higher values may result in more thorough but slower responses."
|
||||
},
|
||||
"cline.chromeExecutablePath": {
|
||||
"type": "string",
|
||||
"default": null,
|
||||
"description": "Path to Chrome executable for browser use functionality. If not set, the extension will attempt to find or download it automatically."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,7 +233,7 @@
|
||||
"@vscode/test-cli": "^0.0.9",
|
||||
"@vscode/test-electron": "^2.4.0",
|
||||
"chai": "^4.3.10",
|
||||
"esbuild": "^0.21.5",
|
||||
"esbuild": "^0.25.0",
|
||||
"eslint": "^8.57.0",
|
||||
"husky": "^9.1.7",
|
||||
"npm-run-all": "^4.1.5",
|
||||
@@ -236,7 +246,7 @@
|
||||
"@anthropic-ai/sdk": "^0.26.0",
|
||||
"@anthropic-ai/vertex-sdk": "^0.4.1",
|
||||
"@google/generative-ai": "^0.18.0",
|
||||
"@mistralai/mistralai": "^1.3.6",
|
||||
"@mistralai/mistralai": "^1.5.0",
|
||||
"@modelcontextprotocol/sdk": "^1.0.1",
|
||||
"@types/clone-deep": "^4.0.4",
|
||||
"@types/get-folder-size": "^3.0.4",
|
||||
@@ -271,6 +281,6 @@
|
||||
"tree-sitter-wasms": "^0.1.11",
|
||||
"turndown": "^7.2.0",
|
||||
"web-tree-sitter": "^0.22.6",
|
||||
"zod": "^3.23.8"
|
||||
"zod": "^3.24.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,35 +8,50 @@ 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
|
||||
private client: AnthropicBedrock | any
|
||||
private initializationPromise: Promise<void>
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.initializationPromise = this.initializeClient()
|
||||
}
|
||||
|
||||
const clientConfig: any = {
|
||||
private async initializeClient() {
|
||||
let clientConfig: any = {
|
||||
awsRegion: this.options.awsRegion || "us-east-1",
|
||||
}
|
||||
|
||||
if (this.options.awsUseProfile) {
|
||||
// Use profile-based credentials if enabled
|
||||
if (this.options.awsProfile) {
|
||||
clientConfig.credentials = fromIni({
|
||||
profile: this.options.awsProfile,
|
||||
})
|
||||
} else {
|
||||
// Use default profile if no specific profile is set
|
||||
clientConfig.credentials = fromIni()
|
||||
}
|
||||
} 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
|
||||
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)
|
||||
}
|
||||
|
||||
this.client = new AnthropicBedrock(clientConfig)
|
||||
}
|
||||
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
|
||||
@@ -13,7 +13,7 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
this.options = options
|
||||
this.client = new OpenAI({
|
||||
baseURL: this.options.liteLlmBaseUrl || "http://localhost:4000",
|
||||
apiKey: "not-needed",
|
||||
apiKey: this.options.liteLlmApiKey || "noop",
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ export class MistralHandler implements ApiHandler {
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new Mistral({
|
||||
serverURL: "https://api.mistral.ai",
|
||||
apiKey: this.options.mistralApiKey,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ export class OpenAiHandler implements ApiHandler {
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
return {
|
||||
id: this.options.openAiModelId ?? "",
|
||||
info: openAiModelInfoSaneDefaults,
|
||||
info: this.options.openAiModelInfo ?? openAiModelInfoSaneDefaults,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ApiHandler } from "../"
|
||||
import { ApiHandlerOptions, QwenModelId, ModelInfo, qwenDefaultModelId, qwenModels } from "../../shared/api"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
|
||||
export class QwenHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
@@ -34,17 +35,21 @@ export class QwenHandler implements ApiHandler {
|
||||
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const model = this.getModel()
|
||||
const isDeepseekReasoner = model.id.includes("deepseek-r1")
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
if (isDeepseekReasoner) {
|
||||
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
|
||||
}
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: model.id,
|
||||
max_completion_tokens: model.info.maxTokens,
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
...(model.id === "deepseek-r1" ? {} : { temperature: 0 }),
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../..
|
||||
import { ApiHandler } from "../index"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
|
||||
export class RequestyHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
@@ -16,30 +15,32 @@ export class RequestyHandler implements ApiHandler {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://router.requesty.ai/v1",
|
||||
apiKey: this.options.requestyApiKey,
|
||||
defaultHeaders: {
|
||||
"HTTP-Referer": "https://cline.bot",
|
||||
"X-Title": "Cline",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const modelId = this.options.requestyModelId ?? ""
|
||||
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
|
||||
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
if (isDeepseekReasoner) {
|
||||
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
|
||||
}
|
||||
|
||||
// @ts-ignore-next-line
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: modelId,
|
||||
messages: openAiMessages,
|
||||
temperature: 0,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
...(modelId === "openai/o3-mini" ? { reasoning_effort: this.options.o3MiniReasoningEffort || "medium" } : {}),
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
@@ -56,11 +57,25 @@ export class RequestyHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Requesty usage includes an extra field for Anthropic use cases.
|
||||
// Safely cast the prompt token details section to the appropriate structure.
|
||||
interface RequestyUsage extends OpenAI.CompletionUsage {
|
||||
prompt_tokens_details?: {
|
||||
caching_tokens?: number
|
||||
cached_tokens?: number
|
||||
}
|
||||
total_cost?: number
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
const usage = chunk.usage as RequestyUsage
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
inputTokens: usage.prompt_tokens || 0,
|
||||
outputTokens: usage.completion_tokens || 0,
|
||||
cacheWriteTokens: usage.prompt_tokens_details?.caching_tokens || undefined,
|
||||
cacheReadTokens: usage.prompt_tokens_details?.cached_tokens || undefined,
|
||||
totalCost: usage.total_cost || undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,14 +86,11 @@ export async function* streamOpenRouterFormatRequest(
|
||||
|
||||
let temperature = 0
|
||||
let topP: number | undefined = undefined
|
||||
// Handle models based on deepseek-r1
|
||||
if (model.id.startsWith("deepseek/deepseek-r1") || model.id === "perplexity/sonar-reasoning") {
|
||||
// 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
|
||||
// Recommended values from DeepSeek
|
||||
temperature = 0.7
|
||||
topP = 0.95
|
||||
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -1,30 +1,24 @@
|
||||
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 while merging consecutive messages with the same role.
|
||||
* Converts Anthropic messages to OpenAI format and merges 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 combined
|
||||
* @returns Array of OpenAI messages where consecutive messages with the same role are merged together
|
||||
*/
|
||||
export function convertToR1Format(messages: AnthropicMessage[]): Message[] {
|
||||
return messages.reduce<Message[]>((merged, message) => {
|
||||
export function convertToR1Format(messages: Anthropic.Messages.MessageParam[]): OpenAI.Chat.ChatCompletionMessageParam[] {
|
||||
return messages.reduce<OpenAI.Chat.ChatCompletionMessageParam[]>((merged, message) => {
|
||||
const lastMessage = merged[merged.length - 1]
|
||||
let messageContent: string | (ContentPartText | ContentPartImage)[] = ""
|
||||
let messageContent: string | (OpenAI.Chat.ChatCompletionContentPartText | OpenAI.Chat.ChatCompletionContentPartImage)[] =
|
||||
""
|
||||
let hasImages = false
|
||||
|
||||
// Convert content to appropriate format
|
||||
if (Array.isArray(message.content)) {
|
||||
const textParts: string[] = []
|
||||
const imageParts: ContentPartImage[] = []
|
||||
const imageParts: OpenAI.Chat.ChatCompletionContentPartImage[] = []
|
||||
|
||||
message.content.forEach((part) => {
|
||||
if (part.type === "text") {
|
||||
@@ -40,7 +34,7 @@ export function convertToR1Format(messages: AnthropicMessage[]): Message[] {
|
||||
})
|
||||
|
||||
if (hasImages) {
|
||||
const parts: (ContentPartText | ContentPartImage)[] = []
|
||||
const parts: (OpenAI.Chat.ChatCompletionContentPartText | OpenAI.Chat.ChatCompletionContentPartImage)[] = []
|
||||
if (textParts.length > 0) {
|
||||
parts.push({ type: "text", text: textParts.join("\n") })
|
||||
}
|
||||
@@ -53,13 +47,11 @@ export function convertToR1Format(messages: AnthropicMessage[]): Message[] {
|
||||
messageContent = message.content
|
||||
}
|
||||
|
||||
// If last message has same role, merge the content
|
||||
// If the last message has the same role, merge the content
|
||||
if (lastMessage?.role === message.role) {
|
||||
if (typeof lastMessage.content === "string" && typeof messageContent === "string") {
|
||||
lastMessage.content += `\n${messageContent}`
|
||||
}
|
||||
// If either has image content, convert both to array format
|
||||
else {
|
||||
} else {
|
||||
const lastContent = Array.isArray(lastMessage.content)
|
||||
? lastMessage.content
|
||||
: [{ type: "text" as const, text: lastMessage.content || "" }]
|
||||
@@ -69,30 +61,32 @@ export function convertToR1Format(messages: AnthropicMessage[]): Message[] {
|
||||
: [{ type: "text" as const, text: messageContent }]
|
||||
|
||||
if (message.role === "assistant") {
|
||||
const mergedContent = [...lastContent, ...newContent] as AssistantMessage["content"]
|
||||
const mergedContent = [
|
||||
...lastContent,
|
||||
...newContent,
|
||||
] as OpenAI.Chat.ChatCompletionAssistantMessageParam["content"]
|
||||
lastMessage.content = mergedContent
|
||||
} else {
|
||||
const mergedContent = [...lastContent, ...newContent] as UserMessage["content"]
|
||||
const mergedContent = [...lastContent, ...newContent] as OpenAI.Chat.ChatCompletionUserMessageParam["content"]
|
||||
lastMessage.content = mergedContent
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Add as new message with the correct type based on role
|
||||
// Adds new message with the correct type based on role
|
||||
if (message.role === "assistant") {
|
||||
const newMessage: AssistantMessage = {
|
||||
const newMessage: OpenAI.Chat.ChatCompletionAssistantMessageParam = {
|
||||
role: "assistant",
|
||||
content: messageContent as AssistantMessage["content"],
|
||||
content: messageContent as OpenAI.Chat.ChatCompletionAssistantMessageParam["content"],
|
||||
}
|
||||
merged.push(newMessage)
|
||||
} else {
|
||||
const newMessage: UserMessage = {
|
||||
const newMessage: OpenAI.Chat.ChatCompletionUserMessageParam = {
|
||||
role: "user",
|
||||
content: messageContent as UserMessage["content"],
|
||||
content: messageContent as OpenAI.Chat.ChatCompletionUserMessageParam["content"],
|
||||
}
|
||||
merged.push(newMessage)
|
||||
}
|
||||
}
|
||||
|
||||
return merged
|
||||
}, [])
|
||||
}
|
||||
|
||||
+75
-57
@@ -354,15 +354,17 @@ export class Cline {
|
||||
break
|
||||
}
|
||||
|
||||
// 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)
|
||||
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 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()
|
||||
|
||||
@@ -1260,12 +1262,12 @@ export class Cline {
|
||||
throw new Error("MCP hub not available")
|
||||
}
|
||||
|
||||
let systemPrompt = await SYSTEM_PROMPT(
|
||||
cwd,
|
||||
this.api.getModel().info.supportsComputerUse ?? false,
|
||||
mcpHub,
|
||||
this.browserSettings,
|
||||
)
|
||||
const disableBrowserTool = vscode.workspace.getConfiguration("cline").get<boolean>("disableBrowserTool") ?? false
|
||||
const modelSupportsComputerUse = this.api.getModel().info.supportsComputerUse ?? false
|
||||
|
||||
const supportsComputerUse = modelSupportsComputerUse && !disableBrowserTool // only enable computer use if the model supports it and the user hasn't disabled it
|
||||
|
||||
let systemPrompt = await SYSTEM_PROMPT(cwd, supportsComputerUse, mcpHub, this.browserSettings)
|
||||
|
||||
let settingsCustomInstructions = this.customInstructions?.trim()
|
||||
const clineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
|
||||
@@ -1540,37 +1542,44 @@ export class Cline {
|
||||
this.didAlreadyUseTool = true
|
||||
}
|
||||
|
||||
// The user can approve, reject, or provide feedback (rejection). However the user may also send a message along with an approval, in which case we add a separate user message with this feedback.
|
||||
const pushAdditionalToolFeedback = (feedback?: string, images?: string[]) => {
|
||||
if (!feedback && !images) {
|
||||
return
|
||||
}
|
||||
const content = formatResponse.toolResult(
|
||||
`The user provided the following feedback:\n<feedback>\n${feedback}\n</feedback>`,
|
||||
images,
|
||||
)
|
||||
if (typeof content === "string") {
|
||||
this.userMessageContent.push({
|
||||
type: "text",
|
||||
text: content,
|
||||
})
|
||||
} else {
|
||||
this.userMessageContent.push(...content)
|
||||
}
|
||||
}
|
||||
|
||||
const askApproval = async (type: ClineAsk, partialMessage?: string) => {
|
||||
const { response, text, images } = await this.ask(type, partialMessage, false)
|
||||
if (response !== "yesButtonClicked") {
|
||||
if (response === "messageResponse") {
|
||||
await this.say("user_feedback", text, images)
|
||||
pushToolResult(formatResponse.toolResult(formatResponse.toolDeniedWithFeedback(text), images))
|
||||
// this.userMessageContent.push({
|
||||
// type: "text",
|
||||
// text: `${toolDescription()}`,
|
||||
// })
|
||||
// this.toolResults.push({
|
||||
// type: "tool_result",
|
||||
// tool_use_id: toolUseId,
|
||||
// content: this.formatToolResponseWithImages(
|
||||
// await this.formatToolDeniedFeedback(text),
|
||||
// images
|
||||
// ),
|
||||
// })
|
||||
this.didRejectTool = true
|
||||
return false
|
||||
}
|
||||
// User pressed reject button or responded with a message, which we treat as a rejection
|
||||
pushToolResult(formatResponse.toolDenied())
|
||||
// this.toolResults.push({
|
||||
// type: "tool_result",
|
||||
// tool_use_id: toolUseId,
|
||||
// content: await this.formatToolDenied(),
|
||||
// })
|
||||
this.didRejectTool = true
|
||||
if (text || images?.length) {
|
||||
pushAdditionalToolFeedback(text, images)
|
||||
await this.say("user_feedback", text, images)
|
||||
}
|
||||
this.didRejectTool = true // Prevent further tool uses in this message
|
||||
return false
|
||||
} else {
|
||||
// User hit the approve button, and may have provided feedback
|
||||
if (text || images?.length) {
|
||||
pushAdditionalToolFeedback(text, images)
|
||||
await this.say("user_feedback", text, images)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const showNotificationForApprovalIfAutoApprovalEnabled = (message: string) => {
|
||||
@@ -1809,24 +1818,23 @@ export class Cline {
|
||||
let didApprove = true
|
||||
const { response, text, images } = await this.ask("tool", completeMessage, false)
|
||||
if (response !== "yesButtonClicked") {
|
||||
// User either sent a message or pressed reject button
|
||||
// TODO: add similar context for other tool denial responses, to emphasize ie that a command was not run
|
||||
const fileDeniedNote = fileExists
|
||||
? "The file was not updated, and maintains its original contents."
|
||||
: "The file was not created."
|
||||
if (response === "messageResponse") {
|
||||
pushToolResult(`The user denied this operation. ${fileDeniedNote}`)
|
||||
if (text || images?.length) {
|
||||
pushAdditionalToolFeedback(text, images)
|
||||
await this.say("user_feedback", text, images)
|
||||
}
|
||||
this.didRejectTool = true
|
||||
didApprove = false
|
||||
} else {
|
||||
// User hit the approve button, and may have provided feedback
|
||||
if (text || images?.length) {
|
||||
pushAdditionalToolFeedback(text, images)
|
||||
await this.say("user_feedback", text, images)
|
||||
pushToolResult(
|
||||
formatResponse.toolResult(
|
||||
`The user denied this operation. ${fileDeniedNote}\nThe user provided the following feedback:\n<feedback>\n${text}\n</feedback>`,
|
||||
images,
|
||||
),
|
||||
)
|
||||
this.didRejectTool = true
|
||||
didApprove = false
|
||||
} else {
|
||||
pushToolResult(`The user denied this operation. ${fileDeniedNote}`)
|
||||
this.didRejectTool = true
|
||||
didApprove = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2704,22 +2712,33 @@ export class Cline {
|
||||
// }
|
||||
|
||||
this.isAwaitingPlanResponse = true
|
||||
const { text, images } = await this.ask("plan_mode_response", response, false)
|
||||
let { text, images } = await this.ask("plan_mode_response", response, false)
|
||||
this.isAwaitingPlanResponse = false
|
||||
|
||||
// webview invoke sendMessage will send this marker in order to put webview into the proper state (responding to an ask) and as a flag to extension that the user switched to ACT mode.
|
||||
if (text === "PLAN_MODE_TOGGLE_RESPONSE") {
|
||||
text = ""
|
||||
}
|
||||
|
||||
if (this.didRespondToPlanAskBySwitchingMode) {
|
||||
// await this.say("user_feedback", text ?? "", images)
|
||||
pushToolResult(
|
||||
formatResponse.toolResult(
|
||||
`[The user has switched to ACT MODE, so you may now proceed with the task.]`,
|
||||
`[The user has switched to ACT MODE, so you may now proceed with the task.]` +
|
||||
(text
|
||||
? `\n\nThe user also provided the following message when switching to ACT MODE:\n<user_message>\n${text}\n</user_message>`
|
||||
: ""),
|
||||
images,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
await this.say("user_feedback", text ?? "", images)
|
||||
// if we didn't switch to ACT MODE, then we can just send the user_feedback message
|
||||
pushToolResult(formatResponse.toolResult(`<user_message>\n${text}\n</user_message>`, images))
|
||||
}
|
||||
|
||||
if (text || images?.length) {
|
||||
await this.say("user_feedback", text ?? "", images)
|
||||
}
|
||||
|
||||
//
|
||||
break
|
||||
}
|
||||
@@ -3121,7 +3140,6 @@ 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) {
|
||||
|
||||
@@ -7,27 +7,32 @@ import fs from "fs/promises"
|
||||
import { extractTextFromFile } from "../../integrations/misc/extract-text"
|
||||
import { isBinaryFile } from "isbinaryfile"
|
||||
import { diagnosticsToProblemsString } from "../../integrations/diagnostics"
|
||||
import { getLatestTerminalOutput } from "../../integrations/terminal/get-latest-output"
|
||||
import { getCommitInfo } from "../../utils/git"
|
||||
import { getWorkingState } from "../../utils/git"
|
||||
|
||||
export function openMention(mention?: string): void {
|
||||
if (!mention) {
|
||||
return
|
||||
}
|
||||
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
||||
if (!cwd) {
|
||||
return
|
||||
}
|
||||
|
||||
if (mention.startsWith("/")) {
|
||||
const relPath = mention.slice(1)
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
||||
if (!cwd) {
|
||||
return
|
||||
}
|
||||
const absPath = path.resolve(cwd, relPath)
|
||||
if (mention.endsWith("/")) {
|
||||
vscode.commands.executeCommand("revealInExplorer", vscode.Uri.file(absPath))
|
||||
// vscode.commands.executeCommand("vscode.openFolder", , { forceNewWindow: false }) opens in new window
|
||||
} else {
|
||||
openFile(absPath)
|
||||
}
|
||||
} else if (mention === "problems") {
|
||||
vscode.commands.executeCommand("workbench.actions.view.problems")
|
||||
} else if (mention === "terminal") {
|
||||
vscode.commands.executeCommand("workbench.action.terminal.focus")
|
||||
} else if (mention.startsWith("http")) {
|
||||
vscode.env.openExternal(vscode.Uri.parse(mention))
|
||||
}
|
||||
@@ -46,6 +51,12 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher
|
||||
: `'${mentionPath}' (see below for file content)`
|
||||
} else if (mention === "problems") {
|
||||
return `Workspace Problems (see below for diagnostics)`
|
||||
} else if (mention === "terminal") {
|
||||
return `Terminal Output (see below for output)`
|
||||
} else if (mention === "git-changes") {
|
||||
return `Working directory changes (see below for details)`
|
||||
} else if (/^[a-f0-9]{7,40}$/.test(mention)) {
|
||||
return `Git commit '${mention}' (see below for commit info)`
|
||||
}
|
||||
return match
|
||||
})
|
||||
@@ -99,6 +110,27 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher
|
||||
} catch (error) {
|
||||
parsedText += `\n\n<workspace_diagnostics>\nError fetching diagnostics: ${error.message}\n</workspace_diagnostics>`
|
||||
}
|
||||
} else if (mention === "terminal") {
|
||||
try {
|
||||
const terminalOutput = await getLatestTerminalOutput()
|
||||
parsedText += `\n\n<terminal_output>\n${terminalOutput}\n</terminal_output>`
|
||||
} catch (error) {
|
||||
parsedText += `\n\n<terminal_output>\nError fetching terminal output: ${error.message}\n</terminal_output>`
|
||||
}
|
||||
} else if (mention === "git-changes") {
|
||||
try {
|
||||
const workingState = await getWorkingState(cwd)
|
||||
parsedText += `\n\n<git_working_state>\n${workingState}\n</git_working_state>`
|
||||
} catch (error) {
|
||||
parsedText += `\n\n<git_working_state>\nError fetching working state: ${error.message}\n</git_working_state>`
|
||||
}
|
||||
} else if (/^[a-f0-9]{7,40}$/.test(mention)) {
|
||||
try {
|
||||
const commitInfo = await getCommitInfo(mention, cwd)
|
||||
parsedText += `\n\n<git_commit hash="${mention}">\n${commitInfo}\n</git_commit>`
|
||||
} catch (error) {
|
||||
parsedText += `\n\n<git_commit hash="${mention}">\nError fetching commit info: ${error.message}\n</git_commit>`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,6 @@ import { ClineIgnoreController, LOCK_TEXT_SYMBOL } from "../ignore/ClineIgnoreCo
|
||||
export const formatResponse = {
|
||||
toolDenied: () => `The user denied this operation.`,
|
||||
|
||||
toolDeniedWithFeedback: (feedback?: string) =>
|
||||
`The user denied this operation and provided the following feedback:\n<feedback>\n${feedback}\n</feedback>`,
|
||||
|
||||
toolError: (error?: string) => `The tool execution failed with the following error:\n<error>\n${error}\n</error>`,
|
||||
|
||||
clineIgnoreError: (path: string) =>
|
||||
|
||||
@@ -331,7 +331,24 @@ ${
|
||||
<access_mcp_resource>
|
||||
<server_name>weather-server</server_name>
|
||||
<uri>weather://san-francisco/current</uri>
|
||||
</access_mcp_resource>`
|
||||
</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>`
|
||||
: ""
|
||||
}
|
||||
|
||||
@@ -750,7 +767,7 @@ IMPORTANT: Regardless of what else you see in the MCP settings file, you must de
|
||||
|
||||
(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.
|
||||
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.)
|
||||
|
||||
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?"
|
||||
|
||||
@@ -865,9 +882,10 @@ 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.
|
||||
- Once you've gained more context about the user's request, you should architect a detailed plan for how you will 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.
|
||||
- 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.
|
||||
|
||||
====
|
||||
|
||||
+397
-104
@@ -15,6 +15,7 @@ import { getTheme } from "../../integrations/theme/getTheme"
|
||||
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
|
||||
import { McpHub } from "../../services/mcp/McpHub"
|
||||
import { UserInfo } from "../../shared/UserInfo"
|
||||
import { McpDownloadResponse, McpMarketplaceCatalog, McpMarketplaceItem, McpServer } from "../../shared/mcp"
|
||||
import { ApiProvider, ModelInfo } from "../../shared/api"
|
||||
import { findLast } from "../../shared/array"
|
||||
import { ExtensionMessage, ExtensionState, Platform } from "../../shared/ExtensionMessage"
|
||||
@@ -28,6 +29,9 @@ 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"
|
||||
|
||||
/*
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
@@ -50,6 +54,7 @@ type SecretKey =
|
||||
| "togetherApiKey"
|
||||
| "qwenApiKey"
|
||||
| "mistralApiKey"
|
||||
| "liteLlmApiKey"
|
||||
| "authNonce"
|
||||
type GlobalStateKey =
|
||||
| "apiProvider"
|
||||
@@ -65,6 +70,7 @@ type GlobalStateKey =
|
||||
| "taskHistory"
|
||||
| "openAiBaseUrl"
|
||||
| "openAiModelId"
|
||||
| "openAiModelInfo"
|
||||
| "ollamaModelId"
|
||||
| "ollamaBaseUrl"
|
||||
| "lmStudioModelId"
|
||||
@@ -86,6 +92,7 @@ type GlobalStateKey =
|
||||
| "qwenApiLine"
|
||||
| "requestyModelId"
|
||||
| "togetherModelId"
|
||||
| "mcpMarketplaceCatalog"
|
||||
|
||||
export const GlobalFileNames = {
|
||||
apiConversationHistory: "api_conversation_history.json",
|
||||
@@ -104,7 +111,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
private cline?: Cline
|
||||
workspaceTracker?: WorkspaceTracker
|
||||
mcpHub?: McpHub
|
||||
private latestAnnouncementId = "jan-20-2025" // update to some unique identifier when we add a new announcement
|
||||
private latestAnnouncementId = "feb-19-2025" // update to some unique identifier when we add a new announcement
|
||||
|
||||
constructor(
|
||||
readonly context: vscode.ExtensionContext,
|
||||
@@ -329,15 +336,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
|
||||
// Use a nonce to only allow a specific script to be run.
|
||||
/*
|
||||
content security policy of your webview to only allow scripts that have a specific nonce
|
||||
create a content security policy meta tag so that only loading scripts with a nonce is allowed
|
||||
As your extension grows you will likely want to add custom styles, fonts, and/or images to your webview. If you do, you will need to update the content security policy meta tag to explicity allow for these resources. E.g.
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; font-src ${webview.cspSource}; img-src ${webview.cspSource} https:; script-src 'nonce-${nonce}';">
|
||||
content security policy of your webview to only allow scripts that have a specific nonce
|
||||
create a content security policy meta tag so that only loading scripts with a nonce is allowed
|
||||
As your extension grows you will likely want to add custom styles, fonts, and/or images to your webview. If you do, you will need to update the content security policy meta tag to explicity allow for these resources. E.g.
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; font-src ${webview.cspSource}; img-src ${webview.cspSource} https:; script-src 'nonce-${nonce}';">
|
||||
- 'unsafe-inline' is required for styles due to vscode-webview-toolkit's dynamic style injection
|
||||
- since we pass base64 images to the webview, we need to specify img-src ${webview.cspSource} data:;
|
||||
|
||||
in meta tag we add nonce attribute: A cryptographic nonce (only used once) to allow scripts. The server must generate a unique nonce value each time it transmits a policy. It is critical to provide a nonce that cannot be guessed as bypassing a resource's policy is otherwise trivial.
|
||||
*/
|
||||
in meta tag we add nonce attribute: A cryptographic nonce (only used once) to allow scripts. The server must generate a unique nonce value each time it transmits a policy. It is critical to provide a nonce that cannot be guessed as bypassing a resource's policy is otherwise trivial.
|
||||
*/
|
||||
const nonce = getNonce()
|
||||
|
||||
// Tip: Install the es6-string-html VS Code extension to enable code highlighting below
|
||||
@@ -397,6 +404,17 @@ 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)
|
||||
@@ -410,6 +428,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
break
|
||||
case "newTask":
|
||||
// Code that should run in response to the hello message command
|
||||
@@ -441,6 +460,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
openAiBaseUrl,
|
||||
openAiApiKey,
|
||||
openAiModelId,
|
||||
openAiModelInfo,
|
||||
ollamaModelId,
|
||||
ollamaBaseUrl,
|
||||
lmStudioModelId,
|
||||
@@ -461,6 +481,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
vsCodeLmModelSelector,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmModelId,
|
||||
liteLlmApiKey,
|
||||
qwenApiLine,
|
||||
} = message.apiConfiguration
|
||||
await this.updateGlobalState("apiProvider", apiProvider)
|
||||
@@ -479,6 +500,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
await this.updateGlobalState("openAiBaseUrl", openAiBaseUrl)
|
||||
await this.storeSecret("openAiApiKey", openAiApiKey)
|
||||
await this.updateGlobalState("openAiModelId", openAiModelId)
|
||||
await this.updateGlobalState("openAiModelInfo", openAiModelInfo)
|
||||
await this.updateGlobalState("ollamaModelId", ollamaModelId)
|
||||
await this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl)
|
||||
await this.updateGlobalState("lmStudioModelId", lmStudioModelId)
|
||||
@@ -491,6 +513,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
await this.storeSecret("togetherApiKey", togetherApiKey)
|
||||
await this.storeSecret("qwenApiKey", qwenApiKey)
|
||||
await this.storeSecret("mistralApiKey", mistralApiKey)
|
||||
await this.storeSecret("liteLlmApiKey", liteLlmApiKey)
|
||||
await this.updateGlobalState("azureApiVersion", azureApiVersion)
|
||||
await this.updateGlobalState("openRouterModelId", openRouterModelId)
|
||||
await this.updateGlobalState("openRouterModelInfo", openRouterModelInfo)
|
||||
@@ -527,104 +550,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
break
|
||||
case "chatSettings":
|
||||
case "togglePlanActMode":
|
||||
if (message.chatSettings) {
|
||||
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":
|
||||
case "cline":
|
||||
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)
|
||||
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":
|
||||
case "cline":
|
||||
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)
|
||||
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: "[Proceeding with the task...]",
|
||||
})
|
||||
} else {
|
||||
this.cancelTask()
|
||||
}
|
||||
}
|
||||
await this.togglePlanActModeWithChatSettings(message.chatSettings, message.chatContent)
|
||||
}
|
||||
break
|
||||
// case "relaunchChromeDebugMode":
|
||||
@@ -764,6 +692,10 @@ 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) {
|
||||
@@ -771,6 +703,69 @@ 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!)
|
||||
@@ -795,6 +790,31 @@ 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) {
|
||||
try {
|
||||
const commits = await searchCommits(message.text || "", cwd)
|
||||
await this.postMessageToWebview({
|
||||
type: "commitSearchResults",
|
||||
commits,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`Error searching commits: ${JSON.stringify(error)}`)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case "openExtensionSettings": {
|
||||
const settingsFilter = message.text || ""
|
||||
await vscode.commands.executeCommand(
|
||||
@@ -812,6 +832,107 @@ 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":
|
||||
case "cline":
|
||||
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":
|
||||
case "cline":
|
||||
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()
|
||||
|
||||
if (this.cline) {
|
||||
this.cline.updateChatSettings(chatSettings)
|
||||
if (this.cline.isAwaitingPlanResponse && didSwitchToActMode) {
|
||||
this.cline.didRespondToPlanAskBySwitchingMode = true
|
||||
// Use chatContent if provided, otherwise use default 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
|
||||
@@ -1015,6 +1136,171 @@ 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
|
||||
const task = `Set up the MCP server from ${mcpDetails.githubUrl}.
|
||||
Use "${mcpDetails.mcpId}" as the server name in cline_mcp_settings.json.
|
||||
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) {
|
||||
@@ -1405,6 +1691,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
openAiBaseUrl,
|
||||
openAiApiKey,
|
||||
openAiModelId,
|
||||
openAiModelInfo,
|
||||
ollamaModelId,
|
||||
ollamaBaseUrl,
|
||||
lmStudioModelId,
|
||||
@@ -1436,6 +1723,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
previousModeModelId,
|
||||
previousModeModelInfo,
|
||||
qwenApiLine,
|
||||
liteLlmApiKey,
|
||||
] = await Promise.all([
|
||||
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
|
||||
this.getGlobalState("apiModelId") as Promise<string | undefined>,
|
||||
@@ -1454,6 +1742,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
this.getGlobalState("openAiBaseUrl") as Promise<string | undefined>,
|
||||
this.getSecret("openAiApiKey") as Promise<string | undefined>,
|
||||
this.getGlobalState("openAiModelId") as Promise<string | undefined>,
|
||||
this.getGlobalState("openAiModelInfo") as Promise<ModelInfo | undefined>,
|
||||
this.getGlobalState("ollamaModelId") as Promise<string | undefined>,
|
||||
this.getGlobalState("ollamaBaseUrl") as Promise<string | undefined>,
|
||||
this.getGlobalState("lmStudioModelId") as Promise<string | undefined>,
|
||||
@@ -1485,6 +1774,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
this.getGlobalState("previousModeModelId") as Promise<string | undefined>,
|
||||
this.getGlobalState("previousModeModelInfo") as Promise<ModelInfo | undefined>,
|
||||
this.getGlobalState("qwenApiLine") as Promise<string | undefined>,
|
||||
this.getSecret("liteLlmApiKey") as Promise<string | undefined>,
|
||||
])
|
||||
|
||||
let apiProvider: ApiProvider
|
||||
@@ -1524,6 +1814,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
openAiBaseUrl,
|
||||
openAiApiKey,
|
||||
openAiModelId,
|
||||
openAiModelInfo,
|
||||
ollamaModelId,
|
||||
ollamaBaseUrl,
|
||||
lmStudioModelId,
|
||||
@@ -1546,6 +1837,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
o3MiniReasoningEffort,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmModelId,
|
||||
liteLlmApiKey,
|
||||
},
|
||||
lastShownAnnouncementId,
|
||||
customInstructions,
|
||||
@@ -1638,6 +1930,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
"qwenApiKey",
|
||||
"mistralApiKey",
|
||||
"clineApiKey",
|
||||
"liteLlmApiKey",
|
||||
]
|
||||
for (const key of secretKeys) {
|
||||
await this.storeSecret(key, undefined)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import * as vscode from "vscode"
|
||||
|
||||
/**
|
||||
* Gets the contents of the active terminal
|
||||
* @returns The terminal contents as a string
|
||||
*/
|
||||
export async function getLatestTerminalOutput(): Promise<string> {
|
||||
// Store original clipboard content to restore later
|
||||
const originalClipboard = await vscode.env.clipboard.readText()
|
||||
|
||||
try {
|
||||
// Select terminal content
|
||||
await vscode.commands.executeCommand("workbench.action.terminal.selectAll")
|
||||
|
||||
// Copy selection to clipboard
|
||||
await vscode.commands.executeCommand("workbench.action.terminal.copySelection")
|
||||
|
||||
// Clear the selection
|
||||
await vscode.commands.executeCommand("workbench.action.terminal.clearSelection")
|
||||
|
||||
// Get terminal contents from clipboard
|
||||
let terminalContents = (await vscode.env.clipboard.readText()).trim()
|
||||
|
||||
// Check if there's actually a terminal open
|
||||
if (terminalContents === originalClipboard) {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Clean up command separation
|
||||
const lines = terminalContents.split("\n")
|
||||
const lastLine = lines.pop()?.trim()
|
||||
if (lastLine) {
|
||||
let i = lines.length - 1
|
||||
while (i >= 0 && !lines[i].trim().startsWith(lastLine)) {
|
||||
i--
|
||||
}
|
||||
terminalContents = lines.slice(Math.max(i, 0)).join("\n")
|
||||
}
|
||||
|
||||
return terminalContents
|
||||
} finally {
|
||||
// Restore original clipboard content
|
||||
await vscode.env.clipboard.writeText(originalClipboard)
|
||||
}
|
||||
}
|
||||
@@ -42,11 +42,15 @@ export class BrowserSession {
|
||||
await fs.mkdir(puppeteerDir, { recursive: true })
|
||||
}
|
||||
|
||||
// if chromium doesn't exist, this will download it to path.join(puppeteerDir, ".chromium-browser-snapshots")
|
||||
// if it does exist it will return the path to existing chromium
|
||||
const stats: PCRStats = await PCR({
|
||||
downloadPath: puppeteerDir,
|
||||
})
|
||||
const chromeExecutablePath = vscode.workspace.getConfiguration("cline").get<string>("chromeExecutablePath")
|
||||
if (chromeExecutablePath && !(await fileExistsAtPath(chromeExecutablePath))) {
|
||||
throw new Error(`Chrome executable not found at path: ${chromeExecutablePath}`)
|
||||
}
|
||||
const stats: PCRStats = chromeExecutablePath
|
||||
? { puppeteer: require("puppeteer-core"), executablePath: chromeExecutablePath }
|
||||
: // if chromium doesn't exist, this will download it to path.join(puppeteerDir, ".chromium-browser-snapshots")
|
||||
// if it does exist it will return the path to existing chromium
|
||||
await PCR({ downloadPath: puppeteerDir })
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
@@ -472,6 +472,10 @@ export class McpHub {
|
||||
})
|
||||
}
|
||||
|
||||
async sendLatestMcpServers() {
|
||||
await this.notifyWebviewOfServerChanges()
|
||||
}
|
||||
|
||||
// Using server
|
||||
|
||||
// Public methods for server management
|
||||
@@ -632,6 +636,33 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
this.removeAllFileWatchers()
|
||||
for (const connection of this.connections) {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface ChatContent {
|
||||
message?: string
|
||||
images?: string[]
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
// 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 { ApiConfiguration, ModelInfo } from "./api"
|
||||
import { AutoApprovalSettings } from "./AutoApprovalSettings"
|
||||
import { BrowserSettings } from "./BrowserSettings"
|
||||
import { ChatSettings } from "./ChatSettings"
|
||||
import { HistoryItem } from "./HistoryItem"
|
||||
import { McpServer } from "./mcp"
|
||||
import { McpServer, McpMarketplaceCatalog, McpMarketplaceItem, McpDownloadResponse } from "./mcp"
|
||||
|
||||
// webview will hold state
|
||||
export interface ExtensionMessage {
|
||||
@@ -27,6 +28,9 @@ export interface ExtensionMessage {
|
||||
| "requestVsCodeLmModels"
|
||||
| "emailSubscribed"
|
||||
| "authCallback"
|
||||
| "mcpMarketplaceCatalog"
|
||||
| "mcpDownloadDetails"
|
||||
| "commitSearchResults"
|
||||
text?: string
|
||||
action?:
|
||||
| "chatButtonClicked"
|
||||
@@ -48,6 +52,10 @@ export interface ExtensionMessage {
|
||||
openAiModels?: string[]
|
||||
mcpServers?: McpServer[]
|
||||
customToken?: string
|
||||
mcpMarketplaceCatalog?: McpMarketplaceCatalog
|
||||
error?: string
|
||||
mcpDownloadDetails?: McpDownloadResponse
|
||||
commits?: GitCommit[]
|
||||
}
|
||||
|
||||
export type Platform = "aix" | "darwin" | "freebsd" | "linux" | "openbsd" | "sunos" | "win32" | "unknown"
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AutoApprovalSettings } from "./AutoApprovalSettings"
|
||||
import { BrowserSettings } from "./BrowserSettings"
|
||||
import { ChatSettings } from "./ChatSettings"
|
||||
import { UserInfo } from "./UserInfo"
|
||||
import { ChatContent } from "./ChatContent"
|
||||
|
||||
export interface WebviewMessage {
|
||||
type:
|
||||
@@ -29,9 +30,10 @@ export interface WebviewMessage {
|
||||
| "refreshOpenAiModels"
|
||||
| "openMcpSettings"
|
||||
| "restartMcpServer"
|
||||
| "deleteMcpServer"
|
||||
| "autoApprovalSettings"
|
||||
| "browserSettings"
|
||||
| "chatSettings"
|
||||
| "togglePlanActMode"
|
||||
| "checkpointDiff"
|
||||
| "checkpointRestore"
|
||||
| "taskCompletionViewChanges"
|
||||
@@ -45,6 +47,12 @@ export interface WebviewMessage {
|
||||
| "subscribeEmail"
|
||||
| "authStateChanged"
|
||||
| "authCallback"
|
||||
| "fetchMcpMarketplace"
|
||||
| "downloadMcp"
|
||||
| "silentlyRefreshMcpMarketplace"
|
||||
| "searchCommits"
|
||||
| "showMcpView"
|
||||
| "fetchLatestMcpServersFromHub"
|
||||
// | "relaunchChromeDebugMode"
|
||||
text?: string
|
||||
disabled?: boolean
|
||||
@@ -56,6 +64,8 @@ export interface WebviewMessage {
|
||||
autoApprovalSettings?: AutoApprovalSettings
|
||||
browserSettings?: BrowserSettings
|
||||
chatSettings?: ChatSettings
|
||||
chatContent?: ChatContent
|
||||
mcpId?: string
|
||||
|
||||
// For toggleToolAutoApprove
|
||||
serverName?: string
|
||||
|
||||
+157
-35
@@ -23,6 +23,7 @@ export interface ApiHandlerOptions {
|
||||
clineApiKey?: string
|
||||
liteLlmBaseUrl?: string
|
||||
liteLlmModelId?: string
|
||||
liteLlmApiKey?: string
|
||||
anthropicBaseUrl?: string
|
||||
openRouterApiKey?: string
|
||||
openRouterModelId?: string
|
||||
@@ -39,6 +40,7 @@ export interface ApiHandlerOptions {
|
||||
openAiBaseUrl?: string
|
||||
openAiApiKey?: string
|
||||
openAiModelId?: string
|
||||
openAiModelInfo?: ModelInfo
|
||||
ollamaModelId?: string
|
||||
ollamaBaseUrl?: string
|
||||
lmStudioModelId?: string
|
||||
@@ -451,85 +453,205 @@ export const deepSeekModels = {
|
||||
export type QwenModelId = keyof typeof qwenModels
|
||||
export const qwenDefaultModelId: QwenModelId = "qwen-coder-plus-latest"
|
||||
export const qwenModels = {
|
||||
"qwen2.5-coder-32b-instruct": {
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.002,
|
||||
outputPrice: 0.006,
|
||||
cacheWritesPrice: 0.002,
|
||||
cacheReadsPrice: 0.006,
|
||||
},
|
||||
"qwen2.5-coder-14b-instruct": {
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.002,
|
||||
outputPrice: 0.006,
|
||||
cacheWritesPrice: 0.002,
|
||||
cacheReadsPrice: 0.006,
|
||||
},
|
||||
"qwen2.5-coder-7b-instruct": {
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.001,
|
||||
outputPrice: 0.002,
|
||||
cacheWritesPrice: 0.001,
|
||||
cacheReadsPrice: 0.002,
|
||||
},
|
||||
"qwen2.5-coder-3b-instruct": {
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 32_768,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0,
|
||||
outputPrice: 0.0,
|
||||
cacheWritesPrice: 0.0,
|
||||
cacheReadsPrice: 0.0,
|
||||
},
|
||||
"qwen2.5-coder-1.5b-instruct": {
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 32_768,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0,
|
||||
outputPrice: 0.0,
|
||||
cacheWritesPrice: 0.0,
|
||||
cacheReadsPrice: 0.0,
|
||||
},
|
||||
"qwen2.5-coder-0.5b-instruct": {
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 32_768,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0,
|
||||
outputPrice: 0.0,
|
||||
cacheWritesPrice: 0.0,
|
||||
cacheReadsPrice: 0.0,
|
||||
},
|
||||
"qwen-coder-plus-latest": {
|
||||
maxTokens: 129_024,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0035,
|
||||
outputPrice: 0.007,
|
||||
cacheWritesPrice: 0.0035,
|
||||
cacheReadsPrice: 0.007,
|
||||
inputPrice: 3.5,
|
||||
outputPrice: 7,
|
||||
cacheWritesPrice: 3.5,
|
||||
cacheReadsPrice: 7,
|
||||
},
|
||||
"qwen-plus-latest": {
|
||||
maxTokens: 129_024,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0008,
|
||||
outputPrice: 0.002,
|
||||
cacheWritesPrice: 0.0004,
|
||||
cacheReadsPrice: 0.001,
|
||||
inputPrice: 0.8,
|
||||
outputPrice: 2,
|
||||
cacheWritesPrice: 0.8,
|
||||
cacheReadsPrice: 0.2,
|
||||
},
|
||||
"qwen-turbo-latest": {
|
||||
maxTokens: 1_000_000,
|
||||
contextWindow: 1_000_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0003,
|
||||
outputPrice: 0.0006,
|
||||
cacheWritesPrice: 0.00015,
|
||||
cacheReadsPrice: 0.0003,
|
||||
inputPrice: 0.8,
|
||||
outputPrice: 2,
|
||||
cacheWritesPrice: 0.8,
|
||||
cacheReadsPrice: 2,
|
||||
},
|
||||
"qwen-max-latest": {
|
||||
maxTokens: 30_720,
|
||||
contextWindow: 32_768,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0112,
|
||||
outputPrice: 0.0448,
|
||||
cacheWritesPrice: 0.0056,
|
||||
cacheReadsPrice: 0.0224,
|
||||
inputPrice: 2.4,
|
||||
outputPrice: 9.6,
|
||||
cacheWritesPrice: 2.4,
|
||||
cacheReadsPrice: 9.6,
|
||||
},
|
||||
"qwen-coder-plus": {
|
||||
maxTokens: 129_024,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0035,
|
||||
outputPrice: 0.007,
|
||||
cacheWritesPrice: 0.0035,
|
||||
cacheReadsPrice: 0.007,
|
||||
inputPrice: 3.5,
|
||||
outputPrice: 7,
|
||||
cacheWritesPrice: 3.5,
|
||||
cacheReadsPrice: 7,
|
||||
},
|
||||
"qwen-plus": {
|
||||
maxTokens: 129_024,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0008,
|
||||
outputPrice: 0.002,
|
||||
cacheWritesPrice: 0.0004,
|
||||
cacheReadsPrice: 0.001,
|
||||
inputPrice: 0.8,
|
||||
outputPrice: 2,
|
||||
cacheWritesPrice: 0.8,
|
||||
cacheReadsPrice: 0.2,
|
||||
},
|
||||
"qwen-turbo": {
|
||||
maxTokens: 1_000_000,
|
||||
contextWindow: 1_000_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0003,
|
||||
outputPrice: 0.0006,
|
||||
cacheWritesPrice: 0.00015,
|
||||
cacheReadsPrice: 0.0003,
|
||||
inputPrice: 0.3,
|
||||
outputPrice: 0.6,
|
||||
cacheWritesPrice: 0.3,
|
||||
cacheReadsPrice: 0.6,
|
||||
},
|
||||
"qwen-max": {
|
||||
maxTokens: 30_720,
|
||||
contextWindow: 32_768,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0112,
|
||||
outputPrice: 0.0448,
|
||||
cacheWritesPrice: 0.0056,
|
||||
cacheReadsPrice: 0.0224,
|
||||
inputPrice: 2.4,
|
||||
outputPrice: 9.6,
|
||||
cacheWritesPrice: 2.4,
|
||||
cacheReadsPrice: 9.6,
|
||||
},
|
||||
"deepseek-v3": {
|
||||
maxTokens: 8_000,
|
||||
contextWindow: 64_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0.28,
|
||||
cacheWritesPrice: 0.14,
|
||||
cacheReadsPrice: 0.014,
|
||||
},
|
||||
"deepseek-r1": {
|
||||
maxTokens: 8_000,
|
||||
contextWindow: 64_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0,
|
||||
outputPrice: 2.19,
|
||||
cacheWritesPrice: 0.55,
|
||||
cacheReadsPrice: 0.14,
|
||||
},
|
||||
"qwen-vl-max": {
|
||||
maxTokens: 30_720,
|
||||
contextWindow: 32_768,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 3,
|
||||
outputPrice: 9,
|
||||
cacheWritesPrice: 3,
|
||||
cacheReadsPrice: 9,
|
||||
},
|
||||
"qwen-vl-max-latest": {
|
||||
maxTokens: 129_024,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 3,
|
||||
outputPrice: 9,
|
||||
cacheWritesPrice: 3,
|
||||
cacheReadsPrice: 9,
|
||||
},
|
||||
"qwen-vl-plus": {
|
||||
maxTokens: 6_000,
|
||||
contextWindow: 8_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 1.5,
|
||||
outputPrice: 4.5,
|
||||
cacheWritesPrice: 1.5,
|
||||
cacheReadsPrice: 4.5,
|
||||
},
|
||||
"qwen-vl-plus-latest": {
|
||||
maxTokens: 129_024,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 1.5,
|
||||
outputPrice: 4.5,
|
||||
cacheWritesPrice: 1.5,
|
||||
cacheReadsPrice: 4.5,
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
@@ -617,9 +739,9 @@ export const mistralModels = {
|
||||
export type LiteLLMModelId = string
|
||||
export const liteLlmDefaultModelId = "gpt-3.5-turbo"
|
||||
export const liteLlmModelInfoSaneDefaults: ModelInfo = {
|
||||
maxTokens: 4096,
|
||||
contextWindow: 8192,
|
||||
supportsImages: false,
|
||||
maxTokens: -1,
|
||||
contextWindow: 128_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
|
||||
@@ -7,42 +7,48 @@ Mention regex:
|
||||
|
||||
- **Regex Breakdown**:
|
||||
- `/@`:
|
||||
- **@**: The mention must start with the '@' symbol.
|
||||
- **@**: The mention must start with the '@' symbol.
|
||||
|
||||
- `((?:\/|\w+:\/\/)[^\s]+?|problems\b)`:
|
||||
- **Capturing Group (`(...)`)**: Captures the part of the string that matches one of the specified patterns.
|
||||
- `(?:\/|\w+:\/\/)`:
|
||||
- **Non-Capturing Group (`(?:...)`)**: Groups the alternatives without capturing them for back-referencing.
|
||||
- `\/`:
|
||||
- **Slash (`/`)**: Indicates that the mention is a file or folder path starting with a '/'.
|
||||
- `|`: Logical OR.
|
||||
- `\w+:\/\/`:
|
||||
- **Protocol (`\w+://`)**: Matches URLs that start with a word character sequence followed by '://', such as 'http://', 'https://', 'ftp://', etc.
|
||||
- `[^\s]+?`:
|
||||
- **Non-Whitespace Characters (`[^\s]+`)**: Matches one or more characters that are not whitespace.
|
||||
- **Non-Greedy (`+?`)**: Ensures the smallest possible match, preventing the inclusion of trailing punctuation.
|
||||
- `|`: Logical OR.
|
||||
- `problems\b`:
|
||||
- **Capturing Group (`(...)`)**: Captures the part of the string that matches one of the specified patterns.
|
||||
- `(?:\/|\w+:\/\/)`:
|
||||
- **Non-Capturing Group (`(?:...)`)**: Groups the alternatives without capturing them for back-referencing.
|
||||
- `\/`:
|
||||
- **Slash (`/`)**: Indicates that the mention is a file or folder path starting with a '/'.
|
||||
- `|`: Logical OR.
|
||||
- `\w+:\/\/`:
|
||||
- **Protocol (`\w+://`)**: Matches URLs that start with a word character sequence followed by '://', such as 'http://', 'https://', 'ftp://', etc.
|
||||
- `[^\s]+?`:
|
||||
- **Non-Whitespace Characters (`[^\s]+`)**: Matches one or more characters that are not whitespace.
|
||||
- **Non-Greedy (`+?`)**: Ensures the smallest possible match, preventing the inclusion of trailing punctuation.
|
||||
- `|`: Logical OR.
|
||||
- `problems\b`:
|
||||
- **Exact Word ('problems')**: Matches the exact word 'problems'.
|
||||
- **Word Boundary (`\b`)**: Ensures that 'problems' is matched as a whole word and not as part of another word (e.g., 'problematic').
|
||||
- `terminal\b`:
|
||||
- **Exact Word ('terminal')**: Matches the exact word 'terminal'.
|
||||
- **Word Boundary (`\b`)**: Ensures that 'terminal' is matched as a whole word and not as part of another word (e.g., 'terminals').
|
||||
|
||||
- `(?=[.,;:!?]?(?=[\s\r\n]|$))`:
|
||||
- **Positive Lookahead (`(?=...)`)**: Ensures that the match is followed by specific patterns without including them in the match.
|
||||
- `[.,;:!?]?`:
|
||||
- **Optional Punctuation (`[.,;:!?]?`)**: Matches zero or one of the specified punctuation marks.
|
||||
- `(?=[\s\r\n]|$)`:
|
||||
- **Nested Positive Lookahead (`(?=[\s\r\n]|$)`)**: Ensures that the punctuation (if present) is followed by a whitespace character, a line break, or the end of the string.
|
||||
- **Positive Lookahead (`(?=...)`)**: Ensures that the match is followed by specific patterns without including them in the match.
|
||||
- `[.,;:!?]?`:
|
||||
- **Optional Punctuation (`[.,;:!?]?`)**: Matches zero or one of the specified punctuation marks.
|
||||
- `(?=[\s\r\n]|$)`:
|
||||
- **Nested Positive Lookahead (`(?=[\s\r\n]|$)`)**: Ensures that the punctuation (if present) is followed by a whitespace character, a line break, or the end of the string.
|
||||
|
||||
- **Summary**:
|
||||
- The regex effectively matches:
|
||||
- Mentions that are file or folder paths starting with '/' and containing any non-whitespace characters (including periods within the path).
|
||||
- URLs that start with a protocol (like 'http://') followed by any non-whitespace characters (including query parameters).
|
||||
- The exact word 'problems'.
|
||||
- Mentions that are file or folder paths starting with '/' and containing any non-whitespace characters (including periods within the path).
|
||||
- URLs that start with a protocol (like 'http://') followed by any non-whitespace characters (including query parameters).
|
||||
- The exact word 'problems'.
|
||||
- The exact word 'terminal'.
|
||||
- The exact word 'git-changes'.
|
||||
- It ensures that any trailing punctuation marks (such as ',', '.', '!', etc.) are not included in the matched mention, allowing the punctuation to follow the mention naturally in the text.
|
||||
|
||||
- **Global Regex**:
|
||||
- `mentionRegexGlobal`: Creates a global version of the `mentionRegex` to find all matches within a given string.
|
||||
|
||||
*/
|
||||
export const mentionRegex = /@((?:\/|\w+:\/\/)[^\s]+?|problems\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/
|
||||
export const mentionRegex =
|
||||
/@((?:\/|\w+:\/\/)[^\s]+?|[a-f0-9]{7,40}\b|problems\b|terminal\b|git-changes\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/
|
||||
export const mentionRegexGlobal = new RegExp(mentionRegex.source, "g")
|
||||
|
||||
@@ -66,3 +66,39 @@ 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
|
||||
}
|
||||
|
||||
@@ -34,4 +34,14 @@ describe("Extension Tests", function () {
|
||||
await vscode.commands.executeCommand("cline.historyButtonClicked")
|
||||
// Success if no error thrown
|
||||
})
|
||||
|
||||
it("should handle advanced settings configuration", async () => {
|
||||
// Test browser session setting
|
||||
await vscode.workspace.getConfiguration().update("cline.disableBrowserTool", true, true)
|
||||
const updatedConfig = vscode.workspace.getConfiguration("cline")
|
||||
expect(updatedConfig.get("disableBrowserTool")).to.be.true
|
||||
|
||||
// Reset settings
|
||||
await vscode.workspace.getConfiguration().update("cline.disableBrowserTool", undefined, true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -28,7 +28,13 @@ describe("Chat Integration Tests", () => {
|
||||
vscode.postMessage({ type: 'newTask', text: message.text });
|
||||
break;
|
||||
case 'toggleMode':
|
||||
vscode.postMessage({ type: 'chatSettings', chatSettings: { mode: 'act' } });
|
||||
vscode.postMessage({
|
||||
type: 'togglePlanActMode',
|
||||
chatSettings: { mode: 'act' },
|
||||
chatContent: {
|
||||
message: "message test",
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'invoke':
|
||||
if (message.invoke === 'primaryButtonClick') {
|
||||
@@ -78,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 === "chatSettings") {
|
||||
if (message.type === "togglePlanActMode") {
|
||||
resolve(message)
|
||||
}
|
||||
})
|
||||
@@ -92,6 +98,25 @@ describe("Chat Integration Tests", () => {
|
||||
assert.equal(stateChange.chatSettings.mode, "act")
|
||||
})
|
||||
|
||||
it("should toggle between plan and act modes with messages", async () => {
|
||||
// Set up state change listener
|
||||
const stateChangePromise = new Promise<any>((resolve) => {
|
||||
panel.webview.onDidReceiveMessage((message) => {
|
||||
if (message.type === "togglePlanActMode") {
|
||||
resolve(message)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Trigger mode toggle
|
||||
await panel.webview.postMessage({ type: "toggleMode" })
|
||||
|
||||
// Verify mode changed
|
||||
const stateChange = await stateChangePromise
|
||||
assert.equal(stateChange.chatSettings.mode, "act")
|
||||
assert.equal(stateChange.chatContent.message, "message test")
|
||||
})
|
||||
|
||||
it("should handle tool approval flow", async () => {
|
||||
// Set up approval listener
|
||||
const approvalPromise = new Promise<any>((resolve) => {
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import { exec } from "child_process"
|
||||
import { promisify } from "util"
|
||||
|
||||
const execAsync = promisify(exec)
|
||||
const GIT_OUTPUT_LINE_LIMIT = 500
|
||||
|
||||
export interface GitCommit {
|
||||
hash: string
|
||||
shortHash: string
|
||||
subject: string
|
||||
author: string
|
||||
date: string
|
||||
}
|
||||
|
||||
async function checkGitRepo(cwd: string): Promise<boolean> {
|
||||
try {
|
||||
await execAsync("git rev-parse --git-dir", { cwd })
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function checkGitInstalled(): Promise<boolean> {
|
||||
try {
|
||||
await execAsync("git --version")
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function searchCommits(query: string, cwd: string): Promise<GitCommit[]> {
|
||||
try {
|
||||
const isInstalled = await checkGitInstalled()
|
||||
if (!isInstalled) {
|
||||
console.error("Git is not installed")
|
||||
return []
|
||||
}
|
||||
|
||||
const isRepo = await checkGitRepo(cwd)
|
||||
if (!isRepo) {
|
||||
console.error("Not a git repository")
|
||||
return []
|
||||
}
|
||||
|
||||
// Search commits by hash or message, limiting to 10 results
|
||||
const { stdout } = await execAsync(
|
||||
`git log -n 10 --format="%H%n%h%n%s%n%an%n%ad" --date=short ` + `--grep="${query}" --regexp-ignore-case`,
|
||||
{ cwd },
|
||||
)
|
||||
|
||||
let output = stdout
|
||||
if (!output.trim() && /^[a-f0-9]+$/i.test(query)) {
|
||||
// If no results from grep search and query looks like a hash, try searching by hash
|
||||
const { stdout: hashStdout } = await execAsync(
|
||||
`git log -n 10 --format="%H%n%h%n%s%n%an%n%ad" --date=short ` + `--author-date-order ${query}`,
|
||||
{ cwd },
|
||||
).catch(() => ({ stdout: "" }))
|
||||
|
||||
if (!hashStdout.trim()) {
|
||||
return []
|
||||
}
|
||||
|
||||
output = hashStdout
|
||||
}
|
||||
|
||||
const commits: GitCommit[] = []
|
||||
const lines = output
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter((line) => line !== "--")
|
||||
|
||||
for (let i = 0; i < lines.length; i += 5) {
|
||||
commits.push({
|
||||
hash: lines[i],
|
||||
shortHash: lines[i + 1],
|
||||
subject: lines[i + 2],
|
||||
author: lines[i + 3],
|
||||
date: lines[i + 4],
|
||||
})
|
||||
}
|
||||
|
||||
return commits
|
||||
} catch (error) {
|
||||
console.error("Error searching commits:", error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCommitInfo(hash: string, cwd: string): Promise<string> {
|
||||
try {
|
||||
const isInstalled = await checkGitInstalled()
|
||||
if (!isInstalled) {
|
||||
return "Git is not installed"
|
||||
}
|
||||
|
||||
const isRepo = await checkGitRepo(cwd)
|
||||
if (!isRepo) {
|
||||
return "Not a git repository"
|
||||
}
|
||||
|
||||
// Get commit info, stats, and diff separately
|
||||
const { stdout: info } = await execAsync(`git show --format="%H%n%h%n%s%n%an%n%ad%n%b" --no-patch ${hash}`, {
|
||||
cwd,
|
||||
})
|
||||
const [fullHash, shortHash, subject, author, date, body] = info.trim().split("\n")
|
||||
|
||||
const { stdout: stats } = await execAsync(`git show --stat --format="" ${hash}`, { cwd })
|
||||
|
||||
const { stdout: diff } = await execAsync(`git show --format="" ${hash}`, { cwd })
|
||||
|
||||
const summary = [
|
||||
`Commit: ${shortHash} (${fullHash})`,
|
||||
`Author: ${author}`,
|
||||
`Date: ${date}`,
|
||||
`\nMessage: ${subject}`,
|
||||
body ? `\nDescription:\n${body}` : "",
|
||||
"\nFiles Changed:",
|
||||
stats.trim(),
|
||||
"\nFull Changes:",
|
||||
].join("\n")
|
||||
|
||||
const output = summary + "\n\n" + diff.trim()
|
||||
return truncateOutput(output)
|
||||
} catch (error) {
|
||||
console.error("Error getting commit info:", error)
|
||||
return `Failed to get commit info: ${error instanceof Error ? error.message : String(error)}`
|
||||
}
|
||||
}
|
||||
|
||||
export async function getWorkingState(cwd: string): Promise<string> {
|
||||
try {
|
||||
const isInstalled = await checkGitInstalled()
|
||||
if (!isInstalled) {
|
||||
return "Git is not installed"
|
||||
}
|
||||
|
||||
const isRepo = await checkGitRepo(cwd)
|
||||
if (!isRepo) {
|
||||
return "Not a git repository"
|
||||
}
|
||||
|
||||
// Get status of working directory
|
||||
const { stdout: status } = await execAsync("git status --short", { cwd })
|
||||
if (!status.trim()) {
|
||||
return "No changes in working directory"
|
||||
}
|
||||
|
||||
// Get all changes (both staged and unstaged) compared to HEAD
|
||||
const { stdout: diff } = await execAsync("git diff HEAD", { cwd })
|
||||
const output = `Working directory changes:\n\n${status}\n\n${diff}`.trim()
|
||||
return truncateOutput(output)
|
||||
} catch (error) {
|
||||
console.error("Error getting working state:", error)
|
||||
return `Failed to get working state: ${error instanceof Error ? error.message : String(error)}`
|
||||
}
|
||||
}
|
||||
|
||||
function truncateOutput(content: string): string {
|
||||
if (!GIT_OUTPUT_LINE_LIMIT) {
|
||||
return content
|
||||
}
|
||||
|
||||
const lines = content.split("\n")
|
||||
if (lines.length <= GIT_OUTPUT_LINE_LIMIT) {
|
||||
return content
|
||||
}
|
||||
|
||||
const beforeLimit = Math.floor(GIT_OUTPUT_LINE_LIMIT * 0.2) // 20% of lines before
|
||||
const afterLimit = GIT_OUTPUT_LINE_LIMIT - beforeLimit // remaining 80% after
|
||||
return [
|
||||
...lines.slice(0, beforeLimit),
|
||||
`\n[...${lines.length - GIT_OUTPUT_LINE_LIMIT} lines omitted...]\n`,
|
||||
...lines.slice(-afterLimit),
|
||||
].join("\n")
|
||||
}
|
||||
Generated
+2193
-83
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,8 @@
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"firebase": "^11.3.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"fzf": "^0.5.2",
|
||||
"mermaid": "^11.4.1",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
||||
@@ -98,18 +98,26 @@ config.module.rules[1].oneOf.forEach((rule) => {
|
||||
}
|
||||
})
|
||||
|
||||
// Disable code splitting
|
||||
config.optimization.splitChunks = {
|
||||
cacheGroups: {
|
||||
default: false,
|
||||
// Force all code into a single bundle for VS Code webview compatibility.
|
||||
// This is necessary for:
|
||||
// 1. Mermaid.js to work properly (prevents async chunk loading)
|
||||
// 2. Consistent CSP nonce handling (single bundle = single nonce)
|
||||
config.optimization = {
|
||||
...config.optimization,
|
||||
splitChunks: {
|
||||
cacheGroups: {
|
||||
default: false,
|
||||
},
|
||||
name: "main", // Forces all chunks (dynamic import() calls, for example those used by Mermaid) into one bundle - this is what actually prevents code splitting
|
||||
},
|
||||
runtimeChunk: false,
|
||||
}
|
||||
|
||||
// Disable code chunks
|
||||
config.optimization.runtimeChunk = false
|
||||
|
||||
// Rename main.{hash}.js to main.js
|
||||
config.output.filename = "static/js/[name].js"
|
||||
// Ensure all chunks are named 'main' to match our CSP nonce setup
|
||||
config.output = {
|
||||
...config.output,
|
||||
filename: "static/js/[name].js",
|
||||
}
|
||||
|
||||
// Rename main.{hash}.css to main.css
|
||||
config.plugins[5].options.filename = "static/css/[name].css"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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
|
||||
@@ -30,29 +31,34 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
</h3>
|
||||
<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
|
||||
<li>
|
||||
<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.
|
||||
<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
|
||||
</VSCodeLink>
|
||||
.
|
||||
</li>
|
||||
<li>
|
||||
<b>Quick API/model switching</b> with a new popup menu under the chat field
|
||||
<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.
|
||||
</li>
|
||||
<li>
|
||||
<b>VS Code LM API</b> lets you use models from other extensions like GitHub Copilot
|
||||
Use <code>@terminal</code> to reference terminal contents, and <code>@git</code> to reference working changes
|
||||
and commits!
|
||||
</li>
|
||||
<li>
|
||||
<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>
|
||||
New visual indicator for checkpoints after edits & commands, and automatic checkpoint at the start of each
|
||||
task.
|
||||
</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,
|
||||
@@ -109,9 +115,12 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
}}
|
||||
/>
|
||||
<p style={{ margin: "0" }}>
|
||||
Join our{" "}
|
||||
Join us on{" "}
|
||||
<VSCodeLink style={{ display: "inline" }} href="https://x.com/cline">
|
||||
X,
|
||||
</VSCodeLink>{" "}
|
||||
<VSCodeLink style={{ display: "inline" }} href="https://discord.gg/cline">
|
||||
discord
|
||||
discord,
|
||||
</VSCodeLink>{" "}
|
||||
or{" "}
|
||||
<VSCodeLink style={{ display: "inline" }} href="https://www.reddit.com/r/cline/">
|
||||
|
||||
@@ -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 } from "../../utils/mcp"
|
||||
import { findMatchingResourceOrTemplate, getMcpServerDisplayName } from "../../utils/mcp"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { CheckpointControls, CheckpointOverlay } from "../common/CheckpointControls"
|
||||
import CodeAccordian, { cleanPathPrefix } from "../common/CodeAccordian"
|
||||
@@ -101,7 +101,7 @@ const ChatRow = memo(
|
||||
export default ChatRow
|
||||
|
||||
export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifiedMessage, isLast }: ChatRowContentProps) => {
|
||||
const { mcpServers } = useExtensionState()
|
||||
const { mcpServers, mcpMarketplaceCatalog } = useExtensionState()
|
||||
|
||||
const [seeNewChangesDisabled, setSeeNewChangesDisabled] = useState(false)
|
||||
|
||||
@@ -202,9 +202,12 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
||||
marginBottom: "-1.5px",
|
||||
}}></span>
|
||||
),
|
||||
<span style={{ color: normalColor, fontWeight: "bold" }}>
|
||||
<span style={{ color: normalColor, fontWeight: "bold", wordBreak: "break-word" }}>
|
||||
Cline wants to {mcpServerUse.type === "use_mcp_tool" ? "use a tool" : "access a resource"} on the{" "}
|
||||
<code>{mcpServerUse.serverName}</code> MCP server:
|
||||
<code style={{ wordBreak: "break-all" }}>
|
||||
{getMcpServerDisplayName(mcpServerUse.serverName, mcpMarketplaceCatalog)}
|
||||
</code>{" "}
|
||||
MCP server:
|
||||
</span>,
|
||||
]
|
||||
case "completion_result":
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
|
||||
import DynamicTextArea from "react-textarea-autosize"
|
||||
import { useClickAway, useWindowSize } from "react-use"
|
||||
import { useClickAway, useEvent, useWindowSize } from "react-use"
|
||||
import styled from "styled-components"
|
||||
import { mentionRegex, mentionRegexGlobal } from "../../../../src/shared/context-mentions"
|
||||
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import {
|
||||
ContextMenuOptionType,
|
||||
@@ -12,16 +13,16 @@ import {
|
||||
removeMention,
|
||||
shouldShowContextMenu,
|
||||
} from "../../utils/context-mentions"
|
||||
import { useMetaKeyDetection, useShortcut } from "../../utils/hooks"
|
||||
import { validateApiConfiguration, validateModelId } from "../../utils/validate"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
|
||||
import Thumbnails from "../common/Thumbnails"
|
||||
import Tooltip from "../common/Tooltip"
|
||||
import ApiOptions, { normalizeApiConfiguration } from "../settings/ApiOptions"
|
||||
import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
|
||||
import ContextMenu from "./ContextMenu"
|
||||
import { useShortcut } from "../../utils/hooks"
|
||||
import Tooltip from "../common/Tooltip"
|
||||
import { useMetaKeyDetection } from "../../utils/hooks"
|
||||
import { ChatSettings } from "../../../../src/shared/ChatSettings"
|
||||
|
||||
interface ChatTextAreaProps {
|
||||
inputValue: string
|
||||
@@ -215,6 +216,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
) => {
|
||||
const { filePaths, chatSettings, apiConfiguration, openRouterModels, platform } = useExtensionState()
|
||||
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
|
||||
const [gitCommits, setGitCommits] = useState<any[]>([])
|
||||
|
||||
const [thumbnailsHeight, setThumbnailsHeight] = useState(0)
|
||||
const [textAreaBaseHeight, setTextAreaBaseHeight] = useState<number | undefined>(undefined)
|
||||
const [showContextMenu, setShowContextMenu] = useState(false)
|
||||
@@ -234,15 +237,47 @@ 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)
|
||||
|
||||
// Add a ref to track previous menu state
|
||||
const prevShowModelSelector = useRef(showModelSelector)
|
||||
|
||||
// Fetch git commits when Git is selected or when typing a hash
|
||||
useEffect(() => {
|
||||
if (selectedType === ContextMenuOptionType.Git || /^[a-f0-9]+$/i.test(searchQuery)) {
|
||||
vscode.postMessage({
|
||||
type: "searchCommits",
|
||||
text: searchQuery || "",
|
||||
})
|
||||
}
|
||||
}, [selectedType, searchQuery])
|
||||
|
||||
const handleMessage = useCallback((event: MessageEvent) => {
|
||||
const message: ExtensionMessage = event.data
|
||||
switch (message.type) {
|
||||
case "commitSearchResults": {
|
||||
const commits =
|
||||
message.commits?.map((commit: any) => ({
|
||||
type: ContextMenuOptionType.Git,
|
||||
value: commit.hash,
|
||||
label: commit.subject,
|
||||
description: `${commit.shortHash} by ${commit.author} on ${commit.date}`,
|
||||
})) || []
|
||||
setGitCommits(commits)
|
||||
break
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEvent("message", handleMessage)
|
||||
|
||||
const queryItems = useMemo(() => {
|
||||
return [
|
||||
{ type: ContextMenuOptionType.Problems, value: "problems" },
|
||||
{ type: ContextMenuOptionType.Terminal, value: "terminal" },
|
||||
...gitCommits,
|
||||
...filePaths
|
||||
.map((file) => "/" + file)
|
||||
.map((path) => ({
|
||||
@@ -250,7 +285,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
value: path,
|
||||
})),
|
||||
]
|
||||
}, [filePaths])
|
||||
}, [filePaths, gitCommits])
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
@@ -274,7 +309,11 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
return
|
||||
}
|
||||
|
||||
if (type === ContextMenuOptionType.File || type === ContextMenuOptionType.Folder) {
|
||||
if (
|
||||
type === ContextMenuOptionType.File ||
|
||||
type === ContextMenuOptionType.Folder ||
|
||||
type === ContextMenuOptionType.Git
|
||||
) {
|
||||
if (!value) {
|
||||
setSelectedType(type)
|
||||
setSearchQuery("")
|
||||
@@ -293,6 +332,10 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
insertValue = value || ""
|
||||
} else if (type === ContextMenuOptionType.Problems) {
|
||||
insertValue = "problems"
|
||||
} else if (type === ContextMenuOptionType.Terminal) {
|
||||
insertValue = "terminal"
|
||||
} else if (type === ContextMenuOptionType.Git) {
|
||||
insertValue = value || ""
|
||||
}
|
||||
|
||||
const { newValue, mentionIndex } = insertMention(textAreaRef.current.value, cursorPosition, insertValue)
|
||||
@@ -612,17 +655,21 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
setTimeout(() => {
|
||||
const newMode = chatSettings.mode === "plan" ? "act" : "plan"
|
||||
vscode.postMessage({
|
||||
type: "chatSettings",
|
||||
type: "togglePlanActMode",
|
||||
chatSettings: {
|
||||
mode: newMode,
|
||||
},
|
||||
chatContent: {
|
||||
message: inputValue.trim() ? inputValue : undefined,
|
||||
images: selectedImages.length > 0 ? selectedImages : undefined,
|
||||
},
|
||||
})
|
||||
// Focus the textarea after mode toggle with slight delay
|
||||
setTimeout(() => {
|
||||
textAreaRef.current?.focus()
|
||||
}, 100)
|
||||
}, changeModeDelay)
|
||||
}, [chatSettings.mode, showModelSelector, submitApiConfig])
|
||||
}, [chatSettings.mode, showModelSelector, submitApiConfig, inputValue, selectedImages])
|
||||
|
||||
useShortcut("Meta+Shift+a", onModeToggle, { disableTextInputs: false }) // important that we don't disable the text input here
|
||||
|
||||
@@ -892,7 +939,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
borderTop: 0,
|
||||
borderColor: "transparent",
|
||||
borderBottom: `${thumbnailsHeight + 6}px solid transparent`,
|
||||
padding: "9px 49px 3px 9px",
|
||||
padding: "9px 28px 3px 9px",
|
||||
}}
|
||||
/>
|
||||
<DynamicTextArea
|
||||
@@ -1086,12 +1133,23 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
</ModelContainer>
|
||||
</ButtonGroup>
|
||||
<Tooltip
|
||||
tipText={`In ${chatSettings.mode === "act" ? "Act" : "Plan"} mode, Cline will ${chatSettings.mode === "act" ? "complete the task immediately" : "gather information to architect a plan"}`}
|
||||
visible={shownTooltipMode !== null}
|
||||
tipText={`In ${shownTooltipMode === "act" ? "Act" : "Plan"} mode, Cline will ${shownTooltipMode === "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"}>Plan</SwitchOption>
|
||||
<SwitchOption isActive={chatSettings.mode === "act"}>Act</SwitchOption>
|
||||
<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>
|
||||
</SwitchContainer>
|
||||
</Tooltip>
|
||||
</ControlsContainer>
|
||||
|
||||
@@ -324,67 +324,99 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
/*
|
||||
This logic depends on the useEffect[messages] above to set clineAsk, after which buttons are shown and we then send an askResponse to the extension.
|
||||
*/
|
||||
const handlePrimaryButtonClick = useCallback(() => {
|
||||
switch (clineAsk) {
|
||||
case "api_req_failed":
|
||||
case "command":
|
||||
case "command_output":
|
||||
case "tool":
|
||||
case "browser_action_launch":
|
||||
case "use_mcp_server":
|
||||
case "resume_task":
|
||||
case "mistake_limit_reached":
|
||||
case "auto_approval_max_req_reached":
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "yesButtonClicked",
|
||||
})
|
||||
break
|
||||
case "completion_result":
|
||||
case "resume_completed_task":
|
||||
// extension waiting for feedback. but we can just present a new task button
|
||||
startNewTask()
|
||||
break
|
||||
}
|
||||
setTextAreaDisabled(true)
|
||||
setClineAsk(undefined)
|
||||
setEnableButtons(false)
|
||||
// setPrimaryButtonText(undefined)
|
||||
// setSecondaryButtonText(undefined)
|
||||
disableAutoScrollRef.current = false
|
||||
}, [clineAsk, startNewTask])
|
||||
const handlePrimaryButtonClick = useCallback(
|
||||
(text?: string, images?: string[]) => {
|
||||
const trimmedInput = text?.trim()
|
||||
switch (clineAsk) {
|
||||
case "api_req_failed":
|
||||
case "command":
|
||||
case "command_output":
|
||||
case "tool":
|
||||
case "browser_action_launch":
|
||||
case "use_mcp_server":
|
||||
case "resume_task":
|
||||
case "mistake_limit_reached":
|
||||
case "auto_approval_max_req_reached":
|
||||
if (trimmedInput || (images && images.length > 0)) {
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "yesButtonClicked",
|
||||
text: trimmedInput,
|
||||
images: images,
|
||||
})
|
||||
} else {
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "yesButtonClicked",
|
||||
})
|
||||
}
|
||||
// Clear input state after sending
|
||||
setInputValue("")
|
||||
setSelectedImages([])
|
||||
break
|
||||
case "completion_result":
|
||||
case "resume_completed_task":
|
||||
// extension waiting for feedback. but we can just present a new task button
|
||||
startNewTask()
|
||||
break
|
||||
}
|
||||
setTextAreaDisabled(true)
|
||||
setClineAsk(undefined)
|
||||
setEnableButtons(false)
|
||||
// setPrimaryButtonText(undefined)
|
||||
// setSecondaryButtonText(undefined)
|
||||
disableAutoScrollRef.current = false
|
||||
},
|
||||
[clineAsk, startNewTask],
|
||||
)
|
||||
|
||||
const handleSecondaryButtonClick = useCallback(() => {
|
||||
if (isStreaming) {
|
||||
vscode.postMessage({ type: "cancelTask" })
|
||||
setDidClickCancel(true)
|
||||
return
|
||||
}
|
||||
const handleSecondaryButtonClick = useCallback(
|
||||
(text?: string, images?: string[]) => {
|
||||
const trimmedInput = text?.trim()
|
||||
if (isStreaming) {
|
||||
vscode.postMessage({ type: "cancelTask" })
|
||||
setDidClickCancel(true)
|
||||
return
|
||||
}
|
||||
|
||||
switch (clineAsk) {
|
||||
case "api_req_failed":
|
||||
case "mistake_limit_reached":
|
||||
case "auto_approval_max_req_reached":
|
||||
startNewTask()
|
||||
break
|
||||
case "command":
|
||||
case "tool":
|
||||
case "browser_action_launch":
|
||||
case "use_mcp_server":
|
||||
// responds to the API with a "This operation failed" and lets it try again
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "noButtonClicked",
|
||||
})
|
||||
break
|
||||
}
|
||||
setTextAreaDisabled(true)
|
||||
setClineAsk(undefined)
|
||||
setEnableButtons(false)
|
||||
// setPrimaryButtonText(undefined)
|
||||
// setSecondaryButtonText(undefined)
|
||||
disableAutoScrollRef.current = false
|
||||
}, [clineAsk, startNewTask, isStreaming])
|
||||
switch (clineAsk) {
|
||||
case "api_req_failed":
|
||||
case "mistake_limit_reached":
|
||||
case "auto_approval_max_req_reached":
|
||||
startNewTask()
|
||||
break
|
||||
case "command":
|
||||
case "tool":
|
||||
case "browser_action_launch":
|
||||
case "use_mcp_server":
|
||||
if (trimmedInput || (images && images.length > 0)) {
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "noButtonClicked",
|
||||
text: trimmedInput,
|
||||
images: images,
|
||||
})
|
||||
} else {
|
||||
// responds to the API with a "This operation failed" and lets it try again
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "noButtonClicked",
|
||||
})
|
||||
}
|
||||
// Clear input state after sending
|
||||
setInputValue("")
|
||||
setSelectedImages([])
|
||||
break
|
||||
}
|
||||
setTextAreaDisabled(true)
|
||||
setClineAsk(undefined)
|
||||
setEnableButtons(false)
|
||||
// setPrimaryButtonText(undefined)
|
||||
// setSecondaryButtonText(undefined)
|
||||
disableAutoScrollRef.current = false
|
||||
},
|
||||
[clineAsk, startNewTask, isStreaming],
|
||||
)
|
||||
|
||||
const handleTaskCloseButtonClick = useCallback(() => {
|
||||
startNewTask()
|
||||
@@ -426,10 +458,10 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
handleSendMessage(message.text ?? "", message.images ?? [])
|
||||
break
|
||||
case "primaryButtonClick":
|
||||
handlePrimaryButtonClick()
|
||||
handlePrimaryButtonClick(message.text ?? "", message.images ?? [])
|
||||
break
|
||||
case "secondaryButtonClick":
|
||||
handleSecondaryButtonClick()
|
||||
handleSecondaryButtonClick(message.text ?? "", message.images ?? [])
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -869,7 +901,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
flex: secondaryButtonText ? 1 : 2,
|
||||
marginRight: secondaryButtonText ? "6px" : "0",
|
||||
}}
|
||||
onClick={handlePrimaryButtonClick}>
|
||||
onClick={() => handlePrimaryButtonClick(inputValue, selectedImages)}>
|
||||
{primaryButtonText}
|
||||
</VSCodeButton>
|
||||
)}
|
||||
@@ -881,7 +913,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
flex: isStreaming ? 2 : 1,
|
||||
marginLeft: isStreaming ? 0 : "6px",
|
||||
}}
|
||||
onClick={handleSecondaryButtonClick}>
|
||||
onClick={() => handleSecondaryButtonClick(inputValue, selectedImages)}>
|
||||
{isStreaming ? "Cancel" : secondaryButtonText}
|
||||
</VSCodeButton>
|
||||
)}
|
||||
|
||||
@@ -48,10 +48,33 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
|
||||
switch (option.type) {
|
||||
case ContextMenuOptionType.Problems:
|
||||
return <span>Problems</span>
|
||||
case ContextMenuOptionType.Terminal:
|
||||
return <span>Terminal</span>
|
||||
case ContextMenuOptionType.URL:
|
||||
return <span>Paste URL to fetch contents</span>
|
||||
case ContextMenuOptionType.NoResults:
|
||||
return <span>No results found</span>
|
||||
case ContextMenuOptionType.Git:
|
||||
if (option.value) {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
|
||||
<span style={{ lineHeight: "1.2" }}>{option.label}</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "0.85em",
|
||||
opacity: 0.7,
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
lineHeight: "1.2",
|
||||
}}>
|
||||
{option.description}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return <span>Git Commits</span>
|
||||
}
|
||||
case ContextMenuOptionType.File:
|
||||
case ContextMenuOptionType.Folder:
|
||||
if (option.value) {
|
||||
@@ -85,8 +108,12 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
|
||||
return "folder"
|
||||
case ContextMenuOptionType.Problems:
|
||||
return "warning"
|
||||
case ContextMenuOptionType.Terminal:
|
||||
return "terminal"
|
||||
case ContextMenuOptionType.URL:
|
||||
return "link"
|
||||
case ContextMenuOptionType.Git:
|
||||
return "git-commit"
|
||||
case ContextMenuOptionType.NoResults:
|
||||
return "info"
|
||||
default:
|
||||
@@ -161,7 +188,9 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
|
||||
/>
|
||||
{renderOptionContent(option)}
|
||||
</div>
|
||||
{(option.type === ContextMenuOptionType.File || option.type === ContextMenuOptionType.Folder) &&
|
||||
{(option.type === ContextMenuOptionType.File ||
|
||||
option.type === ContextMenuOptionType.Folder ||
|
||||
option.type === ContextMenuOptionType.Git) &&
|
||||
!option.value && (
|
||||
<i
|
||||
className="codicon codicon-chevron-right"
|
||||
@@ -173,7 +202,10 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
|
||||
/>
|
||||
)}
|
||||
{(option.type === ContextMenuOptionType.Problems ||
|
||||
((option.type === ContextMenuOptionType.File || option.type === ContextMenuOptionType.Folder) &&
|
||||
option.type === ContextMenuOptionType.Terminal ||
|
||||
((option.type === ContextMenuOptionType.File ||
|
||||
option.type === ContextMenuOptionType.Folder ||
|
||||
option.type === ContextMenuOptionType.Git) &&
|
||||
option.value)) && (
|
||||
<i
|
||||
className="codicon codicon-add"
|
||||
|
||||
@@ -100,14 +100,20 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
||||
}, [task.text, windowWidth])
|
||||
|
||||
const isCostAvailable = useMemo(() => {
|
||||
const openAiCompatHasPricing =
|
||||
apiConfiguration?.apiProvider === "openai" &&
|
||||
apiConfiguration?.openAiModelInfo?.inputPrice &&
|
||||
apiConfiguration?.openAiModelInfo?.outputPrice
|
||||
if (openAiCompatHasPricing) {
|
||||
return true
|
||||
}
|
||||
return (
|
||||
apiConfiguration?.apiProvider !== "openai" &&
|
||||
apiConfiguration?.apiProvider !== "vscode-lm" &&
|
||||
apiConfiguration?.apiProvider !== "ollama" &&
|
||||
apiConfiguration?.apiProvider !== "lmstudio" &&
|
||||
apiConfiguration?.apiProvider !== "gemini"
|
||||
)
|
||||
}, [apiConfiguration?.apiProvider])
|
||||
}, [apiConfiguration?.apiProvider, apiConfiguration?.openAiModelInfo])
|
||||
|
||||
const shouldShowPromptCacheInfo =
|
||||
doesModelSupportPromptCache && apiConfiguration?.apiProvider !== "openrouter" && apiConfiguration?.apiProvider !== "cline"
|
||||
@@ -423,7 +429,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{/* {ContextWindowComponent} */}
|
||||
{ContextWindowComponent}
|
||||
{isCostAvailable && (
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
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,10 +1,11 @@
|
||||
import { memo, useEffect } from "react"
|
||||
import React, { 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
|
||||
@@ -220,7 +221,27 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
|
||||
],
|
||||
rehypeReactOptions: {
|
||||
components: {
|
||||
pre: ({ node, ...preProps }: any) => <StyledPre {...preProps} theme={theme} />,
|
||||
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} />
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
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, "<").replace(/>/g, ">")
|
||||
})
|
||||
.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;
|
||||
`
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "../../utils/vscStyles"
|
||||
|
||||
interface TooltipProps {
|
||||
visible: boolean
|
||||
hintText: string
|
||||
tipText: string
|
||||
children: React.ReactNode
|
||||
@@ -38,14 +39,9 @@ const Hint = styled.div`
|
||||
margin-top: 2px;
|
||||
`
|
||||
|
||||
const Tooltip: React.FC<TooltipProps> = ({ tipText, hintText, children }) => {
|
||||
const [visible, setVisible] = useState(false)
|
||||
|
||||
const showTooltip = () => setVisible(true)
|
||||
const hideTooltip = () => setVisible(false)
|
||||
|
||||
const Tooltip: React.FC<TooltipProps> = ({ visible, tipText, hintText, children }) => {
|
||||
return (
|
||||
<div style={{ position: "relative", display: "inline-block" }} onMouseEnter={showTooltip} onMouseLeave={hideTooltip}>
|
||||
<div style={{ position: "relative", display: "inline-block" }}>
|
||||
{children}
|
||||
{visible && (
|
||||
<TooltipBody>
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { VSCodeButton, VSCodeLink, VSCodePanels, VSCodePanelTab, VSCodePanelView } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useState } from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { McpServer } from "../../../../src/shared/mcp"
|
||||
import McpToolRow from "./McpToolRow"
|
||||
import McpResourceRow from "./McpResourceRow"
|
||||
import McpMarketplaceView from "./marketplace/McpMarketplaceView"
|
||||
import styled from "styled-components"
|
||||
import { getMcpServerDisplayName } from "../../utils/mcp"
|
||||
import DangerButton from "../common/DangerButton"
|
||||
|
||||
type McpViewProps = {
|
||||
onDone: () => void
|
||||
@@ -12,6 +16,16 @@ type McpViewProps = {
|
||||
|
||||
const McpView = ({ onDone }: McpViewProps) => {
|
||||
const { mcpServers: servers } = useExtensionState()
|
||||
const [activeTab, setActiveTab] = useState("marketplace")
|
||||
|
||||
const handleTabChange = (tab: string) => {
|
||||
setActiveTab(tab)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
vscode.postMessage({ type: "silentlyRefreshMcpMarketplace" })
|
||||
vscode.postMessage({ type: "fetchLatestMcpServersFromHub" })
|
||||
}, [])
|
||||
|
||||
// const [servers, setServers] = useState<McpServer[]>([
|
||||
// // Add some mock servers for testing
|
||||
@@ -90,86 +104,144 @@ const McpView = ({ onDone }: McpViewProps) => {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "10px 17px 10px 20px",
|
||||
padding: "10px 17px 5px 20px",
|
||||
}}>
|
||||
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>MCP Servers</h3>
|
||||
<VSCodeButton onClick={onDone}>Done</VSCodeButton>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, overflow: "auto", padding: "0 20px" }}>
|
||||
<div style={{ flex: 1, overflow: "auto" }}>
|
||||
{/* Tabs container */}
|
||||
<div
|
||||
style={{
|
||||
color: "var(--vscode-foreground)",
|
||||
fontSize: "13px",
|
||||
marginBottom: "16px",
|
||||
marginTop: "5px",
|
||||
display: "flex",
|
||||
gap: "1px",
|
||||
padding: "0 20px 0 20px",
|
||||
borderBottom: "1px solid var(--vscode-panel-border)",
|
||||
}}>
|
||||
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>
|
||||
<TabButton isActive={activeTab === "marketplace"} onClick={() => handleTabChange("marketplace")}>
|
||||
Marketplace
|
||||
</TabButton>
|
||||
<TabButton isActive={activeTab === "installed"} onClick={() => handleTabChange("installed")}>
|
||||
Installed
|
||||
</TabButton>
|
||||
</div>
|
||||
|
||||
{servers.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "10px",
|
||||
}}>
|
||||
{servers.map((server) => (
|
||||
<ServerRow key={server.name} server={server} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{/* Content container */}
|
||||
<div style={{ width: "100%" }}>
|
||||
{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>
|
||||
|
||||
{/* Server Configuration Button */}
|
||||
{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>
|
||||
)}
|
||||
|
||||
<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>
|
||||
{/* 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>
|
||||
|
||||
{/* 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) {
|
||||
@@ -195,6 +267,14 @@ const ServerRow = ({ server }: { server: McpServer }) => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
setIsDeleting(true)
|
||||
vscode.postMessage({
|
||||
type: "deleteMcpServer",
|
||||
serverName: server.name,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: "10px" }}>
|
||||
<div
|
||||
@@ -211,7 +291,18 @@ const ServerRow = ({ server }: { server: McpServer }) => {
|
||||
{!server.error && (
|
||||
<span className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`} style={{ marginRight: "8px" }} />
|
||||
)}
|
||||
<span style={{ flex: 1 }}>{server.name}</span>
|
||||
<span
|
||||
style={{
|
||||
flex: 1,
|
||||
overflow: "hidden",
|
||||
wordBreak: "break-all",
|
||||
whiteSpace: "normal",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginRight: "4px",
|
||||
}}>
|
||||
{getMcpServerDisplayName(server.name, mcpMarketplaceCatalog)}
|
||||
</span>
|
||||
<div style={{ display: "flex", alignItems: "center", marginRight: "8px" }} onClick={(e) => e.stopPropagation()}>
|
||||
<div
|
||||
role="switch"
|
||||
@@ -379,6 +470,17 @@ const ServerRow = ({ server }: { server: McpServer }) => {
|
||||
}}>
|
||||
{server.status === "connecting" ? "Restarting..." : "Restart Server"}
|
||||
</VSCodeButton>
|
||||
|
||||
<DangerButton
|
||||
// appearance="secondary"
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
style={{
|
||||
width: "calc(100% - 14px)",
|
||||
margin: "5px 7px 3px 7px",
|
||||
}}>
|
||||
{isDeleting ? "Deleting..." : "Delete Server"}
|
||||
</DangerButton>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
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
|
||||
@@ -0,0 +1,286 @@
|
||||
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
|
||||
@@ -0,0 +1,45 @@
|
||||
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
|
||||
@@ -42,6 +42,7 @@ import VSCodeButtonLink from "../common/VSCodeButtonLink"
|
||||
import OpenRouterModelPicker, { ModelDescriptionMarkdown } from "./OpenRouterModelPicker"
|
||||
import styled from "styled-components"
|
||||
import * as vscodemodels from "vscode"
|
||||
import { getAsVar, VSC_DESCRIPTION_FOREGROUND } from "../../utils/vscStyles"
|
||||
|
||||
interface ApiOptionsProps {
|
||||
showModelOptions: boolean
|
||||
@@ -81,6 +82,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
|
||||
const [vsCodeLmModels, setVsCodeLmModels] = useState<vscodemodels.LanguageModelChatSelector[]>([])
|
||||
const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl)
|
||||
const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion)
|
||||
const [modelConfigurationSelected, setModelConfigurationSelected] = useState(false)
|
||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
|
||||
|
||||
const handleClineLogin = () => {
|
||||
@@ -734,6 +736,127 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
|
||||
placeholder={`Default: ${azureOpenAiDefaultApiVersion}`}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
color: getAsVar(VSC_DESCRIPTION_FOREGROUND),
|
||||
display: "flex",
|
||||
margin: "10px 0",
|
||||
cursor: "pointer",
|
||||
alignItems: "center",
|
||||
}}
|
||||
onClick={() => setModelConfigurationSelected((val) => !val)}>
|
||||
<span
|
||||
className={`codicon ${modelConfigurationSelected ? "codicon-chevron-down" : "codicon-chevron-right"}`}
|
||||
style={{
|
||||
marginRight: "4px",
|
||||
}}></span>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 700,
|
||||
textTransform: "uppercase",
|
||||
}}>
|
||||
Model Configuration
|
||||
</span>
|
||||
</div>
|
||||
{modelConfigurationSelected && (
|
||||
<>
|
||||
<VSCodeCheckbox
|
||||
checked={apiConfiguration?.openAiModelInfo?.supportsImages}
|
||||
onChange={(e: any) => {
|
||||
const isChecked = e.target.checked === true
|
||||
let modelInfo = apiConfiguration?.openAiModelInfo
|
||||
? apiConfiguration.openAiModelInfo
|
||||
: { ...openAiModelInfoSaneDefaults }
|
||||
modelInfo.supportsImages = isChecked
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
openAiModelInfo: modelInfo,
|
||||
})
|
||||
}}>
|
||||
Supports Images
|
||||
</VSCodeCheckbox>
|
||||
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
|
||||
<VSCodeTextField
|
||||
value={
|
||||
apiConfiguration?.openAiModelInfo?.contextWindow
|
||||
? apiConfiguration.openAiModelInfo.contextWindow.toString()
|
||||
: openAiModelInfoSaneDefaults.contextWindow?.toString()
|
||||
}
|
||||
style={{ flex: 1 }}
|
||||
onInput={(input: any) => {
|
||||
let modelInfo = apiConfiguration?.openAiModelInfo
|
||||
? apiConfiguration.openAiModelInfo
|
||||
: { ...openAiModelInfoSaneDefaults }
|
||||
modelInfo.contextWindow = Number(input.target.value)
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
openAiModelInfo: modelInfo,
|
||||
})
|
||||
}}>
|
||||
<span style={{ fontWeight: 500 }}>Context Window Size</span>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={
|
||||
apiConfiguration?.openAiModelInfo?.maxTokens
|
||||
? apiConfiguration.openAiModelInfo.maxTokens.toString()
|
||||
: openAiModelInfoSaneDefaults.maxTokens?.toString()
|
||||
}
|
||||
style={{ flex: 1 }}
|
||||
onInput={(input: any) => {
|
||||
let modelInfo = apiConfiguration?.openAiModelInfo
|
||||
? apiConfiguration.openAiModelInfo
|
||||
: { ...openAiModelInfoSaneDefaults }
|
||||
modelInfo.maxTokens = input.target.value
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
openAiModelInfo: modelInfo,
|
||||
})
|
||||
}}>
|
||||
<span style={{ fontWeight: 500 }}>Max Output Tokens</span>
|
||||
</VSCodeTextField>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
|
||||
<VSCodeTextField
|
||||
value={
|
||||
apiConfiguration?.openAiModelInfo?.inputPrice
|
||||
? apiConfiguration.openAiModelInfo.inputPrice.toString()
|
||||
: openAiModelInfoSaneDefaults.inputPrice?.toString()
|
||||
}
|
||||
style={{ flex: 1 }}
|
||||
onInput={(input: any) => {
|
||||
let modelInfo = apiConfiguration?.openAiModelInfo
|
||||
? apiConfiguration.openAiModelInfo
|
||||
: { ...openAiModelInfoSaneDefaults }
|
||||
modelInfo.inputPrice = input.target.value
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
openAiModelInfo: modelInfo,
|
||||
})
|
||||
}}>
|
||||
<span style={{ fontWeight: 500 }}>Input Price / 1M tokens</span>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={
|
||||
apiConfiguration?.openAiModelInfo?.outputPrice
|
||||
? apiConfiguration.openAiModelInfo.outputPrice.toString()
|
||||
: openAiModelInfoSaneDefaults.outputPrice?.toString()
|
||||
}
|
||||
style={{ flex: 1 }}
|
||||
onInput={(input: any) => {
|
||||
let modelInfo = apiConfiguration?.openAiModelInfo
|
||||
? apiConfiguration.openAiModelInfo
|
||||
: { ...openAiModelInfoSaneDefaults }
|
||||
modelInfo.outputPrice = input.target.value
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
openAiModelInfo: modelInfo,
|
||||
})
|
||||
}}>
|
||||
<span style={{ fontWeight: 500 }}>Output Price / 1M tokens</span>
|
||||
</VSCodeTextField>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
@@ -940,6 +1063,14 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
|
||||
|
||||
{selectedProvider === "litellm" && (
|
||||
<div>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.liteLlmApiKey || ""}
|
||||
style={{ width: "100%" }}
|
||||
type="password"
|
||||
onInput={handleInputChange("liteLlmApiKey")}
|
||||
placeholder="Default: noop">
|
||||
<span style={{ fontWeight: 500 }}>API Key</span>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.liteLlmBaseUrl || ""}
|
||||
style={{ width: "100%" }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import { render, screen, fireEvent } from "@testing-library/react"
|
||||
import { describe, it, expect, vi } from "vitest"
|
||||
import ApiOptions from "../ApiOptions"
|
||||
import { ExtensionStateContextProvider } from "../../../context/ExtensionStateContext"
|
||||
@@ -94,3 +94,59 @@ describe("ApiOptions Component", () => {
|
||||
expect(modelIdInput).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => {
|
||||
const actual = await importOriginal()
|
||||
return {
|
||||
...actual,
|
||||
// your mocked methods
|
||||
useExtensionState: vi.fn(() => ({
|
||||
apiConfiguration: {
|
||||
apiProvider: "openai",
|
||||
requestyApiKey: "",
|
||||
requestyModelId: "",
|
||||
},
|
||||
setApiConfiguration: vi.fn(),
|
||||
uriScheme: "vscode",
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
describe("OpenApiInfoOptions", () => {
|
||||
const mockPostMessage = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
global.vscode = { postMessage: mockPostMessage }
|
||||
})
|
||||
|
||||
it("renders OpenAI Supports Images input", () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ApiOptions showModelOptions={true} />
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
const apiKeyInput = screen.getByText("Supports Images")
|
||||
expect(apiKeyInput).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders OpenAI Context Window Size input", () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ApiOptions showModelOptions={true} />
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
const orgIdInput = screen.getByText("Context Window Size")
|
||||
expect(orgIdInput).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders OpenAI Max Output Tokens input", () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ApiOptions showModelOptions={true} />
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
const modelInput = screen.getByText("Max Output Tokens")
|
||||
expect(modelInput).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 { McpServer } from "../../../src/shared/mcp"
|
||||
import { McpMarketplaceCatalog, McpServer } from "../../../src/shared/mcp"
|
||||
import { convertTextMateToHljs } from "../utils/textMateToHljs"
|
||||
import { vscode } from "../utils/vscode"
|
||||
import { DEFAULT_BROWSER_SETTINGS } from "../../../src/shared/BrowserSettings"
|
||||
@@ -17,6 +17,7 @@ interface ExtensionStateContextType extends ExtensionState {
|
||||
openRouterModels: Record<string, ModelInfo>
|
||||
openAiModels: string[]
|
||||
mcpServers: McpServer[]
|
||||
mcpMarketplaceCatalog: McpMarketplaceCatalog
|
||||
filePaths: string[]
|
||||
setApiConfiguration: (config: ApiConfiguration) => void
|
||||
setCustomInstructions: (value?: string) => void
|
||||
@@ -48,7 +49,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) {
|
||||
@@ -64,6 +65,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
config.openAiApiKey,
|
||||
config.ollamaModelId,
|
||||
config.lmStudioModelId,
|
||||
config.liteLlmApiKey,
|
||||
config.geminiApiKey,
|
||||
config.openAiNativeApiKey,
|
||||
config.deepSeekApiKey,
|
||||
@@ -120,6 +122,12 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
setMcpServers(message.mcpServers ?? [])
|
||||
break
|
||||
}
|
||||
case "mcpMarketplaceCatalog": {
|
||||
if (message.mcpMarketplaceCatalog) {
|
||||
setMcpMarketplaceCatalog(message.mcpMarketplaceCatalog)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
@@ -137,6 +145,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
openRouterModels,
|
||||
openAiModels,
|
||||
mcpServers,
|
||||
mcpMarketplaceCatalog,
|
||||
filePaths,
|
||||
setApiConfiguration: (value) =>
|
||||
setState((prevState) => ({
|
||||
|
||||
@@ -45,14 +45,14 @@ describe("useMetaKeyDetection", () => {
|
||||
// mock the detect functions
|
||||
const { result } = renderHook(() => useMetaKeyDetection("win32"))
|
||||
expect(result.current[0]).toBe("windows")
|
||||
expect(result.current[1]).toBe("⊞ Win")
|
||||
expect(result.current[1]).toBe("Win")
|
||||
})
|
||||
|
||||
it("should detect Mac OS and metaKey from platform", () => {
|
||||
// mock the detect functions
|
||||
const { result } = renderHook(() => useMetaKeyDetection("darwin"))
|
||||
expect(result.current[0]).toBe("mac")
|
||||
expect(result.current[1]).toBe("⌘ Command")
|
||||
expect(result.current[1]).toBe("CMD")
|
||||
})
|
||||
|
||||
it("should detect Linux OS and metaKey from platform", () => {
|
||||
|
||||
@@ -4,12 +4,12 @@ import { detectMetaKeyChar } from "../platformUtils"
|
||||
describe("detectMetaKeyChar", () => {
|
||||
it("should return ⌘ Command for darwin platform", () => {
|
||||
const result = detectMetaKeyChar("darwin")
|
||||
expect(result).toBe("⌘ Command")
|
||||
expect(result).toBe("CMD")
|
||||
})
|
||||
|
||||
it("should return ⊞ Win for win32 platform", () => {
|
||||
const result = detectMetaKeyChar("win32")
|
||||
expect(result).toBe("⊞ Win")
|
||||
expect(result).toBe("Win")
|
||||
})
|
||||
|
||||
it("should return Alt for linux platform", () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { mentionRegex } from "../../../src/shared/context-mentions"
|
||||
import { Fzf } from "fzf"
|
||||
|
||||
export function insertMention(text: string, position: number, value: string): { newValue: string; mentionIndex: number } {
|
||||
const beforeCursor = text.slice(0, position)
|
||||
@@ -46,13 +47,17 @@ export enum ContextMenuOptionType {
|
||||
File = "file",
|
||||
Folder = "folder",
|
||||
Problems = "problems",
|
||||
Terminal = "terminal",
|
||||
URL = "url",
|
||||
Git = "git",
|
||||
NoResults = "noResults",
|
||||
}
|
||||
|
||||
export interface ContextMenuQueryItem {
|
||||
type: ContextMenuOptionType
|
||||
value?: string
|
||||
label?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export function getContextMenuOptions(
|
||||
@@ -60,6 +65,13 @@ export function getContextMenuOptions(
|
||||
selectedType: ContextMenuOptionType | null = null,
|
||||
queryItems: ContextMenuQueryItem[],
|
||||
): ContextMenuQueryItem[] {
|
||||
const workingChanges: ContextMenuQueryItem = {
|
||||
type: ContextMenuOptionType.Git,
|
||||
value: "git-changes",
|
||||
label: "Working changes",
|
||||
description: "Current uncommitted changes",
|
||||
}
|
||||
|
||||
if (query === "") {
|
||||
if (selectedType === ContextMenuOptionType.File) {
|
||||
const files = queryItems
|
||||
@@ -81,30 +93,102 @@ export function getContextMenuOptions(
|
||||
return folders.length > 0 ? folders : [{ type: ContextMenuOptionType.NoResults }]
|
||||
}
|
||||
|
||||
if (selectedType === ContextMenuOptionType.Git) {
|
||||
const commits = queryItems.filter((item) => item.type === ContextMenuOptionType.Git)
|
||||
return commits.length > 0 ? [workingChanges, ...commits] : [workingChanges]
|
||||
}
|
||||
|
||||
return [
|
||||
{ type: ContextMenuOptionType.URL },
|
||||
{ type: ContextMenuOptionType.Problems },
|
||||
{ type: ContextMenuOptionType.Terminal },
|
||||
{ type: ContextMenuOptionType.Git },
|
||||
{ type: ContextMenuOptionType.Folder },
|
||||
{ type: ContextMenuOptionType.File },
|
||||
]
|
||||
}
|
||||
|
||||
const lowerQuery = query.toLowerCase()
|
||||
const suggestions: ContextMenuQueryItem[] = []
|
||||
|
||||
// Check for top-level option matches
|
||||
if ("git".startsWith(lowerQuery)) {
|
||||
suggestions.push({
|
||||
type: ContextMenuOptionType.Git,
|
||||
label: "Git Commits",
|
||||
description: "Search repository history",
|
||||
})
|
||||
} else if ("git-changes".startsWith(lowerQuery)) {
|
||||
suggestions.push(workingChanges)
|
||||
}
|
||||
if ("problems".startsWith(lowerQuery)) {
|
||||
suggestions.push({ type: ContextMenuOptionType.Problems })
|
||||
}
|
||||
if (query.startsWith("http")) {
|
||||
return [{ type: ContextMenuOptionType.URL, value: query }]
|
||||
} else {
|
||||
const matchingItems = queryItems.filter((item) => item.value?.toLowerCase().includes(lowerQuery))
|
||||
suggestions.push({ type: ContextMenuOptionType.URL, value: query })
|
||||
}
|
||||
|
||||
if (matchingItems.length > 0) {
|
||||
return matchingItems.map((item) => ({
|
||||
type: item.type,
|
||||
value: item.value,
|
||||
}))
|
||||
// Add exact SHA matches to suggestions
|
||||
if (/^[a-f0-9]{7,40}$/i.test(lowerQuery)) {
|
||||
const exactMatches = queryItems.filter(
|
||||
(item) => item.type === ContextMenuOptionType.Git && item.value?.toLowerCase() === lowerQuery,
|
||||
)
|
||||
if (exactMatches.length > 0) {
|
||||
suggestions.push(...exactMatches)
|
||||
} else {
|
||||
return [{ type: ContextMenuOptionType.NoResults }]
|
||||
// If no exact match but valid SHA format, add as option
|
||||
suggestions.push({
|
||||
type: ContextMenuOptionType.Git,
|
||||
value: lowerQuery,
|
||||
label: `Commit ${lowerQuery}`,
|
||||
description: "Git commit hash",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Create searchable strings array for fzf
|
||||
const searchableItems = queryItems.map((item) => ({
|
||||
original: item,
|
||||
searchStr: [item.value, item.label, item.description].filter(Boolean).join(" "),
|
||||
}))
|
||||
|
||||
// Initialize fzf instance for fuzzy search
|
||||
const fzf = new Fzf(searchableItems, {
|
||||
selector: (item) => item.searchStr,
|
||||
})
|
||||
|
||||
// Get fuzzy matching items
|
||||
const matchingItems = query ? fzf.find(query).map((result) => result.item.original) : []
|
||||
|
||||
// Separate matches by type
|
||||
const fileMatches = matchingItems.filter(
|
||||
(item) => item.type === ContextMenuOptionType.File || item.type === ContextMenuOptionType.Folder,
|
||||
)
|
||||
const gitMatches = matchingItems.filter((item) => item.type === ContextMenuOptionType.Git)
|
||||
const otherMatches = matchingItems.filter(
|
||||
(item) =>
|
||||
item.type !== ContextMenuOptionType.File &&
|
||||
item.type !== ContextMenuOptionType.Folder &&
|
||||
item.type !== ContextMenuOptionType.Git,
|
||||
)
|
||||
|
||||
// Combine suggestions with matching items in the desired order
|
||||
if (suggestions.length > 0 || matchingItems.length > 0) {
|
||||
const allItems = [...suggestions, ...fileMatches, ...gitMatches, ...otherMatches]
|
||||
|
||||
// Remove duplicates based on type and value
|
||||
const seen = new Set()
|
||||
const deduped = allItems.filter((item) => {
|
||||
const key = `${item.type}-${item.value}`
|
||||
if (seen.has(key)) return false
|
||||
seen.add(key)
|
||||
return true
|
||||
})
|
||||
|
||||
return deduped
|
||||
}
|
||||
|
||||
return [{ type: ContextMenuOptionType.NoResults }]
|
||||
}
|
||||
|
||||
export function shouldShowContextMenu(text: string, position: number): boolean {
|
||||
@@ -121,8 +205,8 @@ export function shouldShowContextMenu(text: string, position: number): boolean {
|
||||
// Don't show the menu if it's a URL
|
||||
if (textAfterAt.toLowerCase().startsWith("http")) return false
|
||||
|
||||
// Don't show the menu if it's a problems
|
||||
if (textAfterAt.toLowerCase().startsWith("problems")) return false
|
||||
// Don't show the menu if it's a problems or terminal
|
||||
if (textAfterAt.toLowerCase().startsWith("problems") || textAfterAt.toLowerCase().startsWith("terminal")) return false
|
||||
|
||||
// NOTE: it's okay that menu shows when there's trailing punctuation since user could be inputting a path with marks
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { McpResource, McpResourceTemplate } from "../../../src/shared/mcp"
|
||||
import { McpMarketplaceCatalog, McpResource, McpResourceTemplate } from "../../../src/shared/mcp"
|
||||
|
||||
/**
|
||||
* Matches a URI against an array of URI templates and returns the matching template
|
||||
@@ -40,3 +40,17 @@ 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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
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 re‐schedule if any item in `deps` changed,
|
||||
// or if `delay` changed.
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [delay, ...deps])
|
||||
}
|
||||
Reference in New Issue
Block a user