diff --git a/.roo/rules-code/use-safeWriteJson.md b/.roo/rules-code/use-safeWriteJson.md new file mode 100644 index 0000000000..21e42553da --- /dev/null +++ b/.roo/rules-code/use-safeWriteJson.md @@ -0,0 +1,6 @@ +# JSON File Writing Must Be Atomic + +- You MUST use `safeWriteJson(filePath: string, data: any): Promise` from `src/utils/safeWriteJson.ts` instead of `JSON.stringify` with file-write operations +- `safeWriteJson` will create parent directories if necessary, so do not call `mkdir` prior to `safeWriteJson` +- `safeWriteJson` prevents data corruption via atomic writes with locking and streams the write to minimize memory footprint +- Test files are exempt from this rule diff --git a/.roo/rules-issue-fixer/1_Workflow.xml b/.roo/rules-issue-fixer/1_Workflow.xml index 8fe7778448..40971a1064 100644 --- a/.roo/rules-issue-fixer/1_Workflow.xml +++ b/.roo/rules-issue-fixer/1_Workflow.xml @@ -491,4 +491,62 @@ ``` + + + Monitor PR Checks + + After the PR is created, monitor the CI/CD checks to ensure they pass: + + + gh pr checks --watch + + + This command will: + 1. Display all CI/CD checks configured for the repository + 2. Show the status of each check in real-time + 3. Update automatically as checks complete + 4. Exit when all checks have finished running + + Monitor the output and note: + - Which checks are running (e.g., tests, linting, build) + - Any checks that fail and their error messages + - The overall status of the PR checks + + If any checks fail: + 1. Analyze the failure logs + 2. Identify what needs to be fixed + 3. Ask the user if they want you to address the failures + + + The PR checks have completed. Here's the status: + + [Show check results - passing/failing] + + [If all pass]: All checks have passed successfully! The PR is ready for review. + + [If any fail]: Some checks have failed: + - [Failed check 1]: [Brief error description] + - [Failed check 2]: [Brief error description] + + Would you like me to fix these issues? + + Yes, please fix the failing checks + Show me the detailed error logs + I'll handle the failures manually + The PR is fine as-is, these failures are expected + + + + If user wants fixes: + 1. Create a plan to address each failure + 2. Make necessary code changes + 3. Commit and push the fixes + 4. Monitor checks again to ensure they pass + + Important notes: + - The --watch flag will keep the command running until all checks complete + - This step helps ensure the PR meets all quality standards before review + - Early detection of CI/CD failures saves reviewer time + + \ No newline at end of file diff --git a/.roo/rules-pr-fixer/1_workflow.xml b/.roo/rules-pr-fixer/1_workflow.xml index c27004c430..845a05d4ea 100644 --- a/.roo/rules-pr-fixer/1_workflow.xml +++ b/.roo/rules-pr-fixer/1_workflow.xml @@ -47,6 +47,13 @@ Resolve conflicts by rebasing the PR branch and force-pushing. + + + Verify that the pushed changes resolve the issues. + + Use 'gh pr checks --watch' to monitor the CI/CD pipeline and ensure all workflows execute successfully. + + diff --git a/.roo/rules-pr-fixer/3_common_patterns.xml b/.roo/rules-pr-fixer/3_common_patterns.xml index 848be1482d..659aa7d07f 100644 --- a/.roo/rules-pr-fixer/3_common_patterns.xml +++ b/.roo/rules-pr-fixer/3_common_patterns.xml @@ -45,4 +45,10 @@ gh pr checkout + + After pushing changes, use this command to monitor the CI/CD pipeline in real-time. + + diff --git a/.roo/rules-pr-fixer/4_tool_usage.xml b/.roo/rules-pr-fixer/4_tool_usage.xml index e6f828b9f2..10361dc3ec 100644 --- a/.roo/rules-pr-fixer/4_tool_usage.xml +++ b/.roo/rules-pr-fixer/4_tool_usage.xml @@ -9,8 +9,13 @@ gh pr checks After getting comments, to check the technical status. Quickly identifies if there are failing automated checks that need investigation. - - + + + gh pr checks --watch + After pushing a fix, to confirm that the changes have resolved the CI/CD failures. + Provides real-time feedback on whether the fix was successful. + + diff --git a/.roo/rules-pr-fixer/5_examples.xml b/.roo/rules-pr-fixer/5_examples.xml index 77905cc3a6..e34a79421f 100644 --- a/.roo/rules-pr-fixer/5_examples.xml +++ b/.roo/rules-pr-fixer/5_examples.xml @@ -81,6 +81,15 @@ + + After pushing the changes, watch the PR checks to confirm the fix. + + + gh pr checks --watch + + + Confirm that all checks are passing after the fix. + diff --git a/.roo/rules-pr-reviewer/1_workflow.xml b/.roo/rules-pr-reviewer/1_workflow.xml index c27efaf4d5..31b70d981d 100644 --- a/.roo/rules-pr-reviewer/1_workflow.xml +++ b/.roo/rules-pr-reviewer/1_workflow.xml @@ -100,7 +100,7 @@ - Examine existing PR comments to understand the current state of discussion. Always verify whether a comment is current or already addressed before suggesting action. + Examine existing PR comments to understand the current state of discussion. When reading the comments and reviews, you must verify which are resolved by reading the files they refer to, since they might already be resolved. This prevents you from making redundant suggestions. @@ -163,12 +163,10 @@ [Summary of findings organized by priority] Would you like me to: - 1. Submit these as individual review comments - 2. Create a comprehensive review with all comments - 3. Modify any of the suggestions - 4. Skip the review submission + 1. Create a comprehensive review with all comments + 2. Modify any of the suggestions + 3. Skip the review submission - Submit as individual review comments Create a comprehensive review Let me modify the suggestions first Skip submission - just wanted the analysis @@ -180,9 +178,22 @@ Submit Review - Based on user preference, submit the review: + Based on user preference, submit the review as a comprehensive review: - For individual comments: + 1. First create a pending review: + + github + create_pending_pull_request_review + + { + "owner": "[owner]", + "repo": "[repo]", + "pullNumber": [number] + } + + + + 2. Add comments to the pending review using: github add_pull_request_review_comment_to_pending_review @@ -199,22 +210,6 @@ - For comprehensive review: - 1. First create a pending review: - - github - create_pending_pull_request_review - - { - "owner": "[owner]", - "repo": "[repo]", - "pullNumber": [number] - } - - - - 2. Add comments to the pending review - 3. Submit the review: github diff --git a/.roo/rules-pr-reviewer/2_best_practices.xml b/.roo/rules-pr-reviewer/2_best_practices.xml index 29adcba946..69ba8088f0 100644 --- a/.roo/rules-pr-reviewer/2_best_practices.xml +++ b/.roo/rules-pr-reviewer/2_best_practices.xml @@ -2,7 +2,7 @@ - Always fetch and review the entire PR diff before commenting - Check for and review any associated issue for context - Check out the PR locally for better context understanding - - Review existing comments to avoid duplicate feedback + - Review existing comments and verify against the current code to avoid redundant feedback on already resolved issues - Focus on the changes made, not unrelated code - Ensure all changes are directly related to the linked issue - Use a friendly, curious tone in all comments diff --git a/.roo/rules-pr-reviewer/3_common_mistakes_to_avoid.xml b/.roo/rules-pr-reviewer/3_common_mistakes_to_avoid.xml index 6a5b707c12..0868956e87 100644 --- a/.roo/rules-pr-reviewer/3_common_mistakes_to_avoid.xml +++ b/.roo/rules-pr-reviewer/3_common_mistakes_to_avoid.xml @@ -7,7 +7,7 @@ - Using markdown headings (###, ##, #) in review comments - Using excessive markdown formatting when plain text would suffice - Submitting comments without user preview/approval - - Ignoring existing PR comments and discussions + - Ignoring existing PR comments or failing to verify if they have already been resolved by checking the code - Forgetting to check for an associated issue for additional context - Missing critical security or performance issues - Not checking for proper i18n in UI changes diff --git a/.roo/rules-translate/001-general-rules.md b/.roo/rules-translate/001-general-rules.md index 2b747f77b9..e27b9793e2 100644 --- a/.roo/rules-translate/001-general-rules.md +++ b/.roo/rules-translate/001-general-rules.md @@ -64,8 +64,10 @@ 1. Identify where the string appears in the UI/codebase 2. Understand the context and purpose of the string 3. Update English translation first - 4. Create appropriate translations for all other supported languages - 5. Validate your changes with the missing translations script + 4. Use the `` tool to find JSON keys that are near new keys in English translations but do not yet exist in the other language files for `` SEARCH context + 5. Create appropriate translations for all other supported languages utilizing the `search_files` result using `` without reading every file. + 6. Do not output the translated text into the chat, just modify the files. + 7. Validate your changes with the missing translations script - Flag or comment if an English source string is incomplete ("please see this...") to avoid truncated or unclear translations - For UI elements, distinguish between: - Button labels: Use short imperative commands ("Save", "Cancel") diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d0265bef8..6099da527f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Roo Code Changelog +## [3.22.0] - 2025-06-25 + +- Add 1-click task sharing +- Add support for loading rules from a global .roo directory (thanks @samhvw8!) +- Modes selector improvements (thanks @brunobergher!) +- Use safeWriteJson for all JSON file writes to avoid task history corruption (thanks @KJ7LNW!) +- Improve YAML error handling when editing modes +- Register importSettings as VSCode command (thanks @shivamd1810!) +- Add default task names for empty tasks (thanks @daniel-lxs!) +- Improve translation workflow to avoid unnecessary file reads (thanks @KJ7LNW!) +- Allow write_to_file to handle newline-only and empty content (thanks @Githubguy132010!) +- Address multiple memory leaks in CodeBlock component (thanks @kiwina!) +- Memory cleanup (thanks @xyOz-dev!) +- Fix port handling bug in code indexing for HTTPS URLs (thanks @benashby!) +- Improve Bedrock error handling for throttling and streaming contexts +- Handle long Claude code messages (thanks @daniel-lxs!) +- Fixes to Claude Code caching and image upload +- Disable reasoning budget UI controls for Claude Code provider +- Remove temperature parameter for Azure OpenAI reasoning models (thanks @ExactDoug!) +- Allowed commands import/export (thanks @catrielmuller!) +- Add VS Code setting to disable quick fix context actions (thanks @OlegOAndreev!) + ## [3.21.5] - 2025-06-23 - Fix Qdrant URL prefix handling for QdrantClient initialization (thanks @CW-B-W!) diff --git a/README.md b/README.md index f936ac6bc8..af209a1429 100644 --- a/README.md +++ b/README.md @@ -176,40 +176,40 @@ Thanks to all our contributors who have helped make Roo Code better! -| mrubens
mrubens
| saoudrizwan
saoudrizwan
| cte
cte
| samhvw8
samhvw8
| daniel-lxs
daniel-lxs
| hannesrudolph
hannesrudolph
| -| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -| KJ7LNW
KJ7LNW
| a8trejo
a8trejo
| ColemanRoo
ColemanRoo
| canrobins13
canrobins13
| stea9499
stea9499
| joemanley201
joemanley201
| -| System233
System233
| jquanton
jquanton
| nissa-seru
nissa-seru
| NyxJae
NyxJae
| jr
jr
| MuriloFP
MuriloFP
| -| elianiva
elianiva
| d-oit
d-oit
| punkpeye
punkpeye
| wkordalski
wkordalski
| sachasayan
sachasayan
| Smartsheet-JB-Brown
Smartsheet-JB-Brown
| -| monotykamary
monotykamary
| cannuri
cannuri
| xyOz-dev
xyOz-dev
| feifei325
feifei325
| zhangtony239
zhangtony239
| qdaxb
qdaxb
| -| shariqriazz
shariqriazz
| pugazhendhi-m
pugazhendhi-m
| vigneshsubbiah16
vigneshsubbiah16
| lloydchang
lloydchang
| dtrugman
dtrugman
| Szpadel
Szpadel
| -| chrarnoldus
chrarnoldus
| diarmidmackenzie
diarmidmackenzie
| olweraltuve
olweraltuve
| psv2522
psv2522
| Premshay
Premshay
| lupuletic
lupuletic
| -| kiwina
kiwina
| aheizi
aheizi
| PeterDaveHello
PeterDaveHello
| afshawnlotfi
afshawnlotfi
| RaySinner
RaySinner
| nbihan-mediware
nbihan-mediware
| -| ChuKhaLi
ChuKhaLi
| hassoncs
hassoncs
| emshvac
emshvac
| kyle-apex
kyle-apex
| noritaka1166
noritaka1166
| pdecat
pdecat
| -| SannidhyaSah
SannidhyaSah
| StevenTCramer
StevenTCramer
| Lunchb0ne
Lunchb0ne
| SmartManoj
SmartManoj
| vagadiya
vagadiya
| slytechnical
slytechnical
| -| dleffel
dleffel
| arthurauffray
arthurauffray
| upamune
upamune
| NamesMT
NamesMT
| taylorwilsdon
taylorwilsdon
| sammcj
sammcj
| -| Ruakij
Ruakij
| p12tic
p12tic
| gtaylor
gtaylor
| aitoroses
aitoroses
| mr-ryan-james
mr-ryan-james
| heyseth
heyseth
| -| taisukeoe
taisukeoe
| avtc
avtc
| dlab-anton
dlab-anton
| eonghk
eonghk
| kcwhite
kcwhite
| ronyblum
ronyblum
| -| teddyOOXX
teddyOOXX
| vincentsong
vincentsong
| yongjer
yongjer
| zeozeozeo
zeozeozeo
| ashktn
ashktn
| franekp
franekp
| -| yt3trees
yt3trees
| anton-otee
anton-otee
| benzntech
benzntech
| axkirillov
axkirillov
| bramburn
bramburn
| olearycrew
olearycrew
| -| snoyiatk
snoyiatk
| GitlyHallows
GitlyHallows
| ross
ross
| philfung
philfung
| napter
napter
| mdp
mdp
| -| SplittyDev
SplittyDev
| jcbdev
jcbdev
| julionav
julionav
| Chenjiayuan195
Chenjiayuan195
| kinandan
kinandan
| jwcraig
jwcraig
| -| shoopapa
shoopapa
| im47cn
im47cn
| hongzio
hongzio
| hatsu38
hatsu38
| GOODBOY008
GOODBOY008
| forestyoo
forestyoo
| -| dqroid
dqroid
| dairui1
dairui1
| bannzai
bannzai
| axmo
axmo
| asychin
asychin
| amittell
amittell
| -| Yoshino-Yukitaro
Yoshino-Yukitaro
| Yikai-Liao
Yikai-Liao
| zxdvd
zxdvd
| vladstudio
vladstudio
| tmsjngx0
tmsjngx0
| tgfjt
tgfjt
| -| maekawataiki
maekawataiki
| nevermorec
nevermorec
| PretzelVector
PretzelVector
| zetaloop
zetaloop
| cdlliuy
cdlliuy
| user202729
user202729
| -| student20880
student20880
| shohei-ihaya
shohei-ihaya
| shaybc
shaybc
| seedlord
seedlord
| samir-nimbly
samir-nimbly
| robertheadley
robertheadley
| -| refactorthis
refactorthis
| qingyuan1109
qingyuan1109
| pokutuna
pokutuna
| philipnext
philipnext
| village-way
village-way
| oprstchn
oprstchn
| -| nobu007
nobu007
| mosleyit
mosleyit
| moqimoqidea
moqimoqidea
| mlopezr
mlopezr
| mecab
mecab
| olup
olup
| -| lightrabbit
lightrabbit
| kohii
kohii
| AlexandruSmirnov
AlexandruSmirnov
| edwin-truthsearch-io
edwin-truthsearch-io
| EamonNerbonne
EamonNerbonne
| dbasclpy
dbasclpy
| -| dflatline
dflatline
| Deon588
Deon588
| dleen
dleen
| devxpain
devxpain
| chadgauth
chadgauth
| brunobergher
brunobergher
| -| thecolorblue
thecolorblue
| bogdan0083
bogdan0083
| benashby
benashby
| Atlogit
Atlogit
| atlasgong
atlasgong
| andreastempsch
andreastempsch
| -| alasano
alasano
| QuinsZouls
QuinsZouls
| HadesArchitect
HadesArchitect
| alarno
alarno
| nexon33
nexon33
| adilhafeez
adilhafeez
| -| adamwlarson
adamwlarson
| adamhill
adamhill
| AMHesch
AMHesch
| samsilveira
samsilveira
| 01Rian
01Rian
| RSO
RSO
| -| SECKainersdorfer
SECKainersdorfer
| R-omk
R-omk
| Sarke
Sarke
| kvokka
kvokka
| ecmasx
ecmasx
| mollux
mollux
| -| marvijo-code
marvijo-code
| markijbema
markijbema
| mamertofabian
mamertofabian
| monkeyDluffy6017
monkeyDluffy6017
| libertyteeth
libertyteeth
| shtse8
shtse8
| -| Rexarrior
Rexarrior
| KanTakahiro
KanTakahiro
| ksze
ksze
| Jdo300
Jdo300
| hesara
hesara
| DeXtroTip
DeXtroTip
| -| pfitz
pfitz
| celestial-vault
celestial-vault
| linegel
linegel
| | | | +| mrubens
mrubens
| saoudrizwan
saoudrizwan
| cte
cte
| samhvw8
samhvw8
| daniel-lxs
daniel-lxs
| hannesrudolph
hannesrudolph
| +| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| KJ7LNW
KJ7LNW
| a8trejo
a8trejo
| ColemanRoo
ColemanRoo
| canrobins13
canrobins13
| stea9499
stea9499
| joemanley201
joemanley201
| +| System233
System233
| jquanton
jquanton
| nissa-seru
nissa-seru
| jr
jr
| NyxJae
NyxJae
| MuriloFP
MuriloFP
| +| elianiva
elianiva
| d-oit
d-oit
| punkpeye
punkpeye
| wkordalski
wkordalski
| xyOz-dev
xyOz-dev
| sachasayan
sachasayan
| +| Smartsheet-JB-Brown
Smartsheet-JB-Brown
| monotykamary
monotykamary
| cannuri
cannuri
| feifei325
feifei325
| zhangtony239
zhangtony239
| qdaxb
qdaxb
| +| shariqriazz
shariqriazz
| pugazhendhi-m
pugazhendhi-m
| dtrugman
dtrugman
| lloydchang
lloydchang
| vigneshsubbiah16
vigneshsubbiah16
| Szpadel
Szpadel
| +| lupuletic
lupuletic
| kiwina
kiwina
| Premshay
Premshay
| psv2522
psv2522
| olweraltuve
olweraltuve
| diarmidmackenzie
diarmidmackenzie
| +| chrarnoldus
chrarnoldus
| PeterDaveHello
PeterDaveHello
| aheizi
aheizi
| afshawnlotfi
afshawnlotfi
| RaySinner
RaySinner
| nbihan-mediware
nbihan-mediware
| +| ChuKhaLi
ChuKhaLi
| hassoncs
hassoncs
| emshvac
emshvac
| kyle-apex
kyle-apex
| noritaka1166
noritaka1166
| pdecat
pdecat
| +| SannidhyaSah
SannidhyaSah
| StevenTCramer
StevenTCramer
| Lunchb0ne
Lunchb0ne
| SmartManoj
SmartManoj
| vagadiya
vagadiya
| slytechnical
slytechnical
| +| dleffel
dleffel
| arthurauffray
arthurauffray
| upamune
upamune
| NamesMT
NamesMT
| taylorwilsdon
taylorwilsdon
| sammcj
sammcj
| +| Ruakij
Ruakij
| p12tic
p12tic
| gtaylor
gtaylor
| aitoroses
aitoroses
| benzntech
benzntech
| mr-ryan-james
mr-ryan-james
| +| heyseth
heyseth
| taisukeoe
taisukeoe
| avtc
avtc
| dlab-anton
dlab-anton
| eonghk
eonghk
| kcwhite
kcwhite
| +| ronyblum
ronyblum
| teddyOOXX
teddyOOXX
| vincentsong
vincentsong
| yongjer
yongjer
| zeozeozeo
zeozeozeo
| ashktn
ashktn
| +| franekp
franekp
| yt3trees
yt3trees
| axkirillov
axkirillov
| anton-otee
anton-otee
| bramburn
bramburn
| olearycrew
olearycrew
| +| brunobergher
brunobergher
| snoyiatk
snoyiatk
| GitlyHallows
GitlyHallows
| jcbdev
jcbdev
| Chenjiayuan195
Chenjiayuan195
| julionav
julionav
| +| SplittyDev
SplittyDev
| mdp
mdp
| napter
napter
| ross
ross
| philfung
philfung
| dairui1
dairui1
| +| dqroid
dqroid
| forestyoo
forestyoo
| GOODBOY008
GOODBOY008
| hatsu38
hatsu38
| hongzio
hongzio
| im47cn
im47cn
| +| shoopapa
shoopapa
| jwcraig
jwcraig
| nevermorec
nevermorec
| bannzai
bannzai
| axmo
axmo
| asychin
asychin
| +| amittell
amittell
| Yoshino-Yukitaro
Yoshino-Yukitaro
| Yikai-Liao
Yikai-Liao
| zxdvd
zxdvd
| vladstudio
vladstudio
| tmsjngx0
tmsjngx0
| +| Githubguy132010
Githubguy132010
| tgfjt
tgfjt
| maekawataiki
maekawataiki
| PretzelVector
PretzelVector
| zetaloop
zetaloop
| cdlliuy
cdlliuy
| +| user202729
user202729
| student20880
student20880
| shohei-ihaya
shohei-ihaya
| shaybc
shaybc
| seedlord
seedlord
| samir-nimbly
samir-nimbly
| +| robertheadley
robertheadley
| refactorthis
refactorthis
| qingyuan1109
qingyuan1109
| pokutuna
pokutuna
| philipnext
philipnext
| village-way
village-way
| +| oprstchn
oprstchn
| nobu007
nobu007
| mosleyit
mosleyit
| moqimoqidea
moqimoqidea
| mlopezr
mlopezr
| mecab
mecab
| +| olup
olup
| lightrabbit
lightrabbit
| kohii
kohii
| kinandan
kinandan
| linegel
linegel
| edwin-truthsearch-io
edwin-truthsearch-io
| +| EamonNerbonne
EamonNerbonne
| dbasclpy
dbasclpy
| dflatline
dflatline
| Deon588
Deon588
| dleen
dleen
| devxpain
devxpain
| +| CW-B-W
CW-B-W
| chadgauth
chadgauth
| thecolorblue
thecolorblue
| bogdan0083
bogdan0083
| benashby
benashby
| Atlogit
Atlogit
| +| atlasgong
atlasgong
| andreastempsch
andreastempsch
| alasano
alasano
| QuinsZouls
QuinsZouls
| HadesArchitect
HadesArchitect
| alarno
alarno
| +| nexon33
nexon33
| adilhafeez
adilhafeez
| adamwlarson
adamwlarson
| adamhill
adamhill
| AMHesch
AMHesch
| AlexandruSmirnov
AlexandruSmirnov
| +| samsilveira
samsilveira
| 01Rian
01Rian
| RSO
RSO
| SECKainersdorfer
SECKainersdorfer
| R-omk
R-omk
| Sarke
Sarke
| +| OlegOAndreev
OlegOAndreev
| kvokka
kvokka
| ecmasx
ecmasx
| mollux
mollux
| marvijo-code
marvijo-code
| markijbema
markijbema
| +| mamertofabian
mamertofabian
| monkeyDluffy6017
monkeyDluffy6017
| libertyteeth
libertyteeth
| shtse8
shtse8
| Rexarrior
Rexarrior
| KanTakahiro
KanTakahiro
| +| ksze
ksze
| Jdo300
Jdo300
| hesara
hesara
| DeXtroTip
DeXtroTip
| pfitz
pfitz
| celestial-vault
celestial-vault
| diff --git a/locales/ca/README.md b/locales/ca/README.md index 871c295cac..bf342dda2c 100644 --- a/locales/ca/README.md +++ b/locales/ca/README.md @@ -184,37 +184,37 @@ Gràcies a tots els nostres col·laboradors que han ajudat a millorar Roo Code! |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|samhvw8
samhvw8
|daniel-lxs
daniel-lxs
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| |KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
|joemanley201
joemanley201
| -|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|NyxJae
NyxJae
|jr
jr
|MuriloFP
MuriloFP
| -|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
| -|monotykamary
monotykamary
|cannuri
cannuri
|xyOz-dev
xyOz-dev
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| -|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|vigneshsubbiah16
vigneshsubbiah16
|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
| -|chrarnoldus
chrarnoldus
|diarmidmackenzie
diarmidmackenzie
|olweraltuve
olweraltuve
|psv2522
psv2522
|Premshay
Premshay
|lupuletic
lupuletic
| -|kiwina
kiwina
|aheizi
aheizi
|PeterDaveHello
PeterDaveHello
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| +|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|jr
jr
|NyxJae
NyxJae
|MuriloFP
MuriloFP
| +|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
| +|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| +|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|dtrugman
dtrugman
|lloydchang
lloydchang
|vigneshsubbiah16
vigneshsubbiah16
|Szpadel
Szpadel
| +|lupuletic
lupuletic
|kiwina
kiwina
|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
| +|chrarnoldus
chrarnoldus
|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| |ChuKhaLi
ChuKhaLi
|hassoncs
hassoncs
|emshvac
emshvac
|kyle-apex
kyle-apex
|noritaka1166
noritaka1166
|pdecat
pdecat
| |SannidhyaSah
SannidhyaSah
|StevenTCramer
StevenTCramer
|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
| |dleffel
dleffel
|arthurauffray
arthurauffray
|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
| -|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
| -|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
|ronyblum
ronyblum
| -|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
| -|yt3trees
yt3trees
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|bramburn
bramburn
|olearycrew
olearycrew
| -|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|ross
ross
|philfung
philfung
|napter
napter
|mdp
mdp
| -|SplittyDev
SplittyDev
|jcbdev
jcbdev
|julionav
julionav
|Chenjiayuan195
Chenjiayuan195
|kinandan
kinandan
|jwcraig
jwcraig
| -|shoopapa
shoopapa
|im47cn
im47cn
|hongzio
hongzio
|hatsu38
hatsu38
|GOODBOY008
GOODBOY008
|forestyoo
forestyoo
| -|dqroid
dqroid
|dairui1
dairui1
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
| -|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
| -|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
| -|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
| -|lightrabbit
lightrabbit
|kohii
kohii
|AlexandruSmirnov
AlexandruSmirnov
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
| -|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
|chadgauth
chadgauth
|brunobergher
brunobergher
| -|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andreastempsch
andreastempsch
| -|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
| -|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
| -|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
| -|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
| -|pfitz
pfitz
|celestial-vault
celestial-vault
|linegel
linegel
| | | | +|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|benzntech
benzntech
|mr-ryan-james
mr-ryan-james
| +|heyseth
heyseth
|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
| +|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
| +|franekp
franekp
|yt3trees
yt3trees
|axkirillov
axkirillov
|anton-otee
anton-otee
|bramburn
bramburn
|olearycrew
olearycrew
| +|brunobergher
brunobergher
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
|julionav
julionav
| +|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|ross
ross
|philfung
philfung
|dairui1
dairui1
| +|dqroid
dqroid
|forestyoo
forestyoo
|GOODBOY008
GOODBOY008
|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
| +|shoopapa
shoopapa
|jwcraig
jwcraig
|nevermorec
nevermorec
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
| +|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
| +|Githubguy132010
Githubguy132010
|tgfjt
tgfjt
|maekawataiki
maekawataiki
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
| +|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
| +|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
| +|olup
olup
|lightrabbit
lightrabbit
|kohii
kohii
|kinandan
kinandan
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| +|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
| +|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
| +|atlasgong
atlasgong
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| +|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|AlexandruSmirnov
AlexandruSmirnov
| +|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
| +|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
| +|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
| +|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|celestial-vault
celestial-vault
| ## Llicència diff --git a/locales/de/README.md b/locales/de/README.md index c5b19d46af..1c1eb41f19 100644 --- a/locales/de/README.md +++ b/locales/de/README.md @@ -184,37 +184,37 @@ Danke an alle unsere Mitwirkenden, die geholfen haben, Roo Code zu verbessern! |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|samhvw8
samhvw8
|daniel-lxs
daniel-lxs
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| |KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
|joemanley201
joemanley201
| -|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|NyxJae
NyxJae
|jr
jr
|MuriloFP
MuriloFP
| -|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
| -|monotykamary
monotykamary
|cannuri
cannuri
|xyOz-dev
xyOz-dev
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| -|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|vigneshsubbiah16
vigneshsubbiah16
|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
| -|chrarnoldus
chrarnoldus
|diarmidmackenzie
diarmidmackenzie
|olweraltuve
olweraltuve
|psv2522
psv2522
|Premshay
Premshay
|lupuletic
lupuletic
| -|kiwina
kiwina
|aheizi
aheizi
|PeterDaveHello
PeterDaveHello
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| +|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|jr
jr
|NyxJae
NyxJae
|MuriloFP
MuriloFP
| +|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
| +|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| +|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|dtrugman
dtrugman
|lloydchang
lloydchang
|vigneshsubbiah16
vigneshsubbiah16
|Szpadel
Szpadel
| +|lupuletic
lupuletic
|kiwina
kiwina
|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
| +|chrarnoldus
chrarnoldus
|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| |ChuKhaLi
ChuKhaLi
|hassoncs
hassoncs
|emshvac
emshvac
|kyle-apex
kyle-apex
|noritaka1166
noritaka1166
|pdecat
pdecat
| |SannidhyaSah
SannidhyaSah
|StevenTCramer
StevenTCramer
|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
| |dleffel
dleffel
|arthurauffray
arthurauffray
|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
| -|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
| -|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
|ronyblum
ronyblum
| -|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
| -|yt3trees
yt3trees
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|bramburn
bramburn
|olearycrew
olearycrew
| -|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|ross
ross
|philfung
philfung
|napter
napter
|mdp
mdp
| -|SplittyDev
SplittyDev
|jcbdev
jcbdev
|julionav
julionav
|Chenjiayuan195
Chenjiayuan195
|kinandan
kinandan
|jwcraig
jwcraig
| -|shoopapa
shoopapa
|im47cn
im47cn
|hongzio
hongzio
|hatsu38
hatsu38
|GOODBOY008
GOODBOY008
|forestyoo
forestyoo
| -|dqroid
dqroid
|dairui1
dairui1
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
| -|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
| -|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
| -|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
| -|lightrabbit
lightrabbit
|kohii
kohii
|AlexandruSmirnov
AlexandruSmirnov
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
| -|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
|chadgauth
chadgauth
|brunobergher
brunobergher
| -|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andreastempsch
andreastempsch
| -|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
| -|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
| -|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
| -|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
| -|pfitz
pfitz
|celestial-vault
celestial-vault
|linegel
linegel
| | | | +|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|benzntech
benzntech
|mr-ryan-james
mr-ryan-james
| +|heyseth
heyseth
|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
| +|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
| +|franekp
franekp
|yt3trees
yt3trees
|axkirillov
axkirillov
|anton-otee
anton-otee
|bramburn
bramburn
|olearycrew
olearycrew
| +|brunobergher
brunobergher
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
|julionav
julionav
| +|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|ross
ross
|philfung
philfung
|dairui1
dairui1
| +|dqroid
dqroid
|forestyoo
forestyoo
|GOODBOY008
GOODBOY008
|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
| +|shoopapa
shoopapa
|jwcraig
jwcraig
|nevermorec
nevermorec
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
| +|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
| +|Githubguy132010
Githubguy132010
|tgfjt
tgfjt
|maekawataiki
maekawataiki
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
| +|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
| +|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
| +|olup
olup
|lightrabbit
lightrabbit
|kohii
kohii
|kinandan
kinandan
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| +|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
| +|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
| +|atlasgong
atlasgong
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| +|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|AlexandruSmirnov
AlexandruSmirnov
| +|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
| +|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
| +|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
| +|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|celestial-vault
celestial-vault
| ## Lizenz diff --git a/locales/es/README.md b/locales/es/README.md index efd25ae17d..aa15874979 100644 --- a/locales/es/README.md +++ b/locales/es/README.md @@ -184,37 +184,37 @@ Usamos [changesets](https://github.com/changesets/changesets) para versionar y p |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|samhvw8
samhvw8
|daniel-lxs
daniel-lxs
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| |KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
|joemanley201
joemanley201
| -|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|NyxJae
NyxJae
|jr
jr
|MuriloFP
MuriloFP
| -|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
| -|monotykamary
monotykamary
|cannuri
cannuri
|xyOz-dev
xyOz-dev
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| -|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|vigneshsubbiah16
vigneshsubbiah16
|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
| -|chrarnoldus
chrarnoldus
|diarmidmackenzie
diarmidmackenzie
|olweraltuve
olweraltuve
|psv2522
psv2522
|Premshay
Premshay
|lupuletic
lupuletic
| -|kiwina
kiwina
|aheizi
aheizi
|PeterDaveHello
PeterDaveHello
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| +|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|jr
jr
|NyxJae
NyxJae
|MuriloFP
MuriloFP
| +|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
| +|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| +|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|dtrugman
dtrugman
|lloydchang
lloydchang
|vigneshsubbiah16
vigneshsubbiah16
|Szpadel
Szpadel
| +|lupuletic
lupuletic
|kiwina
kiwina
|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
| +|chrarnoldus
chrarnoldus
|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| |ChuKhaLi
ChuKhaLi
|hassoncs
hassoncs
|emshvac
emshvac
|kyle-apex
kyle-apex
|noritaka1166
noritaka1166
|pdecat
pdecat
| |SannidhyaSah
SannidhyaSah
|StevenTCramer
StevenTCramer
|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
| |dleffel
dleffel
|arthurauffray
arthurauffray
|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
| -|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
| -|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
|ronyblum
ronyblum
| -|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
| -|yt3trees
yt3trees
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|bramburn
bramburn
|olearycrew
olearycrew
| -|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|ross
ross
|philfung
philfung
|napter
napter
|mdp
mdp
| -|SplittyDev
SplittyDev
|jcbdev
jcbdev
|julionav
julionav
|Chenjiayuan195
Chenjiayuan195
|kinandan
kinandan
|jwcraig
jwcraig
| -|shoopapa
shoopapa
|im47cn
im47cn
|hongzio
hongzio
|hatsu38
hatsu38
|GOODBOY008
GOODBOY008
|forestyoo
forestyoo
| -|dqroid
dqroid
|dairui1
dairui1
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
| -|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
| -|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
| -|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
| -|lightrabbit
lightrabbit
|kohii
kohii
|AlexandruSmirnov
AlexandruSmirnov
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
| -|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
|chadgauth
chadgauth
|brunobergher
brunobergher
| -|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andreastempsch
andreastempsch
| -|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
| -|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
| -|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
| -|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
| -|pfitz
pfitz
|celestial-vault
celestial-vault
|linegel
linegel
| | | | +|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|benzntech
benzntech
|mr-ryan-james
mr-ryan-james
| +|heyseth
heyseth
|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
| +|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
| +|franekp
franekp
|yt3trees
yt3trees
|axkirillov
axkirillov
|anton-otee
anton-otee
|bramburn
bramburn
|olearycrew
olearycrew
| +|brunobergher
brunobergher
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
|julionav
julionav
| +|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|ross
ross
|philfung
philfung
|dairui1
dairui1
| +|dqroid
dqroid
|forestyoo
forestyoo
|GOODBOY008
GOODBOY008
|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
| +|shoopapa
shoopapa
|jwcraig
jwcraig
|nevermorec
nevermorec
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
| +|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
| +|Githubguy132010
Githubguy132010
|tgfjt
tgfjt
|maekawataiki
maekawataiki
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
| +|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
| +|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
| +|olup
olup
|lightrabbit
lightrabbit
|kohii
kohii
|kinandan
kinandan
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| +|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
| +|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
| +|atlasgong
atlasgong
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| +|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|AlexandruSmirnov
AlexandruSmirnov
| +|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
| +|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
| +|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
| +|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|celestial-vault
celestial-vault
| ## Licencia diff --git a/locales/fr/README.md b/locales/fr/README.md index 893e73cfa0..a7100ba924 100644 --- a/locales/fr/README.md +++ b/locales/fr/README.md @@ -184,37 +184,37 @@ Merci à tous nos contributeurs qui ont aidé à améliorer Roo Code ! |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|samhvw8
samhvw8
|daniel-lxs
daniel-lxs
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| |KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
|joemanley201
joemanley201
| -|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|NyxJae
NyxJae
|jr
jr
|MuriloFP
MuriloFP
| -|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
| -|monotykamary
monotykamary
|cannuri
cannuri
|xyOz-dev
xyOz-dev
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| -|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|vigneshsubbiah16
vigneshsubbiah16
|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
| -|chrarnoldus
chrarnoldus
|diarmidmackenzie
diarmidmackenzie
|olweraltuve
olweraltuve
|psv2522
psv2522
|Premshay
Premshay
|lupuletic
lupuletic
| -|kiwina
kiwina
|aheizi
aheizi
|PeterDaveHello
PeterDaveHello
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| +|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|jr
jr
|NyxJae
NyxJae
|MuriloFP
MuriloFP
| +|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
| +|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| +|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|dtrugman
dtrugman
|lloydchang
lloydchang
|vigneshsubbiah16
vigneshsubbiah16
|Szpadel
Szpadel
| +|lupuletic
lupuletic
|kiwina
kiwina
|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
| +|chrarnoldus
chrarnoldus
|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| |ChuKhaLi
ChuKhaLi
|hassoncs
hassoncs
|emshvac
emshvac
|kyle-apex
kyle-apex
|noritaka1166
noritaka1166
|pdecat
pdecat
| |SannidhyaSah
SannidhyaSah
|StevenTCramer
StevenTCramer
|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
| |dleffel
dleffel
|arthurauffray
arthurauffray
|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
| -|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
| -|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
|ronyblum
ronyblum
| -|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
| -|yt3trees
yt3trees
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|bramburn
bramburn
|olearycrew
olearycrew
| -|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|ross
ross
|philfung
philfung
|napter
napter
|mdp
mdp
| -|SplittyDev
SplittyDev
|jcbdev
jcbdev
|julionav
julionav
|Chenjiayuan195
Chenjiayuan195
|kinandan
kinandan
|jwcraig
jwcraig
| -|shoopapa
shoopapa
|im47cn
im47cn
|hongzio
hongzio
|hatsu38
hatsu38
|GOODBOY008
GOODBOY008
|forestyoo
forestyoo
| -|dqroid
dqroid
|dairui1
dairui1
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
| -|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
| -|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
| -|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
| -|lightrabbit
lightrabbit
|kohii
kohii
|AlexandruSmirnov
AlexandruSmirnov
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
| -|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
|chadgauth
chadgauth
|brunobergher
brunobergher
| -|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andreastempsch
andreastempsch
| -|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
| -|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
| -|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
| -|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
| -|pfitz
pfitz
|celestial-vault
celestial-vault
|linegel
linegel
| | | | +|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|benzntech
benzntech
|mr-ryan-james
mr-ryan-james
| +|heyseth
heyseth
|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
| +|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
| +|franekp
franekp
|yt3trees
yt3trees
|axkirillov
axkirillov
|anton-otee
anton-otee
|bramburn
bramburn
|olearycrew
olearycrew
| +|brunobergher
brunobergher
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
|julionav
julionav
| +|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|ross
ross
|philfung
philfung
|dairui1
dairui1
| +|dqroid
dqroid
|forestyoo
forestyoo
|GOODBOY008
GOODBOY008
|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
| +|shoopapa
shoopapa
|jwcraig
jwcraig
|nevermorec
nevermorec
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
| +|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
| +|Githubguy132010
Githubguy132010
|tgfjt
tgfjt
|maekawataiki
maekawataiki
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
| +|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
| +|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
| +|olup
olup
|lightrabbit
lightrabbit
|kohii
kohii
|kinandan
kinandan
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| +|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
| +|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
| +|atlasgong
atlasgong
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| +|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|AlexandruSmirnov
AlexandruSmirnov
| +|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
| +|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
| +|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
| +|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|celestial-vault
celestial-vault
| ## Licence diff --git a/locales/hi/README.md b/locales/hi/README.md index 3b99b0d1bd..0184824533 100644 --- a/locales/hi/README.md +++ b/locales/hi/README.md @@ -184,37 +184,37 @@ Roo Code को बेहतर बनाने में मदद करने |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|samhvw8
samhvw8
|daniel-lxs
daniel-lxs
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| |KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
|joemanley201
joemanley201
| -|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|NyxJae
NyxJae
|jr
jr
|MuriloFP
MuriloFP
| -|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
| -|monotykamary
monotykamary
|cannuri
cannuri
|xyOz-dev
xyOz-dev
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| -|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|vigneshsubbiah16
vigneshsubbiah16
|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
| -|chrarnoldus
chrarnoldus
|diarmidmackenzie
diarmidmackenzie
|olweraltuve
olweraltuve
|psv2522
psv2522
|Premshay
Premshay
|lupuletic
lupuletic
| -|kiwina
kiwina
|aheizi
aheizi
|PeterDaveHello
PeterDaveHello
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| +|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|jr
jr
|NyxJae
NyxJae
|MuriloFP
MuriloFP
| +|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
| +|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| +|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|dtrugman
dtrugman
|lloydchang
lloydchang
|vigneshsubbiah16
vigneshsubbiah16
|Szpadel
Szpadel
| +|lupuletic
lupuletic
|kiwina
kiwina
|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
| +|chrarnoldus
chrarnoldus
|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| |ChuKhaLi
ChuKhaLi
|hassoncs
hassoncs
|emshvac
emshvac
|kyle-apex
kyle-apex
|noritaka1166
noritaka1166
|pdecat
pdecat
| |SannidhyaSah
SannidhyaSah
|StevenTCramer
StevenTCramer
|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
| |dleffel
dleffel
|arthurauffray
arthurauffray
|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
| -|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
| -|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
|ronyblum
ronyblum
| -|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
| -|yt3trees
yt3trees
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|bramburn
bramburn
|olearycrew
olearycrew
| -|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|ross
ross
|philfung
philfung
|napter
napter
|mdp
mdp
| -|SplittyDev
SplittyDev
|jcbdev
jcbdev
|julionav
julionav
|Chenjiayuan195
Chenjiayuan195
|kinandan
kinandan
|jwcraig
jwcraig
| -|shoopapa
shoopapa
|im47cn
im47cn
|hongzio
hongzio
|hatsu38
hatsu38
|GOODBOY008
GOODBOY008
|forestyoo
forestyoo
| -|dqroid
dqroid
|dairui1
dairui1
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
| -|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
| -|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
| -|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
| -|lightrabbit
lightrabbit
|kohii
kohii
|AlexandruSmirnov
AlexandruSmirnov
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
| -|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
|chadgauth
chadgauth
|brunobergher
brunobergher
| -|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andreastempsch
andreastempsch
| -|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
| -|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
| -|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
| -|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
| -|pfitz
pfitz
|celestial-vault
celestial-vault
|linegel
linegel
| | | | +|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|benzntech
benzntech
|mr-ryan-james
mr-ryan-james
| +|heyseth
heyseth
|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
| +|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
| +|franekp
franekp
|yt3trees
yt3trees
|axkirillov
axkirillov
|anton-otee
anton-otee
|bramburn
bramburn
|olearycrew
olearycrew
| +|brunobergher
brunobergher
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
|julionav
julionav
| +|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|ross
ross
|philfung
philfung
|dairui1
dairui1
| +|dqroid
dqroid
|forestyoo
forestyoo
|GOODBOY008
GOODBOY008
|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
| +|shoopapa
shoopapa
|jwcraig
jwcraig
|nevermorec
nevermorec
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
| +|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
| +|Githubguy132010
Githubguy132010
|tgfjt
tgfjt
|maekawataiki
maekawataiki
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
| +|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
| +|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
| +|olup
olup
|lightrabbit
lightrabbit
|kohii
kohii
|kinandan
kinandan
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| +|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
| +|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
| +|atlasgong
atlasgong
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| +|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|AlexandruSmirnov
AlexandruSmirnov
| +|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
| +|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
| +|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
| +|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|celestial-vault
celestial-vault
| ## लाइसेंस diff --git a/locales/id/README.md b/locales/id/README.md index dc37028884..06f88d3b30 100644 --- a/locales/id/README.md +++ b/locales/id/README.md @@ -178,37 +178,37 @@ Terima kasih kepada semua kontributor kami yang telah membantu membuat Roo Code |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|samhvw8
samhvw8
|daniel-lxs
daniel-lxs
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| |KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
|joemanley201
joemanley201
| -|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|NyxJae
NyxJae
|jr
jr
|MuriloFP
MuriloFP
| -|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
| -|monotykamary
monotykamary
|cannuri
cannuri
|xyOz-dev
xyOz-dev
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| -|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|vigneshsubbiah16
vigneshsubbiah16
|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
| -|chrarnoldus
chrarnoldus
|diarmidmackenzie
diarmidmackenzie
|olweraltuve
olweraltuve
|psv2522
psv2522
|Premshay
Premshay
|lupuletic
lupuletic
| -|kiwina
kiwina
|aheizi
aheizi
|PeterDaveHello
PeterDaveHello
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| +|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|jr
jr
|NyxJae
NyxJae
|MuriloFP
MuriloFP
| +|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
| +|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| +|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|dtrugman
dtrugman
|lloydchang
lloydchang
|vigneshsubbiah16
vigneshsubbiah16
|Szpadel
Szpadel
| +|lupuletic
lupuletic
|kiwina
kiwina
|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
| +|chrarnoldus
chrarnoldus
|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| |ChuKhaLi
ChuKhaLi
|hassoncs
hassoncs
|emshvac
emshvac
|kyle-apex
kyle-apex
|noritaka1166
noritaka1166
|pdecat
pdecat
| |SannidhyaSah
SannidhyaSah
|StevenTCramer
StevenTCramer
|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
| |dleffel
dleffel
|arthurauffray
arthurauffray
|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
| -|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
| -|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
|ronyblum
ronyblum
| -|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
| -|yt3trees
yt3trees
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|bramburn
bramburn
|olearycrew
olearycrew
| -|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|ross
ross
|philfung
philfung
|napter
napter
|mdp
mdp
| -|SplittyDev
SplittyDev
|jcbdev
jcbdev
|julionav
julionav
|Chenjiayuan195
Chenjiayuan195
|kinandan
kinandan
|jwcraig
jwcraig
| -|shoopapa
shoopapa
|im47cn
im47cn
|hongzio
hongzio
|hatsu38
hatsu38
|GOODBOY008
GOODBOY008
|forestyoo
forestyoo
| -|dqroid
dqroid
|dairui1
dairui1
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
| -|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
| -|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
| -|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
| -|lightrabbit
lightrabbit
|kohii
kohii
|AlexandruSmirnov
AlexandruSmirnov
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
| -|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
|chadgauth
chadgauth
|brunobergher
brunobergher
| -|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andreastempsch
andreastempsch
| -|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
| -|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
| -|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
| -|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
| -|pfitz
pfitz
|celestial-vault
celestial-vault
|linegel
linegel
| | | | +|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|benzntech
benzntech
|mr-ryan-james
mr-ryan-james
| +|heyseth
heyseth
|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
| +|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
| +|franekp
franekp
|yt3trees
yt3trees
|axkirillov
axkirillov
|anton-otee
anton-otee
|bramburn
bramburn
|olearycrew
olearycrew
| +|brunobergher
brunobergher
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
|julionav
julionav
| +|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|ross
ross
|philfung
philfung
|dairui1
dairui1
| +|dqroid
dqroid
|forestyoo
forestyoo
|GOODBOY008
GOODBOY008
|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
| +|shoopapa
shoopapa
|jwcraig
jwcraig
|nevermorec
nevermorec
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
| +|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
| +|Githubguy132010
Githubguy132010
|tgfjt
tgfjt
|maekawataiki
maekawataiki
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
| +|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
| +|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
| +|olup
olup
|lightrabbit
lightrabbit
|kohii
kohii
|kinandan
kinandan
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| +|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
| +|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
| +|atlasgong
atlasgong
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| +|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|AlexandruSmirnov
AlexandruSmirnov
| +|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
| +|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
| +|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
| +|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|celestial-vault
celestial-vault
| ## License diff --git a/locales/it/README.md b/locales/it/README.md index e69005f1ca..7d96654b89 100644 --- a/locales/it/README.md +++ b/locales/it/README.md @@ -184,37 +184,37 @@ Grazie a tutti i nostri contributori che hanno aiutato a migliorare Roo Code! |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|samhvw8
samhvw8
|daniel-lxs
daniel-lxs
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| |KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
|joemanley201
joemanley201
| -|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|NyxJae
NyxJae
|jr
jr
|MuriloFP
MuriloFP
| -|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
| -|monotykamary
monotykamary
|cannuri
cannuri
|xyOz-dev
xyOz-dev
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| -|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|vigneshsubbiah16
vigneshsubbiah16
|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
| -|chrarnoldus
chrarnoldus
|diarmidmackenzie
diarmidmackenzie
|olweraltuve
olweraltuve
|psv2522
psv2522
|Premshay
Premshay
|lupuletic
lupuletic
| -|kiwina
kiwina
|aheizi
aheizi
|PeterDaveHello
PeterDaveHello
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| +|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|jr
jr
|NyxJae
NyxJae
|MuriloFP
MuriloFP
| +|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
| +|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| +|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|dtrugman
dtrugman
|lloydchang
lloydchang
|vigneshsubbiah16
vigneshsubbiah16
|Szpadel
Szpadel
| +|lupuletic
lupuletic
|kiwina
kiwina
|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
| +|chrarnoldus
chrarnoldus
|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| |ChuKhaLi
ChuKhaLi
|hassoncs
hassoncs
|emshvac
emshvac
|kyle-apex
kyle-apex
|noritaka1166
noritaka1166
|pdecat
pdecat
| |SannidhyaSah
SannidhyaSah
|StevenTCramer
StevenTCramer
|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
| |dleffel
dleffel
|arthurauffray
arthurauffray
|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
| -|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
| -|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
|ronyblum
ronyblum
| -|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
| -|yt3trees
yt3trees
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|bramburn
bramburn
|olearycrew
olearycrew
| -|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|ross
ross
|philfung
philfung
|napter
napter
|mdp
mdp
| -|SplittyDev
SplittyDev
|jcbdev
jcbdev
|julionav
julionav
|Chenjiayuan195
Chenjiayuan195
|kinandan
kinandan
|jwcraig
jwcraig
| -|shoopapa
shoopapa
|im47cn
im47cn
|hongzio
hongzio
|hatsu38
hatsu38
|GOODBOY008
GOODBOY008
|forestyoo
forestyoo
| -|dqroid
dqroid
|dairui1
dairui1
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
| -|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
| -|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
| -|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
| -|lightrabbit
lightrabbit
|kohii
kohii
|AlexandruSmirnov
AlexandruSmirnov
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
| -|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
|chadgauth
chadgauth
|brunobergher
brunobergher
| -|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andreastempsch
andreastempsch
| -|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
| -|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
| -|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
| -|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
| -|pfitz
pfitz
|celestial-vault
celestial-vault
|linegel
linegel
| | | | +|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|benzntech
benzntech
|mr-ryan-james
mr-ryan-james
| +|heyseth
heyseth
|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
| +|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
| +|franekp
franekp
|yt3trees
yt3trees
|axkirillov
axkirillov
|anton-otee
anton-otee
|bramburn
bramburn
|olearycrew
olearycrew
| +|brunobergher
brunobergher
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
|julionav
julionav
| +|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|ross
ross
|philfung
philfung
|dairui1
dairui1
| +|dqroid
dqroid
|forestyoo
forestyoo
|GOODBOY008
GOODBOY008
|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
| +|shoopapa
shoopapa
|jwcraig
jwcraig
|nevermorec
nevermorec
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
| +|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
| +|Githubguy132010
Githubguy132010
|tgfjt
tgfjt
|maekawataiki
maekawataiki
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
| +|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
| +|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
| +|olup
olup
|lightrabbit
lightrabbit
|kohii
kohii
|kinandan
kinandan
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| +|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
| +|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
| +|atlasgong
atlasgong
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| +|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|AlexandruSmirnov
AlexandruSmirnov
| +|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
| +|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
| +|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
| +|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|celestial-vault
celestial-vault
| ## Licenza diff --git a/locales/ja/README.md b/locales/ja/README.md index 4cabfc4a37..d91743b525 100644 --- a/locales/ja/README.md +++ b/locales/ja/README.md @@ -184,37 +184,37 @@ Roo Codeの改善に貢献してくれたすべての貢献者に感謝します |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|samhvw8
samhvw8
|daniel-lxs
daniel-lxs
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| |KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
|joemanley201
joemanley201
| -|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|NyxJae
NyxJae
|jr
jr
|MuriloFP
MuriloFP
| -|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
| -|monotykamary
monotykamary
|cannuri
cannuri
|xyOz-dev
xyOz-dev
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| -|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|vigneshsubbiah16
vigneshsubbiah16
|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
| -|chrarnoldus
chrarnoldus
|diarmidmackenzie
diarmidmackenzie
|olweraltuve
olweraltuve
|psv2522
psv2522
|Premshay
Premshay
|lupuletic
lupuletic
| -|kiwina
kiwina
|aheizi
aheizi
|PeterDaveHello
PeterDaveHello
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| +|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|jr
jr
|NyxJae
NyxJae
|MuriloFP
MuriloFP
| +|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
| +|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| +|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|dtrugman
dtrugman
|lloydchang
lloydchang
|vigneshsubbiah16
vigneshsubbiah16
|Szpadel
Szpadel
| +|lupuletic
lupuletic
|kiwina
kiwina
|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
| +|chrarnoldus
chrarnoldus
|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| |ChuKhaLi
ChuKhaLi
|hassoncs
hassoncs
|emshvac
emshvac
|kyle-apex
kyle-apex
|noritaka1166
noritaka1166
|pdecat
pdecat
| |SannidhyaSah
SannidhyaSah
|StevenTCramer
StevenTCramer
|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
| |dleffel
dleffel
|arthurauffray
arthurauffray
|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
| -|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
| -|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
|ronyblum
ronyblum
| -|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
| -|yt3trees
yt3trees
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|bramburn
bramburn
|olearycrew
olearycrew
| -|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|ross
ross
|philfung
philfung
|napter
napter
|mdp
mdp
| -|SplittyDev
SplittyDev
|jcbdev
jcbdev
|julionav
julionav
|Chenjiayuan195
Chenjiayuan195
|kinandan
kinandan
|jwcraig
jwcraig
| -|shoopapa
shoopapa
|im47cn
im47cn
|hongzio
hongzio
|hatsu38
hatsu38
|GOODBOY008
GOODBOY008
|forestyoo
forestyoo
| -|dqroid
dqroid
|dairui1
dairui1
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
| -|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
| -|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
| -|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
| -|lightrabbit
lightrabbit
|kohii
kohii
|AlexandruSmirnov
AlexandruSmirnov
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
| -|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
|chadgauth
chadgauth
|brunobergher
brunobergher
| -|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andreastempsch
andreastempsch
| -|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
| -|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
| -|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
| -|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
| -|pfitz
pfitz
|celestial-vault
celestial-vault
|linegel
linegel
| | | | +|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|benzntech
benzntech
|mr-ryan-james
mr-ryan-james
| +|heyseth
heyseth
|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
| +|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
| +|franekp
franekp
|yt3trees
yt3trees
|axkirillov
axkirillov
|anton-otee
anton-otee
|bramburn
bramburn
|olearycrew
olearycrew
| +|brunobergher
brunobergher
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
|julionav
julionav
| +|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|ross
ross
|philfung
philfung
|dairui1
dairui1
| +|dqroid
dqroid
|forestyoo
forestyoo
|GOODBOY008
GOODBOY008
|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
| +|shoopapa
shoopapa
|jwcraig
jwcraig
|nevermorec
nevermorec
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
| +|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
| +|Githubguy132010
Githubguy132010
|tgfjt
tgfjt
|maekawataiki
maekawataiki
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
| +|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
| +|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
| +|olup
olup
|lightrabbit
lightrabbit
|kohii
kohii
|kinandan
kinandan
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| +|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
| +|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
| +|atlasgong
atlasgong
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| +|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|AlexandruSmirnov
AlexandruSmirnov
| +|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
| +|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
| +|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
| +|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|celestial-vault
celestial-vault
| ## ライセンス diff --git a/locales/ko/README.md b/locales/ko/README.md index c86eb7d3cb..d994aa3e11 100644 --- a/locales/ko/README.md +++ b/locales/ko/README.md @@ -184,37 +184,37 @@ Roo Code를 더 좋게 만드는 데 도움을 준 모든 기여자에게 감사 |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|samhvw8
samhvw8
|daniel-lxs
daniel-lxs
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| |KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
|joemanley201
joemanley201
| -|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|NyxJae
NyxJae
|jr
jr
|MuriloFP
MuriloFP
| -|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
| -|monotykamary
monotykamary
|cannuri
cannuri
|xyOz-dev
xyOz-dev
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| -|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|vigneshsubbiah16
vigneshsubbiah16
|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
| -|chrarnoldus
chrarnoldus
|diarmidmackenzie
diarmidmackenzie
|olweraltuve
olweraltuve
|psv2522
psv2522
|Premshay
Premshay
|lupuletic
lupuletic
| -|kiwina
kiwina
|aheizi
aheizi
|PeterDaveHello
PeterDaveHello
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| +|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|jr
jr
|NyxJae
NyxJae
|MuriloFP
MuriloFP
| +|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
| +|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| +|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|dtrugman
dtrugman
|lloydchang
lloydchang
|vigneshsubbiah16
vigneshsubbiah16
|Szpadel
Szpadel
| +|lupuletic
lupuletic
|kiwina
kiwina
|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
| +|chrarnoldus
chrarnoldus
|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| |ChuKhaLi
ChuKhaLi
|hassoncs
hassoncs
|emshvac
emshvac
|kyle-apex
kyle-apex
|noritaka1166
noritaka1166
|pdecat
pdecat
| |SannidhyaSah
SannidhyaSah
|StevenTCramer
StevenTCramer
|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
| |dleffel
dleffel
|arthurauffray
arthurauffray
|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
| -|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
| -|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
|ronyblum
ronyblum
| -|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
| -|yt3trees
yt3trees
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|bramburn
bramburn
|olearycrew
olearycrew
| -|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|ross
ross
|philfung
philfung
|napter
napter
|mdp
mdp
| -|SplittyDev
SplittyDev
|jcbdev
jcbdev
|julionav
julionav
|Chenjiayuan195
Chenjiayuan195
|kinandan
kinandan
|jwcraig
jwcraig
| -|shoopapa
shoopapa
|im47cn
im47cn
|hongzio
hongzio
|hatsu38
hatsu38
|GOODBOY008
GOODBOY008
|forestyoo
forestyoo
| -|dqroid
dqroid
|dairui1
dairui1
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
| -|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
| -|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
| -|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
| -|lightrabbit
lightrabbit
|kohii
kohii
|AlexandruSmirnov
AlexandruSmirnov
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
| -|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
|chadgauth
chadgauth
|brunobergher
brunobergher
| -|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andreastempsch
andreastempsch
| -|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
| -|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
| -|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
| -|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
| -|pfitz
pfitz
|celestial-vault
celestial-vault
|linegel
linegel
| | | | +|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|benzntech
benzntech
|mr-ryan-james
mr-ryan-james
| +|heyseth
heyseth
|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
| +|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
| +|franekp
franekp
|yt3trees
yt3trees
|axkirillov
axkirillov
|anton-otee
anton-otee
|bramburn
bramburn
|olearycrew
olearycrew
| +|brunobergher
brunobergher
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
|julionav
julionav
| +|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|ross
ross
|philfung
philfung
|dairui1
dairui1
| +|dqroid
dqroid
|forestyoo
forestyoo
|GOODBOY008
GOODBOY008
|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
| +|shoopapa
shoopapa
|jwcraig
jwcraig
|nevermorec
nevermorec
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
| +|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
| +|Githubguy132010
Githubguy132010
|tgfjt
tgfjt
|maekawataiki
maekawataiki
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
| +|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
| +|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
| +|olup
olup
|lightrabbit
lightrabbit
|kohii
kohii
|kinandan
kinandan
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| +|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
| +|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
| +|atlasgong
atlasgong
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| +|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|AlexandruSmirnov
AlexandruSmirnov
| +|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
| +|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
| +|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
| +|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|celestial-vault
celestial-vault
| ## 라이선스 diff --git a/locales/nl/README.md b/locales/nl/README.md index 0ccca95ceb..b3094153e1 100644 --- a/locales/nl/README.md +++ b/locales/nl/README.md @@ -184,37 +184,37 @@ Dank aan alle bijdragers die Roo Code beter hebben gemaakt! |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|samhvw8
samhvw8
|daniel-lxs
daniel-lxs
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| |KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
|joemanley201
joemanley201
| -|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|NyxJae
NyxJae
|jr
jr
|MuriloFP
MuriloFP
| -|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
| -|monotykamary
monotykamary
|cannuri
cannuri
|xyOz-dev
xyOz-dev
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| -|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|vigneshsubbiah16
vigneshsubbiah16
|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
| -|chrarnoldus
chrarnoldus
|diarmidmackenzie
diarmidmackenzie
|olweraltuve
olweraltuve
|psv2522
psv2522
|Premshay
Premshay
|lupuletic
lupuletic
| -|kiwina
kiwina
|aheizi
aheizi
|PeterDaveHello
PeterDaveHello
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| +|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|jr
jr
|NyxJae
NyxJae
|MuriloFP
MuriloFP
| +|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
| +|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| +|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|dtrugman
dtrugman
|lloydchang
lloydchang
|vigneshsubbiah16
vigneshsubbiah16
|Szpadel
Szpadel
| +|lupuletic
lupuletic
|kiwina
kiwina
|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
| +|chrarnoldus
chrarnoldus
|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| |ChuKhaLi
ChuKhaLi
|hassoncs
hassoncs
|emshvac
emshvac
|kyle-apex
kyle-apex
|noritaka1166
noritaka1166
|pdecat
pdecat
| |SannidhyaSah
SannidhyaSah
|StevenTCramer
StevenTCramer
|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
| |dleffel
dleffel
|arthurauffray
arthurauffray
|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
| -|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
| -|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
|ronyblum
ronyblum
| -|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
| -|yt3trees
yt3trees
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|bramburn
bramburn
|olearycrew
olearycrew
| -|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|ross
ross
|philfung
philfung
|napter
napter
|mdp
mdp
| -|SplittyDev
SplittyDev
|jcbdev
jcbdev
|julionav
julionav
|Chenjiayuan195
Chenjiayuan195
|kinandan
kinandan
|jwcraig
jwcraig
| -|shoopapa
shoopapa
|im47cn
im47cn
|hongzio
hongzio
|hatsu38
hatsu38
|GOODBOY008
GOODBOY008
|forestyoo
forestyoo
| -|dqroid
dqroid
|dairui1
dairui1
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
| -|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
| -|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
| -|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
| -|lightrabbit
lightrabbit
|kohii
kohii
|AlexandruSmirnov
AlexandruSmirnov
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
| -|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
|chadgauth
chadgauth
|brunobergher
brunobergher
| -|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andreastempsch
andreastempsch
| -|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
| -|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
| -|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
| -|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
| -|pfitz
pfitz
|celestial-vault
celestial-vault
|linegel
linegel
| | | | +|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|benzntech
benzntech
|mr-ryan-james
mr-ryan-james
| +|heyseth
heyseth
|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
| +|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
| +|franekp
franekp
|yt3trees
yt3trees
|axkirillov
axkirillov
|anton-otee
anton-otee
|bramburn
bramburn
|olearycrew
olearycrew
| +|brunobergher
brunobergher
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
|julionav
julionav
| +|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|ross
ross
|philfung
philfung
|dairui1
dairui1
| +|dqroid
dqroid
|forestyoo
forestyoo
|GOODBOY008
GOODBOY008
|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
| +|shoopapa
shoopapa
|jwcraig
jwcraig
|nevermorec
nevermorec
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
| +|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
| +|Githubguy132010
Githubguy132010
|tgfjt
tgfjt
|maekawataiki
maekawataiki
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
| +|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
| +|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
| +|olup
olup
|lightrabbit
lightrabbit
|kohii
kohii
|kinandan
kinandan
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| +|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
| +|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
| +|atlasgong
atlasgong
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| +|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|AlexandruSmirnov
AlexandruSmirnov
| +|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
| +|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
| +|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
| +|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|celestial-vault
celestial-vault
| ## Licentie diff --git a/locales/pl/README.md b/locales/pl/README.md index 608fd86170..687a5721b4 100644 --- a/locales/pl/README.md +++ b/locales/pl/README.md @@ -184,37 +184,37 @@ Dziękujemy wszystkim naszym współtwórcom, którzy pomogli ulepszyć Roo Code |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|samhvw8
samhvw8
|daniel-lxs
daniel-lxs
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| |KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
|joemanley201
joemanley201
| -|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|NyxJae
NyxJae
|jr
jr
|MuriloFP
MuriloFP
| -|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
| -|monotykamary
monotykamary
|cannuri
cannuri
|xyOz-dev
xyOz-dev
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| -|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|vigneshsubbiah16
vigneshsubbiah16
|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
| -|chrarnoldus
chrarnoldus
|diarmidmackenzie
diarmidmackenzie
|olweraltuve
olweraltuve
|psv2522
psv2522
|Premshay
Premshay
|lupuletic
lupuletic
| -|kiwina
kiwina
|aheizi
aheizi
|PeterDaveHello
PeterDaveHello
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| +|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|jr
jr
|NyxJae
NyxJae
|MuriloFP
MuriloFP
| +|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
| +|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| +|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|dtrugman
dtrugman
|lloydchang
lloydchang
|vigneshsubbiah16
vigneshsubbiah16
|Szpadel
Szpadel
| +|lupuletic
lupuletic
|kiwina
kiwina
|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
| +|chrarnoldus
chrarnoldus
|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| |ChuKhaLi
ChuKhaLi
|hassoncs
hassoncs
|emshvac
emshvac
|kyle-apex
kyle-apex
|noritaka1166
noritaka1166
|pdecat
pdecat
| |SannidhyaSah
SannidhyaSah
|StevenTCramer
StevenTCramer
|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
| |dleffel
dleffel
|arthurauffray
arthurauffray
|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
| -|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
| -|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
|ronyblum
ronyblum
| -|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
| -|yt3trees
yt3trees
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|bramburn
bramburn
|olearycrew
olearycrew
| -|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|ross
ross
|philfung
philfung
|napter
napter
|mdp
mdp
| -|SplittyDev
SplittyDev
|jcbdev
jcbdev
|julionav
julionav
|Chenjiayuan195
Chenjiayuan195
|kinandan
kinandan
|jwcraig
jwcraig
| -|shoopapa
shoopapa
|im47cn
im47cn
|hongzio
hongzio
|hatsu38
hatsu38
|GOODBOY008
GOODBOY008
|forestyoo
forestyoo
| -|dqroid
dqroid
|dairui1
dairui1
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
| -|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
| -|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
| -|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
| -|lightrabbit
lightrabbit
|kohii
kohii
|AlexandruSmirnov
AlexandruSmirnov
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
| -|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
|chadgauth
chadgauth
|brunobergher
brunobergher
| -|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andreastempsch
andreastempsch
| -|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
| -|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
| -|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
| -|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
| -|pfitz
pfitz
|celestial-vault
celestial-vault
|linegel
linegel
| | | | +|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|benzntech
benzntech
|mr-ryan-james
mr-ryan-james
| +|heyseth
heyseth
|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
| +|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
| +|franekp
franekp
|yt3trees
yt3trees
|axkirillov
axkirillov
|anton-otee
anton-otee
|bramburn
bramburn
|olearycrew
olearycrew
| +|brunobergher
brunobergher
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
|julionav
julionav
| +|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|ross
ross
|philfung
philfung
|dairui1
dairui1
| +|dqroid
dqroid
|forestyoo
forestyoo
|GOODBOY008
GOODBOY008
|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
| +|shoopapa
shoopapa
|jwcraig
jwcraig
|nevermorec
nevermorec
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
| +|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
| +|Githubguy132010
Githubguy132010
|tgfjt
tgfjt
|maekawataiki
maekawataiki
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
| +|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
| +|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
| +|olup
olup
|lightrabbit
lightrabbit
|kohii
kohii
|kinandan
kinandan
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| +|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
| +|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
| +|atlasgong
atlasgong
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| +|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|AlexandruSmirnov
AlexandruSmirnov
| +|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
| +|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
| +|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
| +|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|celestial-vault
celestial-vault
| ## Licencja diff --git a/locales/pt-BR/README.md b/locales/pt-BR/README.md index e75746682a..8a03cc7703 100644 --- a/locales/pt-BR/README.md +++ b/locales/pt-BR/README.md @@ -184,37 +184,37 @@ Obrigado a todos os nossos contribuidores que ajudaram a tornar o Roo Code melho |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|samhvw8
samhvw8
|daniel-lxs
daniel-lxs
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| |KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
|joemanley201
joemanley201
| -|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|NyxJae
NyxJae
|jr
jr
|MuriloFP
MuriloFP
| -|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
| -|monotykamary
monotykamary
|cannuri
cannuri
|xyOz-dev
xyOz-dev
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| -|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|vigneshsubbiah16
vigneshsubbiah16
|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
| -|chrarnoldus
chrarnoldus
|diarmidmackenzie
diarmidmackenzie
|olweraltuve
olweraltuve
|psv2522
psv2522
|Premshay
Premshay
|lupuletic
lupuletic
| -|kiwina
kiwina
|aheizi
aheizi
|PeterDaveHello
PeterDaveHello
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| +|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|jr
jr
|NyxJae
NyxJae
|MuriloFP
MuriloFP
| +|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
| +|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| +|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|dtrugman
dtrugman
|lloydchang
lloydchang
|vigneshsubbiah16
vigneshsubbiah16
|Szpadel
Szpadel
| +|lupuletic
lupuletic
|kiwina
kiwina
|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
| +|chrarnoldus
chrarnoldus
|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| |ChuKhaLi
ChuKhaLi
|hassoncs
hassoncs
|emshvac
emshvac
|kyle-apex
kyle-apex
|noritaka1166
noritaka1166
|pdecat
pdecat
| |SannidhyaSah
SannidhyaSah
|StevenTCramer
StevenTCramer
|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
| |dleffel
dleffel
|arthurauffray
arthurauffray
|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
| -|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
| -|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
|ronyblum
ronyblum
| -|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
| -|yt3trees
yt3trees
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|bramburn
bramburn
|olearycrew
olearycrew
| -|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|ross
ross
|philfung
philfung
|napter
napter
|mdp
mdp
| -|SplittyDev
SplittyDev
|jcbdev
jcbdev
|julionav
julionav
|Chenjiayuan195
Chenjiayuan195
|kinandan
kinandan
|jwcraig
jwcraig
| -|shoopapa
shoopapa
|im47cn
im47cn
|hongzio
hongzio
|hatsu38
hatsu38
|GOODBOY008
GOODBOY008
|forestyoo
forestyoo
| -|dqroid
dqroid
|dairui1
dairui1
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
| -|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
| -|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
| -|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
| -|lightrabbit
lightrabbit
|kohii
kohii
|AlexandruSmirnov
AlexandruSmirnov
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
| -|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
|chadgauth
chadgauth
|brunobergher
brunobergher
| -|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andreastempsch
andreastempsch
| -|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
| -|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
| -|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
| -|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
| -|pfitz
pfitz
|celestial-vault
celestial-vault
|linegel
linegel
| | | | +|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|benzntech
benzntech
|mr-ryan-james
mr-ryan-james
| +|heyseth
heyseth
|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
| +|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
| +|franekp
franekp
|yt3trees
yt3trees
|axkirillov
axkirillov
|anton-otee
anton-otee
|bramburn
bramburn
|olearycrew
olearycrew
| +|brunobergher
brunobergher
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
|julionav
julionav
| +|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|ross
ross
|philfung
philfung
|dairui1
dairui1
| +|dqroid
dqroid
|forestyoo
forestyoo
|GOODBOY008
GOODBOY008
|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
| +|shoopapa
shoopapa
|jwcraig
jwcraig
|nevermorec
nevermorec
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
| +|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
| +|Githubguy132010
Githubguy132010
|tgfjt
tgfjt
|maekawataiki
maekawataiki
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
| +|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
| +|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
| +|olup
olup
|lightrabbit
lightrabbit
|kohii
kohii
|kinandan
kinandan
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| +|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
| +|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
| +|atlasgong
atlasgong
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| +|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|AlexandruSmirnov
AlexandruSmirnov
| +|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
| +|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
| +|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
| +|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|celestial-vault
celestial-vault
| ## Licença diff --git a/locales/ru/README.md b/locales/ru/README.md index fba6d2193a..96db7bdb3f 100644 --- a/locales/ru/README.md +++ b/locales/ru/README.md @@ -184,37 +184,37 @@ code --install-extension bin/roo-cline-.vsix |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|samhvw8
samhvw8
|daniel-lxs
daniel-lxs
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| |KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
|joemanley201
joemanley201
| -|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|NyxJae
NyxJae
|jr
jr
|MuriloFP
MuriloFP
| -|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
| -|monotykamary
monotykamary
|cannuri
cannuri
|xyOz-dev
xyOz-dev
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| -|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|vigneshsubbiah16
vigneshsubbiah16
|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
| -|chrarnoldus
chrarnoldus
|diarmidmackenzie
diarmidmackenzie
|olweraltuve
olweraltuve
|psv2522
psv2522
|Premshay
Premshay
|lupuletic
lupuletic
| -|kiwina
kiwina
|aheizi
aheizi
|PeterDaveHello
PeterDaveHello
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| +|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|jr
jr
|NyxJae
NyxJae
|MuriloFP
MuriloFP
| +|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
| +|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| +|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|dtrugman
dtrugman
|lloydchang
lloydchang
|vigneshsubbiah16
vigneshsubbiah16
|Szpadel
Szpadel
| +|lupuletic
lupuletic
|kiwina
kiwina
|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
| +|chrarnoldus
chrarnoldus
|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| |ChuKhaLi
ChuKhaLi
|hassoncs
hassoncs
|emshvac
emshvac
|kyle-apex
kyle-apex
|noritaka1166
noritaka1166
|pdecat
pdecat
| |SannidhyaSah
SannidhyaSah
|StevenTCramer
StevenTCramer
|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
| |dleffel
dleffel
|arthurauffray
arthurauffray
|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
| -|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
| -|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
|ronyblum
ronyblum
| -|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
| -|yt3trees
yt3trees
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|bramburn
bramburn
|olearycrew
olearycrew
| -|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|ross
ross
|philfung
philfung
|napter
napter
|mdp
mdp
| -|SplittyDev
SplittyDev
|jcbdev
jcbdev
|julionav
julionav
|Chenjiayuan195
Chenjiayuan195
|kinandan
kinandan
|jwcraig
jwcraig
| -|shoopapa
shoopapa
|im47cn
im47cn
|hongzio
hongzio
|hatsu38
hatsu38
|GOODBOY008
GOODBOY008
|forestyoo
forestyoo
| -|dqroid
dqroid
|dairui1
dairui1
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
| -|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
| -|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
| -|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
| -|lightrabbit
lightrabbit
|kohii
kohii
|AlexandruSmirnov
AlexandruSmirnov
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
| -|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
|chadgauth
chadgauth
|brunobergher
brunobergher
| -|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andreastempsch
andreastempsch
| -|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
| -|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
| -|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
| -|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
| -|pfitz
pfitz
|celestial-vault
celestial-vault
|linegel
linegel
| | | | +|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|benzntech
benzntech
|mr-ryan-james
mr-ryan-james
| +|heyseth
heyseth
|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
| +|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
| +|franekp
franekp
|yt3trees
yt3trees
|axkirillov
axkirillov
|anton-otee
anton-otee
|bramburn
bramburn
|olearycrew
olearycrew
| +|brunobergher
brunobergher
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
|julionav
julionav
| +|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|ross
ross
|philfung
philfung
|dairui1
dairui1
| +|dqroid
dqroid
|forestyoo
forestyoo
|GOODBOY008
GOODBOY008
|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
| +|shoopapa
shoopapa
|jwcraig
jwcraig
|nevermorec
nevermorec
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
| +|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
| +|Githubguy132010
Githubguy132010
|tgfjt
tgfjt
|maekawataiki
maekawataiki
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
| +|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
| +|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
| +|olup
olup
|lightrabbit
lightrabbit
|kohii
kohii
|kinandan
kinandan
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| +|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
| +|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
| +|atlasgong
atlasgong
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| +|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|AlexandruSmirnov
AlexandruSmirnov
| +|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
| +|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
| +|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
| +|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|celestial-vault
celestial-vault
| ## Лицензия diff --git a/locales/tr/README.md b/locales/tr/README.md index a9cb0b5153..6ff6f24c76 100644 --- a/locales/tr/README.md +++ b/locales/tr/README.md @@ -184,37 +184,37 @@ Roo Code'u daha iyi hale getirmeye yardımcı olan tüm katkıda bulunanlara te |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|samhvw8
samhvw8
|daniel-lxs
daniel-lxs
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| |KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
|joemanley201
joemanley201
| -|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|NyxJae
NyxJae
|jr
jr
|MuriloFP
MuriloFP
| -|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
| -|monotykamary
monotykamary
|cannuri
cannuri
|xyOz-dev
xyOz-dev
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| -|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|vigneshsubbiah16
vigneshsubbiah16
|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
| -|chrarnoldus
chrarnoldus
|diarmidmackenzie
diarmidmackenzie
|olweraltuve
olweraltuve
|psv2522
psv2522
|Premshay
Premshay
|lupuletic
lupuletic
| -|kiwina
kiwina
|aheizi
aheizi
|PeterDaveHello
PeterDaveHello
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| +|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|jr
jr
|NyxJae
NyxJae
|MuriloFP
MuriloFP
| +|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
| +|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| +|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|dtrugman
dtrugman
|lloydchang
lloydchang
|vigneshsubbiah16
vigneshsubbiah16
|Szpadel
Szpadel
| +|lupuletic
lupuletic
|kiwina
kiwina
|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
| +|chrarnoldus
chrarnoldus
|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| |ChuKhaLi
ChuKhaLi
|hassoncs
hassoncs
|emshvac
emshvac
|kyle-apex
kyle-apex
|noritaka1166
noritaka1166
|pdecat
pdecat
| |SannidhyaSah
SannidhyaSah
|StevenTCramer
StevenTCramer
|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
| |dleffel
dleffel
|arthurauffray
arthurauffray
|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
| -|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
| -|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
|ronyblum
ronyblum
| -|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
| -|yt3trees
yt3trees
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|bramburn
bramburn
|olearycrew
olearycrew
| -|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|ross
ross
|philfung
philfung
|napter
napter
|mdp
mdp
| -|SplittyDev
SplittyDev
|jcbdev
jcbdev
|julionav
julionav
|Chenjiayuan195
Chenjiayuan195
|kinandan
kinandan
|jwcraig
jwcraig
| -|shoopapa
shoopapa
|im47cn
im47cn
|hongzio
hongzio
|hatsu38
hatsu38
|GOODBOY008
GOODBOY008
|forestyoo
forestyoo
| -|dqroid
dqroid
|dairui1
dairui1
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
| -|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
| -|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
| -|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
| -|lightrabbit
lightrabbit
|kohii
kohii
|AlexandruSmirnov
AlexandruSmirnov
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
| -|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
|chadgauth
chadgauth
|brunobergher
brunobergher
| -|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andreastempsch
andreastempsch
| -|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
| -|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
| -|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
| -|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
| -|pfitz
pfitz
|celestial-vault
celestial-vault
|linegel
linegel
| | | | +|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|benzntech
benzntech
|mr-ryan-james
mr-ryan-james
| +|heyseth
heyseth
|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
| +|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
| +|franekp
franekp
|yt3trees
yt3trees
|axkirillov
axkirillov
|anton-otee
anton-otee
|bramburn
bramburn
|olearycrew
olearycrew
| +|brunobergher
brunobergher
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
|julionav
julionav
| +|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|ross
ross
|philfung
philfung
|dairui1
dairui1
| +|dqroid
dqroid
|forestyoo
forestyoo
|GOODBOY008
GOODBOY008
|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
| +|shoopapa
shoopapa
|jwcraig
jwcraig
|nevermorec
nevermorec
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
| +|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
| +|Githubguy132010
Githubguy132010
|tgfjt
tgfjt
|maekawataiki
maekawataiki
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
| +|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
| +|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
| +|olup
olup
|lightrabbit
lightrabbit
|kohii
kohii
|kinandan
kinandan
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| +|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
| +|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
| +|atlasgong
atlasgong
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| +|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|AlexandruSmirnov
AlexandruSmirnov
| +|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
| +|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
| +|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
| +|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|celestial-vault
celestial-vault
| ## Lisans diff --git a/locales/vi/README.md b/locales/vi/README.md index dceb7c4de1..56d286cd98 100644 --- a/locales/vi/README.md +++ b/locales/vi/README.md @@ -184,37 +184,37 @@ Cảm ơn tất cả những người đóng góp đã giúp cải thiện Roo C |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|samhvw8
samhvw8
|daniel-lxs
daniel-lxs
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| |KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
|joemanley201
joemanley201
| -|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|NyxJae
NyxJae
|jr
jr
|MuriloFP
MuriloFP
| -|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
| -|monotykamary
monotykamary
|cannuri
cannuri
|xyOz-dev
xyOz-dev
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| -|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|vigneshsubbiah16
vigneshsubbiah16
|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
| -|chrarnoldus
chrarnoldus
|diarmidmackenzie
diarmidmackenzie
|olweraltuve
olweraltuve
|psv2522
psv2522
|Premshay
Premshay
|lupuletic
lupuletic
| -|kiwina
kiwina
|aheizi
aheizi
|PeterDaveHello
PeterDaveHello
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| +|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|jr
jr
|NyxJae
NyxJae
|MuriloFP
MuriloFP
| +|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
| +|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| +|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|dtrugman
dtrugman
|lloydchang
lloydchang
|vigneshsubbiah16
vigneshsubbiah16
|Szpadel
Szpadel
| +|lupuletic
lupuletic
|kiwina
kiwina
|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
| +|chrarnoldus
chrarnoldus
|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| |ChuKhaLi
ChuKhaLi
|hassoncs
hassoncs
|emshvac
emshvac
|kyle-apex
kyle-apex
|noritaka1166
noritaka1166
|pdecat
pdecat
| |SannidhyaSah
SannidhyaSah
|StevenTCramer
StevenTCramer
|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
| |dleffel
dleffel
|arthurauffray
arthurauffray
|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
| -|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
| -|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
|ronyblum
ronyblum
| -|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
| -|yt3trees
yt3trees
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|bramburn
bramburn
|olearycrew
olearycrew
| -|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|ross
ross
|philfung
philfung
|napter
napter
|mdp
mdp
| -|SplittyDev
SplittyDev
|jcbdev
jcbdev
|julionav
julionav
|Chenjiayuan195
Chenjiayuan195
|kinandan
kinandan
|jwcraig
jwcraig
| -|shoopapa
shoopapa
|im47cn
im47cn
|hongzio
hongzio
|hatsu38
hatsu38
|GOODBOY008
GOODBOY008
|forestyoo
forestyoo
| -|dqroid
dqroid
|dairui1
dairui1
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
| -|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
| -|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
| -|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
| -|lightrabbit
lightrabbit
|kohii
kohii
|AlexandruSmirnov
AlexandruSmirnov
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
| -|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
|chadgauth
chadgauth
|brunobergher
brunobergher
| -|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andreastempsch
andreastempsch
| -|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
| -|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
| -|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
| -|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
| -|pfitz
pfitz
|celestial-vault
celestial-vault
|linegel
linegel
| | | | +|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|benzntech
benzntech
|mr-ryan-james
mr-ryan-james
| +|heyseth
heyseth
|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
| +|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
| +|franekp
franekp
|yt3trees
yt3trees
|axkirillov
axkirillov
|anton-otee
anton-otee
|bramburn
bramburn
|olearycrew
olearycrew
| +|brunobergher
brunobergher
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
|julionav
julionav
| +|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|ross
ross
|philfung
philfung
|dairui1
dairui1
| +|dqroid
dqroid
|forestyoo
forestyoo
|GOODBOY008
GOODBOY008
|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
| +|shoopapa
shoopapa
|jwcraig
jwcraig
|nevermorec
nevermorec
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
| +|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
| +|Githubguy132010
Githubguy132010
|tgfjt
tgfjt
|maekawataiki
maekawataiki
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
| +|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
| +|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
| +|olup
olup
|lightrabbit
lightrabbit
|kohii
kohii
|kinandan
kinandan
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| +|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
| +|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
| +|atlasgong
atlasgong
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| +|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|AlexandruSmirnov
AlexandruSmirnov
| +|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
| +|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
| +|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
| +|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|celestial-vault
celestial-vault
| ## Giấy Phép diff --git a/locales/zh-CN/README.md b/locales/zh-CN/README.md index d2303ef31e..ad89772d36 100644 --- a/locales/zh-CN/README.md +++ b/locales/zh-CN/README.md @@ -184,37 +184,37 @@ code --install-extension bin/roo-cline-.vsix |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|samhvw8
samhvw8
|daniel-lxs
daniel-lxs
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| |KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
|joemanley201
joemanley201
| -|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|NyxJae
NyxJae
|jr
jr
|MuriloFP
MuriloFP
| -|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
| -|monotykamary
monotykamary
|cannuri
cannuri
|xyOz-dev
xyOz-dev
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| -|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|vigneshsubbiah16
vigneshsubbiah16
|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
| -|chrarnoldus
chrarnoldus
|diarmidmackenzie
diarmidmackenzie
|olweraltuve
olweraltuve
|psv2522
psv2522
|Premshay
Premshay
|lupuletic
lupuletic
| -|kiwina
kiwina
|aheizi
aheizi
|PeterDaveHello
PeterDaveHello
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| +|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|jr
jr
|NyxJae
NyxJae
|MuriloFP
MuriloFP
| +|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
| +|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| +|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|dtrugman
dtrugman
|lloydchang
lloydchang
|vigneshsubbiah16
vigneshsubbiah16
|Szpadel
Szpadel
| +|lupuletic
lupuletic
|kiwina
kiwina
|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
| +|chrarnoldus
chrarnoldus
|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| |ChuKhaLi
ChuKhaLi
|hassoncs
hassoncs
|emshvac
emshvac
|kyle-apex
kyle-apex
|noritaka1166
noritaka1166
|pdecat
pdecat
| |SannidhyaSah
SannidhyaSah
|StevenTCramer
StevenTCramer
|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
| |dleffel
dleffel
|arthurauffray
arthurauffray
|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
| -|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
| -|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
|ronyblum
ronyblum
| -|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
| -|yt3trees
yt3trees
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|bramburn
bramburn
|olearycrew
olearycrew
| -|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|ross
ross
|philfung
philfung
|napter
napter
|mdp
mdp
| -|SplittyDev
SplittyDev
|jcbdev
jcbdev
|julionav
julionav
|Chenjiayuan195
Chenjiayuan195
|kinandan
kinandan
|jwcraig
jwcraig
| -|shoopapa
shoopapa
|im47cn
im47cn
|hongzio
hongzio
|hatsu38
hatsu38
|GOODBOY008
GOODBOY008
|forestyoo
forestyoo
| -|dqroid
dqroid
|dairui1
dairui1
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
| -|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
| -|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
| -|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
| -|lightrabbit
lightrabbit
|kohii
kohii
|AlexandruSmirnov
AlexandruSmirnov
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
| -|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
|chadgauth
chadgauth
|brunobergher
brunobergher
| -|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andreastempsch
andreastempsch
| -|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
| -|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
| -|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
| -|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
| -|pfitz
pfitz
|celestial-vault
celestial-vault
|linegel
linegel
| | | | +|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|benzntech
benzntech
|mr-ryan-james
mr-ryan-james
| +|heyseth
heyseth
|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
| +|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
| +|franekp
franekp
|yt3trees
yt3trees
|axkirillov
axkirillov
|anton-otee
anton-otee
|bramburn
bramburn
|olearycrew
olearycrew
| +|brunobergher
brunobergher
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
|julionav
julionav
| +|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|ross
ross
|philfung
philfung
|dairui1
dairui1
| +|dqroid
dqroid
|forestyoo
forestyoo
|GOODBOY008
GOODBOY008
|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
| +|shoopapa
shoopapa
|jwcraig
jwcraig
|nevermorec
nevermorec
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
| +|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
| +|Githubguy132010
Githubguy132010
|tgfjt
tgfjt
|maekawataiki
maekawataiki
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
| +|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
| +|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
| +|olup
olup
|lightrabbit
lightrabbit
|kohii
kohii
|kinandan
kinandan
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| +|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
| +|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
| +|atlasgong
atlasgong
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| +|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|AlexandruSmirnov
AlexandruSmirnov
| +|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
| +|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
| +|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
| +|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|celestial-vault
celestial-vault
| ## 许可证 diff --git a/locales/zh-TW/README.md b/locales/zh-TW/README.md index 8e709bacb2..245c66205a 100644 --- a/locales/zh-TW/README.md +++ b/locales/zh-TW/README.md @@ -185,37 +185,37 @@ code --install-extension bin/roo-cline-.vsix |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|samhvw8
samhvw8
|daniel-lxs
daniel-lxs
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| |KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
|joemanley201
joemanley201
| -|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|NyxJae
NyxJae
|jr
jr
|MuriloFP
MuriloFP
| -|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
| -|monotykamary
monotykamary
|cannuri
cannuri
|xyOz-dev
xyOz-dev
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| -|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|vigneshsubbiah16
vigneshsubbiah16
|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
| -|chrarnoldus
chrarnoldus
|diarmidmackenzie
diarmidmackenzie
|olweraltuve
olweraltuve
|psv2522
psv2522
|Premshay
Premshay
|lupuletic
lupuletic
| -|kiwina
kiwina
|aheizi
aheizi
|PeterDaveHello
PeterDaveHello
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| +|System233
System233
|jquanton
jquanton
|nissa-seru
nissa-seru
|jr
jr
|NyxJae
NyxJae
|MuriloFP
MuriloFP
| +|elianiva
elianiva
|d-oit
d-oit
|punkpeye
punkpeye
|wkordalski
wkordalski
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
| +|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|qdaxb
qdaxb
| +|shariqriazz
shariqriazz
|pugazhendhi-m
pugazhendhi-m
|dtrugman
dtrugman
|lloydchang
lloydchang
|vigneshsubbiah16
vigneshsubbiah16
|Szpadel
Szpadel
| +|lupuletic
lupuletic
|kiwina
kiwina
|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
| +|chrarnoldus
chrarnoldus
|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|afshawnlotfi
afshawnlotfi
|RaySinner
RaySinner
|nbihan-mediware
nbihan-mediware
| |ChuKhaLi
ChuKhaLi
|hassoncs
hassoncs
|emshvac
emshvac
|kyle-apex
kyle-apex
|noritaka1166
noritaka1166
|pdecat
pdecat
| |SannidhyaSah
SannidhyaSah
|StevenTCramer
StevenTCramer
|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
| |dleffel
dleffel
|arthurauffray
arthurauffray
|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
| -|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
| -|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
|ronyblum
ronyblum
| -|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
| -|yt3trees
yt3trees
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|bramburn
bramburn
|olearycrew
olearycrew
| -|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|ross
ross
|philfung
philfung
|napter
napter
|mdp
mdp
| -|SplittyDev
SplittyDev
|jcbdev
jcbdev
|julionav
julionav
|Chenjiayuan195
Chenjiayuan195
|kinandan
kinandan
|jwcraig
jwcraig
| -|shoopapa
shoopapa
|im47cn
im47cn
|hongzio
hongzio
|hatsu38
hatsu38
|GOODBOY008
GOODBOY008
|forestyoo
forestyoo
| -|dqroid
dqroid
|dairui1
dairui1
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
| -|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
| -|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
| -|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
| -|lightrabbit
lightrabbit
|kohii
kohii
|AlexandruSmirnov
AlexandruSmirnov
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
| -|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
|chadgauth
chadgauth
|brunobergher
brunobergher
| -|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andreastempsch
andreastempsch
| -|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
| -|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
| -|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
| -|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
| -|pfitz
pfitz
|celestial-vault
celestial-vault
|linegel
linegel
| | | | +|Ruakij
Ruakij
|p12tic
p12tic
|gtaylor
gtaylor
|aitoroses
aitoroses
|benzntech
benzntech
|mr-ryan-james
mr-ryan-james
| +|heyseth
heyseth
|taisukeoe
taisukeoe
|avtc
avtc
|dlab-anton
dlab-anton
|eonghk
eonghk
|kcwhite
kcwhite
| +|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
| +|franekp
franekp
|yt3trees
yt3trees
|axkirillov
axkirillov
|anton-otee
anton-otee
|bramburn
bramburn
|olearycrew
olearycrew
| +|brunobergher
brunobergher
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
|julionav
julionav
| +|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|ross
ross
|philfung
philfung
|dairui1
dairui1
| +|dqroid
dqroid
|forestyoo
forestyoo
|GOODBOY008
GOODBOY008
|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
| +|shoopapa
shoopapa
|jwcraig
jwcraig
|nevermorec
nevermorec
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
| +|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|vladstudio
vladstudio
|tmsjngx0
tmsjngx0
| +|Githubguy132010
Githubguy132010
|tgfjt
tgfjt
|maekawataiki
maekawataiki
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|student20880
student20880
|shohei-ihaya
shohei-ihaya
|shaybc
shaybc
|seedlord
seedlord
|samir-nimbly
samir-nimbly
| +|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
| +|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
| +|olup
olup
|lightrabbit
lightrabbit
|kohii
kohii
|kinandan
kinandan
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| +|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|devxpain
devxpain
| +|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
| +|atlasgong
atlasgong
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| +|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|AlexandruSmirnov
AlexandruSmirnov
| +|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
| +|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
| +|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|KanTakahiro
KanTakahiro
| +|ksze
ksze
|Jdo300
Jdo300
|hesara
hesara
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|celestial-vault
celestial-vault
| ## 授權 diff --git a/packages/build/src/__tests__/index.test.ts b/packages/build/src/__tests__/index.test.ts index 0b38287cb0..eda70fac1c 100644 --- a/packages/build/src/__tests__/index.test.ts +++ b/packages/build/src/__tests__/index.test.ts @@ -70,7 +70,7 @@ describe("generatePackageJson", () => { { command: "roo-cline.accountButtonClicked", group: "navigation@6", - when: "activeWebviewPanelId == roo-cline.TabPanelProvider && config.roo-cline.rooCodeCloudEnabled", + when: "activeWebviewPanelId == roo-cline.TabPanelProvider", }, ], }, @@ -183,7 +183,7 @@ describe("generatePackageJson", () => { { command: "roo-code-nightly.accountButtonClicked", group: "navigation@6", - when: "activeWebviewPanelId == roo-code-nightly.TabPanelProvider && config.roo-code-nightly.rooCodeCloudEnabled", + when: "activeWebviewPanelId == roo-code-nightly.TabPanelProvider", }, ], }, diff --git a/packages/cloud/package.json b/packages/cloud/package.json index ac8dd6d05f..d67b5ae7eb 100644 --- a/packages/cloud/package.json +++ b/packages/cloud/package.json @@ -13,7 +13,6 @@ "dependencies": { "@roo-code/telemetry": "workspace:^", "@roo-code/types": "workspace:^", - "axios": "^1.7.4", "zod": "^3.25.61" }, "devDependencies": { diff --git a/packages/cloud/src/AuthService.ts b/packages/cloud/src/AuthService.ts index a02bfcc298..cd8e1362c1 100644 --- a/packages/cloud/src/AuthService.ts +++ b/packages/cloud/src/AuthService.ts @@ -42,8 +42,8 @@ const clerkCreateSessionTokenResponseSchema = z.object({ const clerkMeResponseSchema = z.object({ response: z.object({ - first_name: z.string().optional(), - last_name: z.string().optional(), + first_name: z.string().optional().nullable(), + last_name: z.string().optional().nullable(), image_url: z.string().optional(), primary_email_address_id: z.string().optional(), email_addresses: z @@ -531,7 +531,8 @@ export class AuthService extends EventEmitter { const userInfo: CloudUserInfo = {} - userInfo.name = `${userData.first_name} ${userData.last_name}` + const names = [userData.first_name, userData.last_name].filter((name) => !!name) + userInfo.name = names.length > 0 ? names.join(" ") : undefined const primaryEmailAddressId = userData.primary_email_address_id const emailAddresses = userData.email_addresses diff --git a/packages/cloud/src/CloudService.ts b/packages/cloud/src/CloudService.ts index 2ac4a8ca41..12aafee9dc 100644 --- a/packages/cloud/src/CloudService.ts +++ b/packages/cloud/src/CloudService.ts @@ -1,13 +1,19 @@ import * as vscode from "vscode" -import type { CloudUserInfo, TelemetryEvent, OrganizationAllowList } from "@roo-code/types" +import type { + CloudUserInfo, + TelemetryEvent, + OrganizationAllowList, + ClineMessage, + ShareVisibility, +} from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" import { CloudServiceCallbacks } from "./types" import { AuthService } from "./AuthService" import { SettingsService } from "./SettingsService" import { TelemetryClient } from "./TelemetryClient" -import { ShareService } from "./ShareService" +import { ShareService, TaskNotFoundError } from "./ShareService" export class CloudService { private static _instance: CloudService | null = null @@ -161,9 +167,23 @@ export class CloudService { // ShareService - public async shareTask(taskId: string, visibility: "organization" | "public" = "organization") { + public async shareTask( + taskId: string, + visibility: ShareVisibility = "organization", + clineMessages?: ClineMessage[], + ) { this.ensureInitialized() - return this.shareService!.shareTask(taskId, visibility) + + try { + return await this.shareService!.shareTask(taskId, visibility) + } catch (error) { + if (error instanceof TaskNotFoundError && clineMessages) { + // Backfill messages and retry + await this.telemetryClient!.backfillMessages(clineMessages, taskId) + return await this.shareService!.shareTask(taskId, visibility) + } + throw error + } } public async canShareTask(): Promise { diff --git a/packages/cloud/src/ShareService.ts b/packages/cloud/src/ShareService.ts index 42ea9c9fcf..07176d3e9d 100644 --- a/packages/cloud/src/ShareService.ts +++ b/packages/cloud/src/ShareService.ts @@ -1,4 +1,3 @@ -import axios from "axios" import * as vscode from "vscode" import { shareResponseSchema } from "@roo-code/types" @@ -9,6 +8,13 @@ import { getUserAgent } from "./utils" export type ShareVisibility = "organization" | "public" +export class TaskNotFoundError extends Error { + constructor(taskId?: string) { + super(taskId ? `Task '${taskId}' not found` : "Task not found") + Object.setPrototypeOf(this, TaskNotFoundError.prototype) + } +} + export class ShareService { private authService: AuthService private settingsService: SettingsService @@ -31,19 +37,25 @@ export class ShareService { throw new Error("Authentication required") } - const response = await axios.post( - `${getRooCodeApiUrl()}/api/extension/share`, - { taskId, visibility }, - { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${sessionToken}`, - "User-Agent": getUserAgent(), - }, + const response = await fetch(`${getRooCodeApiUrl()}/api/extension/share`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${sessionToken}`, + "User-Agent": getUserAgent(), }, - ) + body: JSON.stringify({ taskId, visibility }), + signal: AbortSignal.timeout(10000), + }) - const data = shareResponseSchema.parse(response.data) + if (!response.ok) { + if (response.status === 404) { + throw new TaskNotFoundError(taskId) + } + throw new Error(`HTTP ${response.status}: ${response.statusText}`) + } + + const data = shareResponseSchema.parse(await response.json()) this.log("[share] Share link created successfully:", data) if (data.success && data.shareUrl) { diff --git a/packages/cloud/src/TelemetryClient.ts b/packages/cloud/src/TelemetryClient.ts index 1ad892cb97..ea48fcf269 100644 --- a/packages/cloud/src/TelemetryClient.ts +++ b/packages/cloud/src/TelemetryClient.ts @@ -1,4 +1,9 @@ -import { TelemetryEventName, type TelemetryEvent, rooCodeTelemetryEventSchema } from "@roo-code/types" +import { + TelemetryEventName, + type TelemetryEvent, + rooCodeTelemetryEventSchema, + type ClineMessage, +} from "@roo-code/types" import { BaseTelemetryClient } from "@roo-code/telemetry" import { getRooCodeApiUrl } from "./Config" @@ -79,6 +84,66 @@ export class TelemetryClient extends BaseTelemetryClient { } } + public async backfillMessages(messages: ClineMessage[], taskId: string): Promise { + if (!this.authService.isAuthenticated()) { + if (this.debug) { + console.info(`[TelemetryClient#backfillMessages] Skipping: Not authenticated`) + } + return + } + + const token = this.authService.getSessionToken() + + if (!token) { + console.error(`[TelemetryClient#backfillMessages] Unauthorized: No session token available.`) + return + } + + try { + const mergedProperties = await this.getEventProperties({ + event: TelemetryEventName.TASK_MESSAGE, + properties: { taskId }, + }) + + const formData = new FormData() + formData.append("taskId", taskId) + formData.append("properties", JSON.stringify(mergedProperties)) + + formData.append( + "file", + new File([JSON.stringify(messages)], "task.json", { + type: "application/json", + }), + ) + + if (this.debug) { + console.info( + `[TelemetryClient#backfillMessages] Uploading ${messages.length} messages for task ${taskId}`, + ) + } + + // Custom fetch for multipart - don't set Content-Type header (let browser set it) + const response = await fetch(`${getRooCodeApiUrl()}/api/events/backfill`, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + // Note: No Content-Type header - browser will set multipart/form-data with boundary + }, + body: formData, + }) + + if (!response.ok) { + console.error( + `[TelemetryClient#backfillMessages] POST events/backfill -> ${response.status} ${response.statusText}`, + ) + } else if (this.debug) { + console.info(`[TelemetryClient#backfillMessages] Successfully uploaded messages for task ${taskId}`) + } + } catch (error) { + console.error(`[TelemetryClient#backfillMessages] Error uploading messages: ${error}`) + } + } + public override updateTelemetryState(_didUserOptIn: boolean) {} public override isTelemetryEnabled(): boolean { diff --git a/packages/cloud/src/__tests__/CloudService.test.ts b/packages/cloud/src/__tests__/CloudService.test.ts index 869def5945..6ed8c9741c 100644 --- a/packages/cloud/src/__tests__/CloudService.test.ts +++ b/packages/cloud/src/__tests__/CloudService.test.ts @@ -1,10 +1,13 @@ // npx vitest run src/__tests__/CloudService.test.ts import * as vscode from "vscode" +import type { ClineMessage } from "@roo-code/types" import { CloudService } from "../CloudService" import { AuthService } from "../AuthService" import { SettingsService } from "../SettingsService" +import { ShareService, TaskNotFoundError } from "../ShareService" +import { TelemetryClient } from "../TelemetryClient" import { TelemetryService } from "@roo-code/telemetry" import { CloudServiceCallbacks } from "../types" @@ -28,6 +31,10 @@ vi.mock("../AuthService") vi.mock("../SettingsService") +vi.mock("../ShareService") + +vi.mock("../TelemetryClient") + describe("CloudService", () => { let mockContext: vscode.ExtensionContext let mockAuthService: { @@ -36,6 +43,7 @@ describe("CloudService", () => { logout: ReturnType isAuthenticated: ReturnType hasActiveSession: ReturnType + hasOrIsAcquiringActiveSession: ReturnType getUserInfo: ReturnType getState: ReturnType getSessionToken: ReturnType @@ -52,6 +60,13 @@ describe("CloudService", () => { getAllowList: ReturnType dispose: ReturnType } + let mockShareService: { + shareTask: ReturnType + canShareTask: ReturnType + } + let mockTelemetryClient: { + backfillMessages: ReturnType + } let mockTelemetryService: { hasInstance: ReturnType instance: { @@ -63,15 +78,29 @@ describe("CloudService", () => { CloudService.resetInstance() mockContext = { + subscriptions: [], + workspaceState: { + get: vi.fn(), + update: vi.fn(), + keys: vi.fn().mockReturnValue([]), + }, secrets: { get: vi.fn(), store: vi.fn(), delete: vi.fn(), + onDidChange: vi.fn().mockReturnValue({ dispose: vi.fn() }), }, globalState: { get: vi.fn(), update: vi.fn(), + setKeysForSync: vi.fn(), + keys: vi.fn().mockReturnValue([]), }, + extensionUri: { scheme: "file", path: "/mock/path" }, + extensionPath: "/mock/path", + extensionMode: 1, + asAbsolutePath: vi.fn((relativePath: string) => `/mock/path/${relativePath}`), + storageUri: { scheme: "file", path: "/mock/storage" }, extension: { packageJSON: { version: "1.0.0", @@ -85,6 +114,7 @@ describe("CloudService", () => { logout: vi.fn(), isAuthenticated: vi.fn().mockReturnValue(false), hasActiveSession: vi.fn().mockReturnValue(false), + hasOrIsAcquiringActiveSession: vi.fn().mockReturnValue(false), getUserInfo: vi.fn(), getState: vi.fn().mockReturnValue("logged-out"), getSessionToken: vi.fn(), @@ -103,6 +133,15 @@ describe("CloudService", () => { dispose: vi.fn(), } + mockShareService = { + shareTask: vi.fn(), + canShareTask: vi.fn().mockResolvedValue(true), + } + + mockTelemetryClient = { + backfillMessages: vi.fn().mockResolvedValue(undefined), + } + mockTelemetryService = { hasInstance: vi.fn().mockReturnValue(true), instance: { @@ -112,6 +151,8 @@ describe("CloudService", () => { vi.mocked(AuthService).mockImplementation(() => mockAuthService as unknown as AuthService) vi.mocked(SettingsService).mockImplementation(() => mockSettingsService as unknown as SettingsService) + vi.mocked(ShareService).mockImplementation(() => mockShareService as unknown as ShareService) + vi.mocked(TelemetryClient).mockImplementation(() => mockTelemetryClient as unknown as TelemetryClient) vi.mocked(TelemetryService.hasInstance).mockReturnValue(true) Object.defineProperty(TelemetryService, "instance", { @@ -342,4 +383,119 @@ describe("CloudService", () => { expect(mockSettingsService.dispose).toHaveBeenCalled() }) }) + + describe("shareTask with ClineMessage retry logic", () => { + let cloudService: CloudService + + beforeEach(async () => { + // Reset mocks for shareTask tests + vi.clearAllMocks() + + // Reset authentication state for shareTask tests + mockAuthService.isAuthenticated.mockReturnValue(true) + mockAuthService.hasActiveSession.mockReturnValue(true) + mockAuthService.hasOrIsAcquiringActiveSession.mockReturnValue(true) + mockAuthService.getState.mockReturnValue("active") + + cloudService = await CloudService.createInstance(mockContext, {}) + }) + + it("should call shareTask without retry when successful", async () => { + const taskId = "test-task-id" + const visibility = "organization" + const clineMessages: ClineMessage[] = [ + { + ts: Date.now(), + type: "say", + say: "text", + text: "Hello world", + }, + ] + + const expectedResult = { success: true, shareUrl: "https://example.com/share/123" } + mockShareService.shareTask.mockResolvedValue(expectedResult) + + const result = await cloudService.shareTask(taskId, visibility, clineMessages) + + expect(mockShareService.shareTask).toHaveBeenCalledTimes(1) + expect(mockShareService.shareTask).toHaveBeenCalledWith(taskId, visibility) + expect(mockTelemetryClient.backfillMessages).not.toHaveBeenCalled() + expect(result).toEqual(expectedResult) + }) + + it("should retry with backfill when TaskNotFoundError occurs", async () => { + const taskId = "test-task-id" + const visibility = "organization" + const clineMessages: ClineMessage[] = [ + { + ts: Date.now(), + type: "say", + say: "text", + text: "Hello world", + }, + ] + + const expectedResult = { success: true, shareUrl: "https://example.com/share/123" } + + // First call throws TaskNotFoundError, second call succeeds + mockShareService.shareTask + .mockRejectedValueOnce(new TaskNotFoundError(taskId)) + .mockResolvedValueOnce(expectedResult) + + const result = await cloudService.shareTask(taskId, visibility, clineMessages) + + expect(mockShareService.shareTask).toHaveBeenCalledTimes(2) + expect(mockShareService.shareTask).toHaveBeenNthCalledWith(1, taskId, visibility) + expect(mockShareService.shareTask).toHaveBeenNthCalledWith(2, taskId, visibility) + expect(mockTelemetryClient.backfillMessages).toHaveBeenCalledTimes(1) + expect(mockTelemetryClient.backfillMessages).toHaveBeenCalledWith(clineMessages, taskId) + expect(result).toEqual(expectedResult) + }) + + it("should not retry when TaskNotFoundError occurs but no clineMessages provided", async () => { + const taskId = "test-task-id" + const visibility = "organization" + + const taskNotFoundError = new TaskNotFoundError(taskId) + mockShareService.shareTask.mockRejectedValue(taskNotFoundError) + + await expect(cloudService.shareTask(taskId, visibility)).rejects.toThrow(TaskNotFoundError) + + expect(mockShareService.shareTask).toHaveBeenCalledTimes(1) + expect(mockTelemetryClient.backfillMessages).not.toHaveBeenCalled() + }) + + it("should not retry when non-TaskNotFoundError occurs", async () => { + const taskId = "test-task-id" + const visibility = "organization" + const clineMessages: ClineMessage[] = [ + { + ts: Date.now(), + type: "say", + say: "text", + text: "Hello world", + }, + ] + + const genericError = new Error("Some other error") + mockShareService.shareTask.mockRejectedValue(genericError) + + await expect(cloudService.shareTask(taskId, visibility, clineMessages)).rejects.toThrow(genericError) + + expect(mockShareService.shareTask).toHaveBeenCalledTimes(1) + expect(mockTelemetryClient.backfillMessages).not.toHaveBeenCalled() + }) + + it("should work with default parameters", async () => { + const taskId = "test-task-id" + const expectedResult = { success: true, shareUrl: "https://example.com/share/123" } + mockShareService.shareTask.mockResolvedValue(expectedResult) + + const result = await cloudService.shareTask(taskId) + + expect(mockShareService.shareTask).toHaveBeenCalledTimes(1) + expect(mockShareService.shareTask).toHaveBeenCalledWith(taskId, "organization") + expect(result).toEqual(expectedResult) + }) + }) }) diff --git a/packages/cloud/src/__tests__/ShareService.test.ts b/packages/cloud/src/__tests__/ShareService.test.ts index 55fc4e7c38..dd2e5f1ae5 100644 --- a/packages/cloud/src/__tests__/ShareService.test.ts +++ b/packages/cloud/src/__tests__/ShareService.test.ts @@ -1,16 +1,15 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { MockedFunction } from "vitest" -import axios from "axios" import * as vscode from "vscode" -import { ShareService } from "../ShareService" +import { ShareService, TaskNotFoundError } from "../ShareService" import type { AuthService } from "../AuthService" import type { SettingsService } from "../SettingsService" -// Mock axios -vi.mock("axios") -const mockedAxios = axios as any +// Mock fetch +const mockFetch = vi.fn() +global.fetch = mockFetch as any // Mock vscode vi.mock("vscode", () => ({ @@ -53,6 +52,7 @@ describe("ShareService", () => { beforeEach(() => { vi.clearAllMocks() + mockFetch.mockClear() mockLog = vi.fn() mockAuthService = { @@ -70,86 +70,99 @@ describe("ShareService", () => { describe("shareTask", () => { it("should share task with organization visibility and copy to clipboard", async () => { - const mockResponse = { - data: { - success: true, - shareUrl: "https://app.roocode.com/share/abc123", - }, + const mockResponseData = { + success: true, + shareUrl: "https://app.roocode.com/share/abc123", } ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") - mockedAxios.post.mockResolvedValue(mockResponse) + mockFetch.mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue(mockResponseData), + }) const result = await shareService.shareTask("task-123", "organization") expect(result.success).toBe(true) expect(result.shareUrl).toBe("https://app.roocode.com/share/abc123") - expect(mockedAxios.post).toHaveBeenCalledWith( - "https://app.roocode.com/api/extension/share", - { taskId: "task-123", visibility: "organization" }, - { - headers: { - "Content-Type": "application/json", - Authorization: "Bearer session-token", - "User-Agent": "Roo-Code 1.0.0", - }, + expect(mockFetch).toHaveBeenCalledWith("https://app.roocode.com/api/extension/share", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer session-token", + "User-Agent": "Roo-Code 1.0.0", }, - ) + body: JSON.stringify({ taskId: "task-123", visibility: "organization" }), + signal: expect.any(AbortSignal), + }) expect(vscode.env.clipboard.writeText).toHaveBeenCalledWith("https://app.roocode.com/share/abc123") }) it("should share task with public visibility", async () => { - const mockResponse = { - data: { - success: true, - shareUrl: "https://app.roocode.com/share/abc123", - }, + const mockResponseData = { + success: true, + shareUrl: "https://app.roocode.com/share/abc123", } ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") - mockedAxios.post.mockResolvedValue(mockResponse) + mockFetch.mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue(mockResponseData), + }) const result = await shareService.shareTask("task-123", "public") expect(result.success).toBe(true) - expect(mockedAxios.post).toHaveBeenCalledWith( - "https://app.roocode.com/api/extension/share", - { taskId: "task-123", visibility: "public" }, - expect.any(Object), - ) + expect(mockFetch).toHaveBeenCalledWith("https://app.roocode.com/api/extension/share", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer session-token", + "User-Agent": "Roo-Code 1.0.0", + }, + body: JSON.stringify({ taskId: "task-123", visibility: "public" }), + signal: expect.any(AbortSignal), + }) }) it("should default to organization visibility when not specified", async () => { - const mockResponse = { - data: { - success: true, - shareUrl: "https://app.roocode.com/share/abc123", - }, + const mockResponseData = { + success: true, + shareUrl: "https://app.roocode.com/share/abc123", } ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") - mockedAxios.post.mockResolvedValue(mockResponse) + mockFetch.mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue(mockResponseData), + }) const result = await shareService.shareTask("task-123") expect(result.success).toBe(true) - expect(mockedAxios.post).toHaveBeenCalledWith( - "https://app.roocode.com/api/extension/share", - { taskId: "task-123", visibility: "organization" }, - expect.any(Object), - ) + expect(mockFetch).toHaveBeenCalledWith("https://app.roocode.com/api/extension/share", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer session-token", + "User-Agent": "Roo-Code 1.0.0", + }, + body: JSON.stringify({ taskId: "task-123", visibility: "organization" }), + signal: expect.any(AbortSignal), + }) }) it("should handle API error response", async () => { - const mockResponse = { - data: { - success: false, - error: "Task not found", - }, + const mockResponseData = { + success: false, + error: "Task not found", } ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") - mockedAxios.post.mockResolvedValue(mockResponse) + mockFetch.mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue(mockResponseData), + }) const result = await shareService.shareTask("task-123", "organization") @@ -165,10 +178,56 @@ describe("ShareService", () => { it("should handle unexpected errors", async () => { ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") - mockedAxios.post.mockRejectedValue(new Error("Network error")) + mockFetch.mockRejectedValue(new Error("Network error")) await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow("Network error") }) + + it("should throw TaskNotFoundError for 404 responses", async () => { + ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") + mockFetch.mockResolvedValue({ + ok: false, + status: 404, + statusText: "Not Found", + }) + + await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow(TaskNotFoundError) + await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow( + "Task 'task-123' not found", + ) + }) + + it("should throw generic Error for non-404 HTTP errors", async () => { + ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") + mockFetch.mockResolvedValue({ + ok: false, + status: 500, + statusText: "Internal Server Error", + }) + + await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow( + "HTTP 500: Internal Server Error", + ) + await expect(shareService.shareTask("task-123", "organization")).rejects.not.toThrow(TaskNotFoundError) + }) + + it("should create TaskNotFoundError with correct properties", async () => { + ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") + mockFetch.mockResolvedValue({ + ok: false, + status: 404, + statusText: "Not Found", + }) + + try { + await shareService.shareTask("task-123", "organization") + expect.fail("Expected TaskNotFoundError to be thrown") + } catch (error) { + expect(error).toBeInstanceOf(TaskNotFoundError) + expect(error).toBeInstanceOf(Error) + expect((error as TaskNotFoundError).message).toBe("Task 'task-123' not found") + } + }) }) describe("canShareTask", () => { diff --git a/packages/cloud/src/__tests__/TelemetryClient.test.ts b/packages/cloud/src/__tests__/TelemetryClient.test.ts index 85b0fbf5ef..e4c62b1e4e 100644 --- a/packages/cloud/src/__tests__/TelemetryClient.test.ts +++ b/packages/cloud/src/__tests__/TelemetryClient.test.ts @@ -424,4 +424,315 @@ describe("TelemetryClient", () => { await client.shutdown() }) }) + + describe("backfillMessages", () => { + it("should not send request when not authenticated", async () => { + mockAuthService.isAuthenticated.mockReturnValue(false) + const client = new TelemetryClient(mockAuthService, mockSettingsService) + + const messages = [ + { + ts: 1, + type: "say" as const, + say: "text" as const, + text: "test message", + }, + ] + + await client.backfillMessages(messages, "test-task-id") + + expect(mockFetch).not.toHaveBeenCalled() + }) + + it("should not send request when no session token available", async () => { + mockAuthService.getSessionToken.mockReturnValue(null) + const client = new TelemetryClient(mockAuthService, mockSettingsService) + + const messages = [ + { + ts: 1, + type: "say" as const, + say: "text" as const, + text: "test message", + }, + ] + + await client.backfillMessages(messages, "test-task-id") + + expect(mockFetch).not.toHaveBeenCalled() + expect(console.error).toHaveBeenCalledWith( + "[TelemetryClient#backfillMessages] Unauthorized: No session token available.", + ) + }) + + it("should send FormData request with correct structure when authenticated", async () => { + const client = new TelemetryClient(mockAuthService, mockSettingsService) + + const providerProperties = { + appName: "roo-code", + appVersion: "1.0.0", + vscodeVersion: "1.60.0", + platform: "darwin", + editorName: "vscode", + language: "en", + mode: "code", + } + + const mockProvider: TelemetryPropertiesProvider = { + getTelemetryProperties: vi.fn().mockResolvedValue(providerProperties), + } + + client.setProvider(mockProvider) + + const messages = [ + { + ts: 1, + type: "say" as const, + say: "text" as const, + text: "test message 1", + }, + { + ts: 2, + type: "ask" as const, + ask: "followup" as const, + text: "test question", + }, + ] + + await client.backfillMessages(messages, "test-task-id") + + expect(mockFetch).toHaveBeenCalledWith( + "https://app.roocode.com/api/events/backfill", + expect.objectContaining({ + method: "POST", + headers: { + Authorization: "Bearer mock-token", + }, + body: expect.any(FormData), + }), + ) + + // Verify FormData contents + const call = mockFetch.mock.calls[0] + const formData = call[1].body as FormData + + expect(formData.get("taskId")).toBe("test-task-id") + + // Parse and compare properties as objects since JSON.stringify order can vary + const propertiesJson = formData.get("properties") as string + const parsedProperties = JSON.parse(propertiesJson) + expect(parsedProperties).toEqual({ + taskId: "test-task-id", + ...providerProperties, + }) + // The messages are stored as a File object under the "file" key + const fileField = formData.get("file") as File + expect(fileField).toBeInstanceOf(File) + expect(fileField.name).toBe("task.json") + expect(fileField.type).toBe("application/json") + + // Read the file content to verify the messages + const fileContent = await fileField.text() + expect(fileContent).toBe(JSON.stringify(messages)) + }) + + it("should handle provider errors gracefully", async () => { + const client = new TelemetryClient(mockAuthService, mockSettingsService) + + const mockProvider: TelemetryPropertiesProvider = { + getTelemetryProperties: vi.fn().mockRejectedValue(new Error("Provider error")), + } + + client.setProvider(mockProvider) + + const messages = [ + { + ts: 1, + type: "say" as const, + say: "text" as const, + text: "test message", + }, + ] + + await client.backfillMessages(messages, "test-task-id") + + expect(mockFetch).toHaveBeenCalledWith( + "https://app.roocode.com/api/events/backfill", + expect.objectContaining({ + method: "POST", + headers: { + Authorization: "Bearer mock-token", + }, + body: expect.any(FormData), + }), + ) + + // Verify FormData contents - should still work with just taskId + const call = mockFetch.mock.calls[0] + const formData = call[1].body as FormData + + expect(formData.get("taskId")).toBe("test-task-id") + expect(formData.get("properties")).toBe( + JSON.stringify({ + taskId: "test-task-id", + }), + ) + // The messages are stored as a File object under the "file" key + const fileField = formData.get("file") as File + expect(fileField).toBeInstanceOf(File) + expect(fileField.name).toBe("task.json") + expect(fileField.type).toBe("application/json") + + // Read the file content to verify the messages + const fileContent = await fileField.text() + expect(fileContent).toBe(JSON.stringify(messages)) + }) + + it("should work without provider set", async () => { + const client = new TelemetryClient(mockAuthService, mockSettingsService) + + const messages = [ + { + ts: 1, + type: "say" as const, + say: "text" as const, + text: "test message", + }, + ] + + await client.backfillMessages(messages, "test-task-id") + + expect(mockFetch).toHaveBeenCalledWith( + "https://app.roocode.com/api/events/backfill", + expect.objectContaining({ + method: "POST", + headers: { + Authorization: "Bearer mock-token", + }, + body: expect.any(FormData), + }), + ) + + // Verify FormData contents - should work with just taskId + const call = mockFetch.mock.calls[0] + const formData = call[1].body as FormData + + expect(formData.get("taskId")).toBe("test-task-id") + expect(formData.get("properties")).toBe( + JSON.stringify({ + taskId: "test-task-id", + }), + ) + // The messages are stored as a File object under the "file" key + const fileField = formData.get("file") as File + expect(fileField).toBeInstanceOf(File) + expect(fileField.name).toBe("task.json") + expect(fileField.type).toBe("application/json") + + // Read the file content to verify the messages + const fileContent = await fileField.text() + expect(fileContent).toBe(JSON.stringify(messages)) + }) + + it("should handle fetch errors gracefully", async () => { + const client = new TelemetryClient(mockAuthService, mockSettingsService) + + mockFetch.mockRejectedValue(new Error("Network error")) + + const messages = [ + { + ts: 1, + type: "say" as const, + say: "text" as const, + text: "test message", + }, + ] + + await expect(client.backfillMessages(messages, "test-task-id")).resolves.not.toThrow() + + expect(console.error).toHaveBeenCalledWith( + expect.stringContaining( + "[TelemetryClient#backfillMessages] Error uploading messages: Error: Network error", + ), + ) + }) + + it("should handle HTTP error responses", async () => { + const client = new TelemetryClient(mockAuthService, mockSettingsService) + + mockFetch.mockResolvedValue({ + ok: false, + status: 404, + statusText: "Not Found", + }) + + const messages = [ + { + ts: 1, + type: "say" as const, + say: "text" as const, + text: "test message", + }, + ] + + await client.backfillMessages(messages, "test-task-id") + + expect(console.error).toHaveBeenCalledWith( + "[TelemetryClient#backfillMessages] POST events/backfill -> 404 Not Found", + ) + }) + + it("should log debug information when debug is enabled", async () => { + const client = new TelemetryClient(mockAuthService, mockSettingsService, true) + + const messages = [ + { + ts: 1, + type: "say" as const, + say: "text" as const, + text: "test message", + }, + ] + + await client.backfillMessages(messages, "test-task-id") + + expect(console.info).toHaveBeenCalledWith( + "[TelemetryClient#backfillMessages] Uploading 1 messages for task test-task-id", + ) + expect(console.info).toHaveBeenCalledWith( + "[TelemetryClient#backfillMessages] Successfully uploaded messages for task test-task-id", + ) + }) + + it("should handle empty messages array", async () => { + const client = new TelemetryClient(mockAuthService, mockSettingsService) + + await client.backfillMessages([], "test-task-id") + + expect(mockFetch).toHaveBeenCalledWith( + "https://app.roocode.com/api/events/backfill", + expect.objectContaining({ + method: "POST", + headers: { + Authorization: "Bearer mock-token", + }, + body: expect.any(FormData), + }), + ) + + // Verify FormData contents + const call = mockFetch.mock.calls[0] + const formData = call[1].body as FormData + + // The messages are stored as a File object under the "file" key + const fileField = formData.get("file") as File + expect(fileField).toBeInstanceOf(File) + expect(fileField.name).toBe("task.json") + expect(fileField.type).toBe("application/json") + + // Read the file content to verify the empty messages array + const fileContent = await fileField.text() + expect(fileContent).toBe("[]") + }) + }) }) diff --git a/packages/telemetry/src/BaseTelemetryClient.ts b/packages/telemetry/src/BaseTelemetryClient.ts index ab8ab56f59..2eb308b414 100644 --- a/packages/telemetry/src/BaseTelemetryClient.ts +++ b/packages/telemetry/src/BaseTelemetryClient.ts @@ -25,13 +25,21 @@ export abstract class BaseTelemetryClient implements TelemetryClient { : !this.subscription.events.includes(eventName) } + /** + * Determines if a specific property should be included in telemetry events + * Override in subclasses to filter specific properties + */ + protected isPropertyCapturable(_propertyName: string): boolean { + return true + } + protected async getEventProperties(event: TelemetryEvent): Promise { let providerProperties: TelemetryEvent["properties"] = {} const provider = this.providerRef?.deref() if (provider) { try { - // Get the telemetry properties directly from the provider. + // Get properties from the provider providerProperties = await provider.getTelemetryProperties() } catch (error) { // Log error but continue with capturing the event. @@ -43,7 +51,10 @@ export abstract class BaseTelemetryClient implements TelemetryClient { // Merge provider properties with event-specific properties. // Event properties take precedence in case of conflicts. - return { ...providerProperties, ...(event.properties || {}) } + const mergedProperties = { ...providerProperties, ...(event.properties || {}) } + + // Filter out properties that shouldn't be captured by this client + return Object.fromEntries(Object.entries(mergedProperties).filter(([key]) => this.isPropertyCapturable(key))) } public abstract capture(event: TelemetryEvent): Promise diff --git a/packages/telemetry/src/PostHogTelemetryClient.ts b/packages/telemetry/src/PostHogTelemetryClient.ts index 243176ed45..f1c46577df 100644 --- a/packages/telemetry/src/PostHogTelemetryClient.ts +++ b/packages/telemetry/src/PostHogTelemetryClient.ts @@ -13,6 +13,8 @@ import { BaseTelemetryClient } from "./BaseTelemetryClient" export class PostHogTelemetryClient extends BaseTelemetryClient { private client: PostHog private distinctId: string = vscode.env.machineId + // Git repository properties that should be filtered out + private readonly gitPropertyNames = ["repositoryUrl", "repositoryName", "defaultBranch"] constructor(debug = false) { super( @@ -26,6 +28,19 @@ export class PostHogTelemetryClient extends BaseTelemetryClient { this.client = new PostHog(process.env.POSTHOG_API_KEY || "", { host: "https://us.i.posthog.com" }) } + /** + * Filter out git repository properties for PostHog telemetry + * @param propertyName The property name to check + * @returns Whether the property should be included in telemetry events + */ + protected override isPropertyCapturable(propertyName: string): boolean { + // Filter out git repository properties + if (this.gitPropertyNames.includes(propertyName)) { + return false + } + return true + } + public override async capture(event: TelemetryEvent): Promise { if (!this.isTelemetryEnabled() || !this.isEventCapturable(event.event)) { if (this.debug) { diff --git a/packages/telemetry/src/TelemetryService.ts b/packages/telemetry/src/TelemetryService.ts index 728809f8bd..7a11e3d388 100644 --- a/packages/telemetry/src/TelemetryService.ts +++ b/packages/telemetry/src/TelemetryService.ts @@ -152,6 +152,31 @@ export class TelemetryService { this.captureEvent(TelemetryEventName.CONSECUTIVE_MISTAKE_ERROR, { taskId }) } + /** + * Captures when a tab is shown due to user action + * @param tab The tab that was shown + */ + public captureTabShown(tab: string): void { + this.captureEvent(TelemetryEventName.TAB_SHOWN, { tab }) + } + + /** + * Captures when a setting is changed in ModesView + * @param settingName The name of the setting that was changed + */ + public captureModeSettingChanged(settingName: string): void { + this.captureEvent(TelemetryEventName.MODE_SETTINGS_CHANGED, { settingName }) + } + + /** + * Captures when a user creates a new custom mode + * @param modeSlug The slug of the custom mode + * @param modeName The name of the custom mode + */ + public captureCustomModeCreated(modeSlug: string, modeName: string): void { + this.captureEvent(TelemetryEventName.CUSTOM_MODE_CREATED, { modeSlug, modeName }) + } + /** * Captures a marketplace item installation event * @param itemId The unique identifier of the marketplace item diff --git a/packages/telemetry/src/__tests__/PostHogTelemetryClient.test.ts b/packages/telemetry/src/__tests__/PostHogTelemetryClient.test.ts index c94dbdb734..282d1d6c6a 100644 --- a/packages/telemetry/src/__tests__/PostHogTelemetryClient.test.ts +++ b/packages/telemetry/src/__tests__/PostHogTelemetryClient.test.ts @@ -70,6 +70,29 @@ describe("PostHogTelemetryClient", () => { }) }) + describe("isPropertyCapturable", () => { + it("should filter out git repository properties", () => { + const client = new PostHogTelemetryClient() + + const isPropertyCapturable = getPrivateProperty<(propertyName: string) => boolean>( + client, + "isPropertyCapturable", + ).bind(client) + + // Git properties should be filtered out + expect(isPropertyCapturable("repositoryUrl")).toBe(false) + expect(isPropertyCapturable("repositoryName")).toBe(false) + expect(isPropertyCapturable("defaultBranch")).toBe(false) + + // Other properties should be included + expect(isPropertyCapturable("appVersion")).toBe(true) + expect(isPropertyCapturable("vscodeVersion")).toBe(true) + expect(isPropertyCapturable("platform")).toBe(true) + expect(isPropertyCapturable("mode")).toBe(true) + expect(isPropertyCapturable("customProperty")).toBe(true) + }) + }) + describe("getEventProperties", () => { it("should merge provider properties with event properties", async () => { const client = new PostHogTelemetryClient() @@ -112,6 +135,54 @@ describe("PostHogTelemetryClient", () => { expect(mockProvider.getTelemetryProperties).toHaveBeenCalledTimes(1) }) + it("should filter out git repository properties", async () => { + const client = new PostHogTelemetryClient() + + const mockProvider: TelemetryPropertiesProvider = { + getTelemetryProperties: vi.fn().mockResolvedValue({ + appVersion: "1.0.0", + vscodeVersion: "1.60.0", + platform: "darwin", + editorName: "vscode", + language: "en", + mode: "code", + // Git properties that should be filtered out + repositoryUrl: "https://github.com/example/repo", + repositoryName: "example/repo", + defaultBranch: "main", + }), + } + + client.setProvider(mockProvider) + + const getEventProperties = getPrivateProperty< + (event: { event: TelemetryEventName; properties?: Record }) => Promise> + >(client, "getEventProperties").bind(client) + + const result = await getEventProperties({ + event: TelemetryEventName.TASK_CREATED, + properties: { + customProp: "value", + }, + }) + + // Git properties should be filtered out + expect(result).not.toHaveProperty("repositoryUrl") + expect(result).not.toHaveProperty("repositoryName") + expect(result).not.toHaveProperty("defaultBranch") + + // Other properties should be included + expect(result).toEqual({ + appVersion: "1.0.0", + vscodeVersion: "1.60.0", + platform: "darwin", + editorName: "vscode", + language: "en", + mode: "code", + customProp: "value", + }) + }) + it("should handle errors from provider gracefully", async () => { const client = new PostHogTelemetryClient() @@ -211,6 +282,48 @@ describe("PostHogTelemetryClient", () => { }), }) }) + + it("should filter out git repository properties when capturing events", async () => { + const client = new PostHogTelemetryClient() + client.updateTelemetryState(true) + + const mockProvider: TelemetryPropertiesProvider = { + getTelemetryProperties: vi.fn().mockResolvedValue({ + appVersion: "1.0.0", + vscodeVersion: "1.60.0", + platform: "darwin", + editorName: "vscode", + language: "en", + mode: "code", + // Git properties that should be filtered out + repositoryUrl: "https://github.com/example/repo", + repositoryName: "example/repo", + defaultBranch: "main", + }), + } + + client.setProvider(mockProvider) + + await client.capture({ + event: TelemetryEventName.TASK_CREATED, + properties: { test: "value" }, + }) + + expect(mockPostHogClient.capture).toHaveBeenCalledWith({ + distinctId: "test-machine-id", + event: TelemetryEventName.TASK_CREATED, + properties: expect.objectContaining({ + appVersion: "1.0.0", + test: "value", + }), + }) + + // Verify git properties are not included + const captureCall = mockPostHogClient.capture.mock.calls[0][0] + expect(captureCall.properties).not.toHaveProperty("repositoryUrl") + expect(captureCall.properties).not.toHaveProperty("repositoryName") + expect(captureCall.properties).not.toHaveProperty("defaultBranch") + }) }) describe("updateTelemetryState", () => { diff --git a/packages/types/npm/package.json b/packages/types/npm/package.json index 4c5ac24e65..eba5049426 100644 --- a/packages/types/npm/package.json +++ b/packages/types/npm/package.json @@ -1,6 +1,6 @@ { "name": "@roo-code/types", - "version": "1.28.0", + "version": "1.29.0", "description": "TypeScript type definitions for Roo Code.", "publishConfig": { "access": "public", diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index bfd071a755..e713cafa4c 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -104,6 +104,7 @@ export const globalSettingsSchema = z.object({ enhancementApiConfigId: z.string().optional(), historyPreviewCollapsed: z.boolean().optional(), profileThresholds: z.record(z.string(), z.number()).optional(), + hasOpenedModeSelector: z.boolean().optional(), }) export type GlobalSettings = z.infer diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index df6b856ce9..345bc3e311 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -13,6 +13,7 @@ export * from "./message.js" export * from "./mode.js" export * from "./model.js" export * from "./provider-settings.js" +export * from "./sharing.js" export * from "./telemetry.js" export * from "./terminal.js" export * from "./tool.js" diff --git a/packages/types/src/mode.ts b/packages/types/src/mode.ts index dfe95f8d7e..175ec095fc 100644 --- a/packages/types/src/mode.ts +++ b/packages/types/src/mode.ts @@ -66,6 +66,7 @@ export const modeConfigSchema = z.object({ name: z.string().min(1, "Name is required"), roleDefinition: z.string().min(1, "Role definition is required"), whenToUse: z.string().optional(), + description: z.string().optional(), customInstructions: z.string().optional(), groups: groupEntryArraySchema, source: z.enum(["global", "project"]).optional(), @@ -106,6 +107,7 @@ export type CustomModesSettings = z.infer export const promptComponentSchema = z.object({ roleDefinition: z.string().optional(), whenToUse: z.string().optional(), + description: z.string().optional(), customInstructions: z.string().optional(), }) diff --git a/packages/types/src/providers/claude-code.ts b/packages/types/src/providers/claude-code.ts index ebf053a532..d0fff0f2ee 100644 --- a/packages/types/src/providers/claude-code.ts +++ b/packages/types/src/providers/claude-code.ts @@ -5,9 +5,44 @@ import { anthropicModels } from "./anthropic.js" export type ClaudeCodeModelId = keyof typeof claudeCodeModels export const claudeCodeDefaultModelId: ClaudeCodeModelId = "claude-sonnet-4-20250514" export const claudeCodeModels = { - "claude-sonnet-4-20250514": anthropicModels["claude-sonnet-4-20250514"], - "claude-opus-4-20250514": anthropicModels["claude-opus-4-20250514"], - "claude-3-7-sonnet-20250219": anthropicModels["claude-3-7-sonnet-20250219"], - "claude-3-5-sonnet-20241022": anthropicModels["claude-3-5-sonnet-20241022"], - "claude-3-5-haiku-20241022": anthropicModels["claude-3-5-haiku-20241022"], + "claude-sonnet-4-20250514": { + ...anthropicModels["claude-sonnet-4-20250514"], + supportsImages: false, + supportsPromptCache: true, // Claude Code does report cache tokens + supportsReasoningEffort: false, + supportsReasoningBudget: false, + requiredReasoningBudget: false, + }, + "claude-opus-4-20250514": { + ...anthropicModels["claude-opus-4-20250514"], + supportsImages: false, + supportsPromptCache: true, // Claude Code does report cache tokens + supportsReasoningEffort: false, + supportsReasoningBudget: false, + requiredReasoningBudget: false, + }, + "claude-3-7-sonnet-20250219": { + ...anthropicModels["claude-3-7-sonnet-20250219"], + supportsImages: false, + supportsPromptCache: true, // Claude Code does report cache tokens + supportsReasoningEffort: false, + supportsReasoningBudget: false, + requiredReasoningBudget: false, + }, + "claude-3-5-sonnet-20241022": { + ...anthropicModels["claude-3-5-sonnet-20241022"], + supportsImages: false, + supportsPromptCache: true, // Claude Code does report cache tokens + supportsReasoningEffort: false, + supportsReasoningBudget: false, + requiredReasoningBudget: false, + }, + "claude-3-5-haiku-20241022": { + ...anthropicModels["claude-3-5-haiku-20241022"], + supportsImages: false, + supportsPromptCache: true, // Claude Code does report cache tokens + supportsReasoningEffort: false, + supportsReasoningBudget: false, + requiredReasoningBudget: false, + }, } as const satisfies Record diff --git a/packages/types/src/sharing.ts b/packages/types/src/sharing.ts new file mode 100644 index 0000000000..f295798032 --- /dev/null +++ b/packages/types/src/sharing.ts @@ -0,0 +1,8 @@ +/** + * Types related to task sharing functionality + */ + +/** + * Visibility options for sharing tasks + */ +export type ShareVisibility = "organization" | "public" diff --git a/packages/types/src/telemetry.ts b/packages/types/src/telemetry.ts index 7ac38cdd86..0f9ddfa122 100644 --- a/packages/types/src/telemetry.ts +++ b/packages/types/src/telemetry.ts @@ -31,6 +31,10 @@ export enum TelemetryEventName { CHECKPOINT_RESTORED = "Checkpoint Restored", CHECKPOINT_DIFFED = "Checkpoint Diffed", + TAB_SHOWN = "Tab Shown", + MODE_SETTINGS_CHANGED = "Mode Setting Changed", + CUSTOM_MODE_CREATED = "Custom Mode Created", + CONTEXT_CONDENSED = "Context Condensed", SLIDING_WINDOW_TRUNCATION = "Sliding Window Truncation", @@ -46,6 +50,16 @@ export enum TelemetryEventName { MARKETPLACE_TAB_VIEWED = "Marketplace Tab Viewed", MARKETPLACE_INSTALL_BUTTON_CLICKED = "Marketplace Install Button Clicked", + SHARE_BUTTON_CLICKED = "Share Button Clicked", + SHARE_ORGANIZATION_CLICKED = "Share Organization Clicked", + SHARE_PUBLIC_CLICKED = "Share Public Clicked", + SHARE_CONNECT_TO_CLOUD_CLICKED = "Share Connect To Cloud Clicked", + + ACCOUNT_CONNECT_CLICKED = "Account Connect Clicked", + ACCOUNT_CONNECT_SUCCESS = "Account Connect Success", + ACCOUNT_LOGOUT_CLICKED = "Account Logout Clicked", + ACCOUNT_LOGOUT_SUCCESS = "Account Logout Success", + SCHEMA_VALIDATION_ERROR = "Schema Validation Error", DIFF_APPLICATION_ERROR = "Diff Application Error", SHELL_INTEGRATION_ERROR = "Shell Integration Error", @@ -64,6 +78,7 @@ export const appPropertiesSchema = z.object({ editorName: z.string(), language: z.string(), mode: z.string(), + cloudIsAuthenticated: z.boolean().optional(), }) export const taskPropertiesSchema = z.object({ @@ -74,12 +89,20 @@ export const taskPropertiesSchema = z.object({ isSubtask: z.boolean().optional(), }) +export const gitPropertiesSchema = z.object({ + repositoryUrl: z.string().optional(), + repositoryName: z.string().optional(), + defaultBranch: z.string().optional(), +}) + export const telemetryPropertiesSchema = z.object({ ...appPropertiesSchema.shape, ...taskPropertiesSchema.shape, + ...gitPropertiesSchema.shape, }) export type TelemetryProperties = z.infer +export type GitProperties = z.infer /** * TelemetryEvent @@ -113,12 +136,25 @@ export const rooCodeTelemetryEventSchema = z.discriminatedUnion("type", [ TelemetryEventName.AUTHENTICATION_INITIATED, TelemetryEventName.MARKETPLACE_ITEM_INSTALLED, TelemetryEventName.MARKETPLACE_ITEM_REMOVED, + TelemetryEventName.MARKETPLACE_TAB_VIEWED, + TelemetryEventName.MARKETPLACE_INSTALL_BUTTON_CLICKED, + TelemetryEventName.SHARE_BUTTON_CLICKED, + TelemetryEventName.SHARE_ORGANIZATION_CLICKED, + TelemetryEventName.SHARE_PUBLIC_CLICKED, + TelemetryEventName.SHARE_CONNECT_TO_CLOUD_CLICKED, + TelemetryEventName.ACCOUNT_CONNECT_CLICKED, + TelemetryEventName.ACCOUNT_CONNECT_SUCCESS, + TelemetryEventName.ACCOUNT_LOGOUT_CLICKED, + TelemetryEventName.ACCOUNT_LOGOUT_SUCCESS, TelemetryEventName.SCHEMA_VALIDATION_ERROR, TelemetryEventName.DIFF_APPLICATION_ERROR, TelemetryEventName.SHELL_INTEGRATION_ERROR, TelemetryEventName.CONSECUTIVE_MISTAKE_ERROR, TelemetryEventName.CONTEXT_CONDENSED, TelemetryEventName.SLIDING_WINDOW_TRUNCATION, + TelemetryEventName.TAB_SHOWN, + TelemetryEventName.MODE_SETTINGS_CHANGED, + TelemetryEventName.CUSTOM_MODE_CREATED, ]), properties: telemetryPropertiesSchema, }), diff --git a/packages/types/src/vscode.ts b/packages/types/src/vscode.ts index e6640e9bb6..00f6bbbcba 100644 --- a/packages/types/src/vscode.ts +++ b/packages/types/src/vscode.ts @@ -48,6 +48,7 @@ export const commandIds = [ "newTask", "setCustomStoragePath", + "importSettings", "focusInput", "acceptInput", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ab6e2821f..e6406db240 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -359,9 +359,6 @@ importers: '@roo-code/types': specifier: workspace:^ version: link:../types - axios: - specifier: ^1.7.4 - version: 1.9.0 zod: specifier: ^3.25.61 version: 3.25.61 @@ -702,6 +699,9 @@ importers: pretty-bytes: specifier: ^7.0.0 version: 7.0.0 + proper-lockfile: + specifier: ^4.1.2 + version: 4.1.2 ps-tree: specifier: ^1.2.0 version: 1.2.0 @@ -729,6 +729,9 @@ importers: sound-play: specifier: ^1.1.0 version: 1.1.0 + stream-json: + specifier: ^1.8.0 + version: 1.9.1 string-similarity: specifier: ^4.0.4 version: 4.0.4 @@ -805,9 +808,15 @@ importers: '@types/node-ipc': specifier: ^9.2.3 version: 9.2.3 + '@types/proper-lockfile': + specifier: ^4.1.4 + version: 4.1.4 '@types/ps-tree': specifier: ^1.1.6 version: 1.1.6 + '@types/stream-json': + specifier: ^1.7.8 + version: 1.7.8 '@types/string-similarity': specifier: ^4.0.2 version: 4.0.2 @@ -3853,6 +3862,9 @@ packages: '@types/prop-types@15.7.14': resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} + '@types/proper-lockfile@4.1.4': + resolution: {integrity: sha512-uo2ABllncSqg9F1D4nugVl9v93RmjxF6LJzQLMLDdPaXCUIDPeOJ21Gbqi43xNKzBi/WQ0Q0dICqufzQbMjipQ==} + '@types/ps-tree@1.1.6': resolution: {integrity: sha512-PtrlVaOaI44/3pl3cvnlK+GxOM3re2526TJvPvh7W+keHIXdV4TE0ylpPBAcvFQCbGitaTXwL9u+RF7qtVeazQ==} @@ -3864,12 +3876,21 @@ packages: '@types/react@18.3.23': resolution: {integrity: sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==} + '@types/retry@0.12.5': + resolution: {integrity: sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==} + '@types/shell-quote@1.7.5': resolution: {integrity: sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==} '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/stream-chain@2.1.0': + resolution: {integrity: sha512-guDyAl6s/CAzXUOWpGK2bHvdiopLIwpGu8v10+lb9hnQOyo4oj/ZUQFOvqFjKGsE3wJP1fpIesCcMvbXuWsqOg==} + + '@types/stream-json@1.7.8': + resolution: {integrity: sha512-MU1OB1eFLcYWd1LjwKXrxdoPtXSRzRmAnnxs4Js/ayB5O/NvHraWwuOaqMWIebpYwM6khFlsJOHEhI9xK/ab4Q==} + '@types/string-similarity@4.0.2': resolution: {integrity: sha512-LkJQ/jsXtCVMK+sKYAmX/8zEq+/46f1PTQw7YtmQwb74jemS1SlNLmARM2Zml9DgdDTWKAtc5L13WorpHPDjDA==} @@ -7946,6 +7967,9 @@ packages: resolution: {integrity: sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==} engines: {node: '>= 8'} + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + property-information@5.6.0: resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} @@ -8281,6 +8305,10 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -8612,9 +8640,15 @@ packages: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} + stream-chain@2.2.5: + resolution: {integrity: sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==} + stream-combiner@0.0.4: resolution: {integrity: sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==} + stream-json@1.9.1: + resolution: {integrity: sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -13025,7 +13059,6 @@ snapshots: '@types/node@20.19.1': dependencies: undici-types: 6.21.0 - optional: true '@types/node@22.15.29': dependencies: @@ -13033,6 +13066,10 @@ snapshots: '@types/prop-types@15.7.14': {} + '@types/proper-lockfile@4.1.4': + dependencies: + '@types/retry': 0.12.5 + '@types/ps-tree@1.1.6': {} '@types/react-dom@18.3.7(@types/react@18.3.23)': @@ -13044,10 +13081,21 @@ snapshots: '@types/prop-types': 15.7.14 csstype: 3.1.3 + '@types/retry@0.12.5': {} + '@types/shell-quote@1.7.5': {} '@types/stack-utils@2.0.3': {} + '@types/stream-chain@2.1.0': + dependencies: + '@types/node': 20.19.1 + + '@types/stream-json@1.7.8': + dependencies: + '@types/node': 20.19.1 + '@types/stream-chain': 2.1.0 + '@types/string-similarity@4.0.2': {} '@types/stylis@4.2.5': {} @@ -17760,6 +17808,12 @@ snapshots: propagate@2.0.1: {} + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + property-information@5.6.0: dependencies: xtend: 4.0.2 @@ -18234,6 +18288,8 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + retry@0.12.0: {} + reusify@1.1.0: {} rfdc@1.4.1: {} @@ -18643,10 +18699,16 @@ snapshots: stdin-discarder@0.2.2: {} + stream-chain@2.2.5: {} + stream-combiner@0.0.4: dependencies: duplexer: 0.1.2 + stream-json@1.9.1: + dependencies: + stream-chain: 2.2.5 + streamsearch@1.1.0: {} streamx@2.22.0: diff --git a/src/activate/CodeActionProvider.ts b/src/activate/CodeActionProvider.ts index 2646552452..4a0eb1b81e 100644 --- a/src/activate/CodeActionProvider.ts +++ b/src/activate/CodeActionProvider.ts @@ -1,6 +1,7 @@ import * as vscode from "vscode" import { CodeActionName, CodeActionId } from "@roo-code/types" +import { Package } from "../shared/package" import { getCodeActionCommand } from "../utils/commands" import { EditorUtils } from "../integrations/editor/EditorUtils" @@ -36,6 +37,10 @@ export class CodeActionProvider implements vscode.CodeActionProvider { context: vscode.CodeActionContext, ): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> { try { + if (!vscode.workspace.getConfiguration(Package.name).get("enableCodeActions", true)) { + return [] + } + const effectiveRange = EditorUtils.getEffectiveRange(document, range) if (!effectiveRange) { diff --git a/src/activate/__tests__/CodeActionProvider.spec.ts b/src/activate/__tests__/CodeActionProvider.spec.ts index 671dd0927f..8a99f748c1 100644 --- a/src/activate/__tests__/CodeActionProvider.spec.ts +++ b/src/activate/__tests__/CodeActionProvider.spec.ts @@ -25,6 +25,11 @@ vi.mock("vscode", () => ({ Information: 2, Hint: 3, }, + workspace: { + getConfiguration: vi.fn().mockReturnValue({ + get: vi.fn().mockReturnValue(true), + }), + }, })) vi.mock("../../integrations/editor/EditorUtils", () => ({ @@ -94,9 +99,30 @@ describe("CodeActionProvider", () => { expect(actions).toEqual([]) }) + it("should return empty array when enableCodeActions is disabled", () => { + // Mock the configuration to return false for enableCodeActions + const mockGet = vi.fn().mockReturnValue(false) + const mockGetConfiguration = vi.fn().mockReturnValue({ + get: mockGet, + }) + ;(vscode.workspace.getConfiguration as Mock).mockReturnValue(mockGetConfiguration()) + + const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext) + + expect(actions).toEqual([]) + expect(vscode.workspace.getConfiguration).toHaveBeenCalledWith("roo-cline") + expect(mockGet).toHaveBeenCalledWith("enableCodeActions", true) + }) + it("should handle errors gracefully", () => { const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + // Reset the workspace mock to return true for enableCodeActions + const mockGet = vi.fn().mockReturnValue(true) + const mockGetConfiguration = vi.fn().mockReturnValue({ + get: mockGet, + }) + ;(vscode.workspace.getConfiguration as Mock).mockReturnValue(mockGetConfiguration()) ;(EditorUtils.getEffectiveRange as Mock).mockImplementation(() => { throw new Error("Test error") }) diff --git a/src/activate/__tests__/registerCommands.spec.ts b/src/activate/__tests__/registerCommands.spec.ts index e1d23bfcb8..92c129fa03 100644 --- a/src/activate/__tests__/registerCommands.spec.ts +++ b/src/activate/__tests__/registerCommands.spec.ts @@ -16,6 +16,15 @@ vi.mock("vscode", () => ({ window: { createTextEditorDecorationType: vi.fn().mockReturnValue({ dispose: vi.fn() }), }, + workspace: { + workspaceFolders: [ + { + uri: { + fsPath: "/mock/workspace", + }, + }, + ], + }, })) vi.mock("../../core/webview/ClineProvider") diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts index fc30878c7b..8e84981d8a 100644 --- a/src/activate/registerCommands.ts +++ b/src/activate/registerCommands.ts @@ -13,6 +13,8 @@ import { focusPanel } from "../utils/focusPanel" import { registerHumanRelayCallback, unregisterHumanRelayCallback, handleHumanRelayResponse } from "./humanRelay" import { handleNewTask } from "./handleTask" import { CodeIndexManager } from "../services/code-index/manager" +import { importSettingsWithFeedback } from "../core/config/importExport" +import { t } from "../i18n" /** * Helper to get the visible ClineProvider instance or log if not found. @@ -171,6 +173,22 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt const { promptForCustomStoragePath } = await import("../utils/storage") await promptForCustomStoragePath() }, + importSettings: async (filePath?: string) => { + const visibleProvider = getVisibleProviderOrLog(outputChannel) + if (!visibleProvider) { + return + } + + await importSettingsWithFeedback( + { + providerSettingsManager: visibleProvider.providerSettingsManager, + contextProxy: visibleProvider.contextProxy, + customModesManager: visibleProvider.customModesManager, + provider: visibleProvider, + }, + filePath, + ) + }, focusInput: async () => { try { await focusPanel(tabPanel, sidebarPanel) diff --git a/src/api/providers/__tests__/bedrock-error-handling.spec.ts b/src/api/providers/__tests__/bedrock-error-handling.spec.ts new file mode 100644 index 0000000000..53e582c25b --- /dev/null +++ b/src/api/providers/__tests__/bedrock-error-handling.spec.ts @@ -0,0 +1,551 @@ +import { vi } from "vitest" + +// Mock BedrockRuntimeClient and commands +const mockSend = vi.fn() + +// Mock AWS SDK credential providers +vi.mock("@aws-sdk/credential-providers", () => { + return { + fromIni: vi.fn().mockReturnValue({ + accessKeyId: "profile-access-key", + secretAccessKey: "profile-secret-key", + }), + } +}) + +vi.mock("@aws-sdk/client-bedrock-runtime", () => ({ + BedrockRuntimeClient: vi.fn().mockImplementation(() => ({ + send: mockSend, + })), + ConverseStreamCommand: vi.fn(), + ConverseCommand: vi.fn(), +})) + +import { AwsBedrockHandler } from "../bedrock" +import { Anthropic } from "@anthropic-ai/sdk" + +describe("AwsBedrockHandler Error Handling", () => { + let handler: AwsBedrockHandler + + beforeEach(() => { + vi.clearAllMocks() + handler = new AwsBedrockHandler({ + apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", + awsAccessKey: "test-access-key", + awsSecretKey: "test-secret-key", + awsRegion: "us-east-1", + }) + }) + + const createMockError = (options: { + message?: string + name?: string + status?: number + __type?: string + $metadata?: { + httpStatusCode?: number + requestId?: string + extendedRequestId?: string + cfId?: string + [key: string]: any // Allow additional properties + } + }): Error => { + const error = new Error(options.message || "Test error") as any + if (options.name) error.name = options.name + if (options.status) error.status = options.status + if (options.__type) error.__type = options.__type + if (options.$metadata) error.$metadata = options.$metadata + return error + } + + describe("Throttling Error Detection", () => { + it("should detect throttling from HTTP 429 status code", async () => { + const throttleError = createMockError({ + message: "Request failed", + status: 429, + }) + + mockSend.mockRejectedValueOnce(throttleError) + + try { + const result = await handler.completePrompt("test") + expect(result).toContain("throttled or rate limited") + } catch (error) { + expect(error.message).toContain("throttled or rate limited") + } + }) + + it("should detect throttling from AWS SDK $metadata.httpStatusCode", async () => { + const throttleError = createMockError({ + message: "Request failed", + $metadata: { httpStatusCode: 429 }, + }) + + mockSend.mockRejectedValueOnce(throttleError) + + try { + const result = await handler.completePrompt("test") + expect(result).toContain("throttled or rate limited") + } catch (error) { + expect(error.message).toContain("throttled or rate limited") + } + }) + + it("should detect throttling from ThrottlingException name", async () => { + const throttleError = createMockError({ + message: "Request failed", + name: "ThrottlingException", + }) + + mockSend.mockRejectedValueOnce(throttleError) + + try { + const result = await handler.completePrompt("test") + expect(result).toContain("throttled or rate limited") + } catch (error) { + expect(error.message).toContain("throttled or rate limited") + } + }) + + it("should detect throttling from __type field", async () => { + const throttleError = createMockError({ + message: "Request failed", + __type: "ThrottlingException", + }) + + mockSend.mockRejectedValueOnce(throttleError) + + try { + const result = await handler.completePrompt("test") + expect(result).toContain("throttled or rate limited") + } catch (error) { + expect(error.message).toContain("throttled or rate limited") + } + }) + + it("should detect throttling from 'Bedrock is unable to process your request' message", async () => { + const throttleError = createMockError({ + message: "Bedrock is unable to process your request", + }) + + mockSend.mockRejectedValueOnce(throttleError) + + try { + const result = await handler.completePrompt("test") + expect(result).toContain("throttled or rate limited") + } catch (error) { + expect(error.message).toMatch(/throttled or rate limited/) + } + }) + + it("should detect throttling from various message patterns", async () => { + const throttlingMessages = [ + "Request throttled", + "Rate limit exceeded", + "Too many requests", + "Service unavailable due to high demand", + "Server is overloaded", + "System is busy", + "Please wait and try again", + ] + + for (const message of throttlingMessages) { + const throttleError = createMockError({ message }) + mockSend.mockRejectedValueOnce(throttleError) + + try { + await handler.completePrompt("test") + // Should not reach here as completePrompt should throw + throw new Error("Expected error to be thrown") + } catch (error) { + expect(error.message).toContain("throttled or rate limited") + } + } + }) + + it("should display appropriate error information for throttling errors", async () => { + const throttlingError = createMockError({ + message: "Bedrock is unable to process your request", + name: "ThrottlingException", + status: 429, + $metadata: { + httpStatusCode: 429, + requestId: "12345-abcde-67890", + extendedRequestId: "extended-12345", + cfId: "cf-12345", + }, + }) + + mockSend.mockRejectedValueOnce(throttlingError) + + try { + await handler.completePrompt("test") + throw new Error("Expected error to be thrown") + } catch (error) { + // Should contain the main error message + expect(error.message).toContain("throttled or rate limited") + } + }) + }) + + describe("Service Quota Exceeded Detection", () => { + it("should detect service quota exceeded errors", async () => { + const quotaError = createMockError({ + message: "Service quota exceeded for model requests", + }) + + mockSend.mockRejectedValueOnce(quotaError) + + try { + const result = await handler.completePrompt("test") + expect(result).toContain("Service quota exceeded") + } catch (error) { + expect(error.message).toContain("Service quota exceeded") + } + }) + }) + + describe("Model Not Ready Detection", () => { + it("should detect model not ready errors", async () => { + const modelError = createMockError({ + message: "Model is not ready, please try again later", + }) + + mockSend.mockRejectedValueOnce(modelError) + + try { + const result = await handler.completePrompt("test") + expect(result).toContain("Model is not ready") + } catch (error) { + expect(error.message).toContain("Model is not ready") + } + }) + }) + + describe("Internal Server Error Detection", () => { + it("should detect internal server errors", async () => { + const serverError = createMockError({ + message: "Internal server error occurred", + }) + + mockSend.mockRejectedValueOnce(serverError) + + try { + const result = await handler.completePrompt("test") + expect(result).toContain("internal server error") + } catch (error) { + expect(error.message).toContain("internal server error") + } + }) + }) + + describe("Token Limit Detection", () => { + it("should detect enhanced token limit errors", async () => { + const tokenErrors = [ + "Too many tokens in request", + "Token limit exceeded", + "Maximum context length reached", + "Context length exceeds limit", + ] + + for (const message of tokenErrors) { + const tokenError = createMockError({ message }) + mockSend.mockRejectedValueOnce(tokenError) + + try { + await handler.completePrompt("test") + // Should not reach here as completePrompt should throw + throw new Error("Expected error to be thrown") + } catch (error) { + // Either "Too many tokens" for token-specific errors or "throttled" for limit-related errors + expect(error.message).toMatch(/Too many tokens|throttled or rate limited/) + } + } + }) + }) + + describe("Streaming Context Error Handling", () => { + it("should handle throttling errors in streaming context", async () => { + const throttleError = createMockError({ + message: "Bedrock is unable to process your request", + status: 429, + }) + + const mockStream = { + [Symbol.asyncIterator]() { + return { + async next() { + throw throttleError + }, + } + }, + } + + mockSend.mockResolvedValueOnce({ stream: mockStream }) + + const generator = handler.createMessage("system", [{ role: "user", content: "test" }]) + + // For throttling errors, it should throw immediately without yielding chunks + // This allows the retry mechanism to catch and handle it + await expect(async () => { + for await (const chunk of generator) { + // Should not yield any chunks for throttling errors + } + }).rejects.toThrow("Bedrock is unable to process your request") + }) + + it("should yield error chunks for non-throttling errors in streaming context", async () => { + const genericError = createMockError({ + message: "Some other error", + status: 500, + }) + + const mockStream = { + [Symbol.asyncIterator]() { + return { + async next() { + throw genericError + }, + } + }, + } + + mockSend.mockResolvedValueOnce({ stream: mockStream }) + + const generator = handler.createMessage("system", [{ role: "user", content: "test" }]) + + const chunks: any[] = [] + try { + for await (const chunk of generator) { + chunks.push(chunk) + } + } catch (error) { + // Expected to throw after yielding chunks + } + + // Should have yielded error chunks before throwing for non-throttling errors + expect( + chunks.some((chunk) => chunk.type === "text" && chunk.text && chunk.text.includes("Some other error")), + ).toBe(true) + }) + }) + + describe("Error Priority and Specificity", () => { + it("should prioritize HTTP status codes over message patterns", async () => { + // Error with both 429 status and generic message should be detected as throttling + const mixedError = createMockError({ + message: "Some generic error message", + status: 429, + }) + + mockSend.mockRejectedValueOnce(mixedError) + + try { + const result = await handler.completePrompt("test") + expect(result).toContain("throttled or rate limited") + } catch (error) { + expect(error.message).toContain("throttled or rate limited") + } + }) + + it("should prioritize AWS error types over message patterns", async () => { + // Error with ThrottlingException name but different message should still be throttling + const specificError = createMockError({ + message: "Some other error occurred", + name: "ThrottlingException", + }) + + mockSend.mockRejectedValueOnce(specificError) + + try { + const result = await handler.completePrompt("test") + expect(result).toContain("throttled or rate limited") + } catch (error) { + expect(error.message).toContain("throttled or rate limited") + } + }) + }) + + describe("Unknown Error Fallback", () => { + it("should still show unknown error for truly unrecognized errors", async () => { + const unknownError = createMockError({ + message: "Something completely unexpected happened", + }) + + mockSend.mockRejectedValueOnce(unknownError) + + try { + const result = await handler.completePrompt("test") + expect(result).toContain("Unknown Error") + } catch (error) { + expect(error.message).toContain("Unknown Error") + } + }) + }) + + describe("Enhanced Error Throw for Retry System", () => { + it("should throw enhanced error messages for completePrompt to display in retry system", async () => { + const throttlingError = createMockError({ + message: "Too many tokens, rate limited", + status: 429, + $metadata: { + httpStatusCode: 429, + requestId: "test-request-id-12345", + }, + }) + mockSend.mockRejectedValueOnce(throttlingError) + + try { + await handler.completePrompt("test") + throw new Error("Expected error to be thrown") + } catch (error) { + // Should contain the verbose message template + expect(error.message).toContain("Request was throttled or rate limited") + // Should preserve original error properties + expect((error as any).status).toBe(429) + expect((error as any).$metadata.requestId).toBe("test-request-id-12345") + } + }) + + it("should throw enhanced error messages for createMessage streaming to display in retry system", async () => { + const tokenError = createMockError({ + message: "Too many tokens in request", + name: "ValidationException", + $metadata: { + httpStatusCode: 400, + requestId: "token-error-id-67890", + extendedRequestId: "extended-12345", + }, + }) + + const mockStream = { + [Symbol.asyncIterator]() { + return { + async next() { + throw tokenError + }, + } + }, + } + + mockSend.mockResolvedValueOnce({ stream: mockStream }) + + try { + const stream = handler.createMessage("system", [{ role: "user", content: "test" }]) + for await (const chunk of stream) { + // Should not reach here as it should throw an error + } + throw new Error("Expected error to be thrown") + } catch (error) { + // Should contain error codes (note: this will be caught by the non-throttling error path) + expect(error.message).toContain("Too many tokens") + // Should preserve original error properties + expect(error.name).toBe("ValidationException") + expect((error as any).$metadata.requestId).toBe("token-error-id-67890") + } + }) + }) + + describe("Edge Case Test Coverage", () => { + it("should handle concurrent throttling errors correctly", async () => { + const throttlingError = createMockError({ + message: "Bedrock is unable to process your request", + status: 429, + }) + + // Setup multiple concurrent requests that will all fail with throttling + mockSend.mockRejectedValue(throttlingError) + + // Execute multiple concurrent requests + const promises = Array.from({ length: 5 }, () => handler.completePrompt("test")) + + // All should throw with throttling error + const results = await Promise.allSettled(promises) + + results.forEach((result) => { + expect(result.status).toBe("rejected") + if (result.status === "rejected") { + expect(result.reason.message).toContain("throttled or rate limited") + } + }) + }) + + it("should handle mixed error scenarios with both throttling and other indicators", async () => { + // Error with both 429 status (throttling) and validation error message + const mixedError = createMockError({ + message: "ValidationException: Your input is invalid, but also rate limited", + name: "ValidationException", + status: 429, + $metadata: { + httpStatusCode: 429, + requestId: "mixed-error-id", + }, + }) + + mockSend.mockRejectedValueOnce(mixedError) + + try { + await handler.completePrompt("test") + } catch (error) { + // Should be treated as throttling due to 429 status taking priority + expect(error.message).toContain("throttled or rate limited") + // Should still preserve metadata + expect((error as any).$metadata?.requestId).toBe("mixed-error-id") + } + }) + + it("should handle rapid successive retries in streaming context", async () => { + const throttlingError = createMockError({ + message: "ThrottlingException", + name: "ThrottlingException", + }) + + // Mock stream that throws immediately + const mockStream = { + // eslint-disable-next-line require-yield + [Symbol.asyncIterator]: async function* () { + throw throttlingError + }, + } + + mockSend.mockResolvedValueOnce({ stream: mockStream }) + + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "test" }] + + try { + // Should throw immediately without yielding any chunks + const stream = handler.createMessage("", messages) + const chunks = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + // Should not reach here + expect(chunks).toHaveLength(0) + } catch (error) { + // Error should be thrown immediately for retry mechanism + // The error might be a TypeError if the stream iterator fails + expect(error).toBeDefined() + // The important thing is that it throws immediately without yielding chunks + } + }) + + it("should validate error properties exist before accessing them", async () => { + // Error with unusual structure + const unusualError = { + message: "Error with unusual structure", + // Missing typical properties like name, status, etc. + } + + mockSend.mockRejectedValueOnce(unusualError) + + try { + await handler.completePrompt("test") + } catch (error) { + // Should handle gracefully without accessing undefined properties + expect(error.message).toContain("Unknown Error") + // Should not have undefined values in the error message + expect(error.message).not.toContain("undefined") + } + }) + }) +}) diff --git a/src/api/providers/__tests__/claude-code-caching.spec.ts b/src/api/providers/__tests__/claude-code-caching.spec.ts new file mode 100644 index 0000000000..b7f7ff852a --- /dev/null +++ b/src/api/providers/__tests__/claude-code-caching.spec.ts @@ -0,0 +1,305 @@ +import { describe, it, expect, vi, beforeEach } from "vitest" +import { ClaudeCodeHandler } from "../claude-code" +import { runClaudeCode } from "../../../integrations/claude-code/run" +import type { ApiHandlerOptions } from "../../../shared/api" +import type { ClaudeCodeMessage } from "../../../integrations/claude-code/types" +import type { ApiStreamUsageChunk } from "../../transform/stream" +import type { Anthropic } from "@anthropic-ai/sdk" + +// Mock the runClaudeCode function +vi.mock("../../../integrations/claude-code/run", () => ({ + runClaudeCode: vi.fn(), +})) + +describe("ClaudeCodeHandler - Caching Support", () => { + let handler: ClaudeCodeHandler + const mockOptions: ApiHandlerOptions = { + apiKey: "test-key", + apiModelId: "claude-3-5-sonnet-20241022", + claudeCodePath: "/test/path", + } + + beforeEach(() => { + handler = new ClaudeCodeHandler(mockOptions) + vi.clearAllMocks() + }) + + it("should collect cache read tokens from API response", async () => { + const mockStream = async function* (): AsyncGenerator { + // Initial system message + yield { + type: "system", + subtype: "init", + session_id: "test-session", + tools: [], + mcp_servers: [], + apiKeySource: "user", + } as ClaudeCodeMessage + + // Assistant message with cache tokens + const message: Anthropic.Messages.Message = { + id: "msg_123", + type: "message", + role: "assistant", + model: "claude-3-5-sonnet-20241022", + content: [{ type: "text", text: "Hello!", citations: [] }], + usage: { + input_tokens: 100, + output_tokens: 50, + cache_read_input_tokens: 80, // 80 tokens read from cache + cache_creation_input_tokens: 20, // 20 new tokens cached + }, + stop_reason: "end_turn", + stop_sequence: null, + } + + yield { + type: "assistant", + message, + session_id: "test-session", + } as ClaudeCodeMessage + + // Result with cost + yield { + type: "result", + subtype: "success", + result: "success", + total_cost_usd: 0.001, + is_error: false, + duration_ms: 1000, + duration_api_ms: 900, + num_turns: 1, + session_id: "test-session", + } as ClaudeCodeMessage + } + + vi.mocked(runClaudeCode).mockReturnValue(mockStream()) + + const stream = handler.createMessage("System prompt", [{ role: "user", content: "Hello" }]) + + const chunks = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + // Find the usage chunk + const usageChunk = chunks.find((c) => c.type === "usage" && "totalCost" in c) as ApiStreamUsageChunk | undefined + expect(usageChunk).toBeDefined() + expect(usageChunk!.inputTokens).toBe(100) + expect(usageChunk!.outputTokens).toBe(50) + expect(usageChunk!.cacheReadTokens).toBe(80) + expect(usageChunk!.cacheWriteTokens).toBe(20) + }) + + it("should accumulate cache tokens across multiple messages", async () => { + const mockStream = async function* (): AsyncGenerator { + yield { + type: "system", + subtype: "init", + session_id: "test-session", + tools: [], + mcp_servers: [], + apiKeySource: "user", + } as ClaudeCodeMessage + + // First message chunk + const message1: Anthropic.Messages.Message = { + id: "msg_1", + type: "message", + role: "assistant", + model: "claude-3-5-sonnet-20241022", + content: [{ type: "text", text: "Part 1", citations: [] }], + usage: { + input_tokens: 50, + output_tokens: 25, + cache_read_input_tokens: 40, + cache_creation_input_tokens: 10, + }, + stop_reason: null, + stop_sequence: null, + } + + yield { + type: "assistant", + message: message1, + session_id: "test-session", + } as ClaudeCodeMessage + + // Second message chunk + const message2: Anthropic.Messages.Message = { + id: "msg_2", + type: "message", + role: "assistant", + model: "claude-3-5-sonnet-20241022", + content: [{ type: "text", text: "Part 2", citations: [] }], + usage: { + input_tokens: 50, + output_tokens: 25, + cache_read_input_tokens: 30, + cache_creation_input_tokens: 20, + }, + stop_reason: "end_turn", + stop_sequence: null, + } + + yield { + type: "assistant", + message: message2, + session_id: "test-session", + } as ClaudeCodeMessage + + yield { + type: "result", + subtype: "success", + result: "success", + total_cost_usd: 0.002, + is_error: false, + duration_ms: 2000, + duration_api_ms: 1800, + num_turns: 1, + session_id: "test-session", + } as ClaudeCodeMessage + } + + vi.mocked(runClaudeCode).mockReturnValue(mockStream()) + + const stream = handler.createMessage("System prompt", [{ role: "user", content: "Hello" }]) + + const chunks = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + const usageChunk = chunks.find((c) => c.type === "usage" && "totalCost" in c) as ApiStreamUsageChunk | undefined + expect(usageChunk).toBeDefined() + expect(usageChunk!.inputTokens).toBe(100) // 50 + 50 + expect(usageChunk!.outputTokens).toBe(50) // 25 + 25 + expect(usageChunk!.cacheReadTokens).toBe(70) // 40 + 30 + expect(usageChunk!.cacheWriteTokens).toBe(30) // 10 + 20 + }) + + it("should handle missing cache token fields gracefully", async () => { + const mockStream = async function* (): AsyncGenerator { + yield { + type: "system", + subtype: "init", + session_id: "test-session", + tools: [], + mcp_servers: [], + apiKeySource: "user", + } as ClaudeCodeMessage + + // Message without cache tokens + const message: Anthropic.Messages.Message = { + id: "msg_123", + type: "message", + role: "assistant", + model: "claude-3-5-sonnet-20241022", + content: [{ type: "text", text: "Hello!", citations: [] }], + usage: { + input_tokens: 100, + output_tokens: 50, + cache_read_input_tokens: null, + cache_creation_input_tokens: null, + }, + stop_reason: "end_turn", + stop_sequence: null, + } + + yield { + type: "assistant", + message, + session_id: "test-session", + } as ClaudeCodeMessage + + yield { + type: "result", + subtype: "success", + result: "success", + total_cost_usd: 0.001, + is_error: false, + duration_ms: 1000, + duration_api_ms: 900, + num_turns: 1, + session_id: "test-session", + } as ClaudeCodeMessage + } + + vi.mocked(runClaudeCode).mockReturnValue(mockStream()) + + const stream = handler.createMessage("System prompt", [{ role: "user", content: "Hello" }]) + + const chunks = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + const usageChunk = chunks.find((c) => c.type === "usage" && "totalCost" in c) as ApiStreamUsageChunk | undefined + expect(usageChunk).toBeDefined() + expect(usageChunk!.inputTokens).toBe(100) + expect(usageChunk!.outputTokens).toBe(50) + expect(usageChunk!.cacheReadTokens).toBe(0) + expect(usageChunk!.cacheWriteTokens).toBe(0) + }) + + it("should report zero cost for subscription usage", async () => { + const mockStream = async function* (): AsyncGenerator { + // Subscription usage has apiKeySource: "none" + yield { + type: "system", + subtype: "init", + session_id: "test-session", + tools: [], + mcp_servers: [], + apiKeySource: "none", + } as ClaudeCodeMessage + + const message: Anthropic.Messages.Message = { + id: "msg_123", + type: "message", + role: "assistant", + model: "claude-3-5-sonnet-20241022", + content: [{ type: "text", text: "Hello!", citations: [] }], + usage: { + input_tokens: 100, + output_tokens: 50, + cache_read_input_tokens: 80, + cache_creation_input_tokens: 20, + }, + stop_reason: "end_turn", + stop_sequence: null, + } + + yield { + type: "assistant", + message, + session_id: "test-session", + } as ClaudeCodeMessage + + yield { + type: "result", + subtype: "success", + result: "success", + total_cost_usd: 0.001, // This should be ignored for subscription usage + is_error: false, + duration_ms: 1000, + duration_api_ms: 900, + num_turns: 1, + session_id: "test-session", + } as ClaudeCodeMessage + } + + vi.mocked(runClaudeCode).mockReturnValue(mockStream()) + + const stream = handler.createMessage("System prompt", [{ role: "user", content: "Hello" }]) + + const chunks = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + const usageChunk = chunks.find((c) => c.type === "usage" && "totalCost" in c) as ApiStreamUsageChunk | undefined + expect(usageChunk).toBeDefined() + expect(usageChunk!.totalCost).toBe(0) // Should be 0 for subscription usage + }) +}) diff --git a/src/api/providers/__tests__/claude-code.spec.ts b/src/api/providers/__tests__/claude-code.spec.ts index 406864f917..d0dfa68eb8 100644 --- a/src/api/providers/__tests__/claude-code.spec.ts +++ b/src/api/providers/__tests__/claude-code.spec.ts @@ -1,230 +1,503 @@ import { describe, test, expect, vi, beforeEach } from "vitest" import { ClaudeCodeHandler } from "../claude-code" import { ApiHandlerOptions } from "../../../shared/api" +import { ClaudeCodeMessage } from "../../../integrations/claude-code/types" // Mock the runClaudeCode function vi.mock("../../../integrations/claude-code/run", () => ({ runClaudeCode: vi.fn(), })) +// Mock the message filter +vi.mock("../../../integrations/claude-code/message-filter", () => ({ + filterMessagesForClaudeCode: vi.fn((messages) => messages), +})) + const { runClaudeCode } = await import("../../../integrations/claude-code/run") +const { filterMessagesForClaudeCode } = await import("../../../integrations/claude-code/message-filter") const mockRunClaudeCode = vi.mocked(runClaudeCode) - -// Mock the EventEmitter for the process -class MockEventEmitter { - private handlers: { [event: string]: ((...args: any[]) => void)[] } = {} - - on(event: string, handler: (...args: any[]) => void) { - if (!this.handlers[event]) { - this.handlers[event] = [] - } - this.handlers[event].push(handler) - } - - emit(event: string, ...args: any[]) { - if (this.handlers[event]) { - this.handlers[event].forEach((handler) => handler(...args)) - } - } -} +const mockFilterMessages = vi.mocked(filterMessagesForClaudeCode) describe("ClaudeCodeHandler", () => { let handler: ClaudeCodeHandler - let mockProcess: any beforeEach(() => { + vi.clearAllMocks() const options: ApiHandlerOptions = { claudeCodePath: "claude", apiModelId: "claude-3-5-sonnet-20241022", } handler = new ClaudeCodeHandler(options) + }) - const mainEmitter = new MockEventEmitter() - mockProcess = { - stdout: new MockEventEmitter(), - stderr: new MockEventEmitter(), - on: mainEmitter.on.bind(mainEmitter), - emit: mainEmitter.emit.bind(mainEmitter), + test("should create handler with correct model configuration", () => { + const model = handler.getModel() + expect(model.id).toBe("claude-3-5-sonnet-20241022") + expect(model.info.supportsImages).toBe(false) + expect(model.info.supportsPromptCache).toBe(true) // Claude Code now supports prompt caching + }) + + test("should use default model when invalid model provided", () => { + const options: ApiHandlerOptions = { + claudeCodePath: "claude", + apiModelId: "invalid-model", } + const handlerWithInvalidModel = new ClaudeCodeHandler(options) + const model = handlerWithInvalidModel.getModel() - mockRunClaudeCode.mockReturnValue(mockProcess) + expect(model.id).toBe("claude-sonnet-4-20250514") // default model + }) + + test("should filter messages and call runClaudeCode", async () => { + const systemPrompt = "You are a helpful assistant" + const messages = [{ role: "user" as const, content: "Hello" }] + const filteredMessages = [{ role: "user" as const, content: "Hello (filtered)" }] + + mockFilterMessages.mockReturnValue(filteredMessages) + + // Mock empty async generator + const mockGenerator = async function* (): AsyncGenerator { + // Empty generator for basic test + } + mockRunClaudeCode.mockReturnValue(mockGenerator()) + + const stream = handler.createMessage(systemPrompt, messages) + + // Need to start iterating to trigger the call + const iterator = stream[Symbol.asyncIterator]() + await iterator.next() + + // Verify message filtering was called + expect(mockFilterMessages).toHaveBeenCalledWith(messages) + + // Verify runClaudeCode was called with filtered messages + expect(mockRunClaudeCode).toHaveBeenCalledWith({ + systemPrompt, + messages: filteredMessages, + path: "claude", + modelId: "claude-3-5-sonnet-20241022", + }) }) test("should handle thinking content properly", async () => { const systemPrompt = "You are a helpful assistant" const messages = [{ role: "user" as const, content: "Hello" }] - // Start the stream - const stream = handler.createMessage(systemPrompt, messages) - const streamGenerator = stream[Symbol.asyncIterator]() - - // Simulate thinking content response - const thinkingResponse = { - type: "assistant", - message: { - id: "msg_123", - type: "message", - role: "assistant", - model: "claude-3-5-sonnet-20241022", - content: [ - { - type: "thinking", - thinking: "I need to think about this carefully...", - signature: "abc123", + // Mock async generator that yields thinking content + const mockGenerator = async function* (): AsyncGenerator { + yield { + type: "assistant" as const, + message: { + id: "msg_123", + type: "message", + role: "assistant", + model: "claude-3-5-sonnet-20241022", + content: [ + { + type: "thinking", + thinking: "I need to think about this carefully...", + }, + ], + stop_reason: null, + stop_sequence: null, + usage: { + input_tokens: 10, + output_tokens: 20, }, - ], - stop_reason: null, - stop_sequence: null, - usage: { - input_tokens: 10, - output_tokens: 20, - service_tier: "standard" as const, - }, - }, - session_id: "session_123", + } as any, + session_id: "session_123", + } } - // Emit the thinking response and wait for processing - setImmediate(() => { - mockProcess.stdout.emit("data", JSON.stringify(thinkingResponse) + "\n") - setImmediate(() => { - mockProcess.emit("close", 0) - }) - }) + mockRunClaudeCode.mockReturnValue(mockGenerator()) - // Get the result - const result = await streamGenerator.next() + const stream = handler.createMessage(systemPrompt, messages) + const results = [] - expect(result.done).toBe(false) - expect(result.value).toEqual({ + for await (const chunk of stream) { + results.push(chunk) + } + + expect(results).toHaveLength(1) + expect(results[0]).toEqual({ type: "reasoning", text: "I need to think about this carefully...", }) }) + test("should handle redacted thinking content", async () => { + const systemPrompt = "You are a helpful assistant" + const messages = [{ role: "user" as const, content: "Hello" }] + + // Mock async generator that yields redacted thinking content + const mockGenerator = async function* (): AsyncGenerator { + yield { + type: "assistant" as const, + message: { + id: "msg_123", + type: "message", + role: "assistant", + model: "claude-3-5-sonnet-20241022", + content: [ + { + type: "redacted_thinking", + }, + ], + stop_reason: null, + stop_sequence: null, + usage: { + input_tokens: 10, + output_tokens: 20, + }, + } as any, + session_id: "session_123", + } + } + + mockRunClaudeCode.mockReturnValue(mockGenerator()) + + const stream = handler.createMessage(systemPrompt, messages) + const results = [] + + for await (const chunk of stream) { + results.push(chunk) + } + + expect(results).toHaveLength(1) + expect(results[0]).toEqual({ + type: "reasoning", + text: "[Redacted thinking block]", + }) + }) + test("should handle mixed content types", async () => { const systemPrompt = "You are a helpful assistant" const messages = [{ role: "user" as const, content: "Hello" }] - const stream = handler.createMessage(systemPrompt, messages) - const streamGenerator = stream[Symbol.asyncIterator]() - - // Simulate mixed content response - const mixedResponse = { - type: "assistant", - message: { - id: "msg_123", - type: "message", - role: "assistant", - model: "claude-3-5-sonnet-20241022", - content: [ - { - type: "thinking", - thinking: "Let me think about this...", + // Mock async generator that yields mixed content + const mockGenerator = async function* (): AsyncGenerator { + yield { + type: "assistant" as const, + message: { + id: "msg_123", + type: "message", + role: "assistant", + model: "claude-3-5-sonnet-20241022", + content: [ + { + type: "thinking", + thinking: "Let me think about this...", + }, + { + type: "text", + text: "Here's my response!", + }, + ], + stop_reason: null, + stop_sequence: null, + usage: { + input_tokens: 10, + output_tokens: 20, }, - { - type: "text", - text: "Here's my response!", - }, - ], - stop_reason: null, - stop_sequence: null, - usage: { - input_tokens: 10, - output_tokens: 20, - service_tier: "standard" as const, - }, - }, - session_id: "session_123", + } as any, + session_id: "session_123", + } } - // Emit the mixed response and wait for processing - setImmediate(() => { - mockProcess.stdout.emit("data", JSON.stringify(mixedResponse) + "\n") - setImmediate(() => { - mockProcess.emit("close", 0) - }) - }) + mockRunClaudeCode.mockReturnValue(mockGenerator()) - // Get the first result (thinking) - const thinkingResult = await streamGenerator.next() - expect(thinkingResult.done).toBe(false) - expect(thinkingResult.value).toEqual({ + const stream = handler.createMessage(systemPrompt, messages) + const results = [] + + for await (const chunk of stream) { + results.push(chunk) + } + + expect(results).toHaveLength(2) + expect(results[0]).toEqual({ type: "reasoning", text: "Let me think about this...", }) - - // Get the second result (text) - const textResult = await streamGenerator.next() - expect(textResult.done).toBe(false) - expect(textResult.value).toEqual({ + expect(results[1]).toEqual({ type: "text", text: "Here's my response!", }) }) - test("should handle stop_reason with thinking content in error messages", async () => { + test("should handle string chunks from generator", async () => { const systemPrompt = "You are a helpful assistant" const messages = [{ role: "user" as const, content: "Hello" }] - const stream = handler.createMessage(systemPrompt, messages) - const streamGenerator = stream[Symbol.asyncIterator]() - - // Simulate error response with thinking content - const errorResponse = { - type: "assistant", - message: { - id: "msg_123", - type: "message", - role: "assistant", - model: "claude-3-5-sonnet-20241022", - content: [ - { - type: "thinking", - thinking: "This is an error scenario", - }, - ], - stop_reason: "max_tokens", - stop_sequence: null, - usage: { - input_tokens: 10, - output_tokens: 20, - service_tier: "standard" as const, - }, - }, - session_id: "session_123", + // Mock async generator that yields string chunks + const mockGenerator = async function* (): AsyncGenerator { + yield "This is a string chunk" + yield "Another string chunk" } - // Emit the error response and wait for processing - setImmediate(() => { - mockProcess.stdout.emit("data", JSON.stringify(errorResponse) + "\n") - setImmediate(() => { - mockProcess.emit("close", 0) - }) - }) + mockRunClaudeCode.mockReturnValue(mockGenerator()) - // Should throw error with thinking content - await expect(streamGenerator.next()).rejects.toThrow("This is an error scenario") + const stream = handler.createMessage(systemPrompt, messages) + const results = [] + + for await (const chunk of stream) { + results.push(chunk) + } + + expect(results).toHaveLength(2) + expect(results[0]).toEqual({ + type: "text", + text: "This is a string chunk", + }) + expect(results[1]).toEqual({ + type: "text", + text: "Another string chunk", + }) }) - test("should handle incomplete JSON in buffer on process close", async () => { + test("should handle usage and cost tracking with paid usage", async () => { const systemPrompt = "You are a helpful assistant" const messages = [{ role: "user" as const, content: "Hello" }] + // Mock async generator with init, assistant, and result messages + const mockGenerator = async function* (): AsyncGenerator { + // Init message indicating paid usage + yield { + type: "system" as const, + subtype: "init" as const, + session_id: "session_123", + tools: [], + mcp_servers: [], + apiKeySource: "/login managed key", + } + + // Assistant message + yield { + type: "assistant" as const, + message: { + id: "msg_123", + type: "message", + role: "assistant", + model: "claude-3-5-sonnet-20241022", + content: [ + { + type: "text", + text: "Hello there!", + }, + ], + stop_reason: null, + stop_sequence: null, + usage: { + input_tokens: 10, + output_tokens: 20, + cache_read_input_tokens: 5, + cache_creation_input_tokens: 3, + }, + } as any, + session_id: "session_123", + } + + // Result message + yield { + type: "result" as const, + subtype: "success" as const, + total_cost_usd: 0.05, + is_error: false, + duration_ms: 1000, + duration_api_ms: 800, + num_turns: 1, + result: "success", + session_id: "session_123", + } + } + + mockRunClaudeCode.mockReturnValue(mockGenerator()) + const stream = handler.createMessage(systemPrompt, messages) - const streamGenerator = stream[Symbol.asyncIterator]() + const results = [] - // Simulate incomplete JSON data followed by process close - setImmediate(() => { - // Send incomplete JSON (missing closing brace) - mockProcess.stdout.emit("data", '{"type":"assistant","message":{"id":"msg_123"') - setImmediate(() => { - mockProcess.emit("close", 0) - }) + for await (const chunk of stream) { + results.push(chunk) + } + + // Should have text chunk and usage chunk + expect(results).toHaveLength(2) + expect(results[0]).toEqual({ + type: "text", + text: "Hello there!", }) + expect(results[1]).toEqual({ + type: "usage", + inputTokens: 10, + outputTokens: 20, + cacheReadTokens: 5, + cacheWriteTokens: 3, + totalCost: 0.05, // Paid usage, so cost is included + }) + }) - // Should complete without throwing, incomplete JSON should be discarded - const result = await streamGenerator.next() - expect(result.done).toBe(true) + test("should handle usage tracking with subscription (free) usage", async () => { + const systemPrompt = "You are a helpful assistant" + const messages = [{ role: "user" as const, content: "Hello" }] + + // Mock async generator with subscription usage + const mockGenerator = async function* (): AsyncGenerator { + // Init message indicating subscription usage + yield { + type: "system" as const, + subtype: "init" as const, + session_id: "session_123", + tools: [], + mcp_servers: [], + apiKeySource: "none", // Subscription usage + } + + // Assistant message + yield { + type: "assistant" as const, + message: { + id: "msg_123", + type: "message", + role: "assistant", + model: "claude-3-5-sonnet-20241022", + content: [ + { + type: "text", + text: "Hello there!", + }, + ], + stop_reason: null, + stop_sequence: null, + usage: { + input_tokens: 10, + output_tokens: 20, + }, + } as any, + session_id: "session_123", + } + + // Result message + yield { + type: "result" as const, + subtype: "success" as const, + total_cost_usd: 0.05, + is_error: false, + duration_ms: 1000, + duration_api_ms: 800, + num_turns: 1, + result: "success", + session_id: "session_123", + } + } + + mockRunClaudeCode.mockReturnValue(mockGenerator()) + + const stream = handler.createMessage(systemPrompt, messages) + const results = [] + + for await (const chunk of stream) { + results.push(chunk) + } + + // Should have text chunk and usage chunk + expect(results).toHaveLength(2) + expect(results[0]).toEqual({ + type: "text", + text: "Hello there!", + }) + expect(results[1]).toEqual({ + type: "usage", + inputTokens: 10, + outputTokens: 20, + cacheReadTokens: 0, + cacheWriteTokens: 0, + totalCost: 0, // Subscription usage, so cost is 0 + }) + }) + + test("should handle API errors properly", async () => { + const systemPrompt = "You are a helpful assistant" + const messages = [{ role: "user" as const, content: "Hello" }] + + // Mock async generator that yields an API error + const mockGenerator = async function* (): AsyncGenerator { + yield { + type: "assistant" as const, + message: { + id: "msg_123", + type: "message", + role: "assistant", + model: "claude-3-5-sonnet-20241022", + content: [ + { + type: "text", + text: 'API Error: 400 {"error":{"message":"Invalid model name"}}', + }, + ], + stop_reason: "stop_sequence", + stop_sequence: null, + usage: { + input_tokens: 10, + output_tokens: 20, + }, + } as any, + session_id: "session_123", + } + } + + mockRunClaudeCode.mockReturnValue(mockGenerator()) + + const stream = handler.createMessage(systemPrompt, messages) + const iterator = stream[Symbol.asyncIterator]() + + // Should throw an error + await expect(iterator.next()).rejects.toThrow() + }) + + test("should log warning for unsupported tool_use content", async () => { + const systemPrompt = "You are a helpful assistant" + const messages = [{ role: "user" as const, content: "Hello" }] + const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + + // Mock async generator that yields tool_use content + const mockGenerator = async function* (): AsyncGenerator { + yield { + type: "assistant" as const, + message: { + id: "msg_123", + type: "message", + role: "assistant", + model: "claude-3-5-sonnet-20241022", + content: [ + { + type: "tool_use", + id: "tool_123", + name: "test_tool", + input: { test: "data" }, + }, + ], + stop_reason: null, + stop_sequence: null, + usage: { + input_tokens: 10, + output_tokens: 20, + }, + } as any, + session_id: "session_123", + } + } + + mockRunClaudeCode.mockReturnValue(mockGenerator()) + + const stream = handler.createMessage(systemPrompt, messages) + const results = [] + + for await (const chunk of stream) { + results.push(chunk) + } + + // Should log error for unsupported tool_use + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("tool_use is not supported yet")) + + consoleSpy.mockRestore() }) }) diff --git a/src/api/providers/__tests__/lmstudio.spec.ts b/src/api/providers/__tests__/lmstudio.spec.ts index 2679d225df..0adebdeea7 100644 --- a/src/api/providers/__tests__/lmstudio.spec.ts +++ b/src/api/providers/__tests__/lmstudio.spec.ts @@ -71,7 +71,7 @@ describe("LmStudioHandler", () => { mockOptions = { apiModelId: "local-model", lmStudioModelId: "local-model", - lmStudioBaseUrl: "http://localhost:1234/v1", + lmStudioBaseUrl: "http://localhost:1234", } handler = new LmStudioHandler(mockOptions) mockCreate.mockClear() diff --git a/src/api/providers/__tests__/openai.spec.ts b/src/api/providers/__tests__/openai.spec.ts index fc809819e8..86d57ab3f5 100644 --- a/src/api/providers/__tests__/openai.spec.ts +++ b/src/api/providers/__tests__/openai.spec.ts @@ -599,7 +599,7 @@ describe("OpenAiHandler", () => { stream: true, stream_options: { include_usage: true }, reasoning_effort: "medium", - temperature: 0.5, + temperature: undefined, // O3 models do not support deprecated max_tokens but do support max_completion_tokens max_completion_tokens: 32000, }), @@ -640,7 +640,7 @@ describe("OpenAiHandler", () => { stream: true, stream_options: { include_usage: true }, reasoning_effort: "medium", - temperature: 0.7, + temperature: undefined, }), {}, ) @@ -682,7 +682,7 @@ describe("OpenAiHandler", () => { { role: "user", content: "Hello!" }, ], reasoning_effort: "medium", - temperature: 0.3, + temperature: undefined, // O3 models do not support deprecated max_tokens but do support max_completion_tokens max_completion_tokens: 65536, // Using default maxTokens from o3Options }), @@ -712,7 +712,7 @@ describe("OpenAiHandler", () => { expect(mockCreate).toHaveBeenCalledWith( expect.objectContaining({ - temperature: 0, // Default temperature + temperature: undefined, // Temperature is not supported for O3 models }), {}, ) diff --git a/src/api/providers/bedrock.ts b/src/api/providers/bedrock.ts index b5474cce50..d8a370e08f 100644 --- a/src/api/providers/bedrock.ts +++ b/src/api/providers/bedrock.ts @@ -561,16 +561,47 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH // Clear timeout on error clearTimeout(timeoutId) - // Use the extracted error handling method for all errors + // Check if this is a throttling error that should trigger retry logic + const errorType = this.getErrorType(error) + + // For throttling errors, throw immediately without yielding chunks + // This allows the retry mechanism in attemptApiRequest() to catch and handle it + // The retry logic in Task.ts (around line 1817) expects errors to be thrown + // on the first chunk for proper exponential backoff behavior + if (errorType === "THROTTLING") { + if (error instanceof Error) { + throw error + } else { + throw new Error("Throttling error occurred") + } + } + + // For non-throttling errors, use the standard error handling with chunks const errorChunks = this.handleBedrockError(error, true) // true for streaming context // Yield each chunk individually to ensure type compatibility for (const chunk of errorChunks) { yield chunk as any // Cast to any to bypass type checking since we know the structure is correct } - // Re-throw the error + // Re-throw with enhanced error message for retry system + const enhancedErrorMessage = this.formatErrorMessage(error, this.getErrorType(error), true) if (error instanceof Error) { - throw error + const enhancedError = new Error(enhancedErrorMessage) + // Preserve important properties from the original error + enhancedError.name = error.name + // Validate and preserve status property + if ("status" in error && typeof (error as any).status === "number") { + ;(enhancedError as any).status = (error as any).status + } + // Validate and preserve $metadata property + if ( + "$metadata" in error && + typeof (error as any).$metadata === "object" && + (error as any).$metadata !== null + ) { + ;(enhancedError as any).$metadata = (error as any).$metadata + } + throw enhancedError } else { throw new Error("An unknown error occurred") } @@ -638,7 +669,26 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH const errorResult = this.handleBedrockError(error, false) // false for non-streaming context // Since we're in a non-streaming context, we know the result is a string const errorMessage = errorResult as string - throw new Error(errorMessage) + + // Create enhanced error for retry system + const enhancedError = new Error(errorMessage) + if (error instanceof Error) { + // Preserve important properties from the original error + enhancedError.name = error.name + // Validate and preserve status property + if ("status" in error && typeof (error as any).status === "number") { + ;(enhancedError as any).status = (error as any).status + } + // Validate and preserve $metadata property + if ( + "$metadata" in error && + typeof (error as any).$metadata === "object" && + (error as any).$metadata !== null + ) { + ;(enhancedError as any).$metadata = (error as any).$metadata + } + } + throw enhancedError } } @@ -1035,19 +1085,32 @@ Please verify: logLevel: "error", }, THROTTLING: { - patterns: ["throttl", "rate", "limit"], + patterns: [ + "throttl", + "rate", + "limit", + "bedrock is unable to process your request", // AWS Bedrock specific throttling message + "please wait", + "quota exceeded", + "service unavailable", + "busy", + "overloaded", + "too many requests", + "request limit", + "concurrent requests", + ], messageTemplate: `Request was throttled or rate limited. Please try: 1. Reducing the frequency of requests 2. If using a provisioned model, check its throughput settings 3. Contact AWS support to request a quota increase if needed -{formattedErrorDetails} + `, logLevel: "error", }, TOO_MANY_TOKENS: { - patterns: ["too many tokens"], + patterns: ["too many tokens", "token limit exceeded", "context length", "maximum context length"], messageTemplate: `"Too many tokens" error detected. Possible Causes: 1. Input exceeds model's context window limit @@ -1060,7 +1123,49 @@ Suggestions: 2. Split your request into smaller chunks 3. Use a model with a larger context window 4. If rate limited, reduce request frequency -5. Check your Amazon Bedrock quotas and limits`, +5. Check your Amazon Bedrock quotas and limits + +`, + logLevel: "error", + }, + SERVICE_QUOTA_EXCEEDED: { + patterns: ["service quota exceeded", "service quota", "quota exceeded for model"], + messageTemplate: `Service quota exceeded. This error indicates you've reached AWS service limits. + +Please try: +1. Contact AWS support to request a quota increase +2. Reduce request frequency temporarily +3. Check your AWS Bedrock quotas in the AWS console +4. Consider using a different model or region with available capacity + +`, + logLevel: "error", + }, + MODEL_NOT_READY: { + patterns: ["model not ready", "model is not ready", "provisioned throughput not ready", "model loading"], + messageTemplate: `Model is not ready or still loading. This can happen with: +1. Provisioned throughput models that are still initializing +2. Custom models that are being loaded +3. Models that are temporarily unavailable + +Please try: +1. Wait a few minutes and retry +2. Check the model status in AWS Bedrock console +3. Verify the model is properly provisioned + +`, + logLevel: "error", + }, + INTERNAL_SERVER_ERROR: { + patterns: ["internal server error", "internal error", "server error", "service error"], + messageTemplate: `AWS Bedrock internal server error. This is a temporary service issue. + +Please try: +1. Retry the request after a brief delay +2. If the error persists, check AWS service health +3. Contact AWS support if the issue continues + +`, logLevel: "error", }, ON_DEMAND_NOT_SUPPORTED: { @@ -1119,12 +1224,34 @@ Please check: return "GENERIC" } + // Check for HTTP 429 status code (Too Many Requests) + if ((error as any).status === 429 || (error as any).$metadata?.httpStatusCode === 429) { + return "THROTTLING" + } + + // Check for AWS Bedrock specific throttling exception names + if ((error as any).name === "ThrottlingException" || (error as any).__type === "ThrottlingException") { + return "THROTTLING" + } + const errorMessage = error.message.toLowerCase() const errorName = error.name.toLowerCase() - // Check each error type's patterns - for (const [errorType, definition] of Object.entries(AwsBedrockHandler.ERROR_TYPES)) { - if (errorType === "GENERIC") continue // Skip the generic type + // Check each error type's patterns in order of specificity (most specific first) + const errorTypeOrder = [ + "SERVICE_QUOTA_EXCEEDED", // Most specific - check before THROTTLING + "MODEL_NOT_READY", + "TOO_MANY_TOKENS", + "INTERNAL_SERVER_ERROR", + "ON_DEMAND_NOT_SUPPORTED", + "NOT_FOUND", + "ACCESS_DENIED", + "THROTTLING", // Less specific - check after more specific patterns + ] + + for (const errorType of errorTypeOrder) { + const definition = AwsBedrockHandler.ERROR_TYPES[errorType] + if (!definition) continue // If any pattern matches in either message or name, return this error type if (definition.patterns.some((pattern) => errorMessage.includes(pattern) || errorName.includes(pattern))) { @@ -1153,37 +1280,6 @@ Please check: const modelConfig = this.getModel() templateVars.modelId = modelConfig.id templateVars.contextWindow = String(modelConfig.info.contextWindow || "unknown") - - // Format error details - const errorDetails: Record = {} - Object.getOwnPropertyNames(error).forEach((prop) => { - if (prop !== "stack") { - errorDetails[prop] = (error as any)[prop] - } - }) - - // Safely stringify error details to avoid circular references - templateVars.formattedErrorDetails = Object.entries(errorDetails) - .map(([key, value]) => { - let valueStr - if (typeof value === "object" && value !== null) { - try { - // Use a replacer function to handle circular references - valueStr = JSON.stringify(value, (k, v) => { - if (k && typeof v === "object" && v !== null) { - return "[Object]" - } - return v - }) - } catch (e) { - valueStr = "[Complex Object]" - } - } else { - valueStr = String(value) - } - return `- ${key}: ${valueStr}` - }) - .join("\n") } // Add context-specific template variables diff --git a/src/api/providers/claude-code.ts b/src/api/providers/claude-code.ts index a2551732e5..bc72e658fe 100644 --- a/src/api/providers/claude-code.ts +++ b/src/api/providers/claude-code.ts @@ -3,7 +3,7 @@ import { claudeCodeDefaultModelId, type ClaudeCodeModelId, claudeCodeModels } fr import { type ApiHandler } from ".." import { ApiStreamUsageChunk, type ApiStream } from "../transform/stream" import { runClaudeCode } from "../../integrations/claude-code/run" -import { ClaudeCodeMessage } from "../../integrations/claude-code/types" +import { filterMessagesForClaudeCode } from "../../integrations/claude-code/message-filter" import { BaseProvider } from "./base-provider" import { t } from "../../i18n" import { ApiHandlerOptions } from "../../shared/api" @@ -17,61 +17,16 @@ export class ClaudeCodeHandler extends BaseProvider implements ApiHandler { } override async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { + // Filter out image blocks since Claude Code doesn't support them + const filteredMessages = filterMessagesForClaudeCode(messages) + const claudeProcess = runClaudeCode({ systemPrompt, - messages, + messages: filteredMessages, path: this.options.claudeCodePath, modelId: this.getModel().id, }) - const dataQueue: string[] = [] - let processError = null - let errorOutput = "" - let exitCode: number | null = null - let buffer = "" - - claudeProcess.stdout.on("data", (data) => { - buffer += data.toString() - const lines = buffer.split("\n") - - // Keep the last line in buffer as it might be incomplete - buffer = lines.pop() || "" - - // Process complete lines - for (const line of lines) { - const trimmedLine = line.trim() - if (trimmedLine !== "") { - dataQueue.push(trimmedLine) - } - } - }) - - claudeProcess.stderr.on("data", (data) => { - errorOutput += data.toString() - }) - - claudeProcess.on("close", (code) => { - exitCode = code - // Process any remaining data in buffer - const trimmedBuffer = buffer.trim() - if (trimmedBuffer) { - // Validate that the remaining buffer looks like valid JSON before processing - if (this.isLikelyValidJSON(trimmedBuffer)) { - dataQueue.push(trimmedBuffer) - } else { - console.warn( - "Discarding incomplete JSON data on process close:", - trimmedBuffer.substring(0, 100) + (trimmedBuffer.length > 100 ? "..." : ""), - ) - } - buffer = "" - } - }) - - claudeProcess.on("error", (error) => { - processError = error - }) - // Usage is included with assistant messages, // but cost is included in the result chunk let usage: ApiStreamUsageChunk = { @@ -82,72 +37,74 @@ export class ClaudeCodeHandler extends BaseProvider implements ApiHandler { cacheWriteTokens: 0, } - while (exitCode !== 0 || dataQueue.length > 0) { - if (dataQueue.length === 0) { - await new Promise((resolve) => setImmediate(resolve)) - } + let isPaidUsage = true - if (exitCode !== null && exitCode !== 0) { - if (errorOutput) { - throw new Error( - t("common:errors.claudeCode.processExitedWithError", { - exitCode, - output: errorOutput.trim(), - }), - ) - } - throw new Error(t("common:errors.claudeCode.processExited", { exitCode })) - } - - const data = dataQueue.shift() - if (!data) { - continue - } - - const chunk = this.attemptParseChunk(data) - - if (!chunk) { + for await (const chunk of claudeProcess) { + if (typeof chunk === "string") { yield { type: "text", - text: data || "", + text: chunk, } continue } if (chunk.type === "system" && chunk.subtype === "init") { + // Based on my tests, subscription usage sets the `apiKeySource` to "none" + isPaidUsage = chunk.apiKeySource !== "none" continue } if (chunk.type === "assistant" && "message" in chunk) { const message = chunk.message - if (message.stop_reason !== null && message.stop_reason !== "tool_use") { - const firstContent = message.content[0] - const errorMessage = - this.getContentText(firstContent) || - t("common:errors.claudeCode.stoppedWithReason", { reason: message.stop_reason }) + if (message.stop_reason !== null) { + const content = "text" in message.content[0] ? message.content[0] : undefined - if (errorMessage.includes("Invalid model name")) { - throw new Error(errorMessage + `\n\n${t("common:errors.claudeCode.apiKeyModelPlanMismatch")}`) + const isError = content && content.text.startsWith(`API Error`) + if (isError) { + // Error messages are formatted as: `API Error: <> <>` + const errorMessageStart = content.text.indexOf("{") + const errorMessage = content.text.slice(errorMessageStart) + + const error = this.attemptParse(errorMessage) + if (!error) { + throw new Error(content.text) + } + + if (error.error.message.includes("Invalid model name")) { + throw new Error( + content.text + `\n\n${t("common:errors.claudeCode.apiKeyModelPlanMismatch")}`, + ) + } + + throw new Error(errorMessage) } - - throw new Error(errorMessage) } for (const content of message.content) { - if (content.type === "text") { - yield { - type: "text", - text: content.text, - } - } else if (content.type === "thinking") { - yield { - type: "reasoning", - text: content.thinking, - } - } else { - console.warn("Unsupported content type:", content) + switch (content.type) { + case "text": + yield { + type: "text", + text: content.text, + } + break + case "thinking": + yield { + type: "reasoning", + text: content.thinking || "", + } + break + case "redacted_thinking": + yield { + type: "reasoning", + text: "[Redacted thinking block]", + } + break + case "tool_use": + console.error(`tool_use is not supported yet. Received: ${JSON.stringify(content)}`) + break } } @@ -161,16 +118,10 @@ export class ClaudeCodeHandler extends BaseProvider implements ApiHandler { } if (chunk.type === "result" && "result" in chunk) { - // Only use the cost from the CLI if provided - // Don't calculate cost as it may be $0 for subscription users - usage.totalCost = chunk.cost_usd ?? 0 + usage.totalCost = isPaidUsage ? chunk.total_cost_usd : 0 yield usage } - - if (processError) { - throw processError - } } } @@ -187,53 +138,10 @@ export class ClaudeCodeHandler extends BaseProvider implements ApiHandler { } } - private getContentText(content: any): string | undefined { - if (!content) return undefined - switch (content.type) { - case "text": - return content.text - case "thinking": - return content.thinking - default: - return undefined - } - } - - private isLikelyValidJSON(data: string): boolean { - // Basic validation to check if the data looks like it could be valid JSON - const trimmed = data.trim() - if (!trimmed) return false - - // Must start and end with appropriate JSON delimiters - const startsCorrectly = trimmed.startsWith("{") || trimmed.startsWith("[") - const endsCorrectly = trimmed.endsWith("}") || trimmed.endsWith("]") - - if (!startsCorrectly || !endsCorrectly) return false - - // Check for balanced braces/brackets (simple heuristic) - let braceCount = 0 - let bracketCount = 0 - for (const char of trimmed) { - if (char === "{") braceCount++ - else if (char === "}") braceCount-- - else if (char === "[") bracketCount++ - else if (char === "]") bracketCount-- - } - - return braceCount === 0 && bracketCount === 0 - } - - // TODO: Validate instead of parsing - private attemptParseChunk(data: string): ClaudeCodeMessage | null { + private attemptParse(str: string) { try { - return JSON.parse(data) - } catch (error) { - console.error( - "Error parsing chunk:", - error, - "Data:", - data.substring(0, 100) + (data.length > 100 ? "..." : ""), - ) + return JSON.parse(str) + } catch (err) { return null } } diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index 5956187e41..fef700268d 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -2,6 +2,7 @@ import * as path from "path" import fs from "fs/promises" import NodeCache from "node-cache" +import { safeWriteJson } from "../../../utils/safeWriteJson" import { ContextProxy } from "../../../core/config/ContextProxy" import { getCacheDirectoryPath } from "../../../utils/storage" @@ -22,7 +23,7 @@ const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 }) async function writeModels(router: RouterName, data: ModelRecord) { const filename = `${router}_models.json` const cacheDir = await getCacheDirectoryPath(ContextProxy.instance.globalStorageUri.fsPath) - await fs.writeFile(path.join(cacheDir, filename), JSON.stringify(data)) + await safeWriteJson(path.join(cacheDir, filename), data) } async function readModels(router: RouterName): Promise { diff --git a/src/api/providers/fetchers/modelEndpointCache.ts b/src/api/providers/fetchers/modelEndpointCache.ts index c69e7c82a3..256ae84048 100644 --- a/src/api/providers/fetchers/modelEndpointCache.ts +++ b/src/api/providers/fetchers/modelEndpointCache.ts @@ -2,6 +2,7 @@ import * as path from "path" import fs from "fs/promises" import NodeCache from "node-cache" +import { safeWriteJson } from "../../../utils/safeWriteJson" import sanitize from "sanitize-filename" import { ContextProxy } from "../../../core/config/ContextProxy" @@ -18,7 +19,7 @@ const getCacheKey = (router: RouterName, modelId: string) => sanitize(`${router} async function writeModelEndpoints(key: string, data: ModelRecord) { const filename = `${key}_endpoints.json` const cacheDir = await getCacheDirectoryPath(ContextProxy.instance.globalStorageUri.fsPath) - await fs.writeFile(path.join(cacheDir, filename), JSON.stringify(data, null, 2)) + await safeWriteJson(path.join(cacheDir, filename), data) } async function readModelEndpoints(key: string): Promise { diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index b4f256f43a..f5e4e4c985 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -86,7 +86,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl const deepseekReasoner = modelId.includes("deepseek-reasoner") || enabledR1Format const ark = modelUrl.includes(".volces.com") - if (modelId.startsWith("o3-mini")) { + if (modelId.includes("o1") || modelId.includes("o3") || modelId.includes("o4")) { yield* this.handleO3FamilyMessage(modelId, systemPrompt, messages) return } @@ -306,7 +306,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl stream: true, ...(isGrokXAI ? {} : { stream_options: { include_usage: true } }), reasoning_effort: modelInfo.reasoningEffort, - temperature: this.options.modelTemperature ?? 0, + temperature: undefined, } // O3 family models do not support the deprecated max_tokens parameter @@ -331,7 +331,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl ...convertToOpenAiMessages(messages), ], reasoning_effort: modelInfo.reasoningEffort, - temperature: this.options.modelTemperature ?? 0, + temperature: undefined, } // O3 family models do not support the deprecated max_tokens parameter diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 51d97963e7..6565daa238 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -48,13 +48,11 @@ interface CompletionUsage { } total_tokens?: number cost?: number - is_byok?: boolean + cost_details?: { + upstream_inference_cost?: number + } } -// with bring your own key, OpenRouter charges 5% of what it normally would: https://openrouter.ai/docs/use-cases/byok -// so we multiply the cost reported by OpenRouter to get an estimate of what the request actually cost -const BYOK_COST_MULTIPLIER = 20 - export class OpenRouterHandler extends BaseProvider implements SingleCompletionHandler { protected options: ApiHandlerOptions private client: OpenAI @@ -168,11 +166,9 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH type: "usage", inputTokens: lastUsage.prompt_tokens || 0, outputTokens: lastUsage.completion_tokens || 0, - // Waiting on OpenRouter to figure out what this represents in the Gemini case - // and how to best support it. - // cacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens, + cacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens, reasoningTokens: lastUsage.completion_tokens_details?.reasoning_tokens, - totalCost: (lastUsage.is_byok ? BYOK_COST_MULTIPLIER : 1) * (lastUsage.cost || 0), + totalCost: (lastUsage.cost_details?.upstream_inference_cost || 0) + (lastUsage.cost || 0), } } } diff --git a/src/core/assistant-message/parseAssistantMessage.ts b/src/core/assistant-message/parseAssistantMessage.ts index ae848e0ae7..ebb8674c8f 100644 --- a/src/core/assistant-message/parseAssistantMessage.ts +++ b/src/core/assistant-message/parseAssistantMessage.ts @@ -24,7 +24,12 @@ export function parseAssistantMessage(assistantMessage: string): AssistantMessag const paramClosingTag = `` if (currentParamValue.endsWith(paramClosingTag)) { // End of param value. - currentToolUse.params[currentParamName] = currentParamValue.slice(0, -paramClosingTag.length).trim() + // Don't trim content parameters to preserve newlines, but strip first and last newline only + const paramValue = currentParamValue.slice(0, -paramClosingTag.length) + currentToolUse.params[currentParamName] = + currentParamName === "content" + ? paramValue.replace(/^\n/, "").replace(/\n$/, "") + : paramValue.trim() currentParamName = undefined continue } else { @@ -72,9 +77,11 @@ export function parseAssistantMessage(assistantMessage: string): AssistantMessag const contentEndIndex = toolContent.lastIndexOf(contentEndTag) if (contentStartIndex !== -1 && contentEndIndex !== -1 && contentEndIndex > contentStartIndex) { + // Don't trim content to preserve newlines, but strip first and last newline only currentToolUse.params[contentParamName] = toolContent .slice(contentStartIndex, contentEndIndex) - .trim() + .replace(/^\n/, "") + .replace(/\n$/, "") } } @@ -138,7 +145,10 @@ export function parseAssistantMessage(assistantMessage: string): AssistantMessag // Stream did not complete tool call, add it as partial. if (currentParamName) { // Tool call has a parameter that was not completed. - currentToolUse.params[currentParamName] = accumulator.slice(currentParamValueStartIndex).trim() + // Don't trim content parameters to preserve newlines, but strip first and last newline only + const paramValue = accumulator.slice(currentParamValueStartIndex) + currentToolUse.params[currentParamName] = + currentParamName === "content" ? paramValue.replace(/^\n/, "").replace(/\n$/, "") : paramValue.trim() } contentBlocks.push(currentToolUse) diff --git a/src/core/assistant-message/parseAssistantMessageV2.ts b/src/core/assistant-message/parseAssistantMessageV2.ts index 6d3594cf60..7c7526cbdb 100644 --- a/src/core/assistant-message/parseAssistantMessageV2.ts +++ b/src/core/assistant-message/parseAssistantMessageV2.ts @@ -76,13 +76,13 @@ export function parseAssistantMessageV2(assistantMessage: string): AssistantMess ) ) { // Found the closing tag for the parameter. - const value = assistantMessage - .slice( - currentParamValueStart, // Start after the opening tag. - currentCharIndex - closeTag.length + 1, // End before the closing tag. - ) - .trim() - currentToolUse.params[currentParamName] = value + const value = assistantMessage.slice( + currentParamValueStart, // Start after the opening tag. + currentCharIndex - closeTag.length + 1, // End before the closing tag. + ) + // Don't trim content parameters to preserve newlines, but strip first and last newline only + currentToolUse.params[currentParamName] = + currentParamName === "content" ? value.replace(/^\n/, "").replace(/\n$/, "") : value.trim() currentParamName = undefined // Go back to parsing tool content. // We don't continue loop here, need to check for tool close or other params at index i. } else { @@ -146,10 +146,11 @@ export function parseAssistantMessageV2(assistantMessage: string): AssistantMess const contentEnd = toolContentSlice.lastIndexOf(contentEndTag) if (contentStart !== -1 && contentEnd !== -1 && contentEnd > contentStart) { + // Don't trim content to preserve newlines, but strip first and last newline only const contentValue = toolContentSlice .slice(contentStart + contentStartTag.length, contentEnd) - .trim() - + .replace(/^\n/, "") + .replace(/\n$/, "") currentToolUse.params[contentParamName] = contentValue } } @@ -251,9 +252,10 @@ export function parseAssistantMessageV2(assistantMessage: string): AssistantMess // Finalize any open parameter within an open tool use. if (currentToolUse && currentParamName) { - currentToolUse.params[currentParamName] = assistantMessage - .slice(currentParamValueStart) // From param start to end of string. - .trim() + const value = assistantMessage.slice(currentParamValueStart) // From param start to end of string. + // Don't trim content parameters to preserve newlines, but strip first and last newline only + currentToolUse.params[currentParamName] = + currentParamName === "content" ? value.replace(/^\n/, "").replace(/\n$/, "") : value.trim() // Tool use remains partial. } diff --git a/src/core/config/CustomModesManager.ts b/src/core/config/CustomModesManager.ts index 21c2709f90..5ce94b0e38 100644 --- a/src/core/config/CustomModesManager.ts +++ b/src/core/config/CustomModesManager.ts @@ -3,6 +3,7 @@ import * as path from "path" import * as fs from "fs/promises" import * as yaml from "yaml" +import stripBom from "strip-bom" import { type ModeConfig, customModesSettingsSchema } from "@roo-code/types" @@ -11,6 +12,7 @@ import { getWorkspacePath } from "../../utils/path" import { logger } from "../../utils/logging" import { GlobalFileNames } from "../../shared/globalFileNames" import { ensureSettingsDirectoryExists } from "../../utils/globalContext" +import { t } from "../../i18n" const ROOMODES_FILENAME = ".roomodes" @@ -73,12 +75,88 @@ export class CustomModesManager { return exists ? roomodesPath : undefined } + /** + * Regex pattern for problematic characters that need to be cleaned from YAML content + * Includes: + * - \u00A0: Non-breaking space + * - \u200B-\u200D: Zero-width spaces and joiners + * - \u2010-\u2015, \u2212: Various dash characters + * - \u2018-\u2019: Smart single quotes + * - \u201C-\u201D: Smart double quotes + */ + private static readonly PROBLEMATIC_CHARS_REGEX = + // eslint-disable-next-line no-misleading-character-class + /[\u00A0\u200B\u200C\u200D\u2010\u2011\u2012\u2013\u2014\u2015\u2212\u2018\u2019\u201C\u201D]/g + + /** + * Clean invisible and problematic characters from YAML content + */ + private cleanInvisibleCharacters(content: string): string { + // Single pass replacement for all problematic characters + return content.replace(CustomModesManager.PROBLEMATIC_CHARS_REGEX, (match) => { + switch (match) { + case "\u00A0": // Non-breaking space + return " " + case "\u200B": // Zero-width space + case "\u200C": // Zero-width non-joiner + case "\u200D": // Zero-width joiner + return "" + case "\u2018": // Left single quotation mark + case "\u2019": // Right single quotation mark + return "'" + case "\u201C": // Left double quotation mark + case "\u201D": // Right double quotation mark + return '"' + default: // Dash characters (U+2010 through U+2015, U+2212) + return "-" + } + }) + } + + /** + * Parse YAML content with enhanced error handling and preprocessing + */ + private parseYamlSafely(content: string, filePath: string): any { + // Clean the content + let cleanedContent = stripBom(content) + cleanedContent = this.cleanInvisibleCharacters(cleanedContent) + + try { + return yaml.parse(cleanedContent) + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error) + console.error(`[CustomModesManager] Failed to parse YAML from ${filePath}:`, errorMsg) + + // Show user-friendly error message for .roomodes files + if (filePath.endsWith(ROOMODES_FILENAME)) { + const lineMatch = errorMsg.match(/at line (\d+)/) + const line = lineMatch ? lineMatch[1] : "unknown" + vscode.window.showErrorMessage(t("common:customModes.errors.yamlParseError", { line })) + } + + // Return empty object to prevent duplicate error handling + return {} + } + } + private async loadModesFromFile(filePath: string): Promise { try { const content = await fs.readFile(filePath, "utf-8") - const settings = yaml.parse(content) + const settings = this.parseYamlSafely(content, filePath) const result = customModesSettingsSchema.safeParse(settings) + if (!result.success) { + console.error(`[CustomModesManager] Schema validation failed for ${filePath}:`, result.error) + + // Show user-friendly error for .roomodes files + if (filePath.endsWith(ROOMODES_FILENAME)) { + const issues = result.error.issues + .map((issue) => `• ${issue.path.join(".")}: ${issue.message}`) + .join("\n") + + vscode.window.showErrorMessage(t("common:customModes.errors.schemaValidationError", { issues })) + } + return [] } @@ -89,8 +167,11 @@ export class CustomModesManager { // Add source to each mode return result.data.customModes.map((mode) => ({ ...mode, source })) } catch (error) { - const errorMsg = `Failed to load modes from ${filePath}: ${error instanceof Error ? error.message : String(error)}` - console.error(`[CustomModesManager] ${errorMsg}`) + // Only log if the error wasn't already handled in parseYamlSafely + if (!(error as any).alreadyHandled) { + const errorMsg = `Failed to load modes from ${filePath}: ${error instanceof Error ? error.message : String(error)}` + console.error(`[CustomModesManager] ${errorMsg}`) + } return [] } } @@ -124,7 +205,12 @@ export class CustomModesManager { const fileExists = await fileExistsAtPath(filePath) if (!fileExists) { - await this.queueWrite(() => fs.writeFile(filePath, yaml.stringify({ customModes: [] }))) + await this.queueWrite(() => + fs.writeFile( + filePath, + yaml.stringify({ customModes: [] }, { lineWidth: 0, defaultStringType: "PLAIN" }), + ), + ) } return filePath @@ -147,13 +233,12 @@ export class CustomModesManager { await this.getCustomModesFilePath() const content = await fs.readFile(settingsPath, "utf-8") - const errorMessage = - "Invalid custom modes format. Please ensure your settings follow the correct YAML format." + const errorMessage = t("common:customModes.errors.invalidFormat") let config: any try { - config = yaml.parse(content) + config = this.parseYamlSafely(content, settingsPath) } catch (error) { console.error(error) vscode.window.showErrorMessage(errorMessage) @@ -284,7 +369,7 @@ export class CustomModesManager { if (!workspaceFolders || workspaceFolders.length === 0) { logger.error("Failed to update project mode: No workspace folder found", { slug }) - throw new Error("No workspace folder found for project-specific mode") + throw new Error(t("common:customModes.errors.noWorkspaceForProject")) } const workspaceRoot = getWorkspacePath() @@ -318,7 +403,7 @@ export class CustomModesManager { } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) logger.error("Failed to update custom mode", { slug, error: errorMessage }) - vscode.window.showErrorMessage(`Failed to update custom mode: ${errorMessage}`) + vscode.window.showErrorMessage(t("common:customModes.errors.updateFailed", { error: errorMessage })) } } @@ -329,20 +414,20 @@ export class CustomModesManager { content = await fs.readFile(filePath, "utf-8") } catch (error) { // File might not exist yet. - content = yaml.stringify({ customModes: [] }) + content = yaml.stringify({ customModes: [] }, { lineWidth: 0, defaultStringType: "PLAIN" }) } let settings try { - settings = yaml.parse(content) + settings = this.parseYamlSafely(content, filePath) } catch (error) { - console.error(`[CustomModesManager] Failed to parse YAML from ${filePath}:`, error) + // Error already logged in parseYamlSafely settings = { customModes: [] } } settings.customModes = operation(settings.customModes || []) - await fs.writeFile(filePath, yaml.stringify(settings), "utf-8") + await fs.writeFile(filePath, yaml.stringify(settings, { lineWidth: 0, defaultStringType: "PLAIN" }), "utf-8") } private async refreshMergedState(): Promise { @@ -373,7 +458,7 @@ export class CustomModesManager { const globalMode = settingsModes.find((m) => m.slug === slug) if (!projectMode && !globalMode) { - throw new Error("Write error: Mode not found") + throw new Error(t("common:customModes.errors.modeNotFound")) } await this.queueWrite(async () => { @@ -392,23 +477,24 @@ export class CustomModesManager { await this.refreshMergedState() }) } catch (error) { - vscode.window.showErrorMessage( - `Failed to delete custom mode: ${error instanceof Error ? error.message : String(error)}`, - ) + const errorMessage = error instanceof Error ? error.message : String(error) + vscode.window.showErrorMessage(t("common:customModes.errors.deleteFailed", { error: errorMessage })) } } public async resetCustomModes(): Promise { try { const filePath = await this.getCustomModesFilePath() - await fs.writeFile(filePath, yaml.stringify({ customModes: [] })) + await fs.writeFile( + filePath, + yaml.stringify({ customModes: [] }, { lineWidth: 0, defaultStringType: "PLAIN" }), + ) await this.context.globalState.update("customModes", []) this.clearCache() await this.onUpdate() } catch (error) { - vscode.window.showErrorMessage( - `Failed to reset custom modes: ${error instanceof Error ? error.message : String(error)}`, - ) + const errorMessage = error instanceof Error ? error.message : String(error) + vscode.window.showErrorMessage(t("common:customModes.errors.resetFailed", { error: errorMessage })) } } diff --git a/src/core/config/__tests__/CustomModesManager.spec.ts b/src/core/config/__tests__/CustomModesManager.spec.ts index 7791b36ee8..2af801b646 100644 --- a/src/core/config/__tests__/CustomModesManager.spec.ts +++ b/src/core/config/__tests__/CustomModesManager.spec.ts @@ -754,7 +754,7 @@ describe("CustomModesManager", () => { await manager.deleteCustomMode("non-existent-mode") - expect(mockShowError).toHaveBeenCalledWith(expect.stringContaining("Write error")) + expect(mockShowError).toHaveBeenCalledWith("customModes.errors.deleteFailed") }) }) diff --git a/src/core/config/__tests__/CustomModesManager.yamlEdgeCases.spec.ts b/src/core/config/__tests__/CustomModesManager.yamlEdgeCases.spec.ts new file mode 100644 index 0000000000..251a33d211 --- /dev/null +++ b/src/core/config/__tests__/CustomModesManager.yamlEdgeCases.spec.ts @@ -0,0 +1,474 @@ +// npx vitest core/config/__tests__/CustomModesManager.yamlEdgeCases.spec.ts + +import type { Mock } from "vitest" + +import * as path from "path" +import * as fs from "fs/promises" + +import * as yaml from "yaml" +import * as vscode from "vscode" + +import type { ModeConfig } from "@roo-code/types" + +import { fileExistsAtPath } from "../../../utils/fs" +import { getWorkspacePath } from "../../../utils/path" +import { GlobalFileNames } from "../../../shared/globalFileNames" + +import { CustomModesManager } from "../CustomModesManager" + +vi.mock("vscode", () => ({ + workspace: { + workspaceFolders: [], + onDidSaveTextDocument: vi.fn(), + createFileSystemWatcher: vi.fn(), + }, + window: { + showErrorMessage: vi.fn(), + }, +})) + +vi.mock("fs/promises") + +vi.mock("../../../utils/fs") +vi.mock("../../../utils/path") + +describe("CustomModesManager - YAML Edge Cases", () => { + let manager: CustomModesManager + let mockContext: vscode.ExtensionContext + let mockOnUpdate: Mock + let mockWorkspaceFolders: { uri: { fsPath: string } }[] + + const mockStoragePath = `${path.sep}mock${path.sep}settings` + const mockSettingsPath = path.join(mockStoragePath, "settings", GlobalFileNames.customModes) + const mockRoomodes = `${path.sep}mock${path.sep}workspace${path.sep}.roomodes` + + // Helper function to reduce duplication in fs.readFile mocks + const mockFsReadFile = (files: Record) => { + ;(fs.readFile as Mock).mockImplementation(async (path: string) => { + if (files[path]) return files[path] + throw new Error("File not found") + }) + } + + beforeEach(() => { + mockOnUpdate = vi.fn() + mockContext = { + globalState: { + get: vi.fn(), + update: vi.fn(), + keys: vi.fn(() => []), + setKeysForSync: vi.fn(), + }, + globalStorageUri: { + fsPath: mockStoragePath, + }, + } as unknown as vscode.ExtensionContext + + mockWorkspaceFolders = [{ uri: { fsPath: "/mock/workspace" } }] + ;(vscode.workspace as any).workspaceFolders = mockWorkspaceFolders + ;(vscode.workspace.onDidSaveTextDocument as Mock).mockReturnValue({ dispose: vi.fn() }) + ;(getWorkspacePath as Mock).mockReturnValue("/mock/workspace") + ;(fileExistsAtPath as Mock).mockImplementation(async (path: string) => { + return path === mockSettingsPath || path === mockRoomodes + }) + ;(fs.mkdir as Mock).mockResolvedValue(undefined) + ;(fs.readFile as Mock).mockImplementation(async (path: string) => { + if (path === mockSettingsPath) { + return yaml.stringify({ customModes: [] }) + } + throw new Error("File not found") + }) + + // Mock createFileSystemWatcher to prevent file watching in tests + const mockWatcher = { + onDidChange: vi.fn().mockReturnValue({ dispose: vi.fn() }), + onDidCreate: vi.fn().mockReturnValue({ dispose: vi.fn() }), + onDidDelete: vi.fn().mockReturnValue({ dispose: vi.fn() }), + dispose: vi.fn(), + } + ;(vscode.workspace.createFileSystemWatcher as Mock).mockReturnValue(mockWatcher) + + manager = new CustomModesManager(mockContext, mockOnUpdate) + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + describe("BOM (Byte Order Mark) handling", () => { + it("should handle UTF-8 BOM in YAML files", async () => { + const yamlWithBOM = + "\uFEFF" + + yaml.stringify({ + customModes: [ + { + slug: "test-mode", + name: "Test Mode", + roleDefinition: "Test role", + groups: ["read"], + }, + ], + }) + + mockFsReadFile({ + [mockRoomodes]: yamlWithBOM, + [mockSettingsPath]: yaml.stringify({ customModes: [] }), + }) + + const modes = await manager.getCustomModes() + + expect(modes).toHaveLength(1) + expect(modes[0].slug).toBe("test-mode") + expect(modes[0].name).toBe("Test Mode") + }) + + it("should handle UTF-16 BOM in YAML files", async () => { + // When Node.js reads UTF-16 files, the BOM is correctly decoded as \uFEFF + const yamlWithBOM = + "\uFEFF" + + yaml.stringify({ + customModes: [ + { + slug: "utf16-mode", + name: "UTF-16 Mode", + roleDefinition: "Test role", + groups: ["read"], + }, + ], + }) + + mockFsReadFile({ + [mockRoomodes]: yamlWithBOM, + [mockSettingsPath]: yaml.stringify({ customModes: [] }), + }) + + const modes = await manager.getCustomModes() + + expect(modes).toHaveLength(1) + expect(modes[0].slug).toBe("utf16-mode") + }) + }) + + describe("Invisible character handling", () => { + it("should handle non-breaking spaces in YAML", async () => { + // YAML with non-breaking spaces (U+00A0) instead of regular spaces + const yamlWithNonBreakingSpaces = `customModes: + - slug: "test-mode" + name: "Test\u00A0Mode" + roleDefinition: "Test\u00A0role\u00A0with\u00A0non-breaking\u00A0spaces" + groups: ["read"]` + + mockFsReadFile({ + [mockRoomodes]: yamlWithNonBreakingSpaces, + [mockSettingsPath]: yaml.stringify({ customModes: [] }), + }) + + const modes = await manager.getCustomModes() + + expect(modes).toHaveLength(1) + expect(modes[0].name).toBe("Test Mode") // Non-breaking spaces replaced with regular spaces + expect(modes[0].roleDefinition).toBe("Test role with non-breaking spaces") + }) + + it("should handle zero-width characters", async () => { + // YAML with zero-width characters + const yamlWithZeroWidth = `customModes: + - slug: "test-mode" + name: "Test\u200BMode\u200C" + roleDefinition: "Test\u200Drole" + groups: ["read"]` + + mockFsReadFile({ + [mockRoomodes]: yamlWithZeroWidth, + [mockSettingsPath]: yaml.stringify({ customModes: [] }), + }) + + const modes = await manager.getCustomModes() + + expect(modes).toHaveLength(1) + expect(modes[0].name).toBe("TestMode") // Zero-width characters removed + expect(modes[0].roleDefinition).toBe("Testrole") + }) + + it("should normalize various quote characters", async () => { + // Use fancy quotes that will be normalized before YAML parsing + // The fancy quotes will be normalized to standard quotes + const yamlWithFancyQuotes = yaml.stringify({ + customModes: [ + { + slug: "test-mode", + name: "Test Mode", + roleDefinition: "Test role with \u2018fancy\u2019 quotes and \u201Ccurly\u201D quotes", + groups: ["read"], + }, + ], + }) + + mockFsReadFile({ + [mockRoomodes]: yamlWithFancyQuotes, + [mockSettingsPath]: yaml.stringify({ customModes: [] }), + }) + + const modes = await manager.getCustomModes() + + expect(modes).toHaveLength(1) + expect(modes[0].roleDefinition).toBe("Test role with 'fancy' quotes and \"curly\" quotes") + }) + }) + + // Note: YAML anchor/alias support has been removed to reduce complexity + // If needed in the future, users should pre-process their YAML files + + describe("Complex fileRegex handling", () => { + it("should handle complex fileRegex syntax gracefully", async () => { + const yamlWithComplexFileRegex = yaml.stringify({ + customModes: [ + { + slug: "test-mode", + name: "Test Mode", + roleDefinition: "Test role", + groups: [ + "read", + ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], + "browser", + ], + }, + ], + }) + + mockFsReadFile({ + [mockRoomodes]: yamlWithComplexFileRegex, + [mockSettingsPath]: yaml.stringify({ customModes: [] }), + }) + + const modes = await manager.getCustomModes() + + // Should successfully parse the complex fileRegex syntax + expect(modes).toHaveLength(1) + expect(modes[0].groups).toHaveLength(3) + expect(modes[0].groups[1]).toEqual(["edit", { fileRegex: "\\.md$", description: "Markdown files only" }]) + }) + + it("should handle invalid fileRegex syntax with clear error", async () => { + // This YAML has invalid structure that might cause parsing issues + const invalidYaml = `customModes: + - slug: "test-mode" + name: "Test Mode" + roleDefinition: "Test role" + groups: + - read + - ["edit", { fileRegex: "\\.md$" }] # This line has invalid YAML syntax + - browser` + + mockFsReadFile({ + [mockRoomodes]: invalidYaml, + [mockSettingsPath]: yaml.stringify({ customModes: [] }), + }) + + const modes = await manager.getCustomModes() + + // Should handle the error gracefully + expect(modes).toHaveLength(0) + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("customModes.errors.yamlParseError") + }) + }) + + describe("Error messages", () => { + it("should provide detailed syntax error messages with context", async () => { + const invalidYaml = `customModes: + - slug: "test-mode" + name: "Test Mode" + roleDefinition: "Test role + groups: ["read"]` // Missing closing quote + + mockFsReadFile({ + [mockRoomodes]: invalidYaml, + [mockSettingsPath]: yaml.stringify({ customModes: [] }), + }) + + const modes = await manager.getCustomModes() + + // Should fallback to empty array and show detailed error + expect(modes).toHaveLength(0) + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("customModes.errors.yamlParseError") + }) + + it("should provide schema validation error messages", async () => { + const invalidSchema = yaml.stringify({ + customModes: [ + { + slug: "test-mode", + name: "Test Mode", + // Missing required 'roleDefinition' field + groups: ["read"], + }, + ], + }) + + mockFsReadFile({ + [mockRoomodes]: invalidSchema, + [mockSettingsPath]: yaml.stringify({ customModes: [] }), + }) + + const modes = await manager.getCustomModes() + + // Should show schema validation error + expect(modes).toHaveLength(0) + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("customModes.errors.schemaValidationError") + }) + }) + + describe("UTF-8 encoding", () => { + it("should handle special characters and emojis", async () => { + const yamlWithEmojis = yaml.stringify({ + customModes: [ + { + slug: "emoji-mode", + name: "📝 Writing Mode", + roleDefinition: "A mode for writing with emojis 🚀", + groups: ["read", "edit"], + }, + ], + }) + + mockFsReadFile({ + [mockRoomodes]: yamlWithEmojis, + [mockSettingsPath]: yaml.stringify({ customModes: [] }), + }) + + const modes = await manager.getCustomModes() + + expect(modes).toHaveLength(1) + expect(modes[0].name).toBe("📝 Writing Mode") + expect(modes[0].roleDefinition).toBe("A mode for writing with emojis 🚀") + }) + + it("should handle various international characters", async () => { + const yamlWithInternational = yaml.stringify({ + customModes: [ + { + slug: "intl-mode", + name: "Mode Français", + roleDefinition: "Mode für Deutsch, 日本語モード, Режим русский", + groups: ["read"], + }, + ], + }) + + mockFsReadFile({ + [mockRoomodes]: yamlWithInternational, + [mockSettingsPath]: yaml.stringify({ customModes: [] }), + }) + + const modes = await manager.getCustomModes() + + expect(modes).toHaveLength(1) + expect(modes[0].roleDefinition).toContain("für Deutsch") + expect(modes[0].roleDefinition).toContain("日本語モード") + expect(modes[0].roleDefinition).toContain("Режим русский") + }) + }) + + describe("Additional edge cases", () => { + it("should handle mixed line endings (CRLF vs LF)", async () => { + // YAML with mixed line endings + const yamlWithMixedLineEndings = + "customModes:\r\n" + + ' - slug: "test-mode"\n' + + ' name: "Test Mode"\r\n' + + ' roleDefinition: "Test role with mixed line endings"\n' + + ' groups: ["read"]' + + mockFsReadFile({ + [mockRoomodes]: yamlWithMixedLineEndings, + [mockSettingsPath]: yaml.stringify({ customModes: [] }), + }) + + const modes = await manager.getCustomModes() + + expect(modes).toHaveLength(1) + expect(modes[0].slug).toBe("test-mode") + expect(modes[0].roleDefinition).toBe("Test role with mixed line endings") + }) + + it("should handle multiple BOMs in sequence", async () => { + // File with multiple BOMs (edge case from file concatenation) + const yamlWithMultipleBOMs = + "\uFEFF\uFEFF" + + yaml.stringify({ + customModes: [ + { + slug: "multi-bom-mode", + name: "Multi BOM Mode", + roleDefinition: "Test role", + groups: ["read"], + }, + ], + }) + + mockFsReadFile({ + [mockRoomodes]: yamlWithMultipleBOMs, + [mockSettingsPath]: yaml.stringify({ customModes: [] }), + }) + + const modes = await manager.getCustomModes() + + expect(modes).toHaveLength(1) + expect(modes[0].slug).toBe("multi-bom-mode") + }) + + it("should handle deeply nested structures with edge case characters", async () => { + const yamlWithComplexNesting = yaml.stringify({ + customModes: [ + { + slug: "complex-mode", + name: "Complex\u00A0Mode\u2019s Name", + roleDefinition: "Complex role with \u201Cquotes\u201D and \u2014dashes\u2014", + groups: [ + "read", + [ + "edit", + { + fileRegex: "\\.md$", + description: "Markdown files with \u2018special\u2019 chars", + }, + ], + [ + "browser", + { + fileRegex: "\\.html?$", + description: "HTML files\u00A0only", + }, + ], + ], + }, + ], + }) + + mockFsReadFile({ + [mockRoomodes]: yamlWithComplexNesting, + [mockSettingsPath]: yaml.stringify({ customModes: [] }), + }) + + const modes = await manager.getCustomModes() + + expect(modes).toHaveLength(1) + expect(modes[0].name).toBe("Complex Mode's Name") + expect(modes[0].roleDefinition).toBe('Complex role with "quotes" and -dashes-') + expect(modes[0].groups[1]).toEqual([ + "edit", + { + fileRegex: "\\.md$", + description: "Markdown files with 'special' chars", + }, + ]) + expect(modes[0].groups[2]).toEqual([ + "browser", + { + fileRegex: "\\.html?$", + description: "HTML files only", + }, + ]) + }) + }) +}) diff --git a/src/core/config/__tests__/importExport.spec.ts b/src/core/config/__tests__/importExport.spec.ts index 4ba43f475e..1f6bd5f28e 100644 --- a/src/core/config/__tests__/importExport.spec.ts +++ b/src/core/config/__tests__/importExport.spec.ts @@ -8,10 +8,11 @@ import * as vscode from "vscode" import type { ProviderName } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" -import { importSettings, exportSettings } from "../importExport" +import { importSettings, importSettingsFromFile, importSettingsWithFeedback, exportSettings } from "../importExport" import { ProviderSettingsManager } from "../ProviderSettingsManager" import { ContextProxy } from "../ContextProxy" import { CustomModesManager } from "../CustomModesManager" +import { safeWriteJson } from "../../../utils/safeWriteJson" import type { Mock } from "vitest" @@ -19,6 +20,8 @@ vi.mock("vscode", () => ({ window: { showOpenDialog: vi.fn(), showSaveDialog: vi.fn(), + showErrorMessage: vi.fn(), + showInformationMessage: vi.fn(), }, Uri: { file: vi.fn((filePath) => ({ fsPath: filePath })), @@ -30,10 +33,20 @@ vi.mock("fs/promises", () => ({ readFile: vi.fn(), mkdir: vi.fn(), writeFile: vi.fn(), + access: vi.fn(), + constants: { + F_OK: 0, + R_OK: 4, + }, }, readFile: vi.fn(), mkdir: vi.fn(), writeFile: vi.fn(), + access: vi.fn(), + constants: { + F_OK: 0, + R_OK: 4, + }, })) vi.mock("os", () => ({ @@ -43,6 +56,8 @@ vi.mock("os", () => ({ homedir: vi.fn(() => "/mock/home"), })) +vi.mock("../../../utils/safeWriteJson") + describe("importExport", () => { let mockProviderSettingsManager: ReturnType> let mockContextProxy: ReturnType> @@ -93,7 +108,7 @@ describe("importExport", () => { customModesManager: mockCustomModesManager, }) - expect(result).toEqual({ success: false }) + expect(result).toEqual({ success: false, error: "User cancelled file selection" }) expect(vscode.window.showOpenDialog).toHaveBeenCalledWith({ filters: { JSON: ["json"] }, @@ -143,9 +158,12 @@ describe("importExport", () => { expect(mockProviderSettingsManager.export).toHaveBeenCalled() expect(mockProviderSettingsManager.import).toHaveBeenCalledWith({ - ...previousProviderProfiles, currentApiConfigName: "test", - apiConfigs: { test: { apiProvider: "openai" as ProviderName, apiKey: "test-key", id: "test-id" } }, + apiConfigs: { + default: { apiProvider: "anthropic" as ProviderName, id: "default-id" }, + test: { apiProvider: "openai" as ProviderName, apiKey: "test-key", id: "test-id" }, + }, + modeApiConfigs: {}, }) expect(mockContextProxy.setValues).toHaveBeenCalledWith({ mode: "code", autoApprovalEnabled: true }) @@ -216,11 +234,12 @@ describe("importExport", () => { expect(fs.readFile).toHaveBeenCalledWith("/mock/path/settings.json", "utf-8") expect(mockProviderSettingsManager.export).toHaveBeenCalled() expect(mockProviderSettingsManager.import).toHaveBeenCalledWith({ - ...previousProviderProfiles, currentApiConfigName: "test", apiConfigs: { + default: { apiProvider: "anthropic" as ProviderName, id: "default-id" }, test: { apiProvider: "openai" as ProviderName, apiKey: "test-key", id: "test-id" }, }, + modeApiConfigs: {}, }) // Should call setValues with an empty object since globalSettings is missing. @@ -294,9 +313,11 @@ describe("importExport", () => { }) expect(result.success).toBe(true) - expect(result.providerProfiles?.apiConfigs["openai"]).toBeDefined() - expect(result.providerProfiles?.apiConfigs["default"]).toBeDefined() - expect(result.providerProfiles?.apiConfigs["default"].apiProvider).toBe("anthropic") + if (result.success && "providerProfiles" in result) { + expect(result.providerProfiles?.apiConfigs["openai"]).toBeDefined() + expect(result.providerProfiles?.apiConfigs["default"]).toBeDefined() + expect(result.providerProfiles?.apiConfigs["default"].apiProvider).toBe("anthropic") + } }) it("should call updateCustomMode for each custom mode in config", async () => { @@ -334,6 +355,87 @@ describe("importExport", () => { expect(mockCustomModesManager.updateCustomMode).toHaveBeenCalledWith(mode.slug, mode) }) }) + + it("should import settings from provided file path without showing dialog", async () => { + const filePath = "/mock/path/settings.json" + const mockFileContent = JSON.stringify({ + providerProfiles: { + currentApiConfigName: "test", + apiConfigs: { test: { apiProvider: "openai" as ProviderName, apiKey: "test-key", id: "test-id" } }, + }, + globalSettings: { mode: "code", autoApprovalEnabled: true }, + }) + + ;(fs.readFile as Mock).mockResolvedValue(mockFileContent) + ;(fs.access as Mock).mockResolvedValue(undefined) // File exists and is readable + + const previousProviderProfiles = { + currentApiConfigName: "default", + apiConfigs: { default: { apiProvider: "anthropic" as ProviderName, id: "default-id" } }, + } + + mockProviderSettingsManager.export.mockResolvedValue(previousProviderProfiles) + mockProviderSettingsManager.listConfig.mockResolvedValue([ + { name: "test", id: "test-id", apiProvider: "openai" as ProviderName }, + { name: "default", id: "default-id", apiProvider: "anthropic" as ProviderName }, + ]) + mockContextProxy.export.mockResolvedValue({ mode: "code" }) + + const result = await importSettingsFromFile( + { + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + customModesManager: mockCustomModesManager, + }, + vscode.Uri.file(filePath), + ) + + expect(vscode.window.showOpenDialog).not.toHaveBeenCalled() + expect(fs.readFile).toHaveBeenCalledWith(filePath, "utf-8") + expect(result.success).toBe(true) + expect(mockProviderSettingsManager.import).toHaveBeenCalledWith({ + currentApiConfigName: "test", + apiConfigs: { + default: { apiProvider: "anthropic" as ProviderName, id: "default-id" }, + test: { apiProvider: "openai" as ProviderName, apiKey: "test-key", id: "test-id" }, + }, + modeApiConfigs: {}, + }) + expect(mockContextProxy.setValues).toHaveBeenCalledWith({ mode: "code", autoApprovalEnabled: true }) + }) + + it("should return error when provided file path does not exist", async () => { + const filePath = "/nonexistent/path/settings.json" + const accessError = new Error("ENOENT: no such file or directory") + + ;(fs.access as Mock).mockRejectedValue(accessError) + + // Create a mock provider for the test + const mockProvider = { + settingsImportedAt: 0, + postStateToWebview: vi.fn().mockResolvedValue(undefined), + } + + // Mock the showErrorMessage to capture the error + const showErrorMessageSpy = vi.spyOn(vscode.window, "showErrorMessage").mockResolvedValue(undefined) + + await importSettingsWithFeedback( + { + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + customModesManager: mockCustomModesManager, + provider: mockProvider, + }, + filePath, + ) + + expect(vscode.window.showOpenDialog).not.toHaveBeenCalled() + expect(fs.access).toHaveBeenCalledWith(filePath, fs.constants.F_OK | fs.constants.R_OK) + expect(fs.readFile).not.toHaveBeenCalled() + expect(showErrorMessageSpy).toHaveBeenCalledWith(expect.stringContaining("errors.settings_import_failed")) + + showErrorMessageSpy.mockRestore() + }) }) describe("exportSettings", () => { @@ -384,11 +486,10 @@ describe("importExport", () => { expect(mockContextProxy.export).toHaveBeenCalled() expect(fs.mkdir).toHaveBeenCalledWith("/mock/path", { recursive: true }) - expect(fs.writeFile).toHaveBeenCalledWith( - "/mock/path/roo-code-settings.json", - JSON.stringify({ providerProfiles: mockProviderProfiles, globalSettings: mockGlobalSettings }, null, 2), - "utf-8", - ) + expect(safeWriteJson).toHaveBeenCalledWith("/mock/path/roo-code-settings.json", { + providerProfiles: mockProviderProfiles, + globalSettings: mockGlobalSettings, + }) }) it("should include globalSettings when allowedMaxRequests is null", async () => { @@ -417,11 +518,10 @@ describe("importExport", () => { contextProxy: mockContextProxy, }) - expect(fs.writeFile).toHaveBeenCalledWith( - "/mock/path/roo-code-settings.json", - JSON.stringify({ providerProfiles: mockProviderProfiles, globalSettings: mockGlobalSettings }, null, 2), - "utf-8", - ) + expect(safeWriteJson).toHaveBeenCalledWith("/mock/path/roo-code-settings.json", { + providerProfiles: mockProviderProfiles, + globalSettings: mockGlobalSettings, + }) }) it("should handle errors during the export process", async () => { @@ -436,7 +536,8 @@ describe("importExport", () => { }) mockContextProxy.export.mockResolvedValue({ mode: "code" }) - ;(fs.writeFile as Mock).mockRejectedValue(new Error("Write error")) + // Simulate an error during the safeWriteJson operation + ;(safeWriteJson as Mock).mockRejectedValueOnce(new Error("Safe write error")) await exportSettings({ providerSettingsManager: mockProviderSettingsManager, @@ -447,8 +548,10 @@ describe("importExport", () => { expect(mockProviderSettingsManager.export).toHaveBeenCalled() expect(mockContextProxy.export).toHaveBeenCalled() expect(fs.mkdir).toHaveBeenCalledWith("/mock/path", { recursive: true }) - expect(fs.writeFile).toHaveBeenCalled() + expect(safeWriteJson).toHaveBeenCalled() // safeWriteJson is called, but it will throw // The error is caught and the function exits silently. + // Optionally, ensure no error message was shown if that's part of "silent" + // expect(vscode.window.showErrorMessage).not.toHaveBeenCalled(); }) it("should handle errors during directory creation", async () => { @@ -474,7 +577,7 @@ describe("importExport", () => { expect(mockProviderSettingsManager.export).toHaveBeenCalled() expect(mockContextProxy.export).toHaveBeenCalled() expect(fs.mkdir).toHaveBeenCalled() - expect(fs.writeFile).not.toHaveBeenCalled() // Should not be called since mkdir failed. + expect(safeWriteJson).not.toHaveBeenCalled() // Should not be called since mkdir failed. }) it("should use the correct default save location", async () => { diff --git a/src/core/config/importExport.ts b/src/core/config/importExport.ts index 4830a5f987..65638ab95e 100644 --- a/src/core/config/importExport.ts +++ b/src/core/config/importExport.ts @@ -1,3 +1,4 @@ +import { safeWriteJson } from "../../utils/safeWriteJson" import os from "os" import * as path from "path" import fs from "fs/promises" @@ -11,6 +12,7 @@ import { TelemetryService } from "@roo-code/telemetry" import { ProviderSettingsManager, providerProfilesSchema } from "./ProviderSettingsManager" import { ContextProxy } from "./ContextProxy" import { CustomModesManager } from "./CustomModesManager" +import { t } from "../../i18n" type ImportOptions = { providerSettingsManager: ProviderSettingsManager @@ -23,6 +25,18 @@ type ExportOptions = { contextProxy: ContextProxy } +type ImportWithProviderOptions = ImportOptions & { + provider: { + settingsImportedAt?: number + postStateToWebview: () => Promise + } +} + +/** + * Import settings from a file using a file dialog + * @param options - Import options containing managers and proxy + * @returns Promise resolving to import result + */ export const importSettings = async ({ providerSettingsManager, contextProxy, customModesManager }: ImportOptions) => { const uris = await vscode.window.showOpenDialog({ filters: { JSON: ["json"] }, @@ -30,9 +44,22 @@ export const importSettings = async ({ providerSettingsManager, contextProxy, cu }) if (!uris) { - return { success: false } + return { success: false, error: "User cancelled file selection" } } + return await importSettingsFromFile({ providerSettingsManager, contextProxy, customModesManager }, uris[0]) +} + +/** + * Import settings from a specific file + * @param options - Import options containing managers and proxy + * @param fileUri - URI of the file to import from + * @returns Promise resolving to import result + */ +export const importSettingsFromFile = async ( + { providerSettingsManager, contextProxy, customModesManager }: ImportOptions, + fileUri: vscode.Uri, +) => { const schema = z.object({ providerProfiles: providerProfilesSchema, globalSettings: globalSettingsSchema.optional(), @@ -41,7 +68,7 @@ export const importSettings = async ({ providerSettingsManager, contextProxy, cu try { const previousProviderProfiles = await providerSettingsManager.export() - const data = JSON.parse(await fs.readFile(uris[0].fsPath, "utf-8")) + const data = JSON.parse(await fs.readFile(fileUri.fsPath, "utf-8")) const { providerProfiles: newProviderProfiles, globalSettings = {} } = schema.parse(data) const providerProfiles = { @@ -60,7 +87,7 @@ export const importSettings = async ({ providerSettingsManager, contextProxy, cu (globalSettings.customModes ?? []).map((mode) => customModesManager.updateCustomMode(mode.slug, mode)), ) - await providerSettingsManager.import(newProviderProfiles) + await providerSettingsManager.import(providerProfiles) await contextProxy.setValues(globalSettings) // Set the current provider. @@ -116,6 +143,47 @@ export const exportSettings = async ({ providerSettingsManager, contextProxy }: const dirname = path.dirname(uri.fsPath) await fs.mkdir(dirname, { recursive: true }) - await fs.writeFile(uri.fsPath, JSON.stringify({ providerProfiles, globalSettings }, null, 2), "utf-8") + await safeWriteJson(uri.fsPath, { providerProfiles, globalSettings }) } catch (e) {} } + +/** + * Import settings with complete UI feedback and provider state updates + * @param options - Import options with provider instance + * @param filePath - Optional file path to import from. If not provided, a file dialog will be shown. + * @returns Promise that resolves when import is complete + */ +export const importSettingsWithFeedback = async ( + { providerSettingsManager, contextProxy, customModesManager, provider }: ImportWithProviderOptions, + filePath?: string, +) => { + let result + + if (filePath) { + // Validate file path and check if file exists + try { + const fileUri = vscode.Uri.file(filePath) + // Check if file exists and is readable + await fs.access(fileUri.fsPath, fs.constants.F_OK | fs.constants.R_OK) + result = await importSettingsFromFile( + { providerSettingsManager, contextProxy, customModesManager }, + fileUri, + ) + } catch (error) { + result = { + success: false, + error: `Cannot access file at path "${filePath}": ${error instanceof Error ? error.message : "Unknown error"}`, + } + } + } else { + result = await importSettings({ providerSettingsManager, contextProxy, customModesManager }) + } + + if (result.success) { + provider.settingsImportedAt = Date.now() + await provider.postStateToWebview() + await vscode.window.showInformationMessage(t("common:info.settings_imported")) + } else if (result.error) { + await vscode.window.showErrorMessage(t("common:errors.settings_import_failed", { error: result.error })) + } +} diff --git a/src/core/context-tracking/FileContextTracker.ts b/src/core/context-tracking/FileContextTracker.ts index 323bb4122f..5741b62cfc 100644 --- a/src/core/context-tracking/FileContextTracker.ts +++ b/src/core/context-tracking/FileContextTracker.ts @@ -1,3 +1,4 @@ +import { safeWriteJson } from "../../utils/safeWriteJson" import * as path from "path" import * as vscode from "vscode" import { getTaskDirectoryPath } from "../../utils/storage" @@ -130,7 +131,7 @@ export class FileContextTracker { const globalStoragePath = this.getContextProxy()!.globalStorageUri.fsPath const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId) const filePath = path.join(taskDir, GlobalFileNames.taskMetadata) - await fs.writeFile(filePath, JSON.stringify(metadata, null, 2)) + await safeWriteJson(filePath, metadata) } catch (error) { console.error("Failed to save task metadata:", error) } diff --git a/src/core/prompts/sections/__tests__/custom-instructions-global.spec.ts b/src/core/prompts/sections/__tests__/custom-instructions-global.spec.ts new file mode 100644 index 0000000000..75896f31dd --- /dev/null +++ b/src/core/prompts/sections/__tests__/custom-instructions-global.spec.ts @@ -0,0 +1,230 @@ +import * as path from "path" +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest" + +// Use vi.hoisted to ensure mocks are available during hoisting +const { mockHomedir, mockStat, mockReadFile, mockReaddir, mockGetRooDirectoriesForCwd, mockGetGlobalRooDirectory } = + vi.hoisted(() => ({ + mockHomedir: vi.fn(), + mockStat: vi.fn(), + mockReadFile: vi.fn(), + mockReaddir: vi.fn(), + mockGetRooDirectoriesForCwd: vi.fn(), + mockGetGlobalRooDirectory: vi.fn(), + })) + +// Mock os module +vi.mock("os", () => ({ + default: { + homedir: mockHomedir, + }, + homedir: mockHomedir, +})) + +// Mock fs/promises +vi.mock("fs/promises", () => ({ + default: { + stat: mockStat, + readFile: mockReadFile, + readdir: mockReaddir, + }, +})) + +// Mock the roo-config service +vi.mock("../../../../services/roo-config", () => ({ + getRooDirectoriesForCwd: mockGetRooDirectoriesForCwd, + getGlobalRooDirectory: mockGetGlobalRooDirectory, +})) + +import { loadRuleFiles, addCustomInstructions } from "../custom-instructions" + +describe("custom-instructions global .roo support", () => { + const mockCwd = "/mock/project" + const mockHomeDir = "/mock/home" + const globalRooDir = path.join(mockHomeDir, ".roo") + const projectRooDir = path.join(mockCwd, ".roo") + + beforeEach(() => { + vi.clearAllMocks() + mockHomedir.mockReturnValue(mockHomeDir) + mockGetRooDirectoriesForCwd.mockReturnValue([globalRooDir, projectRooDir]) + mockGetGlobalRooDirectory.mockReturnValue(globalRooDir) + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + describe("loadRuleFiles", () => { + it("should load global rules only when project rules do not exist", async () => { + // Mock directory existence checks in order: + // 1. Check if global rules dir exists + // 2. Check if project rules dir doesn't exist + mockStat + .mockResolvedValueOnce({ isDirectory: () => true } as any) // global rules dir exists + .mockResolvedValueOnce({ isFile: () => true } as any) // for the file check inside readTextFilesFromDirectory + .mockRejectedValueOnce(new Error("ENOENT")) // project rules dir doesn't exist + + // Mock directory reading for global rules + mockReaddir.mockResolvedValueOnce([ + { name: "rules.md", isFile: () => true, isSymbolicLink: () => false } as any, + ]) + + // Mock file reading for the rules.md file + mockReadFile.mockResolvedValueOnce("global rule content") + + const result = await loadRuleFiles(mockCwd) + + expect(result).toContain("# Rules from") + expect(result).toContain("rules.md:") + expect(result).toContain("global rule content") + expect(result).not.toContain("project rule content") + }) + + it("should load project rules only when global rules do not exist", async () => { + // Mock directory existence + mockStat + .mockRejectedValueOnce(new Error("ENOENT")) // global rules dir doesn't exist + .mockResolvedValueOnce({ isDirectory: () => true } as any) // project rules dir exists + + // Mock directory reading for project rules + mockReaddir.mockResolvedValueOnce([ + { name: "rules.md", isFile: () => true, isSymbolicLink: () => false } as any, + ]) + + // Mock file reading + mockStat.mockResolvedValueOnce({ isFile: () => true } as any) // for the file check + mockReadFile.mockResolvedValueOnce("project rule content") + + const result = await loadRuleFiles(mockCwd) + + expect(result).toContain("# Rules from") + expect(result).toContain("rules.md:") + expect(result).toContain("project rule content") + expect(result).not.toContain("global rule content") + }) + + it("should merge global and project rules with project rules after global", async () => { + // Mock directory existence - both exist + mockStat + .mockResolvedValueOnce({ isDirectory: () => true } as any) // global rules dir exists + .mockResolvedValueOnce({ isFile: () => true } as any) // global file check + .mockResolvedValueOnce({ isDirectory: () => true } as any) // project rules dir exists + .mockResolvedValueOnce({ isFile: () => true } as any) // project file check + + // Mock directory reading + mockReaddir + .mockResolvedValueOnce([{ name: "global.md", isFile: () => true, isSymbolicLink: () => false } as any]) + .mockResolvedValueOnce([{ name: "project.md", isFile: () => true, isSymbolicLink: () => false } as any]) + + // Mock file reading + mockReadFile.mockResolvedValueOnce("global rule content").mockResolvedValueOnce("project rule content") + + const result = await loadRuleFiles(mockCwd) + + expect(result).toContain("# Rules from") + expect(result).toContain("global.md:") + expect(result).toContain("global rule content") + expect(result).toContain("project.md:") + expect(result).toContain("project rule content") + + // Ensure project rules come after global rules + const globalIndex = result.indexOf("global rule content") + const projectIndex = result.indexOf("project rule content") + expect(globalIndex).toBeLessThan(projectIndex) + }) + + it("should fall back to legacy .roorules file when no .roo/rules directories exist", async () => { + // Mock directory existence - neither exist + mockStat + .mockRejectedValueOnce(new Error("ENOENT")) // global rules dir doesn't exist + .mockRejectedValueOnce(new Error("ENOENT")) // project rules dir doesn't exist + + // Mock legacy file reading + mockReadFile.mockResolvedValueOnce("legacy rule content") + + const result = await loadRuleFiles(mockCwd) + + expect(result).toContain("# Rules from .roorules:") + expect(result).toContain("legacy rule content") + }) + + it("should return empty string when no rules exist anywhere", async () => { + // Mock directory existence - neither exist + mockStat + .mockRejectedValueOnce(new Error("ENOENT")) // global rules dir doesn't exist + .mockRejectedValueOnce(new Error("ENOENT")) // project rules dir doesn't exist + + // Mock legacy file reading - both fail (using safeReadFile which catches errors) + // The safeReadFile function catches ENOENT errors and returns empty string + // So we don't need to mock rejections, just empty responses + mockReadFile + .mockResolvedValueOnce("") // .roorules returns empty (simulating ENOENT caught by safeReadFile) + .mockResolvedValueOnce("") // .clinerules returns empty (simulating ENOENT caught by safeReadFile) + + const result = await loadRuleFiles(mockCwd) + + expect(result).toBe("") + }) + }) + + describe("addCustomInstructions mode-specific rules", () => { + it("should load global and project mode-specific rules", async () => { + const mode = "code" + + // Mock directory existence for mode-specific rules + mockStat + .mockResolvedValueOnce({ isDirectory: () => true } as any) // global rules-code dir exists + .mockResolvedValueOnce({ isFile: () => true } as any) // global mode file check + .mockResolvedValueOnce({ isDirectory: () => true } as any) // project rules-code dir exists + .mockResolvedValueOnce({ isFile: () => true } as any) // project mode file check + .mockRejectedValueOnce(new Error("ENOENT")) // global rules dir doesn't exist (for generic rules) + .mockRejectedValueOnce(new Error("ENOENT")) // project rules dir doesn't exist (for generic rules) + + // Mock directory reading for mode-specific rules + mockReaddir + .mockResolvedValueOnce([ + { name: "global-mode.md", isFile: () => true, isSymbolicLink: () => false } as any, + ]) + .mockResolvedValueOnce([ + { name: "project-mode.md", isFile: () => true, isSymbolicLink: () => false } as any, + ]) + + // Mock file reading for mode-specific rules + mockReadFile + .mockResolvedValueOnce("global mode rule content") + .mockResolvedValueOnce("project mode rule content") + .mockResolvedValueOnce("") // .roorules legacy file (empty) + .mockResolvedValueOnce("") // .clinerules legacy file (empty) + + const result = await addCustomInstructions("", "", mockCwd, mode) + + expect(result).toContain("# Rules from") + expect(result).toContain("global-mode.md:") + expect(result).toContain("global mode rule content") + expect(result).toContain("project-mode.md:") + expect(result).toContain("project mode rule content") + }) + + it("should fall back to legacy mode-specific files when no mode directories exist", async () => { + const mode = "code" + + // Mock directory existence - mode-specific dirs don't exist + mockStat + .mockRejectedValueOnce(new Error("ENOENT")) // global rules-code dir doesn't exist + .mockRejectedValueOnce(new Error("ENOENT")) // project rules-code dir doesn't exist + .mockRejectedValueOnce(new Error("ENOENT")) // global rules dir doesn't exist + .mockRejectedValueOnce(new Error("ENOENT")) // project rules dir doesn't exist + + // Mock legacy mode file reading + mockReadFile + .mockResolvedValueOnce("legacy mode rule content") // .roorules-code + .mockResolvedValueOnce("") // generic .roorules (empty) + .mockResolvedValueOnce("") // generic .clinerules (empty) + + const result = await addCustomInstructions("", "", mockCwd, mode) + + expect(result).toContain("# Rules from .roorules-code:") + expect(result).toContain("legacy mode rule content") + }) + }) +}) diff --git a/src/core/prompts/sections/__tests__/custom-instructions-path-detection.spec.ts b/src/core/prompts/sections/__tests__/custom-instructions-path-detection.spec.ts new file mode 100644 index 0000000000..53272a112b --- /dev/null +++ b/src/core/prompts/sections/__tests__/custom-instructions-path-detection.spec.ts @@ -0,0 +1,66 @@ +import { describe, it, expect, vi } from "vitest" +import * as os from "os" +import * as path from "path" + +describe("custom-instructions path detection", () => { + it("should use exact path comparison instead of string includes", () => { + // Test the logic that our fix implements + const fakeHomeDir = "/Users/john.roo.smith" + const globalRooDir = path.join(fakeHomeDir, ".roo") // "/Users/john.roo.smith/.roo" + const projectRooDir = "/projects/my-project/.roo" + + // Old implementation (fragile): + // const isGlobal = rooDir.includes(path.join(os.homedir(), ".roo")) + // This could fail if the home directory path contains ".roo" elsewhere + + // New implementation (robust): + // const isGlobal = path.resolve(rooDir) === path.resolve(getGlobalRooDirectory()) + + // Test the new logic + const isGlobalForGlobalDir = path.resolve(globalRooDir) === path.resolve(globalRooDir) + const isGlobalForProjectDir = path.resolve(projectRooDir) === path.resolve(globalRooDir) + + expect(isGlobalForGlobalDir).toBe(true) + expect(isGlobalForProjectDir).toBe(false) + + // Verify that the old implementation would have been problematic + // if the home directory contained ".roo" in the path + const oldLogicGlobal = globalRooDir.includes(path.join(fakeHomeDir, ".roo")) + const oldLogicProject = projectRooDir.includes(path.join(fakeHomeDir, ".roo")) + + expect(oldLogicGlobal).toBe(true) // This works + expect(oldLogicProject).toBe(false) // This also works, but is fragile + + // The issue was that if the home directory path itself contained ".roo", + // the includes() check could produce false positives in edge cases + }) + + it("should handle edge cases with path resolution", () => { + // Test various edge cases that exact path comparison handles better + const testCases = [ + { + global: "/Users/test/.roo", + project: "/Users/test/project/.roo", + expected: { global: true, project: false }, + }, + { + global: "/home/user/.roo", + project: "/home/user/.roo", // Same directory + expected: { global: true, project: true }, + }, + { + global: "/Users/john.roo.smith/.roo", + project: "/projects/app/.roo", + expected: { global: true, project: false }, + }, + ] + + testCases.forEach(({ global, project, expected }) => { + const isGlobalForGlobal = path.resolve(global) === path.resolve(global) + const isGlobalForProject = path.resolve(project) === path.resolve(global) + + expect(isGlobalForGlobal).toBe(expected.global) + expect(isGlobalForProject).toBe(expected.project) + }) + }) +}) diff --git a/src/core/prompts/sections/custom-instructions.ts b/src/core/prompts/sections/custom-instructions.ts index f9f4b7dea0..0e1ddfd24f 100644 --- a/src/core/prompts/sections/custom-instructions.ts +++ b/src/core/prompts/sections/custom-instructions.ts @@ -1,10 +1,12 @@ import fs from "fs/promises" import path from "path" +import * as os from "os" import { Dirent } from "fs" import { isLanguage } from "@roo-code/types" import { LANGUAGES } from "../../../shared/language" +import { getRooDirectoriesForCwd, getGlobalRooDirectory } from "../../../services/roo-config" /** * Safely read a file and return its trimmed content @@ -144,30 +146,39 @@ async function readTextFilesFromDirectory(dirPath: string): Promise): string { if (files.length === 0) return "" - return ( - "\n\n" + - files - .map((file) => { - return `# Rules from ${file.filename}:\n${file.content}` - }) - .join("\n\n") - ) + return files + .map((file) => { + return `# Rules from ${file.filename}:\n${file.content}` + }) + .join("\n\n") } /** - * Load rule files from the specified directory + * Load rule files from global and project-local directories + * Global rules are loaded first, then project-local rules which can override global ones */ export async function loadRuleFiles(cwd: string): Promise { - // Check for .roo/rules/ directory - const rooRulesDir = path.join(cwd, ".roo", "rules") - if (await directoryExists(rooRulesDir)) { - const files = await readTextFilesFromDirectory(rooRulesDir) - if (files.length > 0) { - return formatDirectoryContent(rooRulesDir, files) + const rules: string[] = [] + const rooDirectories = getRooDirectoriesForCwd(cwd) + + // Check for .roo/rules/ directories in order (global first, then project-local) + for (const rooDir of rooDirectories) { + const rulesDir = path.join(rooDir, "rules") + if (await directoryExists(rulesDir)) { + const files = await readTextFilesFromDirectory(rulesDir) + if (files.length > 0) { + const content = formatDirectoryContent(rulesDir, files) + rules.push(content) + } } } - // Fall back to existing behavior + // If we found rules in .roo/rules/ directories, return them + if (rules.length > 0) { + return "\n" + rules.join("\n\n") + } + + // Fall back to existing behavior for legacy .roorules/.clinerules files const ruleFiles = [".roorules", ".clinerules"] for (const file of ruleFiles) { @@ -194,18 +205,27 @@ export async function addCustomInstructions( let usedRuleFile = "" if (mode) { - // Check for .roo/rules-${mode}/ directory - const modeRulesDir = path.join(cwd, ".roo", `rules-${mode}`) - if (await directoryExists(modeRulesDir)) { - const files = await readTextFilesFromDirectory(modeRulesDir) - if (files.length > 0) { - modeRuleContent = formatDirectoryContent(modeRulesDir, files) - usedRuleFile = modeRulesDir + const modeRules: string[] = [] + const rooDirectories = getRooDirectoriesForCwd(cwd) + + // Check for .roo/rules-${mode}/ directories in order (global first, then project-local) + for (const rooDir of rooDirectories) { + const modeRulesDir = path.join(rooDir, `rules-${mode}`) + if (await directoryExists(modeRulesDir)) { + const files = await readTextFilesFromDirectory(modeRulesDir) + if (files.length > 0) { + const content = formatDirectoryContent(modeRulesDir, files) + modeRules.push(content) + } } } - // If no directory exists, fall back to existing behavior - if (!modeRuleContent) { + // If we found mode-specific rules in .roo/rules-${mode}/ directories, use them + if (modeRules.length > 0) { + modeRuleContent = "\n" + modeRules.join("\n\n") + usedRuleFile = `rules-${mode} directories` + } else { + // Fall back to existing behavior for legacy files const rooModeRuleFile = `.roorules-${mode}` modeRuleContent = await safeReadFile(path.join(cwd, rooModeRuleFile)) if (modeRuleContent) { diff --git a/src/core/prompts/sections/mcp-servers.ts b/src/core/prompts/sections/mcp-servers.ts index 0af850cbb1..643233ab6f 100644 --- a/src/core/prompts/sections/mcp-servers.ts +++ b/src/core/prompts/sections/mcp-servers.ts @@ -39,7 +39,7 @@ export async function getMcpServersSection( const config = JSON.parse(server.config) return ( - `## ${server.name} (\`${config.command}${config.args && Array.isArray(config.args) ? ` ${config.args.join(" ")}` : ""}\`)` + + `## ${server.name}${config.command ? ` (\`${config.command}${config.args && Array.isArray(config.args) ? ` ${config.args.join(" ")}` : ""}\`)` : ""}` + (server.instructions ? `\n\n### Instructions\n${server.instructions}` : "") + (tools ? `\n\n### Available Tools\n${tools}` : "") + (templates ? `\n\n### Resource Templates\n${templates}` : "") + diff --git a/src/core/task-persistence/apiMessages.ts b/src/core/task-persistence/apiMessages.ts index d6c17bd9b3..f846aaf13f 100644 --- a/src/core/task-persistence/apiMessages.ts +++ b/src/core/task-persistence/apiMessages.ts @@ -1,3 +1,4 @@ +import { safeWriteJson } from "../../utils/safeWriteJson" import * as path from "path" import * as fs from "fs/promises" @@ -78,5 +79,5 @@ export async function saveApiMessages({ }) { const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId) const filePath = path.join(taskDir, GlobalFileNames.apiConversationHistory) - await fs.writeFile(filePath, JSON.stringify(messages)) + await safeWriteJson(filePath, messages) } diff --git a/src/core/task-persistence/taskMessages.ts b/src/core/task-persistence/taskMessages.ts index 3ed5c5099e..63a2eefbaa 100644 --- a/src/core/task-persistence/taskMessages.ts +++ b/src/core/task-persistence/taskMessages.ts @@ -1,3 +1,4 @@ +import { safeWriteJson } from "../../utils/safeWriteJson" import * as path from "path" import * as fs from "fs/promises" @@ -37,5 +38,5 @@ export type SaveTaskMessagesOptions = { export async function saveTaskMessages({ messages, taskId, globalStoragePath }: SaveTaskMessagesOptions) { const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId) const filePath = path.join(taskDir, GlobalFileNames.uiMessages) - await fs.writeFile(filePath, JSON.stringify(messages)) + await safeWriteJson(filePath, messages) } diff --git a/src/core/task-persistence/taskMetadata.ts b/src/core/task-persistence/taskMetadata.ts index 8044acd8ba..1759a72f47 100644 --- a/src/core/task-persistence/taskMetadata.ts +++ b/src/core/task-persistence/taskMetadata.ts @@ -8,6 +8,7 @@ import { combineCommandSequences } from "../../shared/combineCommandSequences" import { getApiMetrics } from "../../shared/getApiMetrics" import { findLastIndex } from "../../shared/array" import { getTaskDirectoryPath } from "../../utils/storage" +import { t } from "../../i18n" const taskSizeCache = new NodeCache({ stdTTL: 30, checkperiod: 5 * 60 }) @@ -27,29 +28,63 @@ export async function taskMetadata({ workspace, }: TaskMetadataOptions) { const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId) - const taskMessage = messages[0] // First message is always the task say. - const lastRelevantMessage = - messages[findLastIndex(messages, (m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task"))] + // Determine message availability upfront + const hasMessages = messages && messages.length > 0 - let taskDirSize = taskSizeCache.get(taskDir) + // Pre-calculate all values based on availability + let timestamp: number + let tokenUsage: ReturnType + let taskDirSize: number + let taskMessage: ClineMessage | undefined - if (taskDirSize === undefined) { - try { - taskDirSize = await getFolderSize.loose(taskDir) - taskSizeCache.set(taskDir, taskDirSize) - } catch (error) { - taskDirSize = 0 + if (!hasMessages) { + // Handle no messages case + timestamp = Date.now() + tokenUsage = { + totalTokensIn: 0, + totalTokensOut: 0, + totalCacheWrites: 0, + totalCacheReads: 0, + totalCost: 0, + contextTokens: 0, + } + taskDirSize = 0 + } else { + // Handle messages case + taskMessage = messages[0] // First message is always the task say. + + const lastRelevantMessage = + messages[findLastIndex(messages, (m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task"))] || + taskMessage + + timestamp = lastRelevantMessage.ts + + tokenUsage = getApiMetrics(combineApiRequests(combineCommandSequences(messages.slice(1)))) + + // Get task directory size + const cachedSize = taskSizeCache.get(taskDir) + + if (cachedSize === undefined) { + try { + taskDirSize = await getFolderSize.loose(taskDir) + taskSizeCache.set(taskDir, taskDirSize) + } catch (error) { + taskDirSize = 0 + } + } else { + taskDirSize = cachedSize } } - const tokenUsage = getApiMetrics(combineApiRequests(combineCommandSequences(messages.slice(1)))) - + // Create historyItem once with pre-calculated values const historyItem: HistoryItem = { id: taskId, number: taskNumber, - ts: lastRelevantMessage.ts, - task: taskMessage.text ?? "", + ts: timestamp, + task: hasMessages + ? taskMessage!.text?.trim() || t("common:tasks.incomplete", { taskNumber }) + : t("common:tasks.no_messages", { taskNumber }), tokensIn: tokenUsage.totalTokensIn, tokensOut: tokenUsage.totalTokensOut, cacheWrites: tokenUsage.totalCacheWrites, diff --git a/src/core/tools/writeToFileTool.ts b/src/core/tools/writeToFileTool.ts index d4469e9099..84f8ef807e 100644 --- a/src/core/tools/writeToFileTool.ts +++ b/src/core/tools/writeToFileTool.ts @@ -73,11 +73,11 @@ export async function writeToFileTool( // pre-processing newContent for cases where weaker models might add artifacts like markdown codeblock markers (deepseek/llama) or extra escape characters (gemini) if (newContent.startsWith("```")) { // cline handles cases where it includes language specifiers like ```python ```js - newContent = newContent.split("\n").slice(1).join("\n").trim() + newContent = newContent.split("\n").slice(1).join("\n") } if (newContent.endsWith("```")) { - newContent = newContent.split("\n").slice(0, -1).join("\n").trim() + newContent = newContent.split("\n").slice(0, -1).join("\n") } if (!cline.api.getModel().id.includes("claude")) { diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index dff2263a06..51cb9a275b 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -30,7 +30,7 @@ import { ORGANIZATION_ALLOW_ALL, } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" -import { CloudService } from "@roo-code/cloud" +import { CloudService, getRooCodeApiUrl } from "@roo-code/cloud" import { t } from "../../i18n" import { setPanel } from "../../activate/registerCommands" @@ -68,6 +68,7 @@ import { webviewMessageHandler } from "./webviewMessageHandler" import { WebviewMessage } from "../../shared/WebviewMessage" import { EMBEDDING_MODEL_PROFILES } from "../../shared/embeddingModels" import { ProfileValidator } from "../../shared/ProfileValidator" +import { getWorkspaceGitInfo } from "../../utils/git" /** * https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts @@ -1303,6 +1304,38 @@ export class ClineProvider return await fileExistsAtPath(promptFilePath) } + /** + * Merges allowed commands from global state and workspace configuration + * with proper validation and deduplication + */ + private mergeAllowedCommands(globalStateCommands?: string[]): string[] { + try { + // Validate and sanitize global state commands + const validGlobalCommands = Array.isArray(globalStateCommands) + ? globalStateCommands.filter((cmd) => typeof cmd === "string" && cmd.trim().length > 0) + : [] + + // Get workspace configuration commands + const workspaceCommands = + vscode.workspace.getConfiguration(Package.name).get("allowedCommands") || [] + + // Validate and sanitize workspace commands + const validWorkspaceCommands = Array.isArray(workspaceCommands) + ? workspaceCommands.filter((cmd) => typeof cmd === "string" && cmd.trim().length > 0) + : [] + + // Combine and deduplicate commands + // Global state takes precedence over workspace configuration + const mergedCommands = [...new Set([...validGlobalCommands, ...validWorkspaceCommands])] + + return mergedCommands + } catch (error) { + console.error("Error merging allowed commands:", error) + // Return empty array as fallback to prevent crashes + return [] + } + } + async getStateToPostToWebview() { const { apiConfiguration, @@ -1314,6 +1347,7 @@ export class ClineProvider alwaysAllowWriteOutsideWorkspace, alwaysAllowWriteProtected, alwaysAllowExecute, + allowedCommands, alwaysAllowBrowser, alwaysAllowMcp, alwaysAllowModeSwitch, @@ -1381,7 +1415,7 @@ export class ClineProvider const telemetryKey = process.env.POSTHOG_API_KEY const machineId = vscode.env.machineId - const allowedCommands = vscode.workspace.getConfiguration(Package.name).get("allowedCommands") || [] + const mergedAllowedCommands = this.mergeAllowedCommands(allowedCommands) const cwd = this.cwd // Check if there's a system prompt override for the current mode @@ -1420,7 +1454,7 @@ export class ClineProvider enableCheckpoints: enableCheckpoints ?? true, shouldShowAnnouncement: telemetrySetting !== "unset" && lastShownAnnouncementId !== this.latestAnnouncementId, - allowedCommands, + allowedCommands: mergedAllowedCommands, soundVolume: soundVolume ?? 0.5, browserViewportSize: browserViewportSize ?? "900x600", screenshotQuality: screenshotQuality ?? 75, @@ -1485,6 +1519,8 @@ export class ClineProvider }, mdmCompliant: this.checkMdmCompliance(), profileThresholds: profileThresholds ?? {}, + cloudApiUrl: getRooCodeApiUrl(), + hasOpenedModeSelector: this.getGlobalState("hasOpenedModeSelector") ?? false, } } @@ -1750,7 +1786,7 @@ export class ClineProvider /** * Returns properties to be included in every telemetry event * This method is called by the telemetry service to get context information - * like the current mode, API provider, etc. + * like the current mode, API provider, git repository information, etc. */ public async getTelemetryProperties(): Promise { const { mode, apiConfiguration, language } = await this.getState() @@ -1758,6 +1794,22 @@ export class ClineProvider const packageJSON = this.context.extension?.packageJSON + // Get Roo Code Cloud authentication state + let cloudIsAuthenticated: boolean | undefined + + try { + if (CloudService.hasInstance()) { + cloudIsAuthenticated = CloudService.instance.isAuthenticated() + } + } catch (error) { + // Silently handle errors to avoid breaking telemetry collection + this.log(`[getTelemetryProperties] Failed to get cloud auth state: ${error}`) + } + + // Get git repository information + const gitInfo = await getWorkspaceGitInfo() + + // Return all properties including git info - clients will filter as needed return { appName: packageJSON?.name ?? Package.name, appVersion: packageJSON?.version ?? Package.version, @@ -1770,6 +1822,8 @@ export class ClineProvider modelId: task?.api?.getModel().id, diffStrategy: task?.diffStrategy?.getName(), isSubtask: task ? !!task.parentTask : undefined, + cloudIsAuthenticated, + ...gitInfo, } } } diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index fabb0aae60..801c6c4774 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -13,6 +13,7 @@ import { experimentDefault } from "../../../shared/experiments" import { setTtsEnabled } from "../../../utils/tts" import { ContextProxy } from "../../config/ContextProxy" import { Task, TaskOptions } from "../../task/Task" +import { safeWriteJson } from "../../../utils/safeWriteJson" import { ClineProvider } from "../ClineProvider" @@ -43,6 +44,8 @@ vi.mock("axios", () => ({ post: vi.fn(), })) +vi.mock("../../../utils/safeWriteJson") + vi.mock("@modelcontextprotocol/sdk/types.js", () => ({ CallToolResultSchema: {}, ListResourcesResultSchema: {}, @@ -308,6 +311,18 @@ vi.mock("../diff/strategies/multi-search-replace", () => ({ })), })) +vi.mock("@roo-code/cloud", () => ({ + CloudService: { + hasInstance: vi.fn().mockReturnValue(true), + get instance() { + return { + isAuthenticated: vi.fn().mockReturnValue(false), + } + }, + }, + getRooCodeApiUrl: vi.fn().mockReturnValue("https://app.roocode.com"), +})) + afterAll(() => { vi.restoreAllMocks() }) @@ -523,6 +538,7 @@ describe("ClineProvider", () => { cloudIsAuthenticated: false, sharingEnabled: false, profileThresholds: {}, + hasOpenedModeSelector: false, } const message: ExtensionMessage = { @@ -1976,11 +1992,8 @@ describe("Project MCP Settings", () => { // Check that fs.mkdir was called with the correct path expect(mockedFs.mkdir).toHaveBeenCalledWith("/test/workspace/.roo", { recursive: true }) - // Check that fs.writeFile was called with default content - expect(mockedFs.writeFile).toHaveBeenCalledWith( - "/test/workspace/.roo/mcp.json", - JSON.stringify({ mcpServers: {} }, null, 2), - ) + // Verify file was created with default content + expect(safeWriteJson).toHaveBeenCalledWith("/test/workspace/.roo/mcp.json", { mcpServers: {} }) // Check that openFile was called expect(openFileSpy).toHaveBeenCalledWith("/test/workspace/.roo/mcp.json") @@ -2089,6 +2102,11 @@ describe("getTelemetryProperties", () => { // Reset mocks vi.clearAllMocks() + // Initialize TelemetryService if not already initialized + if (!TelemetryService.hasInstance()) { + TelemetryService.createInstance([]) + } + // Setup basic mocks mockContext = { globalState: { @@ -2142,6 +2160,96 @@ describe("getTelemetryProperties", () => { expect(properties).toHaveProperty("modelId", "claude-sonnet-4-20250514") }) + + describe("cloud authentication telemetry", () => { + beforeEach(() => { + // Reset all mocks before each test + vi.clearAllMocks() + }) + + test("includes cloud authentication property when user is authenticated", async () => { + // Import the CloudService mock and update it + const { CloudService } = await import("@roo-code/cloud") + const mockCloudService = { + isAuthenticated: vi.fn().mockReturnValue(true), + } + + // Update the existing mock + Object.defineProperty(CloudService, "instance", { + get: vi.fn().mockReturnValue(mockCloudService), + configurable: true, + }) + + const properties = await provider.getTelemetryProperties() + + expect(properties).toHaveProperty("cloudIsAuthenticated", true) + }) + + test("includes cloud authentication property when user is not authenticated", async () => { + // Import the CloudService mock and update it + const { CloudService } = await import("@roo-code/cloud") + const mockCloudService = { + isAuthenticated: vi.fn().mockReturnValue(false), + } + + // Update the existing mock + Object.defineProperty(CloudService, "instance", { + get: vi.fn().mockReturnValue(mockCloudService), + configurable: true, + }) + + const properties = await provider.getTelemetryProperties() + + expect(properties).toHaveProperty("cloudIsAuthenticated", false) + }) + + test("handles CloudService errors gracefully", async () => { + // Import the CloudService mock and update it to throw an error + const { CloudService } = await import("@roo-code/cloud") + Object.defineProperty(CloudService, "instance", { + get: vi.fn().mockImplementation(() => { + throw new Error("CloudService not available") + }), + configurable: true, + }) + + const properties = await provider.getTelemetryProperties() + + // Should still include basic telemetry properties + expect(properties).toHaveProperty("vscodeVersion") + expect(properties).toHaveProperty("platform") + expect(properties).toHaveProperty("appVersion", "1.0.0") + + // Cloud property should be undefined when CloudService is not available + expect(properties).toHaveProperty("cloudIsAuthenticated", undefined) + }) + + test("handles CloudService method errors gracefully", async () => { + // Import the CloudService mock and update it + const { CloudService } = await import("@roo-code/cloud") + const mockCloudService = { + isAuthenticated: vi.fn().mockImplementation(() => { + throw new Error("Authentication check error") + }), + } + + // Update the existing mock + Object.defineProperty(CloudService, "instance", { + get: vi.fn().mockReturnValue(mockCloudService), + configurable: true, + }) + + const properties = await provider.getTelemetryProperties() + + // Should still include basic telemetry properties + expect(properties).toHaveProperty("vscodeVersion") + expect(properties).toHaveProperty("platform") + expect(properties).toHaveProperty("appVersion", "1.0.0") + + // Property that errored should be undefined + expect(properties).toHaveProperty("cloudIsAuthenticated", undefined) + }) + }) }) describe("ClineProvider - Router Models", () => { diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 8bf2f6b95a..cac94aa0ce 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1,3 +1,4 @@ +import { safeWriteJson } from "../../utils/safeWriteJson" import * as path from "path" import fs from "fs/promises" import pWaitFor from "p-wait-for" @@ -27,7 +28,7 @@ import { fileExistsAtPath } from "../../utils/fs" import { playTts, setTtsEnabled, setTtsSpeed, stopTts } from "../../utils/tts" import { singleCompletionHandler } from "../../utils/single-completion-handler" import { searchCommits } from "../../utils/git" -import { exportSettings, importSettings } from "../config/importExport" +import { exportSettings, importSettingsWithFeedback } from "../config/importExport" import { getOpenAiModels } from "../../api/providers/openai" import { getVsCodeLmModels } from "../../api/providers/vscode-lm" import { openMention } from "../mentions" @@ -225,6 +226,7 @@ export const webviewMessageHandler = async ( break case "shareCurrentTask": const shareTaskId = provider.getCurrentCline()?.taskId + const clineMessages = provider.getCurrentCline()?.clineMessages if (!shareTaskId) { vscode.window.showErrorMessage(t("common:errors.share_no_active_task")) break @@ -232,7 +234,7 @@ export const webviewMessageHandler = async ( try { const visibility = message.visibility || "organization" - const result = await CloudService.instance.shareTask(shareTaskId, visibility) + const result = await CloudService.instance.shareTask(shareTaskId, visibility, clineMessages) if (result.success && result.shareUrl) { // Show success notification @@ -241,6 +243,13 @@ export const webviewMessageHandler = async ( ? "common:info.public_share_link_copied" : "common:info.organization_share_link_copied" vscode.window.showInformationMessage(t(messageKey)) + + // Send success feedback to webview for inline display + await provider.postMessageToWebview({ + type: "shareTaskSuccess", + visibility, + text: result.shareUrl, + }) } else { // Handle error const errorMessage = result.error || "Failed to create share link" @@ -316,20 +325,13 @@ export const webviewMessageHandler = async ( provider.exportTaskWithId(message.text!) break case "importSettings": { - const result = await importSettings({ + await importSettingsWithFeedback({ providerSettingsManager: provider.providerSettingsManager, contextProxy: provider.contextProxy, customModesManager: provider.customModesManager, + provider: provider, }) - if (result.success) { - provider.settingsImportedAt = Date.now() - await provider.postStateToWebview() - await vscode.window.showInformationMessage(t("common:info.settings_imported")) - } else if (result.error) { - await vscode.window.showErrorMessage(t("common:errors.settings_import_failed", { error: result.error })) - } - break } case "exportSettings": @@ -558,15 +560,22 @@ export const webviewMessageHandler = async ( case "cancelTask": await provider.cancelTask() break - case "allowedCommands": - await provider.context.globalState.update("allowedCommands", message.commands) + case "allowedCommands": { + // Validate and sanitize the commands array + const commands = message.commands ?? [] + const validCommands = Array.isArray(commands) + ? commands.filter((cmd) => typeof cmd === "string" && cmd.trim().length > 0) + : [] + + await updateGlobalState("allowedCommands", validCommands) // Also update workspace settings. await vscode.workspace .getConfiguration(Package.name) - .update("allowedCommands", message.commands, vscode.ConfigurationTarget.Global) + .update("allowedCommands", validCommands, vscode.ConfigurationTarget.Global) break + } case "openCustomModesSettings": { const customModesFilePath = await provider.customModesManager.getCustomModesFilePath() @@ -600,7 +609,7 @@ export const webviewMessageHandler = async ( const exists = await fileExistsAtPath(mcpPath) if (!exists) { - await fs.writeFile(mcpPath, JSON.stringify({ mcpServers: {} }, null, 2)) + await safeWriteJson(mcpPath, { mcpServers: {} }) } await openFile(mcpPath) @@ -948,8 +957,27 @@ export const webviewMessageHandler = async ( const updatedPrompts = { ...existingPrompts, [message.promptMode]: message.customPrompt } await updateGlobalState("customModePrompts", updatedPrompts) const currentState = await provider.getStateToPostToWebview() - const stateWithPrompts = { ...currentState, customModePrompts: updatedPrompts } + const stateWithPrompts = { + ...currentState, + customModePrompts: updatedPrompts, + hasOpenedModeSelector: currentState.hasOpenedModeSelector ?? false, + } provider.postMessageToWebview({ type: "state", state: stateWithPrompts }) + + if (TelemetryService.hasInstance()) { + // Determine which setting was changed by comparing objects + const oldPrompt = existingPrompts[message.promptMode] || {} + const newPrompt = message.customPrompt + const changedSettings = Object.keys(newPrompt).filter( + (key) => + JSON.stringify((oldPrompt as Record)[key]) !== + JSON.stringify((newPrompt as Record)[key]), + ) + + if (changedSettings.length > 0) { + TelemetryService.instance.captureModeSettingChanged(changedSettings[0]) + } + } } break case "deleteMessage": { @@ -1085,6 +1113,10 @@ export const webviewMessageHandler = async ( await updateGlobalState("showRooIgnoredFiles", message.bool ?? true) await provider.postStateToWebview() break + case "hasOpenedModeSelector": + await updateGlobalState("hasOpenedModeSelector", message.bool ?? true) + await provider.postStateToWebview() + break case "maxReadFileLine": await updateGlobalState("maxReadFileLine", message.value) await provider.postStateToWebview() @@ -1414,12 +1446,41 @@ export const webviewMessageHandler = async ( break case "updateCustomMode": if (message.modeConfig) { + // Check if this is a new mode or an update to an existing mode + const existingModes = await provider.customModesManager.getCustomModes() + const isNewMode = !existingModes.some((mode) => mode.slug === message.modeConfig?.slug) + await provider.customModesManager.updateCustomMode(message.modeConfig.slug, message.modeConfig) // Update state after saving the mode const customModes = await provider.customModesManager.getCustomModes() await updateGlobalState("customModes", customModes) await updateGlobalState("mode", message.modeConfig.slug) await provider.postStateToWebview() + + // Track telemetry for custom mode creation or update + if (TelemetryService.hasInstance()) { + if (isNewMode) { + // This is a new custom mode + TelemetryService.instance.captureCustomModeCreated( + message.modeConfig.slug, + message.modeConfig.name, + ) + } else { + // Determine which setting was changed by comparing objects + const existingMode = existingModes.find((mode) => mode.slug === message.modeConfig?.slug) + const changedSettings = existingMode + ? Object.keys(message.modeConfig).filter( + (key) => + JSON.stringify((existingMode as Record)[key]) !== + JSON.stringify((message.modeConfig as Record)[key]), + ) + : [] + + if (changedSettings.length > 0) { + TelemetryService.instance.captureModeSettingChanged(changedSettings[0]) + } + } + } } break case "deleteCustomMode": @@ -1604,6 +1665,7 @@ export const webviewMessageHandler = async ( ) await provider.postStateToWebview() console.log(`Marketplace item installed and config file opened: ${configFilePath}`) + // Send success message to webview provider.postMessageToWebview({ type: "marketplaceInstallResult", @@ -1656,7 +1718,11 @@ export const webviewMessageHandler = async ( case "switchTab": { if (message.tab) { - // Send a message to the webview to switch to the specified tab + // Capture tab shown event for all switchTab messages (which are user-initiated) + if (TelemetryService.hasInstance()) { + TelemetryService.instance.captureTabShown(message.tab) + } + await provider.postMessageToWebview({ type: "action", action: "switchTab", tab: message.tab }) } break diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index d372a57ce6..8e9f07c100 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -101,7 +101,9 @@ }, "tasks": { "canceled": "Error de tasca: Ha estat aturada i cancel·lada per l'usuari.", - "deleted": "Fallada de tasca: Ha estat aturada i eliminada per l'usuari." + "deleted": "Fallada de tasca: Ha estat aturada i eliminada per l'usuari.", + "incomplete": "Tasca #{{taskNumber}} (Incompleta)", + "no_messages": "Tasca #{{taskNumber}} (Sense missatges)" }, "storage": { "prompt_custom_path": "Introdueix una ruta d'emmagatzematge personalitzada per a l'historial de converses o deixa-ho buit per utilitzar la ubicació predeterminada", @@ -120,6 +122,18 @@ } } }, + "customModes": { + "errors": { + "yamlParseError": "YAML no vàlid al fitxer .roomodes a la línia {{line}}. Comprova:\n• Indentació correcta (utilitza espais, no tabuladors)\n• Cometes i claudàtors coincidents\n• Sintaxi YAML vàlida", + "schemaValidationError": "Format de modes personalitzats no vàlid a .roomodes:\n{{issues}}", + "invalidFormat": "Format de modes personalitzats no vàlid. Assegura't que la teva configuració segueix el format YAML correcte.", + "updateFailed": "Error en actualitzar el mode personalitzat: {{error}}", + "deleteFailed": "Error en eliminar el mode personalitzat: {{error}}", + "resetFailed": "Error en restablir els modes personalitzats: {{error}}", + "modeNotFound": "Error d'escriptura: Mode no trobat", + "noWorkspaceForProject": "No s'ha trobat cap carpeta d'espai de treball per al mode específic del projecte" + } + }, "mdm": { "errors": { "cloud_auth_required": "La teva organització requereix autenticació de Roo Code Cloud. Si us plau, inicia sessió per continuar.", diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index d34d266e44..fc4ce25de1 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -97,7 +97,9 @@ }, "tasks": { "canceled": "Aufgabenfehler: Die Aufgabe wurde vom Benutzer gestoppt und abgebrochen.", - "deleted": "Aufgabenfehler: Die Aufgabe wurde vom Benutzer gestoppt und gelöscht." + "deleted": "Aufgabenfehler: Die Aufgabe wurde vom Benutzer gestoppt und gelöscht.", + "incomplete": "Aufgabe #{{taskNumber}} (Unvollständig)", + "no_messages": "Aufgabe #{{taskNumber}} (Keine Nachrichten)" }, "storage": { "prompt_custom_path": "Gib den benutzerdefinierten Speicherpfad für den Gesprächsverlauf ein, leer lassen für Standardspeicherort", @@ -120,6 +122,18 @@ } } }, + "customModes": { + "errors": { + "yamlParseError": "Ungültiges YAML in .roomodes-Datei in Zeile {{line}}. Bitte überprüfe:\n• Korrekte Einrückung (verwende Leerzeichen, keine Tabs)\n• Passende Anführungszeichen und Klammern\n• Gültige YAML-Syntax", + "schemaValidationError": "Ungültiges Format für benutzerdefinierte Modi in .roomodes:\n{{issues}}", + "invalidFormat": "Ungültiges Format für benutzerdefinierte Modi. Bitte stelle sicher, dass deine Einstellungen dem korrekten YAML-Format folgen.", + "updateFailed": "Fehler beim Aktualisieren des benutzerdefinierten Modus: {{error}}", + "deleteFailed": "Fehler beim Löschen des benutzerdefinierten Modus: {{error}}", + "resetFailed": "Fehler beim Zurücksetzen der benutzerdefinierten Modi: {{error}}", + "modeNotFound": "Schreibfehler: Modus nicht gefunden", + "noWorkspaceForProject": "Kein Arbeitsbereich-Ordner für projektspezifischen Modus gefunden" + } + }, "mdm": { "errors": { "cloud_auth_required": "Deine Organisation erfordert eine Roo Code Cloud-Authentifizierung. Bitte melde dich an, um fortzufahren.", diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 0de1a25354..3f19a1dd50 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -97,7 +97,9 @@ }, "tasks": { "canceled": "Task error: It was stopped and canceled by the user.", - "deleted": "Task failure: It was stopped and deleted by the user." + "deleted": "Task failure: It was stopped and deleted by the user.", + "incomplete": "Task #{{taskNumber}} (Incomplete)", + "no_messages": "Task #{{taskNumber}} (No messages)" }, "storage": { "prompt_custom_path": "Enter custom conversation history storage path, leave empty to use default location", @@ -109,6 +111,18 @@ "task_prompt": "What should Roo do?", "task_placeholder": "Type your task here" }, + "customModes": { + "errors": { + "yamlParseError": "Invalid YAML in .roomodes file at line {{line}}. Please check for:\n• Proper indentation (use spaces, not tabs)\n• Matching quotes and brackets\n• Valid YAML syntax", + "schemaValidationError": "Invalid custom modes format in .roomodes:\n{{issues}}", + "invalidFormat": "Invalid custom modes format. Please ensure your settings follow the correct YAML format.", + "updateFailed": "Failed to update custom mode: {{error}}", + "deleteFailed": "Failed to delete custom mode: {{error}}", + "resetFailed": "Failed to reset custom modes: {{error}}", + "modeNotFound": "Write error: Mode not found", + "noWorkspaceForProject": "No workspace folder found for project-specific mode" + } + }, "mdm": { "errors": { "cloud_auth_required": "Your organization requires Roo Code Cloud authentication. Please sign in to continue.", diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 6da86eb225..4b3177619e 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -97,7 +97,9 @@ }, "tasks": { "canceled": "Error de tarea: Fue detenida y cancelada por el usuario.", - "deleted": "Fallo de tarea: Fue detenida y eliminada por el usuario." + "deleted": "Fallo de tarea: Fue detenida y eliminada por el usuario.", + "incomplete": "Tarea #{{taskNumber}} (Incompleta)", + "no_messages": "Tarea #{{taskNumber}} (Sin mensajes)" }, "storage": { "prompt_custom_path": "Ingresa la ruta de almacenamiento personalizada para el historial de conversaciones, déjala vacía para usar la ubicación predeterminada", @@ -120,6 +122,18 @@ } } }, + "customModes": { + "errors": { + "yamlParseError": "YAML inválido en archivo .roomodes en línea {{line}}. Verifica:\n• Indentación correcta (usa espacios, no tabs)\n• Comillas y corchetes coincidentes\n• Sintaxis YAML válida", + "schemaValidationError": "Formato inválido de modos personalizados en .roomodes:\n{{issues}}", + "invalidFormat": "Formato inválido de modos personalizados. Asegúrate de que tu configuración siga el formato YAML correcto.", + "updateFailed": "Error al actualizar modo personalizado: {{error}}", + "deleteFailed": "Error al eliminar modo personalizado: {{error}}", + "resetFailed": "Error al restablecer modos personalizados: {{error}}", + "modeNotFound": "Error de escritura: Modo no encontrado", + "noWorkspaceForProject": "No se encontró carpeta de espacio de trabajo para modo específico del proyecto" + } + }, "mdm": { "errors": { "cloud_auth_required": "Tu organización requiere autenticación de Roo Code Cloud. Por favor, inicia sesión para continuar.", diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index 1875bb94a7..93cd67ca15 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -97,7 +97,9 @@ }, "tasks": { "canceled": "Erreur de tâche : Elle a été arrêtée et annulée par l'utilisateur.", - "deleted": "Échec de la tâche : Elle a été arrêtée et supprimée par l'utilisateur." + "deleted": "Échec de la tâche : Elle a été arrêtée et supprimée par l'utilisateur.", + "incomplete": "Tâche #{{taskNumber}} (Incomplète)", + "no_messages": "Tâche #{{taskNumber}} (Aucun message)" }, "storage": { "prompt_custom_path": "Entrez le chemin de stockage personnalisé pour l'historique des conversations, laissez vide pour utiliser l'emplacement par défaut", @@ -120,6 +122,18 @@ } } }, + "customModes": { + "errors": { + "yamlParseError": "YAML invalide dans le fichier .roomodes à la ligne {{line}}. Vérifie :\n• L'indentation correcte (utilise des espaces, pas de tabulations)\n• Les guillemets et crochets correspondants\n• La syntaxe YAML valide", + "schemaValidationError": "Format invalide des modes personnalisés dans .roomodes :\n{{issues}}", + "invalidFormat": "Format invalide des modes personnalisés. Assure-toi que tes paramètres suivent le format YAML correct.", + "updateFailed": "Échec de la mise à jour du mode personnalisé : {{error}}", + "deleteFailed": "Échec de la suppression du mode personnalisé : {{error}}", + "resetFailed": "Échec de la réinitialisation des modes personnalisés : {{error}}", + "modeNotFound": "Erreur d'écriture : Mode non trouvé", + "noWorkspaceForProject": "Aucun dossier d'espace de travail trouvé pour le mode spécifique au projet" + } + }, "mdm": { "errors": { "cloud_auth_required": "Votre organisation nécessite une authentification Roo Code Cloud. Veuillez vous connecter pour continuer.", diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index b991b167fe..68788beacc 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -97,7 +97,9 @@ }, "tasks": { "canceled": "टास्क त्रुटि: इसे उपयोगकर्ता द्वारा रोका और रद्द किया गया था।", - "deleted": "टास्क विफलता: इसे उपयोगकर्ता द्वारा रोका और हटाया गया था।" + "deleted": "टास्क विफलता: इसे उपयोगकर्ता द्वारा रोका और हटाया गया था।", + "incomplete": "टास्क #{{taskNumber}} (अधूरा)", + "no_messages": "टास्क #{{taskNumber}} (कोई संदेश नहीं)" }, "storage": { "prompt_custom_path": "वार्तालाप इतिहास के लिए कस्टम स्टोरेज पाथ दर्ज करें, डिफ़ॉल्ट स्थान का उपयोग करने के लिए खाली छोड़ दें", @@ -120,6 +122,18 @@ } } }, + "customModes": { + "errors": { + "yamlParseError": ".roomodes फ़ाइल में लाइन {{line}} पर अमान्य YAML। कृपया जांचें:\n• सही इंडेंटेशन (टैब नहीं, स्पेस का उपयोग करें)\n• मैचिंग कोट्स और ब्रैकेट्स\n• वैध YAML सिंटैक्स", + "schemaValidationError": ".roomodes में अमान्य कस्टम मोड फॉर्मेट:\n{{issues}}", + "invalidFormat": "अमान्य कस्टम मोड फॉर्मेट। कृपया सुनिश्चित करें कि आपकी सेटिंग्स सही YAML फॉर्मेट का पालन करती हैं।", + "updateFailed": "कस्टम मोड अपडेट विफल: {{error}}", + "deleteFailed": "कस्टम मोड डिलीट विफल: {{error}}", + "resetFailed": "कस्टम मोड रीसेट विफल: {{error}}", + "modeNotFound": "लेखन त्रुटि: मोड नहीं मिला", + "noWorkspaceForProject": "प्रोजेक्ट-विशिष्ट मोड के लिए वर्कस्पेस फ़ोल्डर नहीं मिला" + } + }, "mdm": { "errors": { "cloud_auth_required": "आपके संगठन को Roo Code Cloud प्रमाणीकरण की आवश्यकता है। कृपया जारी रखने के लिए साइन इन करें।", diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index f961b88bed..d6bb2ffa98 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -97,7 +97,9 @@ }, "tasks": { "canceled": "Error tugas: Dihentikan dan dibatalkan oleh pengguna.", - "deleted": "Kegagalan tugas: Dihentikan dan dihapus oleh pengguna." + "deleted": "Kegagalan tugas: Dihentikan dan dihapus oleh pengguna.", + "incomplete": "Tugas #{{taskNumber}} (Tidak lengkap)", + "no_messages": "Tugas #{{taskNumber}} (Tidak ada pesan)" }, "storage": { "prompt_custom_path": "Masukkan path penyimpanan riwayat percakapan kustom, biarkan kosong untuk menggunakan lokasi default", @@ -120,6 +122,18 @@ } } }, + "customModes": { + "errors": { + "yamlParseError": "YAML tidak valid dalam file .roomodes pada baris {{line}}. Silakan periksa:\n• Indentasi yang benar (gunakan spasi, bukan tab)\n• Tanda kutip dan kurung yang cocok\n• Sintaks YAML yang valid", + "schemaValidationError": "Format mode kustom tidak valid dalam .roomodes:\n{{issues}}", + "invalidFormat": "Format mode kustom tidak valid. Pastikan pengaturan kamu mengikuti format YAML yang benar.", + "updateFailed": "Gagal memperbarui mode kustom: {{error}}", + "deleteFailed": "Gagal menghapus mode kustom: {{error}}", + "resetFailed": "Gagal mereset mode kustom: {{error}}", + "modeNotFound": "Kesalahan tulis: Mode tidak ditemukan", + "noWorkspaceForProject": "Tidak ditemukan folder workspace untuk mode khusus proyek" + } + }, "mdm": { "errors": { "cloud_auth_required": "Organisasi kamu memerlukan autentikasi Roo Code Cloud. Silakan masuk untuk melanjutkan.", diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index 0cf42e2cbd..0ef6ee5b54 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -97,7 +97,9 @@ }, "tasks": { "canceled": "Errore attività: È stata interrotta e annullata dall'utente.", - "deleted": "Fallimento attività: È stata interrotta ed eliminata dall'utente." + "deleted": "Fallimento attività: È stata interrotta ed eliminata dall'utente.", + "incomplete": "Attività #{{taskNumber}} (Incompleta)", + "no_messages": "Attività #{{taskNumber}} (Nessun messaggio)" }, "storage": { "prompt_custom_path": "Inserisci il percorso di archiviazione personalizzato per la cronologia delle conversazioni, lascia vuoto per utilizzare la posizione predefinita", @@ -120,6 +122,18 @@ } } }, + "customModes": { + "errors": { + "yamlParseError": "YAML non valido nel file .roomodes alla riga {{line}}. Controlla:\n• Indentazione corretta (usa spazi, non tab)\n• Virgolette e parentesi corrispondenti\n• Sintassi YAML valida", + "schemaValidationError": "Formato modalità personalizzate non valido in .roomodes:\n{{issues}}", + "invalidFormat": "Formato modalità personalizzate non valido. Assicurati che le tue impostazioni seguano il formato YAML corretto.", + "updateFailed": "Aggiornamento modalità personalizzata fallito: {{error}}", + "deleteFailed": "Eliminazione modalità personalizzata fallita: {{error}}", + "resetFailed": "Reset modalità personalizzate fallito: {{error}}", + "modeNotFound": "Errore di scrittura: Modalità non trovata", + "noWorkspaceForProject": "Nessuna cartella workspace trovata per la modalità specifica del progetto" + } + }, "mdm": { "errors": { "cloud_auth_required": "La tua organizzazione richiede l'autenticazione Roo Code Cloud. Accedi per continuare.", diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index 9e1107332b..b132470eac 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -97,7 +97,9 @@ }, "tasks": { "canceled": "タスクエラー:ユーザーによって停止およびキャンセルされました。", - "deleted": "タスク失敗:ユーザーによって停止および削除されました。" + "deleted": "タスク失敗:ユーザーによって停止および削除されました。", + "incomplete": "タスク #{{taskNumber}} (未完了)", + "no_messages": "タスク #{{taskNumber}} (メッセージなし)" }, "storage": { "prompt_custom_path": "会話履歴のカスタムストレージパスを入力してください。デフォルトの場所を使用する場合は空のままにしてください", @@ -120,6 +122,18 @@ } } }, + "customModes": { + "errors": { + "yamlParseError": ".roomodes ファイルの {{line}} 行目で無効な YAML です。以下を確認してください:\n• 正しいインデント(タブではなくスペースを使用)\n• 引用符と括弧の対応\n• 有効な YAML 構文", + "schemaValidationError": ".roomodes のカスタムモード形式が無効です:\n{{issues}}", + "invalidFormat": "カスタムモード形式が無効です。設定が正しい YAML 形式に従っていることを確認してください。", + "updateFailed": "カスタムモードの更新に失敗しました:{{error}}", + "deleteFailed": "カスタムモードの削除に失敗しました:{{error}}", + "resetFailed": "カスタムモードのリセットに失敗しました:{{error}}", + "modeNotFound": "書き込みエラー:モードが見つかりません", + "noWorkspaceForProject": "プロジェクト固有モード用のワークスペースフォルダーが見つかりません" + } + }, "mdm": { "errors": { "cloud_auth_required": "あなたの組織では Roo Code Cloud 認証が必要です。続行するにはサインインしてください。", diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index 51614261c1..079bb56a48 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -97,7 +97,9 @@ }, "tasks": { "canceled": "작업 오류: 사용자에 의해 중지 및 취소되었습니다.", - "deleted": "작업 실패: 사용자에 의해 중지 및 삭제되었습니다." + "deleted": "작업 실패: 사용자에 의해 중지 및 삭제되었습니다.", + "incomplete": "작업 #{{taskNumber}} (미완료)", + "no_messages": "작업 #{{taskNumber}} (메시지 없음)" }, "storage": { "prompt_custom_path": "대화 내역을 위한 사용자 지정 저장 경로를 입력하세요. 기본 위치를 사용하려면 비워두세요", @@ -120,6 +122,18 @@ } } }, + "customModes": { + "errors": { + "yamlParseError": ".roomodes 파일의 {{line}}번째 줄에서 유효하지 않은 YAML입니다. 다음을 확인하세요:\n• 올바른 들여쓰기 (탭이 아닌 공백 사용)\n• 일치하는 따옴표와 괄호\n• 유효한 YAML 구문", + "schemaValidationError": ".roomodes의 사용자 정의 모드 형식이 유효하지 않습니다:\n{{issues}}", + "invalidFormat": "사용자 정의 모드 형식이 유효하지 않습니다. 설정이 올바른 YAML 형식을 따르는지 확인하세요.", + "updateFailed": "사용자 정의 모드 업데이트 실패: {{error}}", + "deleteFailed": "사용자 정의 모드 삭제 실패: {{error}}", + "resetFailed": "사용자 정의 모드 재설정 실패: {{error}}", + "modeNotFound": "쓰기 오류: 모드를 찾을 수 없습니다", + "noWorkspaceForProject": "프로젝트별 모드용 작업 공간 폴더를 찾을 수 없습니다" + } + }, "mdm": { "errors": { "cloud_auth_required": "조직에서 Roo Code Cloud 인증이 필요합니다. 계속하려면 로그인하세요.", diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index 973f7331a8..ef27bef3d8 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -97,7 +97,9 @@ }, "tasks": { "canceled": "Taakfout: gestopt en geannuleerd door gebruiker.", - "deleted": "Taakfout: gestopt en verwijderd door gebruiker." + "deleted": "Taakfout: gestopt en verwijderd door gebruiker.", + "incomplete": "Taak #{{taskNumber}} (Onvolledig)", + "no_messages": "Taak #{{taskNumber}} (Geen berichten)" }, "storage": { "prompt_custom_path": "Voer een aangepast opslagpad voor gespreksgeschiedenis in, laat leeg voor standaardlocatie", @@ -120,6 +122,18 @@ } } }, + "customModes": { + "errors": { + "yamlParseError": "Ongeldige YAML in .roomodes bestand op regel {{line}}. Controleer:\n• Juiste inspringing (gebruik spaties, geen tabs)\n• Overeenkomende aanhalingstekens en haakjes\n• Geldige YAML syntaxis", + "schemaValidationError": "Ongeldig aangepaste modi formaat in .roomodes:\n{{issues}}", + "invalidFormat": "Ongeldig aangepaste modi formaat. Zorg ervoor dat je instellingen het juiste YAML formaat volgen.", + "updateFailed": "Aangepaste modus bijwerken mislukt: {{error}}", + "deleteFailed": "Aangepaste modus verwijderen mislukt: {{error}}", + "resetFailed": "Aangepaste modi resetten mislukt: {{error}}", + "modeNotFound": "Schrijffout: Modus niet gevonden", + "noWorkspaceForProject": "Geen workspace map gevonden voor projectspecifieke modus" + } + }, "mdm": { "errors": { "cloud_auth_required": "Je organisatie vereist Roo Code Cloud-authenticatie. Log in om door te gaan.", diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index 71a044ba40..777bbd82b7 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -97,7 +97,9 @@ }, "tasks": { "canceled": "Błąd zadania: Zostało zatrzymane i anulowane przez użytkownika.", - "deleted": "Niepowodzenie zadania: Zostało zatrzymane i usunięte przez użytkownika." + "deleted": "Niepowodzenie zadania: Zostało zatrzymane i usunięte przez użytkownika.", + "incomplete": "Zadanie #{{taskNumber}} (Niekompletne)", + "no_messages": "Zadanie #{{taskNumber}} (Brak wiadomości)" }, "storage": { "prompt_custom_path": "Wprowadź niestandardową ścieżkę przechowywania dla historii konwersacji lub pozostaw puste, aby użyć lokalizacji domyślnej", @@ -120,6 +122,18 @@ } } }, + "customModes": { + "errors": { + "yamlParseError": "Nieprawidłowy YAML w pliku .roomodes w linii {{line}}. Sprawdź:\n• Prawidłowe wcięcia (używaj spacji, nie tabulatorów)\n• Pasujące cudzysłowy i nawiasy\n• Prawidłową składnię YAML", + "schemaValidationError": "Nieprawidłowy format trybów niestandardowych w .roomodes:\n{{issues}}", + "invalidFormat": "Nieprawidłowy format trybów niestandardowych. Upewnij się, że twoje ustawienia są zgodne z prawidłowym formatem YAML.", + "updateFailed": "Aktualizacja trybu niestandardowego nie powiodła się: {{error}}", + "deleteFailed": "Usunięcie trybu niestandardowego nie powiodło się: {{error}}", + "resetFailed": "Resetowanie trybów niestandardowych nie powiodło się: {{error}}", + "modeNotFound": "Błąd zapisu: Tryb nie został znaleziony", + "noWorkspaceForProject": "Nie znaleziono folderu obszaru roboczego dla trybu specyficznego dla projektu" + } + }, "mdm": { "errors": { "cloud_auth_required": "Twoja organizacja wymaga uwierzytelnienia Roo Code Cloud. Zaloguj się, aby kontynuować.", diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index f67383024a..18695588c8 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -101,7 +101,9 @@ }, "tasks": { "canceled": "Erro na tarefa: Foi interrompida e cancelada pelo usuário.", - "deleted": "Falha na tarefa: Foi interrompida e excluída pelo usuário." + "deleted": "Falha na tarefa: Foi interrompida e excluída pelo usuário.", + "incomplete": "Tarefa #{{taskNumber}} (Incompleta)", + "no_messages": "Tarefa #{{taskNumber}} (Sem mensagens)" }, "storage": { "prompt_custom_path": "Digite o caminho de armazenamento personalizado para o histórico de conversas, deixe em branco para usar o local padrão", @@ -120,6 +122,18 @@ } } }, + "customModes": { + "errors": { + "yamlParseError": "YAML inválido no arquivo .roomodes na linha {{line}}. Verifique:\n• Indentação correta (use espaços, não tabs)\n• Aspas e colchetes correspondentes\n• Sintaxe YAML válida", + "schemaValidationError": "Formato de modos personalizados inválido em .roomodes:\n{{issues}}", + "invalidFormat": "Formato de modos personalizados inválido. Certifique-se de que suas configurações seguem o formato YAML correto.", + "updateFailed": "Falha ao atualizar modo personalizado: {{error}}", + "deleteFailed": "Falha ao excluir modo personalizado: {{error}}", + "resetFailed": "Falha ao redefinir modos personalizados: {{error}}", + "modeNotFound": "Erro de escrita: Modo não encontrado", + "noWorkspaceForProject": "Nenhuma pasta de workspace encontrada para modo específico do projeto" + } + }, "mdm": { "errors": { "cloud_auth_required": "Sua organização requer autenticação do Roo Code Cloud. Faça login para continuar.", diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index 8a3798307c..b0e4f58ceb 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -97,7 +97,9 @@ }, "tasks": { "canceled": "Ошибка задачи: Она была остановлена и отменена пользователем.", - "deleted": "Сбой задачи: Она была остановлена и удалена пользователем." + "deleted": "Сбой задачи: Она была остановлена и удалена пользователем.", + "incomplete": "Задача #{{taskNumber}} (Незавершенная)", + "no_messages": "Задача #{{taskNumber}} (Нет сообщений)" }, "storage": { "prompt_custom_path": "Введите пользовательский путь хранения истории разговоров, оставьте пустым для использования расположения по умолчанию", @@ -120,6 +122,18 @@ } } }, + "customModes": { + "errors": { + "yamlParseError": "Недопустимый YAML в файле .roomodes на строке {{line}}. Проверь:\n• Правильные отступы (используй пробелы, не табы)\n• Соответствующие кавычки и скобки\n• Допустимый синтаксис YAML", + "schemaValidationError": "Недопустимый формат пользовательских режимов в .roomodes:\n{{issues}}", + "invalidFormat": "Недопустимый формат пользовательских режимов. Убедись, что твои настройки соответствуют правильному формату YAML.", + "updateFailed": "Не удалось обновить пользовательский режим: {{error}}", + "deleteFailed": "Не удалось удалить пользовательский режим: {{error}}", + "resetFailed": "Не удалось сбросить пользовательские режимы: {{error}}", + "modeNotFound": "Ошибка записи: Режим не найден", + "noWorkspaceForProject": "Не найдена папка рабочего пространства для режима, специфичного для проекта" + } + }, "mdm": { "errors": { "cloud_auth_required": "Ваша организация требует аутентификации Roo Code Cloud. Войдите в систему, чтобы продолжить.", diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index 9902896503..9663cac808 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -97,7 +97,9 @@ }, "tasks": { "canceled": "Görev hatası: Kullanıcı tarafından durduruldu ve iptal edildi.", - "deleted": "Görev başarısız: Kullanıcı tarafından durduruldu ve silindi." + "deleted": "Görev başarısız: Kullanıcı tarafından durduruldu ve silindi.", + "incomplete": "Görev #{{taskNumber}} (Tamamlanmamış)", + "no_messages": "Görev #{{taskNumber}} (Mesaj yok)" }, "storage": { "prompt_custom_path": "Konuşma geçmişi için özel depolama yolunu girin, varsayılan konumu kullanmak için boş bırakın", @@ -120,6 +122,18 @@ } } }, + "customModes": { + "errors": { + "yamlParseError": ".roomodes dosyasının {{line}}. satırında geçersiz YAML. Kontrol et:\n• Doğru girinti (tab değil boşluk kullan)\n• Eşleşen tırnak işaretleri ve parantezler\n• Geçerli YAML sözdizimi", + "schemaValidationError": ".roomodes'ta geçersiz özel mod formatı:\n{{issues}}", + "invalidFormat": "Geçersiz özel mod formatı. Ayarlarının doğru YAML formatını takip ettiğinden emin ol.", + "updateFailed": "Özel mod güncellemesi başarısız: {{error}}", + "deleteFailed": "Özel mod silme başarısız: {{error}}", + "resetFailed": "Özel modları sıfırlama başarısız: {{error}}", + "modeNotFound": "Yazma hatası: Mod bulunamadı", + "noWorkspaceForProject": "Proje özel modu için çalışma alanı klasörü bulunamadı" + } + }, "mdm": { "errors": { "cloud_auth_required": "Kuruluşunuz Roo Code Cloud kimlik doğrulaması gerektiriyor. Devam etmek için giriş yapın.", diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index 93fa9b689b..5a9d0983f8 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -97,7 +97,9 @@ }, "tasks": { "canceled": "Lỗi nhiệm vụ: Nó đã bị dừng và hủy bởi người dùng.", - "deleted": "Nhiệm vụ thất bại: Nó đã bị dừng và xóa bởi người dùng." + "deleted": "Nhiệm vụ thất bại: Nó đã bị dừng và xóa bởi người dùng.", + "incomplete": "Nhiệm vụ #{{taskNumber}} (Chưa hoàn thành)", + "no_messages": "Nhiệm vụ #{{taskNumber}} (Không có tin nhắn)" }, "storage": { "prompt_custom_path": "Nhập đường dẫn lưu trữ tùy chỉnh cho lịch sử hội thoại, để trống để sử dụng vị trí mặc định", @@ -120,6 +122,18 @@ } } }, + "customModes": { + "errors": { + "yamlParseError": "YAML không hợp lệ trong tệp .roomodes tại dòng {{line}}. Vui lòng kiểm tra:\n• Thụt lề đúng (dùng dấu cách, không dùng tab)\n• Dấu ngoặc kép và ngoặc đơn khớp nhau\n• Cú pháp YAML hợp lệ", + "schemaValidationError": "Định dạng chế độ tùy chỉnh không hợp lệ trong .roomodes:\n{{issues}}", + "invalidFormat": "Định dạng chế độ tùy chỉnh không hợp lệ. Vui lòng đảm bảo cài đặt của bạn tuân theo định dạng YAML đúng.", + "updateFailed": "Cập nhật chế độ tùy chỉnh thất bại: {{error}}", + "deleteFailed": "Xóa chế độ tùy chỉnh thất bại: {{error}}", + "resetFailed": "Đặt lại chế độ tùy chỉnh thất bại: {{error}}", + "modeNotFound": "Lỗi ghi: Không tìm thấy chế độ", + "noWorkspaceForProject": "Không tìm thấy thư mục workspace cho chế độ dành riêng cho dự án" + } + }, "mdm": { "errors": { "cloud_auth_required": "Tổ chức của bạn yêu cầu xác thực Roo Code Cloud. Vui lòng đăng nhập để tiếp tục.", diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index f2fedd3c1b..b355c2ec35 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -102,7 +102,9 @@ }, "tasks": { "canceled": "任务错误:它已被用户停止并取消。", - "deleted": "任务失败:它已被用户停止并删除。" + "deleted": "任务失败:它已被用户停止并删除。", + "incomplete": "任务 #{{taskNumber}} (未完成)", + "no_messages": "任务 #{{taskNumber}} (无消息)" }, "storage": { "prompt_custom_path": "输入自定义会话历史存储路径,留空以使用默认位置", @@ -125,6 +127,18 @@ } } }, + "customModes": { + "errors": { + "yamlParseError": ".roomodes 文件第 {{line}} 行 YAML 格式无效。请检查:\n• 正确的缩进(使用空格,不要使用制表符)\n• 匹配的引号和括号\n• 有效的 YAML 语法", + "schemaValidationError": ".roomodes 中自定义模式格式无效:\n{{issues}}", + "invalidFormat": "自定义模式格式无效。请确保你的设置遵循正确的 YAML 格式。", + "updateFailed": "更新自定义模式失败:{{error}}", + "deleteFailed": "删除自定义模式失败:{{error}}", + "resetFailed": "重置自定义模式失败:{{error}}", + "modeNotFound": "写入错误:未找到模式", + "noWorkspaceForProject": "未找到项目特定模式的工作区文件夹" + } + }, "mdm": { "errors": { "cloud_auth_required": "您的组织需要 Roo Code Cloud 身份验证。请登录以继续。", diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index 43f44a47b3..6c1c9747aa 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -97,7 +97,9 @@ }, "tasks": { "canceled": "工作錯誤:它已被使用者停止並取消。", - "deleted": "工作失敗:它已被使用者停止並刪除。" + "deleted": "工作失敗:它已被使用者停止並刪除。", + "incomplete": "工作 #{{taskNumber}} (未完成)", + "no_messages": "工作 #{{taskNumber}} (無訊息)" }, "storage": { "prompt_custom_path": "輸入自訂會話歷史儲存路徑,留空以使用預設位置", @@ -120,6 +122,18 @@ } } }, + "customModes": { + "errors": { + "yamlParseError": ".roomodes 檔案第 {{line}} 行 YAML 格式無效。請檢查:\n• 正確的縮排(使用空格,不要使用定位字元)\n• 匹配的引號和括號\n• 有效的 YAML 語法", + "schemaValidationError": ".roomodes 中自訂模式格式無效:\n{{issues}}", + "invalidFormat": "自訂模式格式無效。請確保你的設定遵循正確的 YAML 格式。", + "updateFailed": "更新自訂模式失敗:{{error}}", + "deleteFailed": "刪除自訂模式失敗:{{error}}", + "resetFailed": "重設自訂模式失敗:{{error}}", + "modeNotFound": "寫入錯誤:未找到模式", + "noWorkspaceForProject": "未找到專案特定模式的工作區資料夾" + } + }, "mdm": { "errors": { "cloud_auth_required": "您的組織需要 Roo Code Cloud 身份驗證。請登入以繼續。", diff --git a/src/integrations/claude-code/__tests__/message-filter.spec.ts b/src/integrations/claude-code/__tests__/message-filter.spec.ts new file mode 100644 index 0000000000..25f4948cb3 --- /dev/null +++ b/src/integrations/claude-code/__tests__/message-filter.spec.ts @@ -0,0 +1,263 @@ +import { describe, test, expect } from "vitest" +import { filterMessagesForClaudeCode } from "../message-filter" +import type { Anthropic } from "@anthropic-ai/sdk" + +describe("filterMessagesForClaudeCode", () => { + test("should pass through string messages unchanged", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: "Hello, this is a simple text message", + }, + ] + + const result = filterMessagesForClaudeCode(messages) + + expect(result).toEqual(messages) + }) + + test("should pass through text-only content blocks unchanged", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "text", + text: "This is a text block", + }, + ], + }, + ] + + const result = filterMessagesForClaudeCode(messages) + + expect(result).toEqual(messages) + }) + + test("should replace image blocks with text placeholders", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "text", + text: "Here's an image:", + }, + { + type: "image", + source: { + type: "base64", + media_type: "image/png", + data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==", + }, + }, + ], + }, + ] + + const result = filterMessagesForClaudeCode(messages) + + expect(result).toEqual([ + { + role: "user", + content: [ + { + type: "text", + text: "Here's an image:", + }, + { + type: "text", + text: "[Image (base64): image/png not supported by Claude Code]", + }, + ], + }, + ]) + }) + + test("should handle image blocks with unknown source types", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "image", + source: undefined as any, + }, + ], + }, + ] + + const result = filterMessagesForClaudeCode(messages) + + expect(result).toEqual([ + { + role: "user", + content: [ + { + type: "text", + text: "[Image (unknown): unknown not supported by Claude Code]", + }, + ], + }, + ]) + }) + + test("should handle mixed content with multiple images", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "text", + text: "Compare these images:", + }, + { + type: "image", + source: { + type: "base64", + media_type: "image/jpeg", + data: "base64data1", + }, + }, + { + type: "text", + text: "and", + }, + { + type: "image", + source: { + type: "base64", + media_type: "image/gif", + data: "base64data2", + }, + }, + { + type: "text", + text: "What do you think?", + }, + ], + }, + ] + + const result = filterMessagesForClaudeCode(messages) + + expect(result).toEqual([ + { + role: "user", + content: [ + { + type: "text", + text: "Compare these images:", + }, + { + type: "text", + text: "[Image (base64): image/jpeg not supported by Claude Code]", + }, + { + type: "text", + text: "and", + }, + { + type: "text", + text: "[Image (base64): image/gif not supported by Claude Code]", + }, + { + type: "text", + text: "What do you think?", + }, + ], + }, + ]) + }) + + test("should handle multiple messages with images", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: "First message with text only", + }, + { + role: "assistant", + content: [ + { + type: "text", + text: "I can help with that.", + }, + ], + }, + { + role: "user", + content: [ + { + type: "text", + text: "Here's an image:", + }, + { + type: "image", + source: { + type: "base64", + media_type: "image/png", + data: "imagedata", + }, + }, + ], + }, + ] + + const result = filterMessagesForClaudeCode(messages) + + expect(result).toEqual([ + { + role: "user", + content: "First message with text only", + }, + { + role: "assistant", + content: [ + { + type: "text", + text: "I can help with that.", + }, + ], + }, + { + role: "user", + content: [ + { + type: "text", + text: "Here's an image:", + }, + { + type: "text", + text: "[Image (base64): image/png not supported by Claude Code]", + }, + ], + }, + ]) + }) + + test("should preserve other content block types unchanged", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "text", + text: "Regular text", + }, + // This would be some other content type that's not an image + { + type: "tool_use" as any, + id: "tool_123", + name: "test_tool", + input: { test: "data" }, + }, + ], + }, + ] + + const result = filterMessagesForClaudeCode(messages) + + expect(result).toEqual(messages) + }) +}) diff --git a/src/integrations/claude-code/__tests__/run.spec.ts b/src/integrations/claude-code/__tests__/run.spec.ts new file mode 100644 index 0000000000..aa8d9fe8d2 --- /dev/null +++ b/src/integrations/claude-code/__tests__/run.spec.ts @@ -0,0 +1,37 @@ +import { describe, test, expect, vi, beforeEach } from "vitest" + +// Mock vscode workspace +vi.mock("vscode", () => ({ + workspace: { + workspaceFolders: [ + { + uri: { + fsPath: "/test/workspace", + }, + }, + ], + }, +})) + +describe("runClaudeCode", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + test("should export runClaudeCode function", async () => { + const { runClaudeCode } = await import("../run") + expect(typeof runClaudeCode).toBe("function") + }) + + test("should be an async generator function", async () => { + const { runClaudeCode } = await import("../run") + const options = { + systemPrompt: "You are a helpful assistant", + messages: [{ role: "user" as const, content: "Hello" }], + } + + const result = runClaudeCode(options) + expect(Symbol.asyncIterator in result).toBe(true) + expect(typeof result[Symbol.asyncIterator]).toBe("function") + }) +}) diff --git a/src/integrations/claude-code/message-filter.ts b/src/integrations/claude-code/message-filter.ts new file mode 100644 index 0000000000..25ffacce6b --- /dev/null +++ b/src/integrations/claude-code/message-filter.ts @@ -0,0 +1,35 @@ +import type { Anthropic } from "@anthropic-ai/sdk" + +/** + * Filters out image blocks from messages since Claude Code doesn't support images. + * Replaces image blocks with text placeholders similar to how VSCode LM provider handles it. + */ +export function filterMessagesForClaudeCode( + messages: Anthropic.Messages.MessageParam[], +): Anthropic.Messages.MessageParam[] { + return messages.map((message) => { + // Handle simple string messages + if (typeof message.content === "string") { + return message + } + + // Handle complex message structures + const filteredContent = message.content.map((block) => { + if (block.type === "image") { + // Replace image blocks with text placeholders + const sourceType = block.source?.type || "unknown" + const mediaType = block.source?.media_type || "unknown" + return { + type: "text" as const, + text: `[Image (${sourceType}): ${mediaType} not supported by Claude Code]`, + } + } + return block + }) + + return { + ...message, + content: filteredContent, + } + }) +} diff --git a/src/integrations/claude-code/run.ts b/src/integrations/claude-code/run.ts index 8bc12c8740..84f1fe0902 100644 --- a/src/integrations/claude-code/run.ts +++ b/src/integrations/claude-code/run.ts @@ -1,21 +1,115 @@ import * as vscode from "vscode" -import Anthropic from "@anthropic-ai/sdk" +import type Anthropic from "@anthropic-ai/sdk" import { execa } from "execa" +import { ClaudeCodeMessage } from "./types" +import readline from "readline" -export function runClaudeCode({ - systemPrompt, - messages, - path, - modelId, -}: { +const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) + +type ClaudeCodeOptions = { systemPrompt: string messages: Anthropic.Messages.MessageParam[] path?: string modelId?: string -}) { +} + +type ProcessState = { + partialData: string | null + error: Error | null + stderrLogs: string + exitCode: number | null +} + +export async function* runClaudeCode(options: ClaudeCodeOptions): AsyncGenerator { + const process = runProcess(options) + + const rl = readline.createInterface({ + input: process.stdout, + }) + + try { + const processState: ProcessState = { + error: null, + stderrLogs: "", + exitCode: null, + partialData: null, + } + + process.stderr.on("data", (data) => { + processState.stderrLogs += data.toString() + }) + + process.on("close", (code) => { + processState.exitCode = code + }) + + process.on("error", (err) => { + processState.error = err + }) + + for await (const line of rl) { + if (processState.error) { + throw processState.error + } + + if (line.trim()) { + const chunk = parseChunk(line, processState) + + if (!chunk) { + continue + } + + yield chunk + } + } + + // We rely on the assistant message. If the output was truncated, it's better having a poorly formatted message + // from which to extract something, than throwing an error/showing the model didn't return any messages. + if (processState.partialData && processState.partialData.startsWith(`{"type":"assistant"`)) { + yield processState.partialData + } + + const { exitCode } = await process + if (exitCode !== null && exitCode !== 0) { + const errorOutput = processState.error?.message || processState.stderrLogs?.trim() + throw new Error( + `Claude Code process exited with code ${exitCode}.${errorOutput ? ` Error output: ${errorOutput}` : ""}`, + ) + } + } finally { + rl.close() + if (!process.killed) { + process.kill() + } + } +} + +// We want the model to use our custom tool format instead of built-in tools. +// Disabling built-in tools prevents tool-only responses and ensures text output. +const claudeCodeTools = [ + "Task", + "Bash", + "Glob", + "Grep", + "LS", + "exit_plan_mode", + "Read", + "Edit", + "MultiEdit", + "Write", + "NotebookRead", + "NotebookEdit", + "WebFetch", + "TodoRead", + "TodoWrite", + "WebSearch", +].join(",") + +const CLAUDE_CODE_TIMEOUT = 600000 // 10 minutes + +function runProcess({ systemPrompt, messages, path, modelId }: ClaudeCodeOptions) { const claudePath = path || "claude" - // TODO: Is it worth using sessions? Where do we store the session ID? const args = [ "-p", JSON.stringify(messages), @@ -24,7 +118,9 @@ export function runClaudeCode({ "--verbose", "--output-format", "stream-json", - // Cline will handle recursive calls + "--disallowedTools", + claudeCodeTools, + // Roo Code will handle recursive calls "--max-turns", "1", ] @@ -33,12 +129,49 @@ export function runClaudeCode({ args.push("--model", modelId) } - const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) return execa(claudePath, args, { stdin: "ignore", stdout: "pipe", stderr: "pipe", - env: process.env, + env: { + ...process.env, + // The default is 32000. However, I've gotten larger responses, so we increase it unless the user specified it. + CLAUDE_CODE_MAX_OUTPUT_TOKENS: process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS || "64000", + }, cwd, + maxBuffer: 1024 * 1024 * 1000, + timeout: CLAUDE_CODE_TIMEOUT, }) } + +function parseChunk(data: string, processState: ProcessState) { + if (processState.partialData) { + processState.partialData += data + + const chunk = attemptParseChunk(processState.partialData) + + if (!chunk) { + return null + } + + processState.partialData = null + return chunk + } + + const chunk = attemptParseChunk(data) + + if (!chunk) { + processState.partialData = data + } + + return chunk +} + +function attemptParseChunk(data: string): ClaudeCodeMessage | null { + try { + return JSON.parse(data) + } catch (error) { + console.error("Error parsing chunk:", error, data.length) + return null + } +} diff --git a/src/integrations/claude-code/types.ts b/src/integrations/claude-code/types.ts index 965a1b8469..36edaee2ed 100644 --- a/src/integrations/claude-code/types.ts +++ b/src/integrations/claude-code/types.ts @@ -1,40 +1,17 @@ +import type { Anthropic } from "@anthropic-ai/sdk" + type InitMessage = { type: "system" subtype: "init" session_id: string tools: string[] mcp_servers: string[] + apiKeySource: "none" | "/login managed key" | string } -type ClaudeCodeContent = - | { - type: "text" - text: string - } - | { - type: "thinking" - thinking: string - signature?: string - } - type AssistantMessage = { type: "assistant" - message: { - id: string - type: "message" - role: "assistant" - model: string - content: ClaudeCodeContent[] - stop_reason: string | null - stop_sequence: null - usage: { - input_tokens: number - cache_creation_input_tokens?: number - cache_read_input_tokens?: number - output_tokens: number - service_tier: "standard" - } - } + message: Anthropic.Messages.Message session_id: string } @@ -45,13 +22,12 @@ type ErrorMessage = { type ResultMessage = { type: "result" subtype: "success" - cost_usd: number + total_cost_usd: number is_error: boolean duration_ms: number duration_api_ms: number num_turns: number result: string - total_cost: number session_id: string } diff --git a/src/integrations/editor/DiffViewProvider.ts b/src/integrations/editor/DiffViewProvider.ts index a138e42b47..b38c55c3e4 100644 --- a/src/integrations/editor/DiffViewProvider.ts +++ b/src/integrations/editor/DiffViewProvider.ts @@ -130,7 +130,8 @@ export class DiffViewProvider { // Replace all content up to the current line with accumulated lines. const edit = new vscode.WorkspaceEdit() const rangeToReplace = new vscode.Range(0, 0, endLine, 0) - const contentToReplace = accumulatedLines.slice(0, endLine + 1).join("\n") + "\n" + const contentToReplace = + accumulatedLines.slice(0, endLine).join("\n") + (accumulatedLines.length > 0 ? "\n" : "") edit.replace(document.uri, rangeToReplace, this.stripAllBOMs(contentToReplace)) await vscode.workspace.applyEdit(edit) // Update decorations. @@ -230,12 +231,11 @@ export class DiffViewProvider { // show a diff with all the EOL differences. const newContentEOL = this.newContent.includes("\r\n") ? "\r\n" : "\n" - // `trimEnd` to fix issue where editor adds in extra new line - // automatically. - const normalizedEditedContent = editedContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() + newContentEOL + // Normalize EOL characters without trimming content + const normalizedEditedContent = editedContent.replace(/\r\n|\n/g, newContentEOL) // Just in case the new content has a mix of varying EOL characters. - const normalizedNewContent = this.newContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() + newContentEOL + const normalizedNewContent = this.newContent.replace(/\r\n|\n/g, newContentEOL) if (normalizedEditedContent !== normalizedNewContent) { // User made changes before approving edit. diff --git a/src/package.json b/src/package.json index 0256de59ad..df40fe3586 100644 --- a/src/package.json +++ b/src/package.json @@ -3,7 +3,7 @@ "displayName": "%extension.displayName%", "description": "%extension.description%", "publisher": "RooVeterinaryInc", - "version": "3.21.5", + "version": "3.22.0", "icon": "assets/icons/icon.png", "galleryBanner": { "color": "#617A91", @@ -80,11 +80,6 @@ "title": "%command.mcpServers.title%", "icon": "$(server)" }, - { - "command": "roo-cline.promptsButtonClicked", - "title": "%command.prompts.title%", - "icon": "$(organization)" - }, { "command": "roo-cline.historyButtonClicked", "title": "%command.history.title%", @@ -103,8 +98,7 @@ { "command": "roo-cline.accountButtonClicked", "title": "Account", - "icon": "$(account)", - "when": "config.roo-cline.rooCodeCloudEnabled" + "icon": "$(account)" }, { "command": "roo-cline.settingsButtonClicked", @@ -161,6 +155,11 @@ "title": "%command.setCustomStoragePath.title%", "category": "%configuration.title%" }, + { + "command": "roo-cline.importSettings", + "title": "%command.importSettings.title%", + "category": "%configuration.title%" + }, { "command": "roo-cline.focusInput", "title": "%command.focusInput.title%", @@ -220,38 +219,33 @@ "when": "view == roo-cline.SidebarProvider" }, { - "command": "roo-cline.promptsButtonClicked", + "command": "roo-cline.mcpButtonClicked", "group": "navigation@2", "when": "view == roo-cline.SidebarProvider" }, { - "command": "roo-cline.mcpButtonClicked", + "command": "roo-cline.marketplaceButtonClicked", "group": "navigation@3", "when": "view == roo-cline.SidebarProvider" }, { - "command": "roo-cline.marketplaceButtonClicked", + "command": "roo-cline.historyButtonClicked", "group": "navigation@4", "when": "view == roo-cline.SidebarProvider" }, { - "command": "roo-cline.historyButtonClicked", + "command": "roo-cline.popoutButtonClicked", "group": "navigation@5", "when": "view == roo-cline.SidebarProvider" }, { - "command": "roo-cline.popoutButtonClicked", + "command": "roo-cline.accountButtonClicked", "group": "navigation@6", "when": "view == roo-cline.SidebarProvider" }, - { - "command": "roo-cline.accountButtonClicked", - "group": "navigation@7", - "when": "view == roo-cline.SidebarProvider && config.roo-cline.rooCodeCloudEnabled" - }, { "command": "roo-cline.settingsButtonClicked", - "group": "navigation@8", + "group": "navigation@7", "when": "view == roo-cline.SidebarProvider" } ], @@ -262,33 +256,28 @@ "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" }, { - "command": "roo-cline.promptsButtonClicked", + "command": "roo-cline.mcpButtonClicked", "group": "navigation@2", "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" }, { - "command": "roo-cline.mcpButtonClicked", + "command": "roo-cline.marketplaceButtonClicked", "group": "navigation@3", "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" }, { - "command": "roo-cline.marketplaceButtonClicked", + "command": "roo-cline.historyButtonClicked", "group": "navigation@4", "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" }, { - "command": "roo-cline.historyButtonClicked", + "command": "roo-cline.accountButtonClicked", "group": "navigation@5", "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" }, - { - "command": "roo-cline.accountButtonClicked", - "group": "navigation@6", - "when": "activeWebviewPanelId == roo-cline.TabPanelProvider && config.roo-cline.rooCodeCloudEnabled" - }, { "command": "roo-cline.settingsButtonClicked", - "group": "navigation@7", + "group": "navigation@6", "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" } ] @@ -340,10 +329,10 @@ "default": "", "description": "%settings.customStoragePath.description%" }, - "roo-cline.rooCodeCloudEnabled": { + "roo-cline.enableCodeActions": { "type": "boolean", - "default": false, - "description": "%settings.rooCodeCloudEnabled.description%" + "default": true, + "description": "%settings.enableCodeActions.description%" } } } @@ -410,6 +399,7 @@ "pdf-parse": "^1.1.1", "pkce-challenge": "^5.0.0", "pretty-bytes": "^7.0.0", + "proper-lockfile": "^4.1.2", "ps-tree": "^1.2.0", "puppeteer-chromium-resolver": "^24.0.0", "puppeteer-core": "^23.4.0", @@ -419,6 +409,7 @@ "serialize-error": "^12.0.0", "simple-git": "^3.27.0", "sound-play": "^1.1.0", + "stream-json": "^1.8.0", "string-similarity": "^4.0.4", "strip-ansi": "^7.1.0", "strip-bom": "^5.0.0", @@ -446,7 +437,9 @@ "@types/node": "20.x", "@types/node-cache": "^4.1.3", "@types/node-ipc": "^9.2.3", + "@types/proper-lockfile": "^4.1.4", "@types/ps-tree": "^1.1.6", + "@types/stream-json": "^1.7.8", "@types/string-similarity": "^4.0.2", "@types/tmp": "^0.2.6", "@types/turndown": "^5.0.5", diff --git a/src/package.nls.ca.json b/src/package.nls.ca.json index a9c3a93dad..f20f269e20 100644 --- a/src/package.nls.ca.json +++ b/src/package.nls.ca.json @@ -9,6 +9,7 @@ "command.openInNewTab.title": "Obrir en una Nova Pestanya", "command.focusInput.title": "Enfocar Camp d'Entrada", "command.setCustomStoragePath.title": "Establir Ruta d'Emmagatzematge Personalitzada", + "command.importSettings.title": "Importar Configuració", "command.terminal.addToContext.title": "Afegir Contingut del Terminal al Context", "command.terminal.fixCommand.title": "Corregir Aquesta Ordre", "command.terminal.explainCommand.title": "Explicar Aquesta Ordre", @@ -30,5 +31,5 @@ "settings.vsCodeLmModelSelector.vendor.description": "El proveïdor del model de llenguatge (p. ex. copilot)", "settings.vsCodeLmModelSelector.family.description": "La família del model de llenguatge (p. ex. gpt-4)", "settings.customStoragePath.description": "Ruta d'emmagatzematge personalitzada. Deixeu-la buida per utilitzar la ubicació predeterminada. Admet rutes absolutes (p. ex. 'D:\\RooCodeStorage')", - "settings.rooCodeCloudEnabled.description": "Habilitar Roo Code Cloud." + "settings.enableCodeActions.description": "Habilitar correccions ràpides de Roo Code." } diff --git a/src/package.nls.de.json b/src/package.nls.de.json index e9496d7ede..781b310668 100644 --- a/src/package.nls.de.json +++ b/src/package.nls.de.json @@ -9,6 +9,7 @@ "command.openInNewTab.title": "In Neuem Tab Öffnen", "command.focusInput.title": "Eingabefeld Fokussieren", "command.setCustomStoragePath.title": "Benutzerdefinierten Speicherpfad Festlegen", + "command.importSettings.title": "Einstellungen Importieren", "command.terminal.addToContext.title": "Terminal-Inhalt zum Kontext Hinzufügen", "command.terminal.fixCommand.title": "Diesen Befehl Reparieren", "command.terminal.explainCommand.title": "Diesen Befehl Erklären", @@ -30,5 +31,5 @@ "settings.vsCodeLmModelSelector.vendor.description": "Der Anbieter des Sprachmodells (z.B. copilot)", "settings.vsCodeLmModelSelector.family.description": "Die Familie des Sprachmodells (z.B. gpt-4)", "settings.customStoragePath.description": "Benutzerdefinierter Speicherpfad. Leer lassen, um den Standardspeicherort zu verwenden. Unterstützt absolute Pfade (z.B. 'D:\\RooCodeStorage')", - "settings.rooCodeCloudEnabled.description": "Aktiviere Roo Code Cloud." + "settings.enableCodeActions.description": "Roo Code Schnelle Problembehebung aktivieren." } diff --git a/src/package.nls.es.json b/src/package.nls.es.json index 1b3e09c17b..4938f5ea64 100644 --- a/src/package.nls.es.json +++ b/src/package.nls.es.json @@ -9,6 +9,7 @@ "command.openInNewTab.title": "Abrir en Nueva Pestaña", "command.focusInput.title": "Enfocar Campo de Entrada", "command.setCustomStoragePath.title": "Establecer Ruta de Almacenamiento Personalizada", + "command.importSettings.title": "Importar Configuración", "command.terminal.addToContext.title": "Añadir Contenido de Terminal al Contexto", "command.terminal.fixCommand.title": "Corregir Este Comando", "command.terminal.explainCommand.title": "Explicar Este Comando", @@ -30,5 +31,5 @@ "settings.vsCodeLmModelSelector.vendor.description": "El proveedor del modelo de lenguaje (ej. copilot)", "settings.vsCodeLmModelSelector.family.description": "La familia del modelo de lenguaje (ej. gpt-4)", "settings.customStoragePath.description": "Ruta de almacenamiento personalizada. Dejar vacío para usar la ubicación predeterminada. Admite rutas absolutas (ej. 'D:\\RooCodeStorage')", - "settings.rooCodeCloudEnabled.description": "Habilitar Roo Code Cloud." + "settings.enableCodeActions.description": "Habilitar correcciones rápidas de Roo Code." } diff --git a/src/package.nls.fr.json b/src/package.nls.fr.json index 0782ecab05..ada2502e4f 100644 --- a/src/package.nls.fr.json +++ b/src/package.nls.fr.json @@ -9,6 +9,7 @@ "command.openInNewTab.title": "Ouvrir dans un Nouvel Onglet", "command.focusInput.title": "Focus sur le Champ de Saisie", "command.setCustomStoragePath.title": "Définir le Chemin de Stockage Personnalisé", + "command.importSettings.title": "Importer les Paramètres", "command.terminal.addToContext.title": "Ajouter le Contenu du Terminal au Contexte", "command.terminal.fixCommand.title": "Corriger cette Commande", "command.terminal.explainCommand.title": "Expliquer cette Commande", @@ -30,5 +31,5 @@ "settings.vsCodeLmModelSelector.vendor.description": "Le fournisseur du modèle de langage (ex: copilot)", "settings.vsCodeLmModelSelector.family.description": "La famille du modèle de langage (ex: gpt-4)", "settings.customStoragePath.description": "Chemin de stockage personnalisé. Laisser vide pour utiliser l'emplacement par défaut. Prend en charge les chemins absolus (ex: 'D:\\RooCodeStorage')", - "settings.rooCodeCloudEnabled.description": "Activer Roo Code Cloud." + "settings.enableCodeActions.description": "Activer les correctifs rapides de Roo Code." } diff --git a/src/package.nls.hi.json b/src/package.nls.hi.json index a1855f4cb6..f06e21cf06 100644 --- a/src/package.nls.hi.json +++ b/src/package.nls.hi.json @@ -9,6 +9,7 @@ "command.openInNewTab.title": "नए टैब में खोलें", "command.focusInput.title": "इनपुट फ़ील्ड पर फोकस करें", "command.setCustomStoragePath.title": "कस्टम स्टोरेज पाथ सेट करें", + "command.importSettings.title": "सेटिंग्स इम्पोर्ट करें", "command.terminal.addToContext.title": "टर्मिनल सामग्री को संदर्भ में जोड़ें", "command.terminal.fixCommand.title": "यह कमांड ठीक करें", "command.terminal.explainCommand.title": "यह कमांड समझाएं", @@ -30,5 +31,5 @@ "settings.vsCodeLmModelSelector.vendor.description": "भाषा मॉडल का विक्रेता (उदा. copilot)", "settings.vsCodeLmModelSelector.family.description": "भाषा मॉडल का परिवार (उदा. gpt-4)", "settings.customStoragePath.description": "कस्टम स्टोरेज पाथ। डिफ़ॉल्ट स्थान का उपयोग करने के लिए खाली छोड़ें। पूर्ण पथ का समर्थन करता है (उदा. 'D:\\RooCodeStorage')", - "settings.rooCodeCloudEnabled.description": "Roo Code Cloud सक्षम करें।" + "settings.enableCodeActions.description": "Roo Code त्वरित सुधार सक्षम करें" } diff --git a/src/package.nls.id.json b/src/package.nls.id.json index 56685d86a5..c7461aab6b 100644 --- a/src/package.nls.id.json +++ b/src/package.nls.id.json @@ -20,6 +20,7 @@ "command.addToContext.title": "Tambahkan ke Konteks", "command.focusInput.title": "Fokus ke Field Input", "command.setCustomStoragePath.title": "Atur Path Penyimpanan Kustom", + "command.importSettings.title": "Impor Pengaturan", "command.terminal.addToContext.title": "Tambahkan Konten Terminal ke Konteks", "command.terminal.fixCommand.title": "Perbaiki Perintah Ini", "command.terminal.explainCommand.title": "Jelaskan Perintah Ini", @@ -30,5 +31,5 @@ "settings.vsCodeLmModelSelector.vendor.description": "Vendor dari model bahasa (misalnya copilot)", "settings.vsCodeLmModelSelector.family.description": "Keluarga dari model bahasa (misalnya gpt-4)", "settings.customStoragePath.description": "Path penyimpanan kustom. Biarkan kosong untuk menggunakan lokasi default. Mendukung path absolut (misalnya 'D:\\RooCodeStorage')", - "settings.rooCodeCloudEnabled.description": "Aktifkan Roo Code Cloud." + "settings.enableCodeActions.description": "Aktifkan perbaikan cepat Roo Code." } diff --git a/src/package.nls.it.json b/src/package.nls.it.json index 0d491db802..cc63935e60 100644 --- a/src/package.nls.it.json +++ b/src/package.nls.it.json @@ -9,6 +9,7 @@ "command.openInNewTab.title": "Apri in Nuova Scheda", "command.focusInput.title": "Focalizza Campo di Input", "command.setCustomStoragePath.title": "Imposta Percorso di Archiviazione Personalizzato", + "command.importSettings.title": "Importa Impostazioni", "command.terminal.addToContext.title": "Aggiungi Contenuto del Terminale al Contesto", "command.terminal.fixCommand.title": "Correggi Questo Comando", "command.terminal.explainCommand.title": "Spiega Questo Comando", @@ -30,5 +31,5 @@ "settings.vsCodeLmModelSelector.vendor.description": "Il fornitore del modello linguistico (es. copilot)", "settings.vsCodeLmModelSelector.family.description": "La famiglia del modello linguistico (es. gpt-4)", "settings.customStoragePath.description": "Percorso di archiviazione personalizzato. Lasciare vuoto per utilizzare la posizione predefinita. Supporta percorsi assoluti (es. 'D:\\RooCodeStorage')", - "settings.rooCodeCloudEnabled.description": "Abilita Roo Code Cloud." + "settings.enableCodeActions.description": "Abilita correzioni rapide di Roo Code." } diff --git a/src/package.nls.ja.json b/src/package.nls.ja.json index 0f8949b1f7..7e601d3c78 100644 --- a/src/package.nls.ja.json +++ b/src/package.nls.ja.json @@ -20,6 +20,7 @@ "command.addToContext.title": "コンテキストに追加", "command.focusInput.title": "入力フィールドにフォーカス", "command.setCustomStoragePath.title": "カスタムストレージパスの設定", + "command.importSettings.title": "設定をインポート", "command.terminal.addToContext.title": "ターミナルの内容をコンテキストに追加", "command.terminal.fixCommand.title": "このコマンドを修正", "command.terminal.explainCommand.title": "このコマンドを説明", @@ -30,5 +31,5 @@ "settings.vsCodeLmModelSelector.vendor.description": "言語モデルのベンダー(例:copilot)", "settings.vsCodeLmModelSelector.family.description": "言語モデルのファミリー(例:gpt-4)", "settings.customStoragePath.description": "カスタムストレージパス。デフォルトの場所を使用する場合は空のままにします。絶対パスをサポートします(例:'D:\\RooCodeStorage')", - "settings.rooCodeCloudEnabled.description": "Roo Code Cloud を有効にする。" + "settings.enableCodeActions.description": "Roo Codeのクイック修正を有効にする。" } diff --git a/src/package.nls.json b/src/package.nls.json index b05dac3b36..b6880b8bfe 100644 --- a/src/package.nls.json +++ b/src/package.nls.json @@ -20,6 +20,7 @@ "command.addToContext.title": "Add To Context", "command.focusInput.title": "Focus Input Field", "command.setCustomStoragePath.title": "Set Custom Storage Path", + "command.importSettings.title": "Import Settings", "command.terminal.addToContext.title": "Add Terminal Content to Context", "command.terminal.fixCommand.title": "Fix This Command", "command.terminal.explainCommand.title": "Explain This Command", @@ -30,5 +31,5 @@ "settings.vsCodeLmModelSelector.vendor.description": "The vendor of the language model (e.g. copilot)", "settings.vsCodeLmModelSelector.family.description": "The family of the language model (e.g. gpt-4)", "settings.customStoragePath.description": "Custom storage path. Leave empty to use the default location. Supports absolute paths (e.g. 'D:\\RooCodeStorage')", - "settings.rooCodeCloudEnabled.description": "Enable Roo Code Cloud." + "settings.enableCodeActions.description": "Enable Roo Code quick fixes" } diff --git a/src/package.nls.ko.json b/src/package.nls.ko.json index beddd14f83..e305da4c4c 100644 --- a/src/package.nls.ko.json +++ b/src/package.nls.ko.json @@ -9,6 +9,7 @@ "command.openInNewTab.title": "새 탭에서 열기", "command.focusInput.title": "입력 필드 포커스", "command.setCustomStoragePath.title": "사용자 지정 저장소 경로 설정", + "command.importSettings.title": "설정 가져오기", "command.terminal.addToContext.title": "터미널 내용을 컨텍스트에 추가", "command.terminal.fixCommand.title": "이 명령어 수정", "command.terminal.explainCommand.title": "이 명령어 설명", @@ -30,5 +31,5 @@ "settings.vsCodeLmModelSelector.vendor.description": "언어 모델 공급자 (예: copilot)", "settings.vsCodeLmModelSelector.family.description": "언어 모델 계열 (예: gpt-4)", "settings.customStoragePath.description": "사용자 지정 저장소 경로. 기본 위치를 사용하려면 비워두세요. 절대 경로를 지원합니다 (예: 'D:\\RooCodeStorage')", - "settings.rooCodeCloudEnabled.description": "Roo Code Cloud 사용 설정" + "settings.enableCodeActions.description": "Roo Code 빠른 수정 사용 설정" } diff --git a/src/package.nls.nl.json b/src/package.nls.nl.json index 6ef27343c7..3cd1880941 100644 --- a/src/package.nls.nl.json +++ b/src/package.nls.nl.json @@ -20,6 +20,7 @@ "command.addToContext.title": "Toevoegen aan Context", "command.focusInput.title": "Focus op Invoerveld", "command.setCustomStoragePath.title": "Aangepast Opslagpad Instellen", + "command.importSettings.title": "Instellingen Importeren", "command.terminal.addToContext.title": "Terminalinhoud aan Context Toevoegen", "command.terminal.fixCommand.title": "Repareer Dit Commando", "command.terminal.explainCommand.title": "Leg Dit Commando Uit", @@ -30,5 +31,5 @@ "settings.vsCodeLmModelSelector.vendor.description": "De leverancier van het taalmodel (bijv. copilot)", "settings.vsCodeLmModelSelector.family.description": "De familie van het taalmodel (bijv. gpt-4)", "settings.customStoragePath.description": "Aangepast opslagpad. Laat leeg om de standaardlocatie te gebruiken. Ondersteunt absolute paden (bijv. 'D:\\RooCodeStorage')", - "settings.rooCodeCloudEnabled.description": "Roo Code Cloud inschakelen." + "settings.enableCodeActions.description": "Snelle correcties van Roo Code inschakelen." } diff --git a/src/package.nls.pl.json b/src/package.nls.pl.json index 1565299f43..275c404d06 100644 --- a/src/package.nls.pl.json +++ b/src/package.nls.pl.json @@ -9,6 +9,7 @@ "command.openInNewTab.title": "Otwórz w Nowej Karcie", "command.focusInput.title": "Fokus na Pole Wprowadzania", "command.setCustomStoragePath.title": "Ustaw Niestandardową Ścieżkę Przechowywania", + "command.importSettings.title": "Importuj Ustawienia", "command.terminal.addToContext.title": "Dodaj Zawartość Terminala do Kontekstu", "command.terminal.fixCommand.title": "Napraw tę Komendę", "command.terminal.explainCommand.title": "Wyjaśnij tę Komendę", @@ -30,5 +31,5 @@ "settings.vsCodeLmModelSelector.vendor.description": "Dostawca modelu językowego (np. copilot)", "settings.vsCodeLmModelSelector.family.description": "Rodzina modelu językowego (np. gpt-4)", "settings.customStoragePath.description": "Niestandardowa ścieżka przechowywania. Pozostaw puste, aby użyć domyślnej lokalizacji. Obsługuje ścieżki bezwzględne (np. 'D:\\RooCodeStorage')", - "settings.rooCodeCloudEnabled.description": "Włącz Roo Code Cloud." + "settings.enableCodeActions.description": "Włącz szybkie poprawki Roo Code." } diff --git a/src/package.nls.pt-BR.json b/src/package.nls.pt-BR.json index ce21b7d7f6..057f255c44 100644 --- a/src/package.nls.pt-BR.json +++ b/src/package.nls.pt-BR.json @@ -9,6 +9,7 @@ "command.openInNewTab.title": "Abrir em Nova Aba", "command.focusInput.title": "Focar Campo de Entrada", "command.setCustomStoragePath.title": "Definir Caminho de Armazenamento Personalizado", + "command.importSettings.title": "Importar Configurações", "command.terminal.addToContext.title": "Adicionar Conteúdo do Terminal ao Contexto", "command.terminal.fixCommand.title": "Corrigir Este Comando", "command.terminal.explainCommand.title": "Explicar Este Comando", @@ -30,5 +31,5 @@ "settings.vsCodeLmModelSelector.vendor.description": "O fornecedor do modelo de linguagem (ex: copilot)", "settings.vsCodeLmModelSelector.family.description": "A família do modelo de linguagem (ex: gpt-4)", "settings.customStoragePath.description": "Caminho de armazenamento personalizado. Deixe vazio para usar o local padrão. Suporta caminhos absolutos (ex: 'D:\\RooCodeStorage')", - "settings.rooCodeCloudEnabled.description": "Habilitar Roo Code Cloud." + "settings.enableCodeActions.description": "Habilitar correções rápidas do Roo Code." } diff --git a/src/package.nls.ru.json b/src/package.nls.ru.json index 5c2b6a030b..02a5bcf93d 100644 --- a/src/package.nls.ru.json +++ b/src/package.nls.ru.json @@ -20,6 +20,7 @@ "command.addToContext.title": "Добавить в контекст", "command.focusInput.title": "Фокус на поле ввода", "command.setCustomStoragePath.title": "Указать путь хранения", + "command.importSettings.title": "Импортировать настройки", "command.terminal.addToContext.title": "Добавить содержимое терминала в контекст", "command.terminal.fixCommand.title": "Исправить эту команду", "command.terminal.explainCommand.title": "Объяснить эту команду", @@ -30,5 +31,5 @@ "settings.vsCodeLmModelSelector.vendor.description": "Поставщик языковой модели (например, copilot)", "settings.vsCodeLmModelSelector.family.description": "Семейство языковой модели (например, gpt-4)", "settings.customStoragePath.description": "Пользовательский путь хранения. Оставьте пустым для использования пути по умолчанию. Поддерживает абсолютные пути (например, 'D:\\RooCodeStorage')", - "settings.rooCodeCloudEnabled.description": "Включить Roo Code Cloud." + "settings.enableCodeActions.description": "Включить быстрые исправления Roo Code." } diff --git a/src/package.nls.tr.json b/src/package.nls.tr.json index 59d50324d6..dda6e9e8d1 100644 --- a/src/package.nls.tr.json +++ b/src/package.nls.tr.json @@ -9,6 +9,7 @@ "command.openInNewTab.title": "Yeni Sekmede Aç", "command.focusInput.title": "Giriş Alanına Odaklan", "command.setCustomStoragePath.title": "Özel Depolama Yolunu Ayarla", + "command.importSettings.title": "Ayarları İçe Aktar", "command.terminal.addToContext.title": "Terminal İçeriğini Bağlama Ekle", "command.terminal.fixCommand.title": "Bu Komutu Düzelt", "command.terminal.explainCommand.title": "Bu Komutu Açıkla", @@ -30,5 +31,5 @@ "settings.vsCodeLmModelSelector.vendor.description": "Dil modelinin sağlayıcısı (örn: copilot)", "settings.vsCodeLmModelSelector.family.description": "Dil modelinin ailesi (örn: gpt-4)", "settings.customStoragePath.description": "Özel depolama yolu. Varsayılan konumu kullanmak için boş bırakın. Mutlak yolları destekler (örn: 'D:\\RooCodeStorage')", - "settings.rooCodeCloudEnabled.description": "Roo Code Cloud'u Etkinleştir." + "settings.enableCodeActions.description": "Roo Code hızlı düzeltmeleri etkinleştir." } diff --git a/src/package.nls.vi.json b/src/package.nls.vi.json index 33f54ebe5c..985465acb7 100644 --- a/src/package.nls.vi.json +++ b/src/package.nls.vi.json @@ -9,6 +9,7 @@ "command.openInNewTab.title": "Mở trong Tab Mới", "command.focusInput.title": "Tập Trung vào Trường Nhập", "command.setCustomStoragePath.title": "Đặt Đường Dẫn Lưu Trữ Tùy Chỉnh", + "command.importSettings.title": "Nhập Cài Đặt", "command.terminal.addToContext.title": "Thêm Nội Dung Terminal vào Ngữ Cảnh", "command.terminal.fixCommand.title": "Sửa Lệnh Này", "command.terminal.explainCommand.title": "Giải Thích Lệnh Này", @@ -30,5 +31,5 @@ "settings.vsCodeLmModelSelector.vendor.description": "Nhà cung cấp mô hình ngôn ngữ (ví dụ: copilot)", "settings.vsCodeLmModelSelector.family.description": "Họ mô hình ngôn ngữ (ví dụ: gpt-4)", "settings.customStoragePath.description": "Đường dẫn lưu trữ tùy chỉnh. Để trống để sử dụng vị trí mặc định. Hỗ trợ đường dẫn tuyệt đối (ví dụ: 'D:\\RooCodeStorage')", - "settings.rooCodeCloudEnabled.description": "Bật Roo Code Cloud." + "settings.enableCodeActions.description": "Bật sửa lỗi nhanh Roo Code." } diff --git a/src/package.nls.zh-CN.json b/src/package.nls.zh-CN.json index ad10328e20..25d4e15c0a 100644 --- a/src/package.nls.zh-CN.json +++ b/src/package.nls.zh-CN.json @@ -9,6 +9,7 @@ "command.openInNewTab.title": "在新标签页中打开", "command.focusInput.title": "聚焦输入框", "command.setCustomStoragePath.title": "设置自定义存储路径", + "command.importSettings.title": "导入设置", "command.terminal.addToContext.title": "将终端内容添加到上下文", "command.terminal.fixCommand.title": "修复此命令", "command.terminal.explainCommand.title": "解释此命令", @@ -30,5 +31,5 @@ "settings.vsCodeLmModelSelector.vendor.description": "语言模型的供应商(例如:copilot)", "settings.vsCodeLmModelSelector.family.description": "语言模型的系列(例如:gpt-4)", "settings.customStoragePath.description": "自定义存储路径。留空以使用默认位置。支持绝对路径(例如:'D:\\RooCodeStorage')", - "settings.rooCodeCloudEnabled.description": "启用 Roo Code Cloud。" + "settings.enableCodeActions.description": "启用 Roo Code 快速修复" } diff --git a/src/package.nls.zh-TW.json b/src/package.nls.zh-TW.json index b903fc6859..918b4bff33 100644 --- a/src/package.nls.zh-TW.json +++ b/src/package.nls.zh-TW.json @@ -9,6 +9,7 @@ "command.openInNewTab.title": "在新分頁中開啟", "command.focusInput.title": "聚焦輸入框", "command.setCustomStoragePath.title": "設定自訂儲存路徑", + "command.importSettings.title": "匯入設定", "command.terminal.addToContext.title": "將終端內容新增到上下文", "command.terminal.fixCommand.title": "修復此命令", "command.terminal.explainCommand.title": "解釋此命令", @@ -30,5 +31,5 @@ "settings.vsCodeLmModelSelector.vendor.description": "語言模型供應商(例如:copilot)", "settings.vsCodeLmModelSelector.family.description": "語言模型系列(例如:gpt-4)", "settings.customStoragePath.description": "自訂儲存路徑。留空以使用預設位置。支援絕對路徑(例如:'D:\\RooCodeStorage')", - "settings.rooCodeCloudEnabled.description": "啟用 Roo Code Cloud。" + "settings.enableCodeActions.description": "啟用 Roo Code 快速修復。" } diff --git a/src/services/code-index/__tests__/cache-manager.spec.ts b/src/services/code-index/__tests__/cache-manager.spec.ts index 27408fdf33..e61a92f3cc 100644 --- a/src/services/code-index/__tests__/cache-manager.spec.ts +++ b/src/services/code-index/__tests__/cache-manager.spec.ts @@ -4,6 +4,14 @@ import { createHash } from "crypto" import debounce from "lodash.debounce" import { CacheManager } from "../cache-manager" +// Mock safeWriteJson utility +vitest.mock("../../../utils/safeWriteJson", () => ({ + safeWriteJson: vitest.fn().mockResolvedValue(undefined), +})) + +// Import the mocked version +import { safeWriteJson } from "../../../utils/safeWriteJson" + // Mock vscode vitest.mock("vscode", () => ({ Uri: { @@ -89,7 +97,7 @@ describe("CacheManager", () => { cacheManager.updateHash(filePath, hash) expect(cacheManager.getHash(filePath)).toBe(hash) - expect(vscode.workspace.fs.writeFile).toHaveBeenCalled() + expect(safeWriteJson).toHaveBeenCalled() }) it("should delete hash and trigger save", () => { @@ -100,7 +108,7 @@ describe("CacheManager", () => { cacheManager.deleteHash(filePath) expect(cacheManager.getHash(filePath)).toBeUndefined() - expect(vscode.workspace.fs.writeFile).toHaveBeenCalled() + expect(safeWriteJson).toHaveBeenCalled() }) it("should return shallow copy of hashes", () => { @@ -125,18 +133,16 @@ describe("CacheManager", () => { cacheManager.updateHash(filePath, hash) - expect(vscode.workspace.fs.writeFile).toHaveBeenCalledWith(mockCachePath, expect.any(Uint8Array)) + expect(safeWriteJson).toHaveBeenCalledWith(mockCachePath.fsPath, expect.any(Object)) // Verify the saved data - const savedData = JSON.parse( - Buffer.from((vscode.workspace.fs.writeFile as Mock).mock.calls[0][1]).toString(), - ) + const savedData = (safeWriteJson as Mock).mock.calls[0][1] expect(savedData).toEqual({ [filePath]: hash }) }) it("should handle save errors gracefully", async () => { const consoleErrorSpy = vitest.spyOn(console, "error").mockImplementation(() => {}) - ;(vscode.workspace.fs.writeFile as Mock).mockRejectedValue(new Error("Save failed")) + ;(safeWriteJson as Mock).mockRejectedValue(new Error("Save failed")) cacheManager.updateHash("test.ts", "hash") @@ -153,19 +159,19 @@ describe("CacheManager", () => { it("should clear cache file and reset state", async () => { cacheManager.updateHash("test.ts", "hash") - // Reset the mock to ensure writeFile succeeds for clearCacheFile - ;(vscode.workspace.fs.writeFile as Mock).mockClear() - ;(vscode.workspace.fs.writeFile as Mock).mockResolvedValue(undefined) + // Reset the mock to ensure safeWriteJson succeeds for clearCacheFile + ;(safeWriteJson as Mock).mockClear() + ;(safeWriteJson as Mock).mockResolvedValue(undefined) await cacheManager.clearCacheFile() - expect(vscode.workspace.fs.writeFile).toHaveBeenCalledWith(mockCachePath, Buffer.from("{}")) + expect(safeWriteJson).toHaveBeenCalledWith(mockCachePath.fsPath, {}) expect(cacheManager.getAllHashes()).toEqual({}) }) it("should handle clear errors gracefully", async () => { const consoleErrorSpy = vitest.spyOn(console, "error").mockImplementation(() => {}) - ;(vscode.workspace.fs.writeFile as Mock).mockRejectedValue(new Error("Save failed")) + ;(safeWriteJson as Mock).mockRejectedValue(new Error("Save failed")) await cacheManager.clearCacheFile() diff --git a/src/services/code-index/cache-manager.ts b/src/services/code-index/cache-manager.ts index f66f933a0b..146db4cd2a 100644 --- a/src/services/code-index/cache-manager.ts +++ b/src/services/code-index/cache-manager.ts @@ -2,6 +2,7 @@ import * as vscode from "vscode" import { createHash } from "crypto" import { ICacheManager } from "./interfaces/cache" import debounce from "lodash.debounce" +import { safeWriteJson } from "../../utils/safeWriteJson" /** * Manages the cache for code indexing @@ -46,7 +47,7 @@ export class CacheManager implements ICacheManager { */ private async _performSave(): Promise { try { - await vscode.workspace.fs.writeFile(this.cachePath, Buffer.from(JSON.stringify(this.fileHashes, null, 2))) + await safeWriteJson(this.cachePath.fsPath, this.fileHashes) } catch (error) { console.error("Failed to save cache:", error) } @@ -57,7 +58,7 @@ export class CacheManager implements ICacheManager { */ async clearCacheFile(): Promise { try { - await vscode.workspace.fs.writeFile(this.cachePath, Buffer.from("{}")) + await safeWriteJson(this.cachePath.fsPath, {}) this.fileHashes = {} } catch (error) { console.error("Failed to clear cache file:", error, this.cachePath) diff --git a/src/services/marketplace/SimpleInstaller.ts b/src/services/marketplace/SimpleInstaller.ts index 75f14b0d4c..7ead5e2442 100644 --- a/src/services/marketplace/SimpleInstaller.ts +++ b/src/services/marketplace/SimpleInstaller.ts @@ -84,7 +84,7 @@ export class SimpleInstaller { // Write back to file await fs.mkdir(path.dirname(filePath), { recursive: true }) - const yamlContent = yaml.stringify(existingData) + const yamlContent = yaml.stringify(existingData, { lineWidth: 0, defaultStringType: "PLAIN" }) await fs.writeFile(filePath, yamlContent, "utf-8") // Calculate approximate line number where the new mode was added @@ -282,7 +282,11 @@ export class SimpleInstaller { existingData.customModes = existingData.customModes.filter((mode: any) => mode.slug !== modeData.slug) // Always write back the file, even if empty - await fs.writeFile(filePath, yaml.stringify(existingData), "utf-8") + await fs.writeFile( + filePath, + yaml.stringify(existingData, { lineWidth: 0, defaultStringType: "PLAIN" }), + "utf-8", + ) } } catch (error: any) { if (error.code === "ENOENT") { diff --git a/src/services/mcp/__tests__/McpHub.spec.ts b/src/services/mcp/__tests__/McpHub.spec.ts index 1ed2993f2a..98ef4514c2 100644 --- a/src/services/mcp/__tests__/McpHub.spec.ts +++ b/src/services/mcp/__tests__/McpHub.spec.ts @@ -5,6 +5,43 @@ import { ServerConfigSchema, McpHub } from "../McpHub" import fs from "fs/promises" import { vi, Mock } from "vitest" +// Mock fs/promises before importing anything that uses it +vi.mock("fs/promises", () => ({ + default: { + access: vi.fn().mockResolvedValue(undefined), + writeFile: vi.fn().mockResolvedValue(undefined), + readFile: vi.fn().mockResolvedValue("{}"), + unlink: vi.fn().mockResolvedValue(undefined), + rename: vi.fn().mockResolvedValue(undefined), + lstat: vi.fn().mockImplementation(() => + Promise.resolve({ + isDirectory: () => true, + }), + ), + mkdir: vi.fn().mockResolvedValue(undefined), + }, + access: vi.fn().mockResolvedValue(undefined), + writeFile: vi.fn().mockResolvedValue(undefined), + readFile: vi.fn().mockResolvedValue("{}"), + unlink: vi.fn().mockResolvedValue(undefined), + rename: vi.fn().mockResolvedValue(undefined), + lstat: vi.fn().mockImplementation(() => + Promise.resolve({ + isDirectory: () => true, + }), + ), + mkdir: vi.fn().mockResolvedValue(undefined), +})) + +// Mock safeWriteJson +vi.mock("../../../utils/safeWriteJson", () => ({ + safeWriteJson: vi.fn(async (filePath, data) => { + // Instead of trying to write to the file system, just call fs.writeFile mock + // This avoids the complex file locking and temp file operations + return fs.writeFile(filePath, JSON.stringify(data), "utf8") + }), +})) + vi.mock("vscode", () => ({ workspace: { createFileSystemWatcher: vi.fn().mockReturnValue({ @@ -56,6 +93,7 @@ describe("McpHub", () => { // Mock console.error to suppress error messages during tests console.error = vi.fn() + const mockUri: Uri = { scheme: "file", authority: "", diff --git a/src/services/mdm/MdmService.ts b/src/services/mdm/MdmService.ts index d0a7bc92ee..67d684b176 100644 --- a/src/services/mdm/MdmService.ts +++ b/src/services/mdm/MdmService.ts @@ -35,8 +35,6 @@ export class MdmService { this.mdmConfig = await this.loadMdmConfig() if (this.mdmConfig) { this.log("[MDM] Loaded MDM configuration:", this.mdmConfig) - // Automatically enable Roo Code Cloud when MDM config is present - await this.ensureCloudEnabled() } else { this.log("[MDM] No MDM configuration found") } @@ -60,23 +58,6 @@ export class MdmService { return this.mdmConfig?.organizationId } - /** - * Ensure Roo Code Cloud is enabled when MDM config is present - */ - private async ensureCloudEnabled(): Promise { - try { - const config = vscode.workspace.getConfiguration(Package.name) - const currentValue = config.get("rooCodeCloudEnabled", false) - - if (!currentValue) { - this.log("[MDM] Enabling Roo Code Cloud due to MDM policy") - await config.update("rooCodeCloudEnabled", true, vscode.ConfigurationTarget.Global) - } - } catch (error) { - this.log("[MDM] Error enabling Roo Code Cloud:", error) - } - } - /** * Check if the current state is compliant with MDM policy */ diff --git a/src/services/mdm/__tests__/MdmService.spec.ts b/src/services/mdm/__tests__/MdmService.spec.ts index a69e74a9b5..81ff61652b 100644 --- a/src/services/mdm/__tests__/MdmService.spec.ts +++ b/src/services/mdm/__tests__/MdmService.spec.ts @@ -340,102 +340,6 @@ describe("MdmService", () => { }) }) - describe("cloud enablement", () => { - it("should enable Roo Code Cloud when MDM config is present and setting is disabled", async () => { - const mockConfig = { - requireCloudAuth: true, - organizationId: "test-org-123", - } - - mockFs.existsSync.mockReturnValue(true) - mockFs.readFileSync.mockReturnValue(JSON.stringify(mockConfig)) - - const mockVsCodeConfig = { - get: vi.fn().mockReturnValue(false), // rooCodeCloudEnabled is false - update: vi.fn().mockResolvedValue(undefined), - } - mockVscode.workspace.getConfiguration.mockReturnValue(mockVsCodeConfig) - - await MdmService.createInstance() - - expect(mockVscode.workspace.getConfiguration).toHaveBeenCalledWith("roo-cline") - expect(mockVsCodeConfig.get).toHaveBeenCalledWith("rooCodeCloudEnabled", false) - expect(mockVsCodeConfig.update).toHaveBeenCalledWith("rooCodeCloudEnabled", true, 1) // ConfigurationTarget.Global - }) - - it("should not update setting when Roo Code Cloud is already enabled", async () => { - const mockConfig = { - requireCloudAuth: true, - organizationId: "test-org-123", - } - - mockFs.existsSync.mockReturnValue(true) - mockFs.readFileSync.mockReturnValue(JSON.stringify(mockConfig)) - - const mockVsCodeConfig = { - get: vi.fn().mockReturnValue(true), // rooCodeCloudEnabled is already true - update: vi.fn().mockResolvedValue(undefined), - } - mockVscode.workspace.getConfiguration.mockReturnValue(mockVsCodeConfig) - - await MdmService.createInstance() - - expect(mockVsCodeConfig.get).toHaveBeenCalledWith("rooCodeCloudEnabled", false) - expect(mockVsCodeConfig.update).not.toHaveBeenCalled() - }) - - it("should enable cloud even when requireCloudAuth is false", async () => { - const mockConfig = { - requireCloudAuth: false, // Cloud auth not required, but config file exists - } - - mockFs.existsSync.mockReturnValue(true) - mockFs.readFileSync.mockReturnValue(JSON.stringify(mockConfig)) - - const mockVsCodeConfig = { - get: vi.fn().mockReturnValue(false), - update: vi.fn().mockResolvedValue(undefined), - } - mockVscode.workspace.getConfiguration.mockReturnValue(mockVsCodeConfig) - - await MdmService.createInstance() - - expect(mockVsCodeConfig.update).toHaveBeenCalledWith("rooCodeCloudEnabled", true, 1) - }) - - it("should not enable cloud when no MDM config exists", async () => { - mockFs.existsSync.mockReturnValue(false) - - const mockVsCodeConfig = { - get: vi.fn().mockReturnValue(false), - update: vi.fn().mockResolvedValue(undefined), - } - mockVscode.workspace.getConfiguration.mockReturnValue(mockVsCodeConfig) - - await MdmService.createInstance() - - expect(mockVsCodeConfig.update).not.toHaveBeenCalled() - }) - - it("should handle VSCode configuration errors gracefully", async () => { - const mockConfig = { - requireCloudAuth: true, - } - - mockFs.existsSync.mockReturnValue(true) - mockFs.readFileSync.mockReturnValue(JSON.stringify(mockConfig)) - - const mockVsCodeConfig = { - get: vi.fn().mockReturnValue(false), - update: vi.fn().mockRejectedValue(new Error("Configuration update failed")), - } - mockVscode.workspace.getConfiguration.mockReturnValue(mockVsCodeConfig) - - // Should not throw - await expect(MdmService.createInstance()).resolves.toBeInstanceOf(MdmService) - }) - }) - describe("singleton pattern", () => { it("should throw error when accessing instance before creation", () => { expect(() => MdmService.getInstance()).toThrow("MdmService not initialized") diff --git a/src/services/roo-config/__tests__/index.spec.ts b/src/services/roo-config/__tests__/index.spec.ts new file mode 100644 index 0000000000..8e9bf929cc --- /dev/null +++ b/src/services/roo-config/__tests__/index.spec.ts @@ -0,0 +1,301 @@ +import * as path from "path" +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest" + +// Use vi.hoisted to ensure mocks are available during hoisting +const { mockStat, mockReadFile, mockHomedir } = vi.hoisted(() => ({ + mockStat: vi.fn(), + mockReadFile: vi.fn(), + mockHomedir: vi.fn(), +})) + +// Mock fs/promises module +vi.mock("fs/promises", () => ({ + default: { + stat: mockStat, + readFile: mockReadFile, + }, +})) + +// Mock os module +vi.mock("os", () => ({ + homedir: mockHomedir, +})) + +import { + getGlobalRooDirectory, + getProjectRooDirectoryForCwd, + directoryExists, + fileExists, + readFileIfExists, + getRooDirectoriesForCwd, + loadConfiguration, +} from "../index" + +describe("RooConfigService", () => { + beforeEach(() => { + vi.clearAllMocks() + mockHomedir.mockReturnValue("/mock/home") + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + describe("getGlobalRooDirectory", () => { + it("should return correct path for global .roo directory", () => { + const result = getGlobalRooDirectory() + expect(result).toBe(path.join("/mock/home", ".roo")) + }) + + it("should handle different home directories", () => { + mockHomedir.mockReturnValue("/different/home") + const result = getGlobalRooDirectory() + expect(result).toBe(path.join("/different/home", ".roo")) + }) + }) + + describe("getProjectRooDirectoryForCwd", () => { + it("should return correct path for given cwd", () => { + const cwd = "/custom/project/path" + const result = getProjectRooDirectoryForCwd(cwd) + expect(result).toBe(path.join(cwd, ".roo")) + }) + }) + + describe("directoryExists", () => { + it("should return true for existing directory", async () => { + mockStat.mockResolvedValue({ isDirectory: () => true } as any) + + const result = await directoryExists("/some/path") + + expect(result).toBe(true) + expect(mockStat).toHaveBeenCalledWith("/some/path") + }) + + it("should return false for non-existing path", async () => { + const error = new Error("ENOENT") as any + error.code = "ENOENT" + mockStat.mockRejectedValue(error) + + const result = await directoryExists("/non/existing/path") + + expect(result).toBe(false) + }) + + it("should return false for ENOTDIR error", async () => { + const error = new Error("ENOTDIR") as any + error.code = "ENOTDIR" + mockStat.mockRejectedValue(error) + + const result = await directoryExists("/not/a/directory") + + expect(result).toBe(false) + }) + + it("should throw unexpected errors", async () => { + const error = new Error("Permission denied") as any + error.code = "EACCES" + mockStat.mockRejectedValue(error) + + await expect(directoryExists("/permission/denied")).rejects.toThrow("Permission denied") + }) + + it("should return false for files", async () => { + mockStat.mockResolvedValue({ isDirectory: () => false } as any) + + const result = await directoryExists("/some/file.txt") + + expect(result).toBe(false) + }) + }) + + describe("fileExists", () => { + it("should return true for existing file", async () => { + mockStat.mockResolvedValue({ isFile: () => true } as any) + + const result = await fileExists("/some/file.txt") + + expect(result).toBe(true) + expect(mockStat).toHaveBeenCalledWith("/some/file.txt") + }) + + it("should return false for non-existing file", async () => { + const error = new Error("ENOENT") as any + error.code = "ENOENT" + mockStat.mockRejectedValue(error) + + const result = await fileExists("/non/existing/file.txt") + + expect(result).toBe(false) + }) + + it("should return false for ENOTDIR error", async () => { + const error = new Error("ENOTDIR") as any + error.code = "ENOTDIR" + mockStat.mockRejectedValue(error) + + const result = await fileExists("/not/a/directory/file.txt") + + expect(result).toBe(false) + }) + + it("should throw unexpected errors", async () => { + const error = new Error("Permission denied") as any + error.code = "EACCES" + mockStat.mockRejectedValue(error) + + await expect(fileExists("/permission/denied/file.txt")).rejects.toThrow("Permission denied") + }) + + it("should return false for directories", async () => { + mockStat.mockResolvedValue({ isFile: () => false } as any) + + const result = await fileExists("/some/directory") + + expect(result).toBe(false) + }) + }) + + describe("readFileIfExists", () => { + it("should return file content for existing file", async () => { + mockReadFile.mockResolvedValue("file content") + + const result = await readFileIfExists("/some/file.txt") + + expect(result).toBe("file content") + expect(mockReadFile).toHaveBeenCalledWith("/some/file.txt", "utf-8") + }) + + it("should return null for non-existing file", async () => { + const error = new Error("ENOENT") as any + error.code = "ENOENT" + mockReadFile.mockRejectedValue(error) + + const result = await readFileIfExists("/non/existing/file.txt") + + expect(result).toBe(null) + }) + + it("should return null for ENOTDIR error", async () => { + const error = new Error("ENOTDIR") as any + error.code = "ENOTDIR" + mockReadFile.mockRejectedValue(error) + + const result = await readFileIfExists("/not/a/directory/file.txt") + + expect(result).toBe(null) + }) + + it("should return null for EISDIR error", async () => { + const error = new Error("EISDIR") as any + error.code = "EISDIR" + mockReadFile.mockRejectedValue(error) + + const result = await readFileIfExists("/is/a/directory") + + expect(result).toBe(null) + }) + + it("should throw unexpected errors", async () => { + const error = new Error("Permission denied") as any + error.code = "EACCES" + mockReadFile.mockRejectedValue(error) + + await expect(readFileIfExists("/permission/denied/file.txt")).rejects.toThrow("Permission denied") + }) + }) + + describe("getRooDirectoriesForCwd", () => { + it("should return directories for given cwd", () => { + const cwd = "/custom/project/path" + + const result = getRooDirectoriesForCwd(cwd) + + expect(result).toEqual([path.join("/mock/home", ".roo"), path.join(cwd, ".roo")]) + }) + }) + + describe("loadConfiguration", () => { + it("should load global configuration only when project does not exist", async () => { + const error = new Error("ENOENT") as any + error.code = "ENOENT" + mockReadFile.mockResolvedValueOnce("global content").mockRejectedValueOnce(error) + + const result = await loadConfiguration("rules/rules.md", "/project/path") + + expect(result).toEqual({ + global: "global content", + project: null, + merged: "global content", + }) + }) + + it("should load project configuration only when global does not exist", async () => { + const error = new Error("ENOENT") as any + error.code = "ENOENT" + mockReadFile.mockRejectedValueOnce(error).mockResolvedValueOnce("project content") + + const result = await loadConfiguration("rules/rules.md", "/project/path") + + expect(result).toEqual({ + global: null, + project: "project content", + merged: "project content", + }) + }) + + it("should merge global and project configurations with project overriding global", async () => { + mockReadFile.mockResolvedValueOnce("global content").mockResolvedValueOnce("project content") + + const result = await loadConfiguration("rules/rules.md", "/project/path") + + expect(result).toEqual({ + global: "global content", + project: "project content", + merged: "global content\n\n# Project-specific rules (override global):\n\nproject content", + }) + }) + + it("should return empty merged content when neither exists", async () => { + const error = new Error("ENOENT") as any + error.code = "ENOENT" + mockReadFile.mockRejectedValueOnce(error).mockRejectedValueOnce(error) + + const result = await loadConfiguration("rules/rules.md", "/project/path") + + expect(result).toEqual({ + global: null, + project: null, + merged: "", + }) + }) + + it("should propagate unexpected errors from global file read", async () => { + const error = new Error("Permission denied") as any + error.code = "EACCES" + mockReadFile.mockRejectedValueOnce(error) + + await expect(loadConfiguration("rules/rules.md", "/project/path")).rejects.toThrow("Permission denied") + }) + + it("should propagate unexpected errors from project file read", async () => { + const globalError = new Error("ENOENT") as any + globalError.code = "ENOENT" + const projectError = new Error("Permission denied") as any + projectError.code = "EACCES" + + mockReadFile.mockRejectedValueOnce(globalError).mockRejectedValueOnce(projectError) + + await expect(loadConfiguration("rules/rules.md", "/project/path")).rejects.toThrow("Permission denied") + }) + + it("should use correct file paths", async () => { + mockReadFile.mockResolvedValue("content") + + await loadConfiguration("rules/rules.md", "/project/path") + + expect(mockReadFile).toHaveBeenCalledWith(path.join("/mock/home", ".roo", "rules/rules.md"), "utf-8") + expect(mockReadFile).toHaveBeenCalledWith(path.join("/project/path", ".roo", "rules/rules.md"), "utf-8") + }) + }) +}) diff --git a/src/services/roo-config/index.ts b/src/services/roo-config/index.ts new file mode 100644 index 0000000000..b46c39e354 --- /dev/null +++ b/src/services/roo-config/index.ts @@ -0,0 +1,252 @@ +import * as path from "path" +import * as os from "os" +import fs from "fs/promises" + +/** + * Gets the global .roo directory path based on the current platform + * + * @returns The absolute path to the global .roo directory + * + * @example Platform-specific paths: + * ``` + * // macOS/Linux: ~/.roo/ + * // Example: /Users/john/.roo + * + * // Windows: %USERPROFILE%\.roo\ + * // Example: C:\Users\john\.roo + * ``` + * + * @example Usage: + * ```typescript + * const globalDir = getGlobalRooDirectory() + * // Returns: "/Users/john/.roo" (on macOS/Linux) + * // Returns: "C:\\Users\\john\\.roo" (on Windows) + * ``` + */ +export function getGlobalRooDirectory(): string { + const homeDir = os.homedir() + return path.join(homeDir, ".roo") +} + +/** + * Gets the project-local .roo directory path for a given cwd + * + * @param cwd - Current working directory (project path) + * @returns The absolute path to the project-local .roo directory + * + * @example + * ```typescript + * const projectDir = getProjectRooDirectoryForCwd('/Users/john/my-project') + * // Returns: "/Users/john/my-project/.roo" + * + * const windowsProjectDir = getProjectRooDirectoryForCwd('C:\\Users\\john\\my-project') + * // Returns: "C:\\Users\\john\\my-project\\.roo" + * ``` + * + * @example Directory structure: + * ``` + * /Users/john/my-project/ + * ├── .roo/ # Project-local configuration directory + * │ ├── rules/ + * │ │ └── rules.md + * │ ├── custom-instructions.md + * │ └── config/ + * │ └── settings.json + * ├── src/ + * │ └── index.ts + * └── package.json + * ``` + */ +export function getProjectRooDirectoryForCwd(cwd: string): string { + return path.join(cwd, ".roo") +} + +/** + * Checks if a directory exists + */ +export async function directoryExists(dirPath: string): Promise { + try { + const stat = await fs.stat(dirPath) + return stat.isDirectory() + } catch (error: any) { + // Only catch expected "not found" errors + if (error.code === "ENOENT" || error.code === "ENOTDIR") { + return false + } + // Re-throw unexpected errors (permission, I/O, etc.) + throw error + } +} + +/** + * Checks if a file exists + */ +export async function fileExists(filePath: string): Promise { + try { + const stat = await fs.stat(filePath) + return stat.isFile() + } catch (error: any) { + // Only catch expected "not found" errors + if (error.code === "ENOENT" || error.code === "ENOTDIR") { + return false + } + // Re-throw unexpected errors (permission, I/O, etc.) + throw error + } +} + +/** + * Reads a file safely, returning null if it doesn't exist + */ +export async function readFileIfExists(filePath: string): Promise { + try { + return await fs.readFile(filePath, "utf-8") + } catch (error: any) { + // Only catch expected "not found" errors + if (error.code === "ENOENT" || error.code === "ENOTDIR" || error.code === "EISDIR") { + return null + } + // Re-throw unexpected errors (permission, I/O, etc.) + throw error + } +} + +/** + * Gets the ordered list of .roo directories to check (global first, then project-local) + * + * @param cwd - Current working directory (project path) + * @returns Array of directory paths to check in order [global, project-local] + * + * @example + * ```typescript + * // For a project at /Users/john/my-project + * const directories = getRooDirectoriesForCwd('/Users/john/my-project') + * // Returns: + * // [ + * // '/Users/john/.roo', // Global directory + * // '/Users/john/my-project/.roo' // Project-local directory + * // ] + * ``` + * + * @example Directory structure: + * ``` + * /Users/john/ + * ├── .roo/ # Global configuration + * │ ├── rules/ + * │ │ └── rules.md + * │ └── custom-instructions.md + * └── my-project/ + * ├── .roo/ # Project-specific configuration + * │ ├── rules/ + * │ │ └── rules.md # Overrides global rules + * │ └── project-notes.md + * └── src/ + * └── index.ts + * ``` + */ +export function getRooDirectoriesForCwd(cwd: string): string[] { + const directories: string[] = [] + + // Add global directory first + directories.push(getGlobalRooDirectory()) + + // Add project-local directory second + directories.push(getProjectRooDirectoryForCwd(cwd)) + + return directories +} + +/** + * Loads configuration from multiple .roo directories with project overriding global + * + * @param relativePath - The relative path within each .roo directory (e.g., 'rules/rules.md') + * @param cwd - Current working directory (project path) + * @returns Object with global and project content, plus merged content + * + * @example + * ```typescript + * // Load rules configuration for a project + * const config = await loadConfiguration('rules/rules.md', '/Users/john/my-project') + * + * // Returns: + * // { + * // global: "Global rules content...", // From ~/.roo/rules/rules.md + * // project: "Project rules content...", // From /Users/john/my-project/.roo/rules/rules.md + * // merged: "Global rules content...\n\n# Project-specific rules (override global):\n\nProject rules content..." + * // } + * ``` + * + * @example File paths resolved: + * ``` + * relativePath: 'rules/rules.md' + * cwd: '/Users/john/my-project' + * + * Reads from: + * - Global: /Users/john/.roo/rules/rules.md + * - Project: /Users/john/my-project/.roo/rules/rules.md + * + * Other common relativePath examples: + * - 'custom-instructions.md' + * - 'config/settings.json' + * - 'templates/component.tsx' + * ``` + * + * @example Merging behavior: + * ``` + * // If only global exists: + * { global: "content", project: null, merged: "content" } + * + * // If only project exists: + * { global: null, project: "content", merged: "content" } + * + * // If both exist: + * { + * global: "global content", + * project: "project content", + * merged: "global content\n\n# Project-specific rules (override global):\n\nproject content" + * } + * ``` + */ +export async function loadConfiguration( + relativePath: string, + cwd: string, +): Promise<{ + global: string | null + project: string | null + merged: string +}> { + const globalDir = getGlobalRooDirectory() + const projectDir = getProjectRooDirectoryForCwd(cwd) + + const globalFilePath = path.join(globalDir, relativePath) + const projectFilePath = path.join(projectDir, relativePath) + + // Read global configuration + const globalContent = await readFileIfExists(globalFilePath) + + // Read project-local configuration + const projectContent = await readFileIfExists(projectFilePath) + + // Merge configurations - project overrides global + let merged = "" + + if (globalContent) { + merged += globalContent + } + + if (projectContent) { + if (merged) { + merged += "\n\n# Project-specific rules (override global):\n\n" + } + merged += projectContent + } + + return { + global: globalContent, + project: projectContent, + merged: merged || "", + } +} + +// Export with backward compatibility alias +export const loadRooConfiguration: typeof loadConfiguration = loadConfiguration diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 9a2c9230bd..73ebf59d4c 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -9,6 +9,7 @@ import type { ClineMessage, OrganizationAllowList, CloudUserInfo, + ShareVisibility, } from "@roo-code/types" import { GitCommit } from "../utils/git" @@ -97,6 +98,7 @@ export interface ExtensionMessage { | "codebaseIndexConfig" | "marketplaceInstallResult" | "marketplaceData" + | "shareTaskSuccess" text?: string payload?: any // Add a generic payload for now, can refine later action?: @@ -145,6 +147,7 @@ export interface ExtensionMessage { tab?: string marketplaceItems?: MarketplaceItem[] marketplaceInstalledMetadata?: MarketplaceInstalledMetadata + visibility?: ShareVisibility } export type ExtensionState = Pick< @@ -253,6 +256,7 @@ export type ExtensionState = Pick< cloudUserInfo: CloudUserInfo | null cloudIsAuthenticated: boolean + cloudApiUrl?: string sharingEnabled: boolean organizationAllowList: OrganizationAllowList @@ -261,6 +265,7 @@ export type ExtensionState = Pick< marketplaceItems?: MarketplaceItem[] marketplaceInstalledMetadata?: { project: Record; global: Record } profileThresholds: Record + hasOpenedModeSelector: boolean } export interface ClineSayTool { diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index dcded2a69d..7efc97e8c7 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -6,6 +6,7 @@ import type { ModeConfig, InstallMarketplaceItemOptions, MarketplaceItem, + ShareVisibility, } from "@roo-code/types" import { marketplaceItemSchema } from "@roo-code/types" @@ -148,6 +149,7 @@ export interface WebviewMessage { | "searchFiles" | "toggleApiConfigPin" | "setHistoryPreviewCollapsed" + | "hasOpenedModeSelector" | "accountButtonClicked" | "rooCloudSignIn" | "rooCloudSignOut" @@ -172,6 +174,7 @@ export interface WebviewMessage { | "fetchMarketplaceData" | "switchTab" | "profileThresholds" + | "shareTaskSuccess" text?: string tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account" disabled?: boolean @@ -209,7 +212,7 @@ export interface WebviewMessage { mpItem?: MarketplaceItem mpInstallOptions?: InstallMarketplaceItemOptions config?: Record // Add config to the payload - visibility?: "organization" | "public" // For share visibility + visibility?: ShareVisibility // For share visibility } export const checkoutDiffPayloadSchema = z.object({ diff --git a/src/shared/modes.ts b/src/shared/modes.ts index 56d41f3c73..53bc2369db 100644 --- a/src/shared/modes.ts +++ b/src/shared/modes.ts @@ -68,6 +68,7 @@ export const modes: readonly ModeConfig[] = [ "You are Roo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.", whenToUse: "Use this mode when you need to write, modify, or refactor code. Ideal for implementing features, fixing bugs, creating new files, or making code improvements across any programming language or framework.", + description: "Write, modify, and refactor code", groups: ["read", "edit", "browser", "command", "mcp"], }, { @@ -77,6 +78,7 @@ export const modes: readonly ModeConfig[] = [ "You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution.", whenToUse: "Use this mode when you need to plan, design, or strategize before implementation. Perfect for breaking down complex problems, creating technical specifications, designing system architecture, or brainstorming solutions before coding.", + description: "Plan and design before implementation", groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], "browser", "mcp"], customInstructions: "1. Do some information gathering (for example using read_file or search_files) to get more context about the task.\n\n2. You should also ask the user clarifying questions to get a better understanding of the task.\n\n3. Once you've gained more context about the user's request, you should create a detailed plan for how to accomplish the task. Include Mermaid diagrams if they help make your plan clearer.\n\n4. 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.\n\n5. Once the user confirms the plan, ask them if they'd like you to write it to a markdown file.\n\n6. Use the switch_mode tool to request that the user switch to another mode to implement the solution.", @@ -88,6 +90,7 @@ export const modes: readonly ModeConfig[] = [ "You are Roo, a knowledgeable technical assistant focused on answering questions and providing information about software development, technology, and related topics.", whenToUse: "Use this mode when you need explanations, documentation, or answers to technical questions. Best for understanding concepts, analyzing existing code, getting recommendations, or learning about technologies without making changes.", + description: "Get answers and explanations", groups: ["read", "browser", "mcp"], customInstructions: "You can analyze code, explain concepts, and access external resources. Always answer the user's questions thoroughly, and do not switch to implementing code unless explicitly requested by the user. Include Mermaid diagrams when they clarify your response.", @@ -99,6 +102,7 @@ export const modes: readonly ModeConfig[] = [ "You are Roo, an expert software debugger specializing in systematic problem diagnosis and resolution.", whenToUse: "Use this mode when you're troubleshooting issues, investigating errors, or diagnosing problems. Specialized in systematic debugging, adding logging, analyzing stack traces, and identifying root causes before applying fixes.", + description: "Diagnose and fix software issues", groups: ["read", "edit", "browser", "command", "mcp"], customInstructions: "Reflect on 5-7 different possible sources of the problem, distill those down to 1-2 most likely sources, and then add logs to validate your assumptions. Explicitly ask the user to confirm the diagnosis before fixing the problem.", @@ -110,6 +114,7 @@ export const modes: readonly ModeConfig[] = [ "You are Roo, a strategic workflow orchestrator who coordinates complex tasks by delegating them to appropriate specialized modes. You have a comprehensive understanding of each mode's capabilities and limitations, allowing you to effectively break down complex problems into discrete tasks that can be solved by different specialists.", whenToUse: "Use this mode for complex, multi-step projects that require coordination across different specialties. Ideal when you need to break down large tasks into subtasks, manage workflows, or coordinate work that spans multiple domains or expertise areas.", + description: "Coordinate tasks across multiple modes", groups: [], customInstructions: "Your role is to coordinate complex workflows by delegating tasks to specialized modes. As an orchestrator, you should:\n\n1. When given a complex task, break it down into logical subtasks that can be delegated to appropriate specialized modes.\n\n2. For each subtask, use the `new_task` tool to delegate. Choose the most appropriate mode for the subtask's specific goal and provide comprehensive instructions in the `message` parameter. These instructions must include:\n * All necessary context from the parent task or previous subtasks required to complete the work.\n * A clearly defined scope, specifying exactly what the subtask should accomplish.\n * An explicit statement that the subtask should *only* perform the work outlined in these instructions and not deviate.\n * An instruction for the subtask to signal completion by using the `attempt_completion` tool, providing a concise yet thorough summary of the outcome in the `result` parameter, keeping in mind that this summary will be the source of truth used to keep track of what was completed on this project.\n * A statement that these specific instructions supersede any conflicting general instructions the subtask's mode might have.\n\n3. Track and manage the progress of all subtasks. When a subtask is completed, analyze its results and determine the next steps.\n\n4. Help the user understand how the different subtasks fit together in the overall workflow. Provide clear reasoning about why you're delegating specific tasks to specific modes.\n\n5. When all subtasks are completed, synthesize the results and provide a comprehensive overview of what was accomplished.\n\n6. Ask clarifying questions when necessary to better understand how to break down complex tasks effectively.\n\n7. Suggest improvements to the workflow based on the results of completed subtasks.\n\nUse subtasks to maintain clarity. If a request significantly shifts focus or requires a different expertise (mode), consider creating a subtask rather than overloading the current one.", @@ -188,10 +193,12 @@ export function getModeSelection(mode: string, promptComponent?: PromptComponent const roleDefinition = modeToUse?.roleDefinition || "" const baseInstructions = modeToUse?.customInstructions || "" + const description = (customMode || builtInMode)?.description || "" return { roleDefinition, baseInstructions, + description, } } @@ -282,6 +289,7 @@ export const defaultPrompts: Readonly = Object.freeze( roleDefinition: mode.roleDefinition, whenToUse: mode.whenToUse, customInstructions: mode.customInstructions, + description: mode.description, }, ]), ), @@ -298,6 +306,7 @@ export async function getAllModesWithPrompts(context: vscode.ExtensionContext): roleDefinition: customModePrompts[mode.slug]?.roleDefinition ?? mode.roleDefinition, whenToUse: customModePrompts[mode.slug]?.whenToUse ?? mode.whenToUse, customInstructions: customModePrompts[mode.slug]?.customInstructions ?? mode.customInstructions, + // description is not overridable via customModePrompts, so we keep the original })) } @@ -321,6 +330,7 @@ export async function getFullModeDetails( // Get the base custom instructions const baseCustomInstructions = promptComponent?.customInstructions || baseMode.customInstructions || "" const baseWhenToUse = promptComponent?.whenToUse || baseMode.whenToUse || "" + const baseDescription = promptComponent?.description || baseMode.description || "" // If we have cwd, load and combine all custom instructions let fullCustomInstructions = baseCustomInstructions @@ -339,6 +349,7 @@ export async function getFullModeDetails( ...baseMode, roleDefinition: promptComponent?.roleDefinition || baseMode.roleDefinition, whenToUse: baseWhenToUse, + description: baseDescription, customInstructions: fullCustomInstructions, } } @@ -353,6 +364,16 @@ export function getRoleDefinition(modeSlug: string, customModes?: ModeConfig[]): return mode.roleDefinition } +// Helper function to safely get description +export function getDescription(modeSlug: string, customModes?: ModeConfig[]): string { + const mode = getModeBySlug(modeSlug, customModes) + if (!mode) { + console.warn(`No mode found for slug: ${modeSlug}`) + return "" + } + return mode.description ?? "" +} + // Helper function to safely get whenToUse export function getWhenToUse(modeSlug: string, customModes?: ModeConfig[]): string { const mode = getModeBySlug(modeSlug, customModes) diff --git a/src/utils/__tests__/autoImportSettings.spec.ts b/src/utils/__tests__/autoImportSettings.spec.ts new file mode 100644 index 0000000000..04d2a6a6c7 --- /dev/null +++ b/src/utils/__tests__/autoImportSettings.spec.ts @@ -0,0 +1,295 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest" + +// Mock dependencies +vi.mock("vscode", () => ({ + workspace: { + getConfiguration: vi.fn(), + }, + window: { + showInformationMessage: vi.fn(), + showWarningMessage: vi.fn(), + }, + Uri: { + file: vi.fn((path: string) => ({ fsPath: path })), + }, +})) + +vi.mock("fs/promises", () => ({ + __esModule: true, + default: { + readFile: vi.fn(), + }, + readFile: vi.fn(), +})) + +vi.mock("path", () => ({ + join: vi.fn((...args: string[]) => args.join("/")), + isAbsolute: vi.fn((p: string) => p.startsWith("/")), + basename: vi.fn((p: string) => p.split("/").pop() || ""), +})) + +vi.mock("os", () => ({ + homedir: vi.fn(() => "/home/user"), +})) + +vi.mock("../fs", () => ({ + fileExistsAtPath: vi.fn(), +})) + +vi.mock("../../core/config/ProviderSettingsManager", async (importOriginal) => { + const originalModule = await importOriginal() + return { + __esModule: true, + // We need to mock the class constructor and its methods, + // but keep other exports (like schemas) as their original values. + ...(originalModule || {}), // Spread original exports + ProviderSettingsManager: vi.fn().mockImplementation(() => ({ + // Mock the class + export: vi.fn().mockResolvedValue({ + apiConfigs: {}, + modeApiConfigs: {}, + currentApiConfigName: "default", + }), + import: vi.fn().mockResolvedValue({ success: true }), + listConfig: vi.fn().mockResolvedValue([]), + })), + } +}) +vi.mock("../../core/config/ContextProxy") +vi.mock("../../core/config/CustomModesManager") + +import { autoImportSettings } from "../autoImportSettings" +import * as vscode from "vscode" +import fsPromises from "fs/promises" +import { fileExistsAtPath } from "../fs" + +describe("autoImportSettings", () => { + let mockProviderSettingsManager: any + let mockContextProxy: any + let mockCustomModesManager: any + let mockOutputChannel: any + let mockProvider: any + + beforeEach(() => { + // Reset all mocks + vi.clearAllMocks() + + // Mock output channel + mockOutputChannel = { + appendLine: vi.fn(), + } + + // Mock provider settings manager + mockProviderSettingsManager = { + export: vi.fn().mockResolvedValue({ + apiConfigs: {}, + modeApiConfigs: {}, + currentApiConfigName: "default", + }), + import: vi.fn().mockResolvedValue({ success: true }), + listConfig: vi.fn().mockResolvedValue([]), + } + + // Mock context proxy + mockContextProxy = { + setValues: vi.fn().mockResolvedValue(undefined), + setValue: vi.fn().mockResolvedValue(undefined), + setProviderSettings: vi.fn().mockResolvedValue(undefined), + } + + // Mock custom modes manager + mockCustomModesManager = { + updateCustomMode: vi.fn().mockResolvedValue(undefined), + } + + // mockProvider must be initialized AFTER its dependencies + mockProvider = { + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + upsertProviderProfile: vi.fn().mockResolvedValue({ success: true }), + postStateToWebview: vi.fn().mockResolvedValue({ success: true }), + } + + // Reset fs mock + vi.mocked(fsPromises.readFile).mockReset() + vi.mocked(fileExistsAtPath).mockReset() + vi.mocked(vscode.workspace.getConfiguration).mockReset() + vi.mocked(vscode.window.showInformationMessage).mockReset() + vi.mocked(vscode.window.showWarningMessage).mockReset() + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it("should skip auto-import when no settings path is specified", async () => { + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({ + get: vi.fn().mockReturnValue(""), + } as any) + + await autoImportSettings(mockOutputChannel, { + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + customModesManager: mockCustomModesManager, + }) + + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith( + "[AutoImport] No auto-import settings path specified, skipping auto-import", + ) + expect(mockProviderSettingsManager.import).not.toHaveBeenCalled() + }) + + it("should skip auto-import when settings file does not exist", async () => { + const settingsPath = "~/Documents/roo-config.json" + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({ + get: vi.fn().mockReturnValue(settingsPath), + } as any) + + // Mock fileExistsAtPath to return false + vi.mocked(fileExistsAtPath).mockResolvedValue(false) + + await autoImportSettings(mockOutputChannel, { + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + customModesManager: mockCustomModesManager, + }) + + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith( + "[AutoImport] Checking for settings file at: /home/user/Documents/roo-config.json", + ) + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith( + "[AutoImport] Settings file not found at /home/user/Documents/roo-config.json, skipping auto-import", + ) + expect(mockProviderSettingsManager.import).not.toHaveBeenCalled() + }) + + it("should successfully import settings when file exists and is valid", async () => { + const settingsPath = "/absolute/path/to/config.json" + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({ + get: vi.fn().mockReturnValue(settingsPath), + } as any) + + // Mock fileExistsAtPath to return true + vi.mocked(fileExistsAtPath).mockResolvedValue(true) + + // Mock fs.readFile to return valid config + const mockSettings = { + providerProfiles: { + currentApiConfigName: "test-config", + apiConfigs: { + "test-config": { + apiProvider: "anthropic", + anthropicApiKey: "test-key", + }, + }, + }, + globalSettings: { + customInstructions: "Test instructions", + }, + } + + vi.mocked(fsPromises.readFile).mockResolvedValue(JSON.stringify(mockSettings) as any) + + await autoImportSettings(mockOutputChannel, { + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + customModesManager: mockCustomModesManager, + }) + + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith( + "[AutoImport] Checking for settings file at: /absolute/path/to/config.json", + ) + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith( + "[AutoImport] Successfully imported settings from /absolute/path/to/config.json", + ) + expect(vscode.window.showInformationMessage).toHaveBeenCalledWith("info.auto_import_success") + expect(mockProviderSettingsManager.import).toHaveBeenCalled() + expect(mockContextProxy.setValues).toHaveBeenCalled() + }) + + it("should handle invalid JSON gracefully", async () => { + const settingsPath = "~/config.json" + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({ + get: vi.fn().mockReturnValue(settingsPath), + } as any) + + // Mock fileExistsAtPath to return true + vi.mocked(fileExistsAtPath).mockResolvedValue(true) + + // Mock fs.readFile to return invalid JSON + vi.mocked(fsPromises.readFile).mockResolvedValue("invalid json" as any) + + await autoImportSettings(mockOutputChannel, { + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + customModesManager: mockCustomModesManager, + }) + + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith( + expect.stringContaining("[AutoImport] Failed to import settings:"), + ) + expect(vscode.window.showWarningMessage).toHaveBeenCalledWith("warnings.auto_import_failed") + expect(mockProviderSettingsManager.import).not.toHaveBeenCalled() + }) + + it("should resolve home directory paths correctly", async () => { + const settingsPath = "~/Documents/config.json" + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({ + get: vi.fn().mockReturnValue(settingsPath), + } as any) + + // Mock fileExistsAtPath to return false (so we can check the resolved path) + vi.mocked(fileExistsAtPath).mockResolvedValue(false) + + await autoImportSettings(mockOutputChannel, { + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + customModesManager: mockCustomModesManager, + }) + + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith( + "[AutoImport] Checking for settings file at: /home/user/Documents/config.json", + ) + }) + + it("should handle relative paths by resolving them to home directory", async () => { + const settingsPath = "Documents/config.json" + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({ + get: vi.fn().mockReturnValue(settingsPath), + } as any) + + // Mock fileExistsAtPath to return false (so we can check the resolved path) + vi.mocked(fileExistsAtPath).mockResolvedValue(false) + + await autoImportSettings(mockOutputChannel, { + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + customModesManager: mockCustomModesManager, + }) + + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith( + "[AutoImport] Checking for settings file at: /home/user/Documents/config.json", + ) + }) + + it("should handle file system errors gracefully", async () => { + const settingsPath = "~/config.json" + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({ + get: vi.fn().mockReturnValue(settingsPath), + } as any) + + // Mock fileExistsAtPath to throw an error + vi.mocked(fileExistsAtPath).mockRejectedValue(new Error("File system error")) + + await autoImportSettings(mockOutputChannel, { + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + customModesManager: mockCustomModesManager, + }) + + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith( + expect.stringContaining("[AutoImport] Unexpected error during auto-import:"), + ) + expect(mockProviderSettingsManager.import).not.toHaveBeenCalled() + }) +}) diff --git a/src/utils/__tests__/git.spec.ts b/src/utils/__tests__/git.spec.ts index 754d041e29..3dc0f6fe24 100644 --- a/src/utils/__tests__/git.spec.ts +++ b/src/utils/__tests__/git.spec.ts @@ -1,6 +1,19 @@ import { ExecException } from "child_process" +import * as vscode from "vscode" +import * as fs from "fs" +import * as path from "path" -import { searchCommits, getCommitInfo, getWorkingState } from "../git" +import { + searchCommits, + getCommitInfo, + getWorkingState, + getGitRepositoryInfo, + sanitizeGitUrl, + extractRepositoryName, + getWorkspaceGitInfo, + GitRepositoryInfo, +} from "../git" +import { truncateOutput } from "../../integrations/misc/extract-text" type ExecFunction = ( command: string, @@ -15,6 +28,24 @@ vitest.mock("child_process", () => ({ exec: vitest.fn(), })) +// Mock fs.promises +vitest.mock("fs", () => ({ + promises: { + access: vitest.fn(), + readFile: vitest.fn(), + }, +})) + +// Create a mock for vscode +const mockWorkspaceFolders = vitest.fn() +vitest.mock("vscode", () => ({ + workspace: { + get workspaceFolders() { + return mockWorkspaceFolders() + }, + }, +})) + // Mock util.promisify to return our own mock function vitest.mock("util", () => ({ promisify: vitest.fn((fn: ExecFunction): PromisifiedExec => { @@ -169,7 +200,6 @@ describe("git utils", () => { if (command === cmd) { callback(null, response) return {} as any - return {} as any } } callback(new Error("Unexpected command")) @@ -217,7 +247,6 @@ describe("git utils", () => { if (command.startsWith(cmd)) { callback(null, response) return {} as any - return {} as any } } callback(new Error("Unexpected command")) @@ -229,6 +258,7 @@ describe("git utils", () => { expect(result).toContain("Author: John Doe") expect(result).toContain("Files Changed:") expect(result).toContain("Full Changes:") + expect(vitest.mocked(truncateOutput)).toHaveBeenCalled() }) it("should return error message when git is not installed", async () => { @@ -297,6 +327,7 @@ describe("git utils", () => { expect(result).toContain("Working directory changes:") expect(result).toContain("src/file1.ts") expect(result).toContain("src/file2.ts") + expect(vitest.mocked(truncateOutput)).toHaveBeenCalled() }) it("should return message when working directory is clean", async () => { @@ -311,7 +342,6 @@ describe("git utils", () => { if (command === cmd) { callback(null, response) return {} as any - return {} as any } } callback(new Error("Unexpected command")) @@ -361,3 +391,315 @@ describe("git utils", () => { }) }) }) + +describe("getGitRepositoryInfo", () => { + const workspaceRoot = "/test/workspace" + const gitDir = path.join(workspaceRoot, ".git") + const configPath = path.join(gitDir, "config") + const headPath = path.join(gitDir, "HEAD") + + beforeEach(() => { + vitest.clearAllMocks() + }) + + it("should return empty object when not a git repository", async () => { + // Mock fs.access to throw error (directory doesn't exist) + vitest.mocked(fs.promises.access).mockRejectedValueOnce(new Error("ENOENT")) + + const result = await getGitRepositoryInfo(workspaceRoot) + + expect(result).toEqual({}) + expect(fs.promises.access).toHaveBeenCalledWith(gitDir) + }) + + it("should extract repository info from git config", async () => { + // Clear previous mocks + vitest.clearAllMocks() + + // Create a spy to track the implementation + const gitSpy = vitest.spyOn(fs.promises, "readFile") + + // Mock successful access to .git directory + vitest.mocked(fs.promises.access).mockResolvedValue(undefined) + + // Mock git config file content + const mockConfig = ` +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true + ignorecase = true + precomposeunicode = true +[remote "origin"] + url = https://github.com/RooCodeInc/Roo-Code.git + fetch = +refs/heads/*:refs/remotes/origin/* +[branch "main"] + remote = origin + merge = refs/heads/main +` + // Mock HEAD file content + const mockHead = "ref: refs/heads/main" + + // Setup the readFile mock to return different values based on the path + gitSpy.mockImplementation((path: any, encoding: any) => { + if (path === configPath) { + return Promise.resolve(mockConfig) + } else if (path === headPath) { + return Promise.resolve(mockHead) + } + return Promise.reject(new Error(`Unexpected path: ${path}`)) + }) + + const result = await getGitRepositoryInfo(workspaceRoot) + + expect(result).toEqual({ + repositoryUrl: "https://github.com/RooCodeInc/Roo-Code.git", + repositoryName: "RooCodeInc/Roo-Code", + defaultBranch: "main", + }) + + // Verify config file was read + expect(gitSpy).toHaveBeenCalledWith(configPath, "utf8") + + // The implementation might not always read the HEAD file if it already found the branch in config + // So we don't assert that it was called + }) + + it("should handle missing repository URL in config", async () => { + // Clear previous mocks + vitest.clearAllMocks() + + // Create a spy to track the implementation + const gitSpy = vitest.spyOn(fs.promises, "readFile") + + // Mock successful access to .git directory + vitest.mocked(fs.promises.access).mockResolvedValue(undefined) + + // Mock git config file without URL + const mockConfig = ` +[core] + repositoryformatversion = 0 + filemode = true + bare = false +` + // Mock HEAD file content + const mockHead = "ref: refs/heads/main" + + // Setup the readFile mock to return different values based on the path + gitSpy.mockImplementation((path: any, encoding: any) => { + if (path === configPath) { + return Promise.resolve(mockConfig) + } else if (path === headPath) { + return Promise.resolve(mockHead) + } + return Promise.reject(new Error(`Unexpected path: ${path}`)) + }) + + const result = await getGitRepositoryInfo(workspaceRoot) + + expect(result).toEqual({ + defaultBranch: "main", + }) + }) + + it("should handle errors when reading git config", async () => { + // Clear previous mocks + vitest.clearAllMocks() + + // Create a spy to track the implementation + const gitSpy = vitest.spyOn(fs.promises, "readFile") + + // Mock successful access to .git directory + vitest.mocked(fs.promises.access).mockResolvedValue(undefined) + + // Setup the readFile mock to return different values based on the path + gitSpy.mockImplementation((path: any, encoding: any) => { + if (path === configPath) { + return Promise.reject(new Error("Failed to read config")) + } else if (path === headPath) { + return Promise.resolve("ref: refs/heads/main") + } + return Promise.reject(new Error(`Unexpected path: ${path}`)) + }) + + const result = await getGitRepositoryInfo(workspaceRoot) + + expect(result).toEqual({ + defaultBranch: "main", + }) + }) + + it("should handle errors when reading HEAD file", async () => { + // Clear previous mocks + vitest.clearAllMocks() + + // Create a spy to track the implementation + const gitSpy = vitest.spyOn(fs.promises, "readFile") + + // Mock successful access to .git directory + vitest.mocked(fs.promises.access).mockResolvedValue(undefined) + + // Setup the readFile mock to return different values based on the path + gitSpy.mockImplementation((path: any, encoding: any) => { + if (path === configPath) { + return Promise.resolve(` +[remote "origin"] + url = https://github.com/RooCodeInc/Roo-Code.git +`) + } else if (path === headPath) { + return Promise.reject(new Error("Failed to read HEAD")) + } + return Promise.reject(new Error(`Unexpected path: ${path}`)) + }) + + const result = await getGitRepositoryInfo(workspaceRoot) + + expect(result).toEqual({ + repositoryUrl: "https://github.com/RooCodeInc/Roo-Code.git", + repositoryName: "RooCodeInc/Roo-Code", + }) + }) +}) + +describe("sanitizeGitUrl", () => { + it("should sanitize HTTPS URLs with credentials", () => { + const url = "https://username:password@github.com/RooCodeInc/Roo-Code.git" + const sanitized = sanitizeGitUrl(url) + + expect(sanitized).toBe("https://github.com/RooCodeInc/Roo-Code.git") + }) + + it("should leave SSH URLs unchanged", () => { + const url = "git@github.com:RooCodeInc/Roo-Code.git" + const sanitized = sanitizeGitUrl(url) + + expect(sanitized).toBe("git@github.com:RooCodeInc/Roo-Code.git") + }) + + it("should leave SSH URLs with ssh:// prefix unchanged", () => { + const url = "ssh://git@github.com/RooCodeInc/Roo-Code.git" + const sanitized = sanitizeGitUrl(url) + + expect(sanitized).toBe("ssh://git@github.com/RooCodeInc/Roo-Code.git") + }) + + it("should remove tokens from other URL formats", () => { + const url = "https://oauth2:ghp_abcdef1234567890abcdef1234567890abcdef@github.com/RooCodeInc/Roo-Code.git" + const sanitized = sanitizeGitUrl(url) + + expect(sanitized).toBe("https://github.com/RooCodeInc/Roo-Code.git") + }) + + it("should handle invalid URLs gracefully", () => { + const url = "not-a-valid-url" + const sanitized = sanitizeGitUrl(url) + + expect(sanitized).toBe("not-a-valid-url") + }) +}) + +describe("extractRepositoryName", () => { + it("should extract repository name from HTTPS URL", () => { + const url = "https://github.com/RooCodeInc/Roo-Code.git" + const repoName = extractRepositoryName(url) + + expect(repoName).toBe("RooCodeInc/Roo-Code") + }) + + it("should extract repository name from HTTPS URL without .git suffix", () => { + const url = "https://github.com/RooCodeInc/Roo-Code" + const repoName = extractRepositoryName(url) + + expect(repoName).toBe("RooCodeInc/Roo-Code") + }) + + it("should extract repository name from SSH URL", () => { + const url = "git@github.com:RooCodeInc/Roo-Code.git" + const repoName = extractRepositoryName(url) + + expect(repoName).toBe("RooCodeInc/Roo-Code") + }) + + it("should extract repository name from SSH URL with ssh:// prefix", () => { + const url = "ssh://git@github.com/RooCodeInc/Roo-Code.git" + const repoName = extractRepositoryName(url) + + expect(repoName).toBe("RooCodeInc/Roo-Code") + }) + + it("should return empty string for unrecognized URL formats", () => { + const url = "not-a-valid-git-url" + const repoName = extractRepositoryName(url) + + expect(repoName).toBe("") + }) + + it("should handle URLs with credentials", () => { + const url = "https://username:password@github.com/RooCodeInc/Roo-Code.git" + const repoName = extractRepositoryName(url) + + expect(repoName).toBe("RooCodeInc/Roo-Code") + }) +}) + +describe("getWorkspaceGitInfo", () => { + const workspaceRoot = "/test/workspace" + + beforeEach(() => { + vitest.clearAllMocks() + }) + + it("should return empty object when no workspace folders", async () => { + // Mock workspace with no folders + mockWorkspaceFolders.mockReturnValue(undefined) + + const result = await getWorkspaceGitInfo() + + expect(result).toEqual({}) + }) + + it("should return git info for the first workspace folder", async () => { + // Clear previous mocks + vitest.clearAllMocks() + + // Mock workspace with one folder + mockWorkspaceFolders.mockReturnValue([{ uri: { fsPath: workspaceRoot }, name: "workspace", index: 0 }]) + + // Create a spy to track the implementation + const gitSpy = vitest.spyOn(fs.promises, "access") + const readFileSpy = vitest.spyOn(fs.promises, "readFile") + + // Mock successful access to .git directory + gitSpy.mockResolvedValue(undefined) + + // Mock git config file content + const mockConfig = ` +[remote "origin"] + url = https://github.com/RooCodeInc/Roo-Code.git +[branch "main"] + remote = origin + merge = refs/heads/main +` + + // Setup the readFile mock to return config content + readFileSpy.mockImplementation((path: any, encoding: any) => { + if (path.includes("config")) { + return Promise.resolve(mockConfig) + } + return Promise.reject(new Error(`Unexpected path: ${path}`)) + }) + + const result = await getWorkspaceGitInfo() + + expect(result).toEqual({ + repositoryUrl: "https://github.com/RooCodeInc/Roo-Code.git", + repositoryName: "RooCodeInc/Roo-Code", + defaultBranch: "main", + }) + + // Verify the fs operations were called with the correct workspace path + expect(gitSpy).toHaveBeenCalled() + expect(readFileSpy).toHaveBeenCalled() + }) +}) diff --git a/src/utils/__tests__/safeWriteJson.test.ts b/src/utils/__tests__/safeWriteJson.test.ts new file mode 100644 index 0000000000..f3b687595a --- /dev/null +++ b/src/utils/__tests__/safeWriteJson.test.ts @@ -0,0 +1,480 @@ +import { vi, describe, test, expect, beforeEach, afterEach, beforeAll, afterAll } from "vitest" +import * as actualFsPromises from "fs/promises" +import * as fsSyncActual from "fs" +import { Writable } from "stream" +import { safeWriteJson } from "../safeWriteJson" +import * as path from "path" +import * as os from "os" + +const originalFsPromisesRename = actualFsPromises.rename +const originalFsPromisesUnlink = actualFsPromises.unlink +const originalFsPromisesWriteFile = actualFsPromises.writeFile +const _originalFsPromisesAccess = actualFsPromises.access +const originalFsPromisesMkdir = actualFsPromises.mkdir + +vi.mock("fs/promises", async () => { + const actual = await vi.importActual("fs/promises") + // Start with all actual implementations. + const mockedFs = { ...actual } + // Selectively wrap functions with vi.fn() if they are spied on + // or have their implementations changed in tests. + // This ensures that other fs.promises functions used by the SUT + // (like proper-lockfile's internals) will use their actual implementations. + mockedFs.writeFile = vi.fn(actual.writeFile) as any + mockedFs.readFile = vi.fn(actual.readFile) as any + mockedFs.rename = vi.fn(actual.rename) as any + mockedFs.unlink = vi.fn(actual.unlink) as any + mockedFs.access = vi.fn(actual.access) as any + mockedFs.mkdtemp = vi.fn(actual.mkdtemp) as any + mockedFs.rm = vi.fn(actual.rm) as any + mockedFs.readdir = vi.fn(actual.readdir) as any + mockedFs.mkdir = vi.fn(actual.mkdir) as any + // fs.stat and fs.lstat will be available via { ...actual } + + return mockedFs +}) + +// Mock the 'fs' module for fsSync.createWriteStream +vi.mock("fs", async () => { + const actualFs = await vi.importActual("fs") + return { + ...actualFs, // Spread actual implementations + createWriteStream: vi.fn(actualFs.createWriteStream) as any, // Default to actual, but mockable + } +}) + +import * as fs from "fs/promises" // This will now be the mocked version + +describe("safeWriteJson", () => { + let originalConsoleError: typeof console.error + + beforeAll(() => { + // Store original console.error + originalConsoleError = console.error + }) + + afterAll(() => { + // Restore original console.error + console.error = originalConsoleError + }) + + let tempDir: string + let currentTestFilePath: string + + beforeEach(async () => { + // Create a temporary directory for each test + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "safeWriteJson-test-")) + + // Create a unique file path for each test + currentTestFilePath = path.join(tempDir, "test-file.json") + + // Pre-create the file with initial content to ensure it exists + // This allows proper-lockfile to acquire a lock on an existing file. + await fs.writeFile(currentTestFilePath, JSON.stringify({ initial: "content" })) + }) + + afterEach(async () => { + // Clean up the temporary directory after each test + await fs.rm(tempDir, { recursive: true, force: true }) + + // Reset all mocks to their actual implementations + vi.restoreAllMocks() + }) + + // Helper function to read file content + async function readFileContent(filePath: string): Promise { + const readContent = await fs.readFile(filePath, "utf-8") + return JSON.parse(readContent) + } + + // Helper function to check if a file exists + async function fileExists(filePath: string): Promise { + try { + await fs.access(filePath) + return true + } catch { + return false + } + } + + // Success Scenarios + // Note: Since we pre-create the file in beforeEach, this test will overwrite it. + // If "creation from non-existence" is critical and locking prevents it, safeWriteJson or locking strategy needs review. + test("should successfully write a new file (overwriting initial content from beforeEach)", async () => { + const data = { message: "Hello, new world!" } + + await safeWriteJson(currentTestFilePath, data) + + const content = await readFileContent(currentTestFilePath) + expect(content).toEqual(data) + }) + + test("should successfully overwrite an existing file", async () => { + const initialData = { message: "Initial content" } + const newData = { message: "Updated content" } + + // Write initial data (overwriting the pre-created file from beforeEach) + await originalFsPromisesWriteFile(currentTestFilePath, JSON.stringify(initialData)) + + await safeWriteJson(currentTestFilePath, newData) + + const content = await readFileContent(currentTestFilePath) + expect(content).toEqual(newData) + }) + + // Failure Scenarios + test("should handle failure when writing to tempNewFilePath", async () => { + // currentTestFilePath exists due to beforeEach, allowing lock acquisition. + const data = { message: "test write failure" } + + const mockErrorStream = new Writable() as any + mockErrorStream._write = (_chunk: any, _encoding: any, callback: any) => { + callback(new Error("Write stream error")) + } + // Add missing WriteStream properties + mockErrorStream.close = vi.fn() + mockErrorStream.bytesWritten = 0 + mockErrorStream.path = "" + mockErrorStream.pending = false + + // Mock createWriteStream to return a stream that errors on write + ;(fsSyncActual.createWriteStream as any).mockImplementationOnce((_path: any, _options: any) => { + return mockErrorStream + }) + + await expect(safeWriteJson(currentTestFilePath, data)).rejects.toThrow("Write stream error") + + // Verify the original file still exists and is unchanged + const exists = await fileExists(currentTestFilePath) + expect(exists).toBe(true) + + // Verify content is unchanged (should still have the initial content from beforeEach) + const content = await readFileContent(currentTestFilePath) + expect(content).toEqual({ initial: "content" }) + }) + + test("should handle failure when renaming filePath to tempBackupFilePath (filePath exists)", async () => { + const initialData = { message: "Initial content, should remain" } + const newData = { message: "New content, should not be written" } + + // Overwrite the pre-created file with specific initial data + await originalFsPromisesWriteFile(currentTestFilePath, JSON.stringify(initialData)) + + const renameSpy = vi.spyOn(fs, "rename") + + // Mock rename to fail on the first call (filePath -> tempBackupFilePath) + renameSpy.mockImplementationOnce(async () => { + throw new Error("Rename to backup failed") + }) + + await expect(safeWriteJson(currentTestFilePath, newData)).rejects.toThrow("Rename to backup failed") + + // Verify the original file still exists with initial content + const content = await readFileContent(currentTestFilePath) + expect(content).toEqual(initialData) + }) + + test("should handle failure when renaming tempNewFilePath to filePath (filePath exists, backup succeeded)", async () => { + const initialData = { message: "Initial content, should be restored" } + const newData = { message: "New content" } + + // Overwrite the pre-created file with specific initial data + await originalFsPromisesWriteFile(currentTestFilePath, JSON.stringify(initialData)) + + const renameSpy = vi.spyOn(fs, "rename") + + // Track rename calls + let renameCallCount = 0 + + // Mock rename to succeed on first call (filePath -> tempBackupFilePath) + // and fail on second call (tempNewFilePath -> filePath) + renameSpy.mockImplementation(async (oldPath, newPath) => { + renameCallCount++ + if (renameCallCount === 1) { + // First call: filePath -> tempBackupFilePath (should succeed) + return originalFsPromisesRename(oldPath, newPath) + } else if (renameCallCount === 2) { + // Second call: tempNewFilePath -> filePath (should fail) + throw new Error("Rename from temp to final failed") + } else if (renameCallCount === 3) { + // Third call: tempBackupFilePath -> filePath (rollback, should succeed) + return originalFsPromisesRename(oldPath, newPath) + } + // Default: use original implementation + return originalFsPromisesRename(oldPath, newPath) + }) + + await expect(safeWriteJson(currentTestFilePath, newData)).rejects.toThrow("Rename from temp to final failed") + + // Verify the file was restored to initial content + const content = await readFileContent(currentTestFilePath) + expect(content).toEqual(initialData) + }) + + // Tests for directory creation functionality + test("should create parent directory if it doesn't exist", async () => { + // Create a path in a non-existent subdirectory of the temp dir + const subDir = path.join(tempDir, "new-subdir") + const filePath = path.join(subDir, "file.json") + const data = { test: "directory creation" } + + // Verify directory doesn't exist + await expect(fs.access(subDir)).rejects.toThrow() + + // Write file + await safeWriteJson(filePath, data) + + // Verify directory was created + await expect(fs.access(subDir)).resolves.toBeUndefined() + + // Verify file was written + const content = await readFileContent(filePath) + expect(content).toEqual(data) + }) + + test("should handle multi-level directory creation", async () => { + // Create a new non-existent subdirectory path with multiple levels + const deepDir = path.join(tempDir, "level1", "level2", "level3") + const filePath = path.join(deepDir, "deep-file.json") + const data = { nested: "deeply" } + + // Verify none of the directories exist + await expect(fs.access(path.join(tempDir, "level1"))).rejects.toThrow() + + // Write file + await safeWriteJson(filePath, data) + + // Verify all directories were created + await expect(fs.access(path.join(tempDir, "level1"))).resolves.toBeUndefined() + await expect(fs.access(path.join(tempDir, "level1", "level2"))).resolves.toBeUndefined() + await expect(fs.access(deepDir)).resolves.toBeUndefined() + + // Verify file was written + const content = await readFileContent(filePath) + expect(content).toEqual(data) + }) + + test("should handle directory creation permission errors", async () => { + // Mock mkdir to simulate a permission error + const mkdirSpy = vi.spyOn(fs, "mkdir") + mkdirSpy.mockImplementationOnce(async () => { + const error = new Error("EACCES: permission denied") as any + error.code = "EACCES" + throw error + }) + + const subDir = path.join(tempDir, "forbidden-dir") + const filePath = path.join(subDir, "file.json") + const data = { test: "permission error" } + + // Should throw the permission error + await expect(safeWriteJson(filePath, data)).rejects.toThrow("EACCES: permission denied") + + // Verify directory was not created + await expect(fs.access(subDir)).rejects.toThrow() + }) + + test("should successfully write to a non-existent file in an existing directory", async () => { + // Create directory but not the file + const subDir = path.join(tempDir, "existing-dir") + await fs.mkdir(subDir) + + const filePath = path.join(subDir, "new-file.json") + const data = { fresh: "file" } + + // Verify file doesn't exist yet + await expect(fs.access(filePath)).rejects.toThrow() + + // Write file + await safeWriteJson(filePath, data) + + // Verify file was created with correct content + const content = await readFileContent(filePath) + expect(content).toEqual(data) + }) + + test("should handle failure when deleting tempBackupFilePath (filePath exists, all renames succeed)", async () => { + const initialData = { message: "Initial content" } + const newData = { message: "Successfully written new content" } + + // Overwrite the pre-created file with specific initial data + await originalFsPromisesWriteFile(currentTestFilePath, JSON.stringify(initialData)) + + const unlinkSpy = vi.spyOn(fs, "unlink") + + // Mock unlink to fail when trying to delete the backup file + unlinkSpy.mockImplementationOnce(async () => { + throw new Error("Failed to delete backup file") + }) + + // The write should succeed even if backup deletion fails + await safeWriteJson(currentTestFilePath, newData) + + // Verify the new content was written successfully + const content = await readFileContent(currentTestFilePath) + expect(content).toEqual(newData) + }) + + // Test for console error suppression during backup deletion + test("should suppress console.error when backup deletion fails", async () => { + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) // Suppress console.error + const initialData = { message: "Initial" } + const newData = { message: "New" } + + await originalFsPromisesWriteFile(currentTestFilePath, JSON.stringify(initialData)) + + // Mock unlink to fail when deleting backup files + const unlinkSpy = vi.spyOn(fs, "unlink") + unlinkSpy.mockImplementation(async (filePath: any) => { + if (filePath.toString().includes(".bak_")) { + throw new Error("Backup deletion failed") + } + return originalFsPromisesUnlink(filePath) + }) + + await safeWriteJson(currentTestFilePath, newData) + + // Verify console.error was called with the expected message + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Successfully wrote"), expect.any(Error)) + + consoleErrorSpy.mockRestore() + unlinkSpy.mockRestore() + }) + + // The expected error message might need to change if the mock behaves differently. + test("should handle failure when renaming tempNewFilePath to filePath (filePath initially exists)", async () => { + // currentTestFilePath exists due to beforeEach. + const initialData = { message: "Initial content" } + const newData = { message: "New content" } + + await originalFsPromisesWriteFile(currentTestFilePath, JSON.stringify(initialData)) + + const renameSpy = vi.spyOn(fs, "rename") + // Mock rename to fail on the second call (tempNewFilePath -> filePath) + // This test assumes that the first rename (filePath -> tempBackupFilePath) succeeds, + // which is the expected behavior when the file exists. + // The existing complex mock in `test("should handle failure when renaming tempNewFilePath to filePath (filePath exists, backup succeeded)"` + // might be more relevant or adaptable here. + + let renameCallCount = 0 + renameSpy.mockImplementation(async (oldPath, newPath) => { + renameCallCount++ + if (renameCallCount === 2) { + // Second call: tempNewFilePath -> filePath (should fail) + throw new Error("Rename failed") + } + // For all other calls, use the original implementation + return originalFsPromisesRename(oldPath, newPath) + }) + + await expect(safeWriteJson(currentTestFilePath, newData)).rejects.toThrow("Rename failed") + + // The file should be restored to its initial content + const content = await readFileContent(currentTestFilePath) + expect(content).toEqual(initialData) + }) + + test("should throw an error if an inter-process lock is already held for the filePath", async () => { + vi.resetModules() // Clear module cache to ensure fresh imports for this test + + const data = { message: "test lock failure" } + + // Create a new file path for this specific test to avoid conflicts + const lockTestFilePath = path.join(tempDir, "lock-test-file.json") + await fs.writeFile(lockTestFilePath, JSON.stringify({ initial: "lock test content" })) + + vi.doMock("proper-lockfile", () => ({ + ...vi.importActual("proper-lockfile"), + lock: vi.fn().mockRejectedValueOnce(new Error("Failed to get lock.")), + })) + + // Re-import safeWriteJson to use the mocked proper-lockfile + const { safeWriteJson: mockedSafeWriteJson } = await import("../safeWriteJson") + + await expect(mockedSafeWriteJson(lockTestFilePath, data)).rejects.toThrow("Failed to get lock.") + + // Clean up + await fs.unlink(lockTestFilePath).catch(() => {}) // Ignore errors if file doesn't exist + vi.unmock("proper-lockfile") // Ensure the mock is removed after this test + }) + test("should release lock even if an error occurs mid-operation", async () => { + const data = { message: "test lock release on error" } + + // Mock createWriteStream to throw an error + const createWriteStreamSpy = vi.spyOn(fsSyncActual, "createWriteStream") + createWriteStreamSpy.mockImplementationOnce((_path: any, _options: any) => { + const errorStream = new Writable() as any + errorStream._write = (_chunk: any, _encoding: any, callback: any) => { + callback(new Error("Stream write error")) + } + // Add missing WriteStream properties + errorStream.close = vi.fn() + errorStream.bytesWritten = 0 + errorStream.path = _path + errorStream.pending = false + return errorStream + }) + + // This should throw but still release the lock + await expect(safeWriteJson(currentTestFilePath, data)).rejects.toThrow("Stream write error") + + // Reset the mock to allow the second call to work normally + createWriteStreamSpy.mockRestore() + + // If the lock wasn't released, this second attempt would fail with a lock error + // Instead, it should succeed (proving the lock was released) + await expect(safeWriteJson(currentTestFilePath, data)).resolves.toBeUndefined() + }) + + test("should handle fs.access error that is not ENOENT", async () => { + const data = { message: "access error test" } + const accessSpy = vi.spyOn(fs, "access").mockImplementationOnce(async () => { + const error = new Error("EACCES: permission denied") as any + error.code = "EACCES" + throw error + }) + + // Create a path that will trigger the access check + const testPath = path.join(tempDir, "access-error-test.json") + + await expect(safeWriteJson(testPath, data)).rejects.toThrow("EACCES: permission denied") + + // Verify access was called + expect(accessSpy).toHaveBeenCalled() + }) + + // Test for rollback failure scenario + test("should log error and re-throw original if rollback fails", async () => { + const initialData = { message: "Initial, should be lost if rollback fails" } + const newData = { message: "New content" } + + await originalFsPromisesWriteFile(currentTestFilePath, JSON.stringify(initialData)) + + const renameSpy = vi.spyOn(fs, "rename") + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) // Suppress console.error + + let renameCallCount = 0 + renameSpy.mockImplementation(async (oldPath, newPath) => { + renameCallCount++ + if (renameCallCount === 2) { + // Second call: tempNewFilePath -> filePath (fail) + throw new Error("Primary rename failed") + } else if (renameCallCount === 3) { + // Third call: tempBackupFilePath -> filePath (rollback, also fail) + throw new Error("Rollback rename failed") + } + return originalFsPromisesRename(oldPath, newPath) + }) + + // Should throw the original error, not the rollback error + await expect(safeWriteJson(currentTestFilePath, newData)).rejects.toThrow("Primary rename failed") + + // Verify console.error was called for the rollback failure + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining("Failed to restore backup"), + expect.objectContaining({ message: "Rollback rename failed" }), + ) + + consoleErrorSpy.mockRestore() + }) +}) diff --git a/src/utils/autoImportSettings.ts b/src/utils/autoImportSettings.ts new file mode 100644 index 0000000000..d5871f3224 --- /dev/null +++ b/src/utils/autoImportSettings.ts @@ -0,0 +1,97 @@ +import * as vscode from "vscode" +import * as path from "path" +import * as os from "os" + +import { Package } from "../shared/package" +import { fileExistsAtPath } from "./fs" +import { t } from "../i18n" + +import { importSettingsFromFile } from "../core/config/importExport" +import { ProviderSettingsManager } from "../core/config/ProviderSettingsManager" +import { ContextProxy } from "../core/config/ContextProxy" +import { CustomModesManager } from "../core/config/CustomModesManager" + +type ImportOptions = { + providerSettingsManager: ProviderSettingsManager + contextProxy: ContextProxy + customModesManager: CustomModesManager +} + +/** + * Automatically imports RooCode settings from a specified path if it exists. + * This function is called during extension activation to allow users to pre-configure + * their settings by placing a settings file at a predefined location. + */ +export async function autoImportSettings( + outputChannel: vscode.OutputChannel, + { providerSettingsManager, contextProxy, customModesManager }: ImportOptions, +): Promise { + try { + // Get the auto-import settings path from VSCode settings + const settingsPath = vscode.workspace.getConfiguration(Package.name).get("autoImportSettingsPath") + + if (!settingsPath || settingsPath.trim() === "") { + outputChannel.appendLine("[AutoImport] No auto-import settings path specified, skipping auto-import") + return + } + + // Resolve the path (handle ~ for home directory and relative paths) + const resolvedPath = resolvePath(settingsPath.trim()) + outputChannel.appendLine(`[AutoImport] Checking for settings file at: ${resolvedPath}`) + + // Check if the file exists + if (!(await fileExistsAtPath(resolvedPath))) { + outputChannel.appendLine(`[AutoImport] Settings file not found at ${resolvedPath}, skipping auto-import`) + return + } + + // Attempt to import the configuration + const fileUri = vscode.Uri.file(resolvedPath) + const result = await importSettingsFromFile( + { + providerSettingsManager, + contextProxy, + customModesManager, + }, + fileUri, + ) + + if (result.success) { + outputChannel.appendLine(`[AutoImport] Successfully imported settings from ${resolvedPath}`) + + // Show a notification to the user + vscode.window.showInformationMessage( + t("common:info.auto_import_success", { filename: path.basename(resolvedPath) }), + ) + } else { + outputChannel.appendLine(`[AutoImport] Failed to import settings: ${result.error}`) + + // Show a warning but don't fail the extension activation + vscode.window.showWarningMessage(t("common:warnings.auto_import_failed", { error: result.error })) + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + outputChannel.appendLine(`[AutoImport] Unexpected error during auto-import: ${errorMessage}`) + + // Log error but don't fail extension activation + console.warn("Auto-import settings error:", error) + } +} + +/** + * Resolves a file path, handling home directory expansion and relative paths + */ +function resolvePath(settingsPath: string): string { + // Handle home directory expansion + if (settingsPath.startsWith("~/")) { + return path.join(os.homedir(), settingsPath.slice(2)) + } + + // Handle absolute paths + if (path.isAbsolute(settingsPath)) { + return settingsPath + } + + // Handle relative paths (relative to home directory for safety) + return path.join(os.homedir(), settingsPath) +} diff --git a/src/utils/git.ts b/src/utils/git.ts index 640af7fd29..7ecf33172b 100644 --- a/src/utils/git.ts +++ b/src/utils/git.ts @@ -1,3 +1,6 @@ +import * as vscode from "vscode" +import * as path from "path" +import { promises as fs } from "fs" import { exec } from "child_process" import { promisify } from "util" import { truncateOutput } from "../integrations/misc/extract-text" @@ -5,6 +8,12 @@ import { truncateOutput } from "../integrations/misc/extract-text" const execAsync = promisify(exec) const GIT_OUTPUT_LINE_LIMIT = 500 +export interface GitRepositoryInfo { + repositoryUrl?: string + repositoryName?: string + defaultBranch?: string +} + export interface GitCommit { hash: string shortHash: string @@ -13,6 +22,146 @@ export interface GitCommit { date: string } +/** + * Extracts git repository information from the workspace's .git directory + * @param workspaceRoot The root path of the workspace + * @returns Git repository information or empty object if not a git repository + */ +export async function getGitRepositoryInfo(workspaceRoot: string): Promise { + try { + const gitDir = path.join(workspaceRoot, ".git") + + // Check if .git directory exists + try { + await fs.access(gitDir) + } catch { + // Not a git repository + return {} + } + + const gitInfo: GitRepositoryInfo = {} + + // Try to read git config file + try { + const configPath = path.join(gitDir, "config") + const configContent = await fs.readFile(configPath, "utf8") + + // Very simple approach - just find any URL line + const urlMatch = configContent.match(/url\s*=\s*(.+?)(?:\r?\n|$)/m) + + if (urlMatch && urlMatch[1]) { + const url = urlMatch[1].trim() + gitInfo.repositoryUrl = sanitizeGitUrl(url) + const repositoryName = extractRepositoryName(url) + if (repositoryName) { + gitInfo.repositoryName = repositoryName + } + } + + // Extract default branch (if available) + const branchMatch = configContent.match(/\[branch "([^"]+)"\]/i) + if (branchMatch && branchMatch[1]) { + gitInfo.defaultBranch = branchMatch[1] + } + } catch (error) { + // Ignore config reading errors + } + + // Try to read HEAD file to get current branch + if (!gitInfo.defaultBranch) { + try { + const headPath = path.join(gitDir, "HEAD") + const headContent = await fs.readFile(headPath, "utf8") + const branchMatch = headContent.match(/ref: refs\/heads\/(.+)/) + if (branchMatch && branchMatch[1]) { + gitInfo.defaultBranch = branchMatch[1].trim() + } + } catch (error) { + // Ignore HEAD reading errors + } + } + + return gitInfo + } catch (error) { + // Return empty object on any error + return {} + } +} + +/** + * Sanitizes a git URL to remove sensitive information like tokens + * @param url The original git URL + * @returns Sanitized URL + */ +export function sanitizeGitUrl(url: string): string { + try { + // Remove credentials from HTTPS URLs + if (url.startsWith("https://")) { + const urlObj = new URL(url) + // Remove username and password + urlObj.username = "" + urlObj.password = "" + return urlObj.toString() + } + + // For SSH URLs, return as-is (they don't contain sensitive tokens) + if (url.startsWith("git@") || url.startsWith("ssh://")) { + return url + } + + // For other formats, return as-is but remove any potential tokens + return url.replace(/:[a-f0-9]{40,}@/gi, "@") + } catch { + // If URL parsing fails, return original (might be SSH format) + return url + } +} + +/** + * Extracts repository name from a git URL + * @param url The git URL + * @returns Repository name or undefined + */ +export function extractRepositoryName(url: string): string { + try { + // Handle different URL formats + const patterns = [ + // HTTPS: https://github.com/user/repo.git -> user/repo + /https:\/\/[^\/]+\/([^\/]+\/[^\/]+?)(?:\.git)?$/, + // SSH: git@github.com:user/repo.git -> user/repo + /git@[^:]+:([^\/]+\/[^\/]+?)(?:\.git)?$/, + // SSH with user: ssh://git@github.com/user/repo.git -> user/repo + /ssh:\/\/[^\/]+\/([^\/]+\/[^\/]+?)(?:\.git)?$/, + ] + + for (const pattern of patterns) { + const match = url.match(pattern) + if (match && match[1]) { + return match[1].replace(/\.git$/, "") + } + } + + return "" + } catch { + return "" + } +} + +/** + * Gets git repository information for the current VSCode workspace + * @returns Git repository information or empty object if not available + */ +export async function getWorkspaceGitInfo(): Promise { + const workspaceFolders = vscode.workspace.workspaceFolders + if (!workspaceFolders || workspaceFolders.length === 0) { + return {} + } + + // Use the first workspace folder + const workspaceRoot = workspaceFolders[0].uri.fsPath + return getGitRepositoryInfo(workspaceRoot) +} + async function checkGitRepo(cwd: string): Promise { try { await execAsync("git rev-parse --git-dir", { cwd }) diff --git a/src/utils/migrateSettings.ts b/src/utils/migrateSettings.ts index 406e5bd051..3007a6bcfb 100644 --- a/src/utils/migrateSettings.ts +++ b/src/utils/migrateSettings.ts @@ -92,8 +92,8 @@ async function migrateCustomModesToYaml(settingsDir: string, outputChannel: vsco // Parse JSON to object (using the yaml library just to be safe/consistent) const customModesData = yaml.parse(jsonContent) - // Convert to YAML - const yamlContent = yaml.stringify(customModesData) + // Convert to YAML with no line width limit to prevent line breaks + const yamlContent = yaml.stringify(customModesData, { lineWidth: 0, defaultStringType: "PLAIN" }) // Write YAML file await fs.writeFile(newYamlPath, yamlContent, "utf-8") diff --git a/src/utils/safeWriteJson.ts b/src/utils/safeWriteJson.ts new file mode 100644 index 0000000000..719bbd7216 --- /dev/null +++ b/src/utils/safeWriteJson.ts @@ -0,0 +1,235 @@ +import * as fs from "fs/promises" +import * as fsSync from "fs" +import * as path from "path" +import * as lockfile from "proper-lockfile" +import Disassembler from "stream-json/Disassembler" +import Stringer from "stream-json/Stringer" + +/** + * Safely writes JSON data to a file. + * - Creates parent directories if they don't exist + * - Uses 'proper-lockfile' for inter-process advisory locking to prevent concurrent writes to the same path. + * - Writes to a temporary file first. + * - If the target file exists, it's backed up before being replaced. + * - Attempts to roll back and clean up in case of errors. + * + * @param {string} filePath - The absolute path to the target file. + * @param {any} data - The data to serialize to JSON and write. + * @returns {Promise} + */ + +async function safeWriteJson(filePath: string, data: any): Promise { + const absoluteFilePath = path.resolve(filePath) + let releaseLock = async () => {} // Initialized to a no-op + + // For directory creation + const dirPath = path.dirname(absoluteFilePath) + + // Ensure directory structure exists with improved reliability + try { + // Create directory with recursive option + await fs.mkdir(dirPath, { recursive: true }) + + // Verify directory exists after creation attempt + await fs.access(dirPath) + } catch (dirError: any) { + console.error(`Failed to create or access directory for ${absoluteFilePath}:`, dirError) + throw dirError + } + + // Acquire the lock before any file operations + try { + releaseLock = await lockfile.lock(absoluteFilePath, { + stale: 31000, // Stale after 31 seconds + update: 10000, // Update mtime every 10 seconds to prevent staleness if operation is long + realpath: false, // the file may not exist yet, which is acceptable + retries: { + // Configuration for retrying lock acquisition + retries: 5, // Number of retries after the initial attempt + factor: 2, // Exponential backoff factor (e.g., 100ms, 200ms, 400ms, ...) + minTimeout: 100, // Minimum time to wait before the first retry (in ms) + maxTimeout: 1000, // Maximum time to wait for any single retry (in ms) + }, + onCompromised: (err) => { + console.error(`Lock at ${absoluteFilePath} was compromised:`, err) + throw err + }, + }) + } catch (lockError) { + // If lock acquisition fails, we throw immediately. + // The releaseLock remains a no-op, so the finally block in the main file operations + // try-catch-finally won't try to release an unacquired lock if this path is taken. + console.error(`Failed to acquire lock for ${absoluteFilePath}:`, lockError) + // Propagate the lock acquisition error + throw lockError + } + + // Variables to hold the actual paths of temp files if they are created. + let actualTempNewFilePath: string | null = null + let actualTempBackupFilePath: string | null = null + + try { + // Step 1: Write data to a new temporary file. + actualTempNewFilePath = path.join( + path.dirname(absoluteFilePath), + `.${path.basename(absoluteFilePath)}.new_${Date.now()}_${Math.random().toString(36).substring(2)}.tmp`, + ) + + await _streamDataToFile(actualTempNewFilePath, data) + + // Step 2: Check if the target file exists. If so, rename it to a backup path. + try { + // Check for target file existence + await fs.access(absoluteFilePath) + // Target exists, create a backup path and rename. + actualTempBackupFilePath = path.join( + path.dirname(absoluteFilePath), + `.${path.basename(absoluteFilePath)}.bak_${Date.now()}_${Math.random().toString(36).substring(2)}.tmp`, + ) + await fs.rename(absoluteFilePath, actualTempBackupFilePath) + } catch (accessError: any) { + // Explicitly type accessError + if (accessError.code !== "ENOENT") { + // An error other than "file not found" occurred during access check. + throw accessError + } + // Target file does not exist, so no backup is made. actualTempBackupFilePath remains null. + } + + // Step 3: Rename the new temporary file to the target file path. + // This is the main "commit" step. + await fs.rename(actualTempNewFilePath, absoluteFilePath) + + // If we reach here, the new file is successfully in place. + // The original actualTempNewFilePath is now the main file, so we shouldn't try to clean it up as "temp". + // Mark as "used" or "committed" + actualTempNewFilePath = null + + // Step 4: If a backup was created, attempt to delete it. + if (actualTempBackupFilePath) { + try { + await fs.unlink(actualTempBackupFilePath) + // Mark backup as handled + actualTempBackupFilePath = null + } catch (unlinkBackupError) { + // Log this error, but do not re-throw. The main operation was successful. + // actualTempBackupFilePath remains set, indicating an orphaned backup. + console.error( + `Successfully wrote ${absoluteFilePath}, but failed to clean up backup ${actualTempBackupFilePath}:`, + unlinkBackupError, + ) + } + } + } catch (originalError) { + console.error(`Operation failed for ${absoluteFilePath}: [Original Error Caught]`, originalError) + + const newFileToCleanupWithinCatch = actualTempNewFilePath + const backupFileToRollbackOrCleanupWithinCatch = actualTempBackupFilePath + + // Attempt rollback if a backup was made + if (backupFileToRollbackOrCleanupWithinCatch) { + try { + await fs.rename(backupFileToRollbackOrCleanupWithinCatch, absoluteFilePath) + // Mark as handled, prevent later unlink of this path + actualTempBackupFilePath = null + } catch (rollbackError) { + // actualTempBackupFilePath (outer scope) remains pointing to backupFileToRollbackOrCleanupWithinCatch + console.error( + `[Catch] Failed to restore backup ${backupFileToRollbackOrCleanupWithinCatch} to ${absoluteFilePath}:`, + rollbackError, + ) + } + } + + // Cleanup the .new file if it exists + if (newFileToCleanupWithinCatch) { + try { + await fs.unlink(newFileToCleanupWithinCatch) + } catch (cleanupError) { + console.error( + `[Catch] Failed to clean up temporary new file ${newFileToCleanupWithinCatch}:`, + cleanupError, + ) + } + } + + // Cleanup the .bak file if it still needs to be (i.e., wasn't successfully restored) + if (actualTempBackupFilePath) { + try { + await fs.unlink(actualTempBackupFilePath) + } catch (cleanupError) { + console.error( + `[Catch] Failed to clean up temporary backup file ${actualTempBackupFilePath}:`, + cleanupError, + ) + } + } + throw originalError // This MUST be the error that rejects the promise. + } finally { + // Release the lock in the main finally block. + try { + // releaseLock will be the actual unlock function if lock was acquired, + // or the initial no-op if acquisition failed. + await releaseLock() + } catch (unlockError) { + // Do not re-throw here, as the originalError from the try/catch (if any) is more important. + console.error(`Failed to release lock for ${absoluteFilePath}:`, unlockError) + } + } +} + +/** + * Helper function to stream JSON data to a file. + * @param targetPath The path to write the stream to. + * @param data The data to stream. + * @returns Promise + */ +async function _streamDataToFile(targetPath: string, data: any): Promise { + // Stream data to avoid high memory usage for large JSON objects. + const fileWriteStream = fsSync.createWriteStream(targetPath, { encoding: "utf8" }) + const disassembler = Disassembler.disassembler() + // Output will be compact JSON as standard Stringer is used. + const stringer = Stringer.stringer() + + return new Promise((resolve, reject) => { + let errorOccurred = false + const handleError = (_streamName: string) => (err: Error) => { + if (!errorOccurred) { + errorOccurred = true + if (!fileWriteStream.destroyed) { + fileWriteStream.destroy(err) + } + reject(err) + } + } + + disassembler.on("error", handleError("Disassembler")) + stringer.on("error", handleError("Stringer")) + fileWriteStream.on("error", (err: Error) => { + if (!errorOccurred) { + errorOccurred = true + reject(err) + } + }) + + fileWriteStream.on("finish", () => { + if (!errorOccurred) { + resolve() + } + }) + + disassembler.pipe(stringer).pipe(fileWriteStream) + + // stream-json's Disassembler might error if `data` is undefined. + // JSON.stringify(undefined) would produce the string "undefined" if it's the root value. + // Writing 'null' is a safer JSON representation for a root undefined value. + if (data === undefined) { + disassembler.write(null) + } else { + disassembler.write(data) + } + disassembler.end() + }) +} + +export { safeWriteJson } diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index e63d8d0f4f..6b33a9b7f7 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -43,6 +43,7 @@ const App = () => { machineId, cloudUserInfo, cloudIsAuthenticated, + cloudApiUrl, renderContext, mdmCompliant, } = useExtensionState() @@ -74,6 +75,7 @@ const App = () => { } setCurrentSection(undefined) + setCurrentMarketplaceTab(undefined) if (settingsRef.current?.checkUnsaveChanges) { settingsRef.current.checkUnsaveChanges(() => setTab(newTab)) @@ -85,6 +87,7 @@ const App = () => { ) const [currentSection, setCurrentSection] = useState(undefined) + const [currentMarketplaceTab, setCurrentMarketplaceTab] = useState(undefined) const onMessage = useCallback( (e: MessageEvent) => { @@ -96,14 +99,17 @@ const App = () => { const targetTab = message.tab as Tab switchTab(targetTab) setCurrentSection(undefined) + setCurrentMarketplaceTab(undefined) } else { // Handle other actions using the mapping const newTab = tabsByMessageAction[message.action] const section = message.values?.section as string | undefined + const marketplaceTab = message.values?.marketplaceTab as string | undefined if (newTab) { switchTab(newTab) setCurrentSection(section) + setCurrentMarketplaceTab(marketplaceTab) } } } @@ -171,12 +177,17 @@ const App = () => { setTab("chat")} targetSection={currentSection} /> )} {tab === "marketplace" && ( - switchTab("chat")} /> + switchTab("chat")} + targetTab={currentMarketplaceTab as "mcp" | "mode" | undefined} + /> )} {tab === "account" && ( switchTab("chat")} /> )} diff --git a/webview-ui/src/__tests__/ContextWindowProgress.spec.tsx b/webview-ui/src/__tests__/ContextWindowProgress.spec.tsx index 5a5ff463ef..6b989a4e74 100644 --- a/webview-ui/src/__tests__/ContextWindowProgress.spec.tsx +++ b/webview-ui/src/__tests__/ContextWindowProgress.spec.tsx @@ -51,7 +51,6 @@ describe("ContextWindowProgress", () => { task: { ts: Date.now(), type: "say" as const, say: "text" as const, text: "Test task" }, tokensIn: 100, tokensOut: 50, - doesModelSupportPromptCache: true, totalCost: 0.001, contextTokens: 1000, onClose: vi.fn(), diff --git a/webview-ui/src/components/account/AccountView.tsx b/webview-ui/src/components/account/AccountView.tsx index a59ac716d7..e3d1a293a7 100644 --- a/webview-ui/src/components/account/AccountView.tsx +++ b/webview-ui/src/components/account/AccountView.tsx @@ -1,21 +1,56 @@ +import { useEffect, useRef } from "react" import { VSCodeButton } from "@vscode/webview-ui-toolkit/react" import type { CloudUserInfo } from "@roo-code/types" +import { TelemetryEventName } from "@roo-code/types" import { useAppTranslation } from "@src/i18n/TranslationContext" import { vscode } from "@src/utils/vscode" +import { telemetryClient } from "@src/utils/TelemetryClient" type AccountViewProps = { userInfo: CloudUserInfo | null isAuthenticated: boolean + cloudApiUrl?: string onDone: () => void } -export const AccountView = ({ userInfo, isAuthenticated, onDone }: AccountViewProps) => { +export const AccountView = ({ userInfo, isAuthenticated, cloudApiUrl, onDone }: AccountViewProps) => { const { t } = useAppTranslation() + const wasAuthenticatedRef = useRef(false) const rooLogoUri = (window as any).IMAGES_BASE_URI + "/roo-logo.svg" + // Track authentication state changes to detect successful logout + useEffect(() => { + if (isAuthenticated) { + wasAuthenticatedRef.current = true + } else if (wasAuthenticatedRef.current && !isAuthenticated) { + // User just logged out successfully + telemetryClient.capture(TelemetryEventName.ACCOUNT_LOGOUT_SUCCESS) + wasAuthenticatedRef.current = false + } + }, [isAuthenticated]) + + const handleConnectClick = () => { + // Send telemetry for account connect action + telemetryClient.capture(TelemetryEventName.ACCOUNT_CONNECT_CLICKED) + vscode.postMessage({ type: "rooCloudSignIn" }) + } + + const handleLogoutClick = () => { + // Send telemetry for account logout action + telemetryClient.capture(TelemetryEventName.ACCOUNT_LOGOUT_CLICKED) + vscode.postMessage({ type: "rooCloudSignOut" }) + } + + const handleVisitCloudWebsite = () => { + // Send telemetry for cloud website visit + telemetryClient.capture(TelemetryEventName.ACCOUNT_CONNECT_CLICKED) + const cloudUrl = cloudApiUrl || "https://app.roocode.com" + vscode.postMessage({ type: "openExternal", url: cloudUrl }) + } + return (
@@ -41,9 +76,9 @@ export const AccountView = ({ userInfo, isAuthenticated, onDone }: AccountViewPr
)}
-

- {userInfo?.name || t("account:unknownUser")} -

+ {userInfo.name && ( +

{userInfo.name}

+ )} {userInfo?.email && (

{userInfo?.email}

)} @@ -62,18 +97,18 @@ export const AccountView = ({ userInfo, isAuthenticated, onDone }: AccountViewPr )}
- vscode.postMessage({ type: "rooCloudSignOut" })} - className="w-full"> + + {t("account:visitCloudWebsite")} + + {t("account:logOut")}
) : ( <> -
-
+
+
+ +
+

+ {t("account:cloudBenefitsTitle")} +

+

+ {t("account:cloudBenefitsSubtitle")} +

+
    +
  • + + {t("account:cloudBenefitHistory")} +
  • +
  • + + {t("account:cloudBenefitSharing")} +
  • +
  • + + {t("account:cloudBenefitMetrics")} +
  • +
+
+
- vscode.postMessage({ type: "rooCloudSignIn" })} - className="w-full"> - {t("account:signIn")} + + {t("account:connect")}
diff --git a/webview-ui/src/components/account/__tests__/AccountView.spec.tsx b/webview-ui/src/components/account/__tests__/AccountView.spec.tsx new file mode 100644 index 0000000000..53b7c0a4ce --- /dev/null +++ b/webview-ui/src/components/account/__tests__/AccountView.spec.tsx @@ -0,0 +1,92 @@ +import { render, screen } from "@testing-library/react" +import { describe, it, expect, vi } from "vitest" +import { AccountView } from "../AccountView" + +// Mock the translation context +vi.mock("@src/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ + t: (key: string) => { + const translations: Record = { + "account:title": "Account", + "settings:common.done": "Done", + "account:signIn": "Connect to Roo Code Cloud", + "account:cloudBenefitsTitle": "Connect to Roo Code Cloud", + "account:cloudBenefitsSubtitle": "Sync your prompts and telemetry to enable:", + "account:cloudBenefitHistory": "Online task history", + "account:cloudBenefitSharing": "Sharing and collaboration features", + "account:cloudBenefitMetrics": "Task, token, and cost-based usage metrics", + "account:logOut": "Log out", + } + return translations[key] || key + }, + }), +})) + +// Mock vscode utilities +vi.mock("@src/utils/vscode", () => ({ + vscode: { + postMessage: vi.fn(), + }, +})) + +// Mock telemetry client +vi.mock("@src/utils/TelemetryClient", () => ({ + telemetryClient: { + capture: vi.fn(), + }, +})) + +// Mock window global for images +Object.defineProperty(window, "IMAGES_BASE_URI", { + value: "/images", + writable: true, +}) + +describe("AccountView", () => { + it("should display benefits when user is not authenticated", () => { + render( + {}} + />, + ) + + // Check that the benefits section is displayed + expect(screen.getByRole("heading", { name: "Connect to Roo Code Cloud" })).toBeInTheDocument() + expect(screen.getByText("Sync your prompts and telemetry to enable:")).toBeInTheDocument() + expect(screen.getByText("Online task history")).toBeInTheDocument() + expect(screen.getByText("Sharing and collaboration features")).toBeInTheDocument() + expect(screen.getByText("Task, token, and cost-based usage metrics")).toBeInTheDocument() + + // Check that the connect button is also present + expect(screen.getByText("account:connect")).toBeInTheDocument() + }) + + it("should not display benefits when user is authenticated", () => { + const mockUserInfo = { + name: "Test User", + email: "test@example.com", + } + + render( + {}} + />, + ) + + // Check that the benefits section is NOT displayed + expect(screen.queryByText("Sync your prompts and telemetry to enable:")).not.toBeInTheDocument() + expect(screen.queryByText("Online task history")).not.toBeInTheDocument() + expect(screen.queryByText("Sharing and collaboration features")).not.toBeInTheDocument() + expect(screen.queryByText("Task, token, and cost-based usage metrics")).not.toBeInTheDocument() + + // Check that user info is displayed instead + expect(screen.getByText("Test User")).toBeInTheDocument() + expect(screen.getByText("test@example.com")).toBeInTheDocument() + }) +}) diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 19f6c8a995..910b671725 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -22,6 +22,7 @@ import { convertToMentionPath } from "@/utils/path-mentions" import { SelectDropdown, DropdownOptionType, Button } from "@/components/ui" import Thumbnails from "../common/Thumbnails" +import ModeSelector from "./ModeSelector" import { MAX_IMAGES_PER_MESSAGE } from "./ChatView" import ContextMenu from "./ContextMenu" import { VolumeX, Pin, Check } from "lucide-react" @@ -74,6 +75,7 @@ const ChatTextArea = forwardRef( currentApiConfigName, listApiConfigMeta, customModes, + customModePrompts, cwd, pinnedApiConfigs, togglePinnedApiConfig, @@ -193,6 +195,8 @@ const ChatTextArea = forwardRef( } }, [inputValue, sendingDisabled, setInputValue, t]) + const allModes = useMemo(() => getAllModes(customModes), [customModes]) + const queryItems = useMemo(() => { return [ { type: ContextMenuOptionType.Problems, value: "problems" }, @@ -322,7 +326,7 @@ const ChatTextArea = forwardRef( selectedType, queryItems, fileSearchResults, - getAllModes(customModes), + allModes, ) const optionsLength = options.length @@ -359,7 +363,7 @@ const ChatTextArea = forwardRef( selectedType, queryItems, fileSearchResults, - getAllModes(customModes), + allModes, )[selectedMenuIndex] if ( selectedOption && @@ -446,7 +450,7 @@ const ChatTextArea = forwardRef( setInputValue, justDeletedSpaceAfterMention, queryItems, - customModes, + allModes, fileSearchResults, handleHistoryNavigation, resetHistoryNavigation, @@ -845,7 +849,7 @@ const ChatTextArea = forwardRef( setSelectedIndex={setSelectedMenuIndex} selectedType={selectedType} queryItems={queryItems} - modes={getAllModes(customModes)} + modes={allModes} loading={searchLoading} dynamicSearchResults={fileSearchResults} /> @@ -997,38 +1001,17 @@ const ChatTextArea = forwardRef(
- ({ - value: mode.slug, - label: mode.name, - type: DropdownOptionType.ITEM, - })), - { - value: "sep-1", - label: t("chat:separator"), - type: DropdownOptionType.SEPARATOR, - }, - { - value: "promptsButtonClicked", - label: t("chat:edit"), - type: DropdownOptionType.ACTION, - }, - ]} onChange={(value) => { - setMode(value as Mode) + setMode(value) vscode.postMessage({ type: "mode", text: value }) }} - shortcutText={modeShortcutText} triggerClassName="w-full" + modeShortcutText={modeShortcutText} + customModes={customModes} + customModePrompts={customModePrompts} />
@@ -1037,6 +1020,7 @@ const ChatTextArea = forwardRef( value={currentConfigId} disabled={selectApiConfigDisabled} title={t("chat:selectApiConfig")} + disableSearch={false} placeholder={displayName} options={[ // Pinned items first. diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 90901c84e9..5456c5b698 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -8,6 +8,8 @@ import { VSCodeButton } from "@vscode/webview-ui-toolkit/react" import useSound from "use-sound" import { LRUCache } from "lru-cache" +import { useDebounceEffect } from "@src/utils/useDebounceEffect" + import type { ClineAsk, ClineMessage } from "@roo-code/types" import { ClineSayBrowserAction, ClineSayTool, ExtensionMessage } from "@roo/ExtensionMessage" @@ -154,6 +156,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction(null) const clineAskRef = useRef(clineAsk) useEffect(() => { @@ -408,6 +411,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + if (isHidden) { + everVisibleMessagesTsRef.current.clear() + } + }, [isHidden]) + useEffect(() => () => everVisibleMessagesTsRef.current.clear(), []) useEffect(() => { @@ -714,17 +723,15 @@ const ChatViewComponent: React.ForwardRefRenderFunction textAreaRef.current?.focus()) - useEffect(() => { - const timer = setTimeout(() => { + useDebounceEffect( + () => { if (!isHidden && !sendingDisabled && !enableButtons) { textAreaRef.current?.focus() } - }, 50) - - return () => { - clearTimeout(timer) - } - }, [isHidden, sendingDisabled, enableButtons]) + }, + 50, + [isHidden, sendingDisabled, enableButtons] + ) const visibleMessages = useMemo(() => { const newVisibleMessages = modifiedMessages.filter((message) => { @@ -1086,6 +1093,14 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + return () => { + if (scrollToBottomSmooth && typeof (scrollToBottomSmooth as any).cancel === 'function') { + (scrollToBottomSmooth as any).cancel() + } + } + }, [scrollToBottomSmooth]) + const scrollToBottomAuto = useCallback(() => { virtuosoRef.current?.scrollTo({ top: Number.MAX_SAFE_INTEGER, @@ -1124,13 +1139,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - let timerId: NodeJS.Timeout | undefined + let timer: NodeJS.Timeout | undefined if (!disableAutoScrollRef.current) { - timerId = setTimeout(() => scrollToBottomSmooth(), 50) + timer = setTimeout(() => scrollToBottomSmooth(), 50) } return () => { - if (timerId) { - clearTimeout(timerId) + if (timer) { + clearTimeout(timer) } } }, [groupedMessages.length, scrollToBottomSmooth]) @@ -1151,21 +1166,23 @@ const ChatViewComponent: React.ForwardRefRenderFunction { // Only show the warning when there's a task but no visible messages yet - if (task && modifiedMessages.length === 0 && !isStreaming) { + if (task && modifiedMessages.length === 0 && !isStreaming && !isHidden) { const timer = setTimeout(() => { setShowCheckpointWarning(true) }, 5000) // 5 seconds return () => clearTimeout(timer) + } else { + setShowCheckpointWarning(false) } - }, [task, modifiedMessages.length, isStreaming]) + }, [task, modifiedMessages.length, isStreaming, isHidden]) // Effect to hide the checkpoint warning when messages appear useEffect(() => { - if (modifiedMessages.length > 0 || isStreaming) { + if (modifiedMessages.length > 0 || isStreaming || isHidden) { setShowCheckpointWarning(false) } - }, [modifiedMessages.length, isStreaming]) + }, [modifiedMessages.length, isStreaming, isHidden]) const placeholderText = task ? t("chat:typeMessage") : t("chat:typeTask") @@ -1239,31 +1256,26 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - // Only proceed if we have an ask and buttons are enabled. + if (autoApproveTimeoutRef.current) { + clearTimeout(autoApproveTimeoutRef.current) + autoApproveTimeoutRef.current = null + } + if (!clineAsk || !enableButtons) { return } const autoApprove = async () => { if (lastMessage?.ask && isAutoApproved(lastMessage)) { - // Note that `isAutoApproved` can only return true if - // lastMessage is an ask of type "browser_action_launch", - // "use_mcp_server", "command", or "tool". - - // Add delay for write operations. if (lastMessage.ask === "tool" && isWriteToolAction(lastMessage)) { - await new Promise((resolve) => setTimeout(resolve, writeDelayMs)) - if (!isMountedRef.current) { - return - } + await new Promise((resolve) => { + autoApproveTimeoutRef.current = setTimeout(resolve, writeDelayMs) + }) } - vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" }) + if (autoApproveTimeoutRef.current === null || autoApproveTimeoutRef.current) { + vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" }) - // This is copied from `handlePrimaryButtonClick`, which we used - // to call from `autoApprove`. I'm not sure how many of these - // things are actually needed. - if (isMountedRef.current) { setSendingDisabled(true) setClineAsk(undefined) setEnableButtons(false) @@ -1271,6 +1283,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + if (autoApproveTimeoutRef.current) { + clearTimeout(autoApproveTimeoutRef.current) + autoApproveTimeoutRef.current = null + } + } }, [ clineAsk, enableButtons, @@ -1352,7 +1371,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction = ({ aria-label={title} title={title} className={buttonClasses} + disabled={disabled} onClick={!disabled ? onClick : undefined} style={{ fontSize: 16.5, ...style }} {...props}> diff --git a/webview-ui/src/components/chat/ModeSelector.tsx b/webview-ui/src/components/chat/ModeSelector.tsx new file mode 100644 index 0000000000..f066dabfa6 --- /dev/null +++ b/webview-ui/src/components/chat/ModeSelector.tsx @@ -0,0 +1,171 @@ +import React from "react" +import { ChevronUp, Check } from "lucide-react" +import { cn } from "@/lib/utils" +import { useRooPortal } from "@/components/ui/hooks/useRooPortal" +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui" +import { IconButton } from "./IconButton" +import { vscode } from "@/utils/vscode" +import { useExtensionState } from "@/context/ExtensionStateContext" +import { useAppTranslation } from "@/i18n/TranslationContext" +import { Mode, getAllModes } from "@roo/modes" +import { ModeConfig, CustomModePrompts } from "@roo-code/types" + +interface ModeSelectorProps { + value: Mode + onChange: (value: Mode) => void + disabled?: boolean + title?: string + triggerClassName?: string + modeShortcutText: string + customModes?: ModeConfig[] + customModePrompts?: CustomModePrompts +} + +export const ModeSelector = ({ + value, + onChange, + disabled = false, + title = "", + triggerClassName = "", + modeShortcutText, + customModes, + customModePrompts, +}: ModeSelectorProps) => { + const [open, setOpen] = React.useState(false) + const portalContainer = useRooPortal("roo-portal") + const { hasOpenedModeSelector, setHasOpenedModeSelector } = useExtensionState() + const { t } = useAppTranslation() + + const trackModeSelectorOpened = () => { + if (!hasOpenedModeSelector) { + setHasOpenedModeSelector(true) + vscode.postMessage({ type: "hasOpenedModeSelector", bool: true }) + } + } + + // Get all modes including custom modes and merge custom prompt descriptions + const modes = React.useMemo(() => { + const allModes = getAllModes(customModes) + return allModes.map((mode) => ({ + ...mode, + description: customModePrompts?.[mode.slug]?.description ?? mode.description, + })) + }, [customModes, customModePrompts]) + + // Find the selected mode + const selectedMode = React.useMemo(() => modes.find((mode) => mode.slug === value), [modes, value]) + + return ( + { + if (isOpen) trackModeSelectorOpened() + setOpen(isOpen) + }} + data-testid="mode-selector-root"> + + + {selectedMode?.name || ""} + + + +
+
+
+

{t("chat:modeSelector.title")}

+
+ { + window.postMessage( + { + type: "action", + action: "marketplaceButtonClicked", + values: { marketplaceTab: "mode" }, + }, + "*", + ) + + setOpen(false) + }} + /> + { + vscode.postMessage({ + type: "switchTab", + tab: "modes", + }) + setOpen(false) + }} + /> +
+
+

+ {t("chat:modeSelector.description")} +
+ {modeShortcutText} +

+
+ + {/* Mode List */} +
+ {modes.map((mode) => ( +
{ + onChange(mode.slug as Mode) + setOpen(false) + }} + data-testid="mode-selector-item"> +
+

{mode.name}

+ {mode.description && ( +

+ {mode.description} +

+ )} +
+ {mode.slug === value ? ( + + ) : ( +
+ )} +
+ ))} +
+
+ + + ) +} + +export default ModeSelector diff --git a/webview-ui/src/components/chat/ShareButton.tsx b/webview-ui/src/components/chat/ShareButton.tsx new file mode 100644 index 0000000000..d8dac0062c --- /dev/null +++ b/webview-ui/src/components/chat/ShareButton.tsx @@ -0,0 +1,259 @@ +import { useState, useEffect, useRef } from "react" +import { useTranslation } from "react-i18next" + +import type { HistoryItem, ShareVisibility } from "@roo-code/types" +import { TelemetryEventName } from "@roo-code/types" + +import { vscode } from "@/utils/vscode" +import { telemetryClient } from "@/utils/TelemetryClient" +import { useExtensionState } from "@/context/ExtensionStateContext" +import { + Button, + Popover, + PopoverContent, + PopoverTrigger, + Command, + CommandList, + CommandItem, + CommandGroup, + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui" + +interface ShareButtonProps { + item?: HistoryItem + disabled?: boolean +} + +export const ShareButton = ({ item, disabled = false }: ShareButtonProps) => { + const [shareDropdownOpen, setShareDropdownOpen] = useState(false) + const [connectModalOpen, setConnectModalOpen] = useState(false) + const [shareSuccess, setShareSuccess] = useState<{ visibility: ShareVisibility; url: string } | null>(null) + const { t } = useTranslation() + const { sharingEnabled, cloudIsAuthenticated, cloudUserInfo } = useExtensionState() + const wasUnauthenticatedRef = useRef(false) + + // Track authentication state changes to auto-open popover after login + useEffect(() => { + if (!cloudIsAuthenticated || !sharingEnabled) { + wasUnauthenticatedRef.current = true + } else if (wasUnauthenticatedRef.current && cloudIsAuthenticated && sharingEnabled) { + // User just authenticated, send telemetry, close modal, and open the popover + telemetryClient.capture(TelemetryEventName.ACCOUNT_CONNECT_SUCCESS) + setConnectModalOpen(false) + setShareDropdownOpen(true) + wasUnauthenticatedRef.current = false + } + }, [cloudIsAuthenticated, sharingEnabled]) + + // Listen for share success messages from the extension + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + const message = event.data + if (message.type === "shareTaskSuccess") { + setShareSuccess({ + visibility: message.visibility, + url: message.text, + }) + // Auto-hide success message and close popover after 5 seconds + setTimeout(() => { + setShareSuccess(null) + setShareDropdownOpen(false) + }, 5000) + } + } + + window.addEventListener("message", handleMessage) + return () => window.removeEventListener("message", handleMessage) + }, []) + + const handleShare = (visibility: ShareVisibility) => { + // Clear any previous success state + setShareSuccess(null) + + // Send telemetry for share action + if (visibility === "organization") { + telemetryClient.capture(TelemetryEventName.SHARE_ORGANIZATION_CLICKED) + } else { + telemetryClient.capture(TelemetryEventName.SHARE_PUBLIC_CLICKED) + } + + vscode.postMessage({ + type: "shareCurrentTask", + visibility, + }) + // Don't close the dropdown immediately - let success message show first + } + + const handleConnectToCloud = () => { + // Send telemetry for connect to cloud action + telemetryClient.capture(TelemetryEventName.SHARE_CONNECT_TO_CLOUD_CLICKED) + + vscode.postMessage({ type: "rooCloudSignIn" }) + setShareDropdownOpen(false) + setConnectModalOpen(false) + } + + const handleShareButtonClick = () => { + // Send telemetry for share button click + telemetryClient.capture(TelemetryEventName.SHARE_BUTTON_CLICKED) + + if (!cloudIsAuthenticated) { + // Show modal for unauthenticated users + setConnectModalOpen(true) + } else { + // Show popover for authenticated users + setShareDropdownOpen(true) + } + } + + // Determine share button state + const getShareButtonState = () => { + if (!cloudIsAuthenticated) { + return { + disabled: false, + title: t("chat:task.share"), + showPopover: false, // We'll show modal instead + } + } else if (!sharingEnabled) { + return { + disabled: true, + title: t("chat:task.sharingDisabledByOrganization"), + showPopover: false, + } + } else { + return { + disabled: false, + title: t("chat:task.share"), + showPopover: true, + } + } + } + + const shareButtonState = getShareButtonState() + + // Don't render if no item ID + if (!item?.id) { + return null + } + + return ( + <> + {shareButtonState.showPopover ? ( + + + + + + {shareSuccess ? ( +
+
+ + + {shareSuccess.visibility === "public" + ? t("chat:task.shareSuccessPublic") + : t("chat:task.shareSuccessOrganization")} + +
+
+ ) : ( + + + + {cloudUserInfo?.organizationName && ( + handleShare("organization")} + className="cursor-pointer"> +
+ +
+ + {t("chat:task.shareWithOrganization")} + + + {t("chat:task.shareWithOrganizationDescription")} + +
+
+
+ )} + handleShare("public")} className="cursor-pointer"> +
+ +
+ {t("chat:task.sharePublicly")} + + {t("chat:task.sharePubliclyDescription")} + +
+
+
+
+
+
+ )} +
+
+ ) : ( + + )} + + {/* Connect to Cloud Modal */} + + + + + {t("account:cloudBenefitsTitle")} + + + +
+
+

+ {t("account:cloudBenefitsSubtitle")} +

+
    +
  • + + {t("account:cloudBenefitSharing")} +
  • +
  • + + {t("account:cloudBenefitHistory")} +
  • +
  • + + {t("account:cloudBenefitMetrics")} +
  • +
+
+ +
+ +
+
+
+
+ + ) +} diff --git a/webview-ui/src/components/chat/TaskActions.tsx b/webview-ui/src/components/chat/TaskActions.tsx index cef27408eb..be93130282 100644 --- a/webview-ui/src/components/chat/TaskActions.tsx +++ b/webview-ui/src/components/chat/TaskActions.tsx @@ -5,20 +5,10 @@ import { useTranslation } from "react-i18next" import type { HistoryItem } from "@roo-code/types" import { vscode } from "@/utils/vscode" -import { useExtensionState } from "@/context/ExtensionStateContext" -import { - Button, - Popover, - PopoverContent, - PopoverTrigger, - Command, - CommandList, - CommandItem, - CommandGroup, -} from "@/components/ui" import { DeleteTaskDialog } from "../history/DeleteTaskDialog" import { IconButton } from "./IconButton" +import { ShareButton } from "./ShareButton" interface TaskActionsProps { item?: HistoryItem @@ -27,66 +17,11 @@ interface TaskActionsProps { export const TaskActions = ({ item, buttonsDisabled }: TaskActionsProps) => { const [deleteTaskId, setDeleteTaskId] = useState(null) - const [shareDropdownOpen, setShareDropdownOpen] = useState(false) const { t } = useTranslation() - const { sharingEnabled } = useExtensionState() - - const handleShare = (visibility: "organization" | "public") => { - vscode.postMessage({ - type: "shareCurrentTask", - visibility, - }) - setShareDropdownOpen(false) - } return (
- {item?.id && sharingEnabled && ( - - - - - - - - - handleShare("organization")} - className="cursor-pointer"> -
- -
- {t("chat:task.shareWithOrganization")} - - {t("chat:task.shareWithOrganizationDescription")} - -
-
-
- handleShare("public")} className="cursor-pointer"> -
- -
- {t("chat:task.sharePublicly")} - - {t("chat:task.sharePubliclyDescription")} - -
-
-
-
-
-
-
-
- )} + {condenseButton} + {!!totalCost && ${totalCost.toFixed(2)}}
)} @@ -184,25 +184,24 @@ const TaskHeader = ({ {!totalCost && }
- {doesModelSupportPromptCache && - ((typeof cacheReads === "number" && cacheReads > 0) || - (typeof cacheWrites === "number" && cacheWrites > 0)) && ( -
- {t("chat:task.cache")} - {typeof cacheWrites === "number" && cacheWrites > 0 && ( - - - {formatLargeNumber(cacheWrites)} - - )} - {typeof cacheReads === "number" && cacheReads > 0 && ( - - - {formatLargeNumber(cacheReads)} - - )} -
- )} + {((typeof cacheReads === "number" && cacheReads > 0) || + (typeof cacheWrites === "number" && cacheWrites > 0)) && ( +
+ {t("chat:task.cache")} + {typeof cacheWrites === "number" && cacheWrites > 0 && ( + + + {formatLargeNumber(cacheWrites)} + + )} + {typeof cacheReads === "number" && cacheReads > 0 && ( + + + {formatLargeNumber(cacheReads)} + + )} +
+ )} {!!totalCost && (
diff --git a/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx new file mode 100644 index 0000000000..4d07a6de46 --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx @@ -0,0 +1,58 @@ +import React from "react" +import { render, screen } from "@testing-library/react" +import { describe, test, expect, vi } from "vitest" +import ModeSelector from "../ModeSelector" +import { Mode } from "@roo/modes" + +// Mock the dependencies +vi.mock("@/utils/vscode", () => ({ + vscode: { + postMessage: vi.fn(), + }, +})) + +vi.mock("@/context/ExtensionStateContext", () => ({ + useExtensionState: () => ({ + hasOpenedModeSelector: false, + setHasOpenedModeSelector: vi.fn(), + }), +})) + +vi.mock("@/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock("@/components/ui/hooks/useRooPortal", () => ({ + useRooPortal: () => document.body, +})) + +describe("ModeSelector", () => { + test("shows custom description from customModePrompts", () => { + const customModePrompts = { + code: { + description: "Custom code mode description", + }, + } + + render( + , + ) + + // The component should be rendered + expect(screen.getByTestId("mode-selector-trigger")).toBeInTheDocument() + }) + + test("falls back to default description when no custom prompt", () => { + render() + + // The component should be rendered + expect(screen.getByTestId("mode-selector-trigger")).toBeInTheDocument() + }) +}) diff --git a/webview-ui/src/components/chat/__tests__/ShareButton.spec.tsx b/webview-ui/src/components/chat/__tests__/ShareButton.spec.tsx new file mode 100644 index 0000000000..f6102dc616 --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/ShareButton.spec.tsx @@ -0,0 +1,325 @@ +import { describe, test, expect, vi, beforeEach } from "vitest" +import { render, screen, fireEvent, waitFor } from "@testing-library/react" +import { ShareButton } from "../ShareButton" +import { useTranslation } from "react-i18next" +import { vscode } from "@/utils/vscode" + +// Mock the vscode utility +vi.mock("@/utils/vscode", () => ({ + vscode: { + postMessage: vi.fn(), + }, +})) + +// Mock react-i18next +vi.mock("react-i18next") + +// Mock the extension state context +vi.mock("@/context/ExtensionStateContext", () => ({ + ExtensionStateContextProvider: ({ children }: { children: React.ReactNode }) => children, + useExtensionState: () => ({ + sharingEnabled: true, + cloudIsAuthenticated: true, + cloudUserInfo: { + id: "test-user", + email: "test@example.com", + organizationName: "Test Organization", + }, + }), +})) + +// Mock telemetry client +vi.mock("@/utils/TelemetryClient", () => ({ + telemetryClient: { + capture: vi.fn(), + }, +})) + +const mockUseTranslation = vi.mocked(useTranslation) +const mockVscode = vi.mocked(vscode) + +describe("ShareButton", () => { + const mockT = vi.fn((key: string) => key) + const mockItem = { + id: "test-task-id", + number: 1, + ts: Date.now(), + task: "Test Task", + tokensIn: 100, + tokensOut: 50, + totalCost: 0.01, + } + + beforeEach(() => { + vi.clearAllMocks() + + mockUseTranslation.mockReturnValue({ + t: mockT, + i18n: {} as any, + ready: true, + } as any) + }) + + test("renders share button", () => { + render() + + const button = screen.getByRole("button") + expect(button).toBeInTheDocument() + }) + + test("opens popover when clicked", async () => { + render() + + const button = screen.getByRole("button") + fireEvent.click(button) + + await waitFor(() => { + expect(screen.getByText("chat:task.shareWithOrganization")).toBeInTheDocument() + }) + }) + + test("sends organization share message when organization button clicked", async () => { + render() + + // Open popover + const button = screen.getByRole("button") + fireEvent.click(button) + + await waitFor(() => { + expect(screen.getByText("chat:task.shareWithOrganization")).toBeInTheDocument() + }) + + // Click organization share button + const orgButton = screen.getByText("chat:task.shareWithOrganization") + fireEvent.click(orgButton) + + expect(mockVscode.postMessage).toHaveBeenCalledWith({ + type: "shareCurrentTask", + visibility: "organization", + }) + }) + + test("sends public share message when public button clicked", async () => { + render() + + // Open popover + const button = screen.getByRole("button") + fireEvent.click(button) + + await waitFor(() => { + expect(screen.getByText("chat:task.sharePublicly")).toBeInTheDocument() + }) + + // Click public share button + const publicButton = screen.getByText("chat:task.sharePublicly") + fireEvent.click(publicButton) + + expect(mockVscode.postMessage).toHaveBeenCalledWith({ + type: "shareCurrentTask", + visibility: "public", + }) + }) + + test("displays success message when shareTaskSuccess message received", async () => { + const mockAddEventListener = vi.fn() + const mockRemoveEventListener = vi.fn() + + // Mock window.addEventListener + Object.defineProperty(window, "addEventListener", { + value: mockAddEventListener, + writable: true, + }) + Object.defineProperty(window, "removeEventListener", { + value: mockRemoveEventListener, + writable: true, + }) + + render() + + // Get the message event listener that was registered + const messageListener = mockAddEventListener.mock.calls.find((call) => call[0] === "message")?.[1] + + expect(messageListener).toBeDefined() + + // Open popover first + const button = screen.getByRole("button") + fireEvent.click(button) + + await waitFor(() => { + expect(screen.getByText("chat:task.shareWithOrganization")).toBeInTheDocument() + }) + + // Simulate receiving a shareTaskSuccess message + const mockEvent = { + data: { + type: "shareTaskSuccess", + visibility: "organization", + text: "https://example.com/share/123", + }, + } + + messageListener(mockEvent) + + await waitFor(() => { + expect(screen.getByText("chat:task.shareSuccessOrganization")).toBeInTheDocument() + }) + }) + + test("displays different success messages based on visibility", async () => { + const mockAddEventListener = vi.fn() + + Object.defineProperty(window, "addEventListener", { + value: mockAddEventListener, + writable: true, + }) + + render() + + const messageListener = mockAddEventListener.mock.calls.find((call) => call[0] === "message")?.[1] + + // Open popover + const button = screen.getByRole("button") + fireEvent.click(button) + + await waitFor(() => { + expect(screen.getByText("chat:task.shareWithOrganization")).toBeInTheDocument() + }) + + // Test public visibility success message + const publicEvent = { + data: { + type: "shareTaskSuccess", + visibility: "public", + text: "https://example.com/share/456", + }, + } + + messageListener(publicEvent) + + await waitFor(() => { + expect(screen.getByText("chat:task.shareSuccessPublic")).toBeInTheDocument() + }) + }) + + test("auto-hides success message after 5 seconds", async () => { + vi.useFakeTimers({ shouldAdvanceTime: true }) + + const mockAddEventListener = vi.fn() + + Object.defineProperty(window, "addEventListener", { + value: mockAddEventListener, + writable: true, + }) + + render() + + const messageListener = mockAddEventListener.mock.calls.find((call) => call[0] === "message")?.[1] + + // Open popover + const button = screen.getByRole("button") + fireEvent.click(button) + + await vi.waitFor(() => { + expect(screen.getByText("chat:task.shareWithOrganization")).toBeInTheDocument() + }) + + // Simulate success message + const mockEvent = { + data: { + type: "shareTaskSuccess", + visibility: "organization", + text: "https://example.com/share/123", + }, + } + + messageListener(mockEvent) + + await vi.waitFor(() => { + expect(screen.getByText("chat:task.shareSuccessOrganization")).toBeInTheDocument() + }) + + // Fast-forward 5 seconds + await vi.advanceTimersByTimeAsync(5000) + + // The success message and share options should both be gone (popover closed) + expect(screen.queryByText("chat:task.shareSuccessOrganization")).not.toBeInTheDocument() + expect(screen.queryByText("chat:task.shareWithOrganization")).not.toBeInTheDocument() + + vi.useRealTimers() + }) + + test("clears previous success state when sharing again", async () => { + vi.useFakeTimers({ shouldAdvanceTime: true }) + + const mockAddEventListener = vi.fn() + + Object.defineProperty(window, "addEventListener", { + value: mockAddEventListener, + writable: true, + }) + + render() + + const messageListener = mockAddEventListener.mock.calls.find((call) => call[0] === "message")?.[1] + + // Open popover + const button = screen.getByRole("button") + fireEvent.click(button) + + await vi.waitFor(() => { + expect(screen.getByText("chat:task.shareWithOrganization")).toBeInTheDocument() + }) + + // Click organization share button first time + const orgButton = screen.getByText("chat:task.shareWithOrganization") + fireEvent.click(orgButton) + + // Verify first share message was sent + expect(mockVscode.postMessage).toHaveBeenCalledWith({ + type: "shareCurrentTask", + visibility: "organization", + }) + + // Clear mock to track new calls + mockVscode.postMessage.mockClear() + + // Show success message + const mockEvent = { + data: { + type: "shareTaskSuccess", + visibility: "organization", + text: "https://example.com/share/123", + }, + } + + messageListener(mockEvent) + + await vi.waitFor(() => { + expect(screen.getByText("chat:task.shareSuccessOrganization")).toBeInTheDocument() + }) + + // Wait for success message to auto-hide after 5 seconds + await vi.advanceTimersByTimeAsync(5000) + + // Success message should be gone and popover should be closed + expect(screen.queryByText("chat:task.shareSuccessOrganization")).not.toBeInTheDocument() + + // Open popover again + fireEvent.click(button) + await vi.waitFor(() => { + expect(screen.getByText("chat:task.shareWithOrganization")).toBeInTheDocument() + }) + + // Click share again + const orgButton2 = screen.getByText("chat:task.shareWithOrganization") + fireEvent.click(orgButton2) + + // Verify the share message was sent again (no success message should be showing) + expect(mockVscode.postMessage).toHaveBeenCalledWith({ + type: "shareCurrentTask", + visibility: "organization", + }) + + vi.useRealTimers() + }) +}) diff --git a/webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx b/webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx new file mode 100644 index 0000000000..c21c5b9331 --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx @@ -0,0 +1,315 @@ +import { render, screen, fireEvent } from "@testing-library/react" +import { vi, describe, it, expect, beforeEach } from "vitest" +import { TaskActions } from "../TaskActions" +import type { HistoryItem } from "@roo-code/types" +import { vscode } from "@/utils/vscode" +import { useExtensionState } from "@/context/ExtensionStateContext" + +// Mock scrollIntoView for JSDOM +Object.defineProperty(Element.prototype, "scrollIntoView", { + value: vi.fn(), + writable: true, +}) + +// Mock the vscode utility +vi.mock("@/utils/vscode", () => ({ + vscode: { + postMessage: vi.fn(), + }, +})) + +// Mock the useExtensionState hook +vi.mock("@/context/ExtensionStateContext", () => ({ + useExtensionState: vi.fn(), +})) + +const mockPostMessage = vi.mocked(vscode.postMessage) +const mockUseExtensionState = vi.mocked(useExtensionState) + +// Mock react-i18next +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string) => { + const translations: Record = { + "chat:task.share": "Share task", + "chat:task.export": "Export task history", + "chat:task.delete": "Delete Task (Shift + Click to skip confirmation)", + "chat:task.shareWithOrganization": "Share with Organization", + "chat:task.shareWithOrganizationDescription": "Only members of your organization can access", + "chat:task.sharePublicly": "Share Publicly", + "chat:task.sharePubliclyDescription": "Anyone with the link can access", + "chat:task.connectToCloud": "Connect to Cloud", + "chat:task.connectToCloudDescription": "Sign in to Roo Code Cloud to share tasks", + "chat:task.sharingDisabledByOrganization": "Sharing disabled by organization", + "account:cloudBenefitsTitle": "Connect to Roo Code Cloud", + "account:cloudBenefitsSubtitle": "Sign in to Roo Code Cloud to share tasks", + "account:cloudBenefitHistory": "Access your task history from anywhere", + "account:cloudBenefitSharing": "Share tasks with your team", + "account:cloudBenefitMetrics": "Track usage and costs", + "account:connect": "Connect", + } + return translations[key] || key + }, + }), + initReactI18next: { + type: "3rdParty", + init: vi.fn(), + }, +})) + +// Mock pretty-bytes +vi.mock("pretty-bytes", () => ({ + default: (bytes: number) => `${bytes} B`, +})) + +describe("TaskActions", () => { + const mockItem: HistoryItem = { + id: "test-task-id", + number: 1, + ts: Date.now(), + task: "Test task", + tokensIn: 100, + tokensOut: 200, + totalCost: 0.01, + size: 1024, + } + + beforeEach(() => { + vi.clearAllMocks() + mockUseExtensionState.mockReturnValue({ + sharingEnabled: true, + cloudIsAuthenticated: true, + cloudUserInfo: { + organizationName: "Test Organization", + }, + } as any) + }) + + describe("Share Button Visibility", () => { + it("renders share button when item has id", () => { + render() + + const shareButton = screen.getByTitle("Share task") + expect(shareButton).toBeInTheDocument() + }) + + it("does not render share button when item has no id", () => { + render() + + const shareButton = screen.queryByTitle("Share task") + expect(shareButton).not.toBeInTheDocument() + }) + + it("renders share button even when not authenticated", () => { + mockUseExtensionState.mockReturnValue({ + sharingEnabled: false, + cloudIsAuthenticated: false, + } as any) + + render() + + const shareButton = screen.getByTitle("Share task") + expect(shareButton).toBeInTheDocument() + }) + }) + + describe("Authenticated User Share Flow", () => { + it("shows organization and public share options when authenticated and sharing enabled", () => { + render() + + const shareButton = screen.getByTitle("Share task") + fireEvent.click(shareButton) + + expect(screen.getByText("Share with Organization")).toBeInTheDocument() + expect(screen.getByText("Share Publicly")).toBeInTheDocument() + }) + + it("sends shareCurrentTask message when organization option is selected", () => { + render() + + const shareButton = screen.getByTitle("Share task") + fireEvent.click(shareButton) + + const orgOption = screen.getByText("Share with Organization") + fireEvent.click(orgOption) + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "shareCurrentTask", + visibility: "organization", + }) + }) + + it("sends shareCurrentTask message when public option is selected", () => { + render() + + const shareButton = screen.getByTitle("Share task") + fireEvent.click(shareButton) + + const publicOption = screen.getByText("Share Publicly") + fireEvent.click(publicOption) + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "shareCurrentTask", + visibility: "public", + }) + }) + + it("does not show organization option when user is not in an organization", () => { + mockUseExtensionState.mockReturnValue({ + sharingEnabled: true, + cloudIsAuthenticated: true, + cloudUserInfo: { + // No organizationName property + }, + } as any) + + render() + + const shareButton = screen.getByTitle("Share task") + fireEvent.click(shareButton) + + expect(screen.queryByText("Share with Organization")).not.toBeInTheDocument() + expect(screen.getByText("Share Publicly")).toBeInTheDocument() + }) + }) + + describe("Unauthenticated User Login Flow", () => { + beforeEach(() => { + mockUseExtensionState.mockReturnValue({ + sharingEnabled: false, + cloudIsAuthenticated: false, + } as any) + }) + + it("shows connect to cloud option when not authenticated", () => { + render() + + const shareButton = screen.getByTitle("Share task") + fireEvent.click(shareButton) + + expect(screen.getByText("Connect to Roo Code Cloud")).toBeInTheDocument() + expect(screen.getByText("Sign in to Roo Code Cloud to share tasks")).toBeInTheDocument() + expect(screen.getByText("Connect")).toBeInTheDocument() + }) + + it("does not show organization and public options when not authenticated", () => { + render() + + const shareButton = screen.getByTitle("Share task") + fireEvent.click(shareButton) + + expect(screen.queryByText("Share with Organization")).not.toBeInTheDocument() + expect(screen.queryByText("Share Publicly")).not.toBeInTheDocument() + }) + + it("sends rooCloudSignIn message when connect to cloud is selected", () => { + render() + + const shareButton = screen.getByTitle("Share task") + fireEvent.click(shareButton) + + const connectOption = screen.getByText("Connect") + fireEvent.click(connectOption) + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "rooCloudSignIn", + }) + }) + }) + + describe("Mixed Authentication States", () => { + it("shows disabled share button when authenticated but sharing not enabled", () => { + mockUseExtensionState.mockReturnValue({ + sharingEnabled: false, + cloudIsAuthenticated: true, + } as any) + + render() + + const shareButton = screen.getByTitle("Sharing disabled by organization") + expect(shareButton).toBeInTheDocument() + expect(shareButton).toBeDisabled() + + // Should not have a popover when sharing is disabled + fireEvent.click(shareButton) + expect(screen.queryByText("Share with Organization")).not.toBeInTheDocument() + expect(screen.queryByText("Connect to Cloud")).not.toBeInTheDocument() + }) + + it("automatically opens popover when user becomes authenticated", () => { + // Start with unauthenticated state + mockUseExtensionState.mockReturnValue({ + sharingEnabled: false, + cloudIsAuthenticated: false, + } as any) + + const { rerender } = render() + + // Verify popover is not open initially + expect(screen.queryByText("Share with Organization")).not.toBeInTheDocument() + + // Simulate user becoming authenticated + mockUseExtensionState.mockReturnValue({ + sharingEnabled: true, + cloudIsAuthenticated: true, + cloudUserInfo: { + organizationName: "Test Organization", + }, + } as any) + + rerender() + + // Verify popover automatically opens and shows sharing options + expect(screen.getByText("Share with Organization")).toBeInTheDocument() + expect(screen.getByText("Share Publicly")).toBeInTheDocument() + }) + }) + + describe("Other Actions", () => { + it("renders export button", () => { + render() + + const exportButton = screen.getByTitle("Export task history") + expect(exportButton).toBeInTheDocument() + }) + + it("sends exportCurrentTask message when export button is clicked", () => { + render() + + const exportButton = screen.getByTitle("Export task history") + fireEvent.click(exportButton) + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "exportCurrentTask", + }) + }) + + it("renders delete button and file size when item has size", () => { + render() + + const deleteButton = screen.getByTitle("Delete Task (Shift + Click to skip confirmation)") + expect(deleteButton).toBeInTheDocument() + expect(screen.getByText("1024 B")).toBeInTheDocument() + }) + + it("does not render delete button when item has no size", () => { + const itemWithoutSize = { ...mockItem, size: 0 } + render() + + const deleteButton = screen.queryByTitle("Delete Task (Shift + Click to skip confirmation)") + expect(deleteButton).not.toBeInTheDocument() + }) + }) + + describe("Button States", () => { + it("disables buttons when buttonsDisabled is true", () => { + render() + + const shareButton = screen.getByTitle("Share task") + const exportButton = screen.getByTitle("Export task history") + + expect(shareButton).toBeDisabled() + expect(exportButton).toBeDisabled() + }) + }) +}) diff --git a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx index 784a263531..9acd0ad5ce 100644 --- a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx @@ -49,7 +49,6 @@ describe("TaskHeader", () => { task: { type: "say", ts: Date.now(), text: "Test task", images: [] }, tokensIn: 100, tokensOut: 50, - doesModelSupportPromptCache: true, totalCost: 0.05, contextTokens: 200, buttonsDisabled: false, diff --git a/webview-ui/src/components/common/CodeBlock.tsx b/webview-ui/src/components/common/CodeBlock.tsx index da3eb6429c..06f929b830 100644 --- a/webview-ui/src/components/common/CodeBlock.tsx +++ b/webview-ui/src/components/common/CodeBlock.tsx @@ -232,6 +232,10 @@ const CodeBlock = memo( const copyButtonWrapperRef = useRef(null) const { showCopyFeedback, copyWithFeedback } = useCopyToClipboard() const { t } = useAppTranslation() + const isMountedRef = useRef(true) + const buttonPositionTimeoutRef = useRef(null) + const collapseTimeout1Ref = useRef(null) + const collapseTimeout2Ref = useRef(null) // Update current language when prop changes, but only if user hasn't // made a selection. @@ -243,17 +247,23 @@ const CodeBlock = memo( } }, [language, currentLanguage]) - // Syntax highlighting with cached Shiki instance. + // Syntax highlighting with cached Shiki instance and mounted state management useEffect(() => { + // Set mounted state at the beginning of this effect + isMountedRef.current = true + const fallback = `
${source || ""}
` const highlight = async () => { // Show plain text if language needs to be loaded. if (currentLanguage && !isLanguageLoaded(currentLanguage)) { - setHighlightedCode(fallback) + if (isMountedRef.current) { + setHighlightedCode(fallback) + } } const highlighter = await getHighlighter(currentLanguage) + if (!isMountedRef.current) return const html = await highlighter.codeToHtml(source || "", { lang: currentLanguage || "txt", @@ -277,14 +287,36 @@ const CodeBlock = memo( }, ] as ShikiTransformer[], }) + if (!isMountedRef.current) return - setHighlightedCode(html) + if (isMountedRef.current) { + setHighlightedCode(html) + } } highlight().catch((e) => { console.error("[CodeBlock] Syntax highlighting error:", e, "\nStack trace:", e.stack) - setHighlightedCode(fallback) + if (isMountedRef.current) { + setHighlightedCode(fallback) + } }) + + // Cleanup function - manage mounted state and clear all timeouts + return () => { + isMountedRef.current = false + if (buttonPositionTimeoutRef.current) { + clearTimeout(buttonPositionTimeoutRef.current) + buttonPositionTimeoutRef.current = null + } + if (collapseTimeout1Ref.current) { + clearTimeout(collapseTimeout1Ref.current) + collapseTimeout1Ref.current = null + } + if (collapseTimeout2Ref.current) { + clearTimeout(collapseTimeout2Ref.current) + collapseTimeout2Ref.current = null + } + } }, [source, currentLanguage, collapsedHeight]) // Check if content height exceeds collapsed height whenever content changes @@ -455,8 +487,15 @@ const CodeBlock = memo( // Update button position and scroll when highlightedCode changes useEffect(() => { if (highlightedCode) { + // Clear any existing timeout before setting a new one + if (buttonPositionTimeoutRef.current) { + clearTimeout(buttonPositionTimeoutRef.current) + } // Update button position - setTimeout(updateCodeBlockButtonPosition, 0) + buttonPositionTimeoutRef.current = setTimeout(() => { + updateCodeBlockButtonPosition() + buttonPositionTimeoutRef.current = null // Optional: Clear ref after execution + }, 0) // Scroll to bottom if needed (immediately after Shiki updates) if (shouldScrollAfterHighlightRef.current) { @@ -479,6 +518,12 @@ const CodeBlock = memo( shouldScrollAfterHighlightRef.current = false } } + // Cleanup function for this effect + return () => { + if (buttonPositionTimeoutRef.current) { + clearTimeout(buttonPositionTimeoutRef.current) + } + } }, [highlightedCode, updateCodeBlockButtonPosition]) // Advanced inertial scroll chaining @@ -682,23 +727,30 @@ const CodeBlock = memo( {showCollapseButton && ( { - // Get the current code block element and scrollable container - const codeBlock = codeBlockRef.current - const scrollContainer = document.querySelector('[data-virtuoso-scroller="true"]') - if (!codeBlock || !scrollContainer) return - + // Get the current code block element + const codeBlock = codeBlockRef.current // Capture ref early // Toggle window shade state setWindowShade(!windowShade) - // After UI updates, ensure code block is visible and update button position - setTimeout( - () => { - codeBlock.scrollIntoView({ behavior: "smooth", block: "nearest" }) + // Clear any previous timeouts + if (collapseTimeout1Ref.current) clearTimeout(collapseTimeout1Ref.current) + if (collapseTimeout2Ref.current) clearTimeout(collapseTimeout2Ref.current) - // Wait for scroll to complete before updating button position - setTimeout(() => { - updateCodeBlockButtonPosition() - }, 50) + // After UI updates, ensure code block is visible and update button position + collapseTimeout1Ref.current = setTimeout( + () => { + if (codeBlock) { + // Check if codeBlock element still exists + codeBlock.scrollIntoView({ behavior: "smooth", block: "nearest" }) + + // Wait for scroll to complete before updating button position + collapseTimeout2Ref.current = setTimeout(() => { + // updateCodeBlockButtonPosition itself should also check for refs if needed + updateCodeBlockButtonPosition() + collapseTimeout2Ref.current = null + }, 50) + } + collapseTimeout1Ref.current = null }, WINDOW_SHADE_SETTINGS.transitionDelayS * 1000 + 50, ) diff --git a/webview-ui/src/components/common/TelemetryBanner.tsx b/webview-ui/src/components/common/TelemetryBanner.tsx index 6ce2f79994..63eb262b05 100644 --- a/webview-ui/src/components/common/TelemetryBanner.tsx +++ b/webview-ui/src/components/common/TelemetryBanner.tsx @@ -45,7 +45,7 @@ const TelemetryBanner = () => { window.postMessage({ type: "action", action: "settingsButtonClicked", - values: { section: "advanced" }, // Link directly to advanced settings with telemetry controls + values: { section: "about" }, // Link directly to about settings with telemetry controls }) } @@ -54,7 +54,12 @@ const TelemetryBanner = () => {
{t("welcome:telemetry.title")}
- {t("welcome:telemetry.anonymousTelemetry")} + , + }} + />
void stateManager: MarketplaceViewStateManager + targetTab?: "mcp" | "mode" } -export function MarketplaceView({ stateManager, onDone }: MarketplaceViewProps) { +export function MarketplaceView({ stateManager, onDone, targetTab }: MarketplaceViewProps) { const { t } = useAppTranslation() const [state, manager] = useStateManager(stateManager) const [hasReceivedInitialState, setHasReceivedInitialState] = useState(false) @@ -26,6 +27,12 @@ export function MarketplaceView({ stateManager, onDone }: MarketplaceViewProps) } }, [state.allItems, hasReceivedInitialState]) + useEffect(() => { + if (targetTab && (targetTab === "mcp" || targetTab === "mode")) { + manager.transition({ type: "SET_ACTIVE_TAB", payload: { tab: targetTab } }) + } + }, [targetTab, manager]) + // Ensure marketplace state manager processes messages when component mounts useEffect(() => { // When the marketplace view first mounts, we need to trigger a state update diff --git a/webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts b/webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts index 372c89a12b..7498ebfc59 100644 --- a/webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts +++ b/webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts @@ -370,6 +370,18 @@ export class MarketplaceViewStateManager { // Error case void this.transition({ type: "FETCH_ERROR" }) } else { + // Check if a specific tab is requested + if ( + message.values?.marketplaceTab && + (message.values.marketplaceTab === "mcp" || message.values.marketplaceTab === "mode") + ) { + // Set the active tab + void this.transition({ + type: "SET_ACTIVE_TAB", + payload: { tab: message.values.marketplaceTab }, + }) + } + // Refresh request void this.transition({ type: "FETCH_ITEMS" }) } diff --git a/webview-ui/src/components/marketplace/components/MarketplaceInstallModal.tsx b/webview-ui/src/components/marketplace/components/MarketplaceInstallModal.tsx index 876f5f3f86..b7e9951b0f 100644 --- a/webview-ui/src/components/marketplace/components/MarketplaceInstallModal.tsx +++ b/webview-ui/src/components/marketplace/components/MarketplaceInstallModal.tsx @@ -195,8 +195,25 @@ export const MarketplaceInstallModal: React.FC = ( } const handlePostInstallAction = (tab: "mcp" | "modes") => { - // Send message to switch to the appropriate tab - vscode.postMessage({ type: "switchTab", tab }) + if (tab === "mcp") { + // Navigate to MCP tab + window.postMessage( + { + type: "action", + action: "mcpButtonClicked", + }, + "*", + ) + } else { + // Navigate to Modes tab + window.postMessage( + { + type: "action", + action: "promptsButtonClicked", + }, + "*", + ) + } // Close the modal onClose() } diff --git a/webview-ui/src/components/modes/ModesView.tsx b/webview-ui/src/components/modes/ModesView.tsx index 18f4bdf3d2..42069de8f8 100644 --- a/webview-ui/src/components/modes/ModesView.tsx +++ b/webview-ui/src/components/modes/ModesView.tsx @@ -5,9 +5,10 @@ import { VSCodeRadio, VSCodeTextArea, VSCodeLink, + VSCodeTextField, } from "@vscode/webview-ui-toolkit/react" import { Trans } from "react-i18next" -import { ChevronsUpDown, X } from "lucide-react" +import { ChevronDown, X } from "lucide-react" import { ModeConfig, GroupEntry, PromptComponent, ToolGroup, modeConfigSchema } from "@roo-code/types" @@ -15,6 +16,7 @@ import { Mode, getRoleDefinition, getWhenToUse, + getDescription, getCustomInstructions, getAllModes, findModeBySlug as findCustomModeBySlug, @@ -105,6 +107,9 @@ const ModesView = ({ onDone }: ModesViewProps) => { if (updatedPrompt.roleDefinition === getRoleDefinition(mode)) { delete updatedPrompt.roleDefinition } + if (updatedPrompt.description === getDescription(mode)) { + delete updatedPrompt.description + } if (updatedPrompt.whenToUse === getWhenToUse(mode)) { delete updatedPrompt.whenToUse } @@ -120,6 +125,7 @@ const ModesView = ({ onDone }: ModesViewProps) => { const updateCustomMode = useCallback((slug: string, modeConfig: ModeConfig) => { const source = modeConfig.source || "global" + vscode.postMessage({ type: "updateCustomMode", slug, @@ -194,6 +200,7 @@ const ModesView = ({ onDone }: ModesViewProps) => { // State for create mode dialog const [newModeName, setNewModeName] = useState("") const [newModeSlug, setNewModeSlug] = useState("") + const [newModeDescription, setNewModeDescription] = useState("") const [newModeRoleDefinition, setNewModeRoleDefinition] = useState("") const [newModeWhenToUse, setNewModeWhenToUse] = useState("") const [newModeCustomInstructions, setNewModeCustomInstructions] = useState("") @@ -203,6 +210,7 @@ const ModesView = ({ onDone }: ModesViewProps) => { // Field-specific error states const [nameError, setNameError] = useState("") const [slugError, setSlugError] = useState("") + const [descriptionError, setDescriptionError] = useState("") const [roleDefinitionError, setRoleDefinitionError] = useState("") const [groupsError, setGroupsError] = useState("") @@ -211,6 +219,7 @@ const ModesView = ({ onDone }: ModesViewProps) => { // Reset form fields setNewModeName("") setNewModeSlug("") + setNewModeDescription("") setNewModeGroups(availableGroups) setNewModeRoleDefinition("") setNewModeWhenToUse("") @@ -219,6 +228,7 @@ const ModesView = ({ onDone }: ModesViewProps) => { // Reset error states setNameError("") setSlugError("") + setDescriptionError("") setRoleDefinitionError("") setGroupsError("") }, []) @@ -252,6 +262,7 @@ const ModesView = ({ onDone }: ModesViewProps) => { // Clear previous errors setNameError("") setSlugError("") + setDescriptionError("") setRoleDefinitionError("") setGroupsError("") @@ -259,6 +270,7 @@ const ModesView = ({ onDone }: ModesViewProps) => { const newMode: ModeConfig = { slug: newModeSlug, name: newModeName, + description: newModeDescription.trim() || undefined, roleDefinition: newModeRoleDefinition.trim(), whenToUse: newModeWhenToUse.trim() || undefined, customInstructions: newModeCustomInstructions.trim() || undefined, @@ -282,6 +294,9 @@ const ModesView = ({ onDone }: ModesViewProps) => { case "slug": setSlugError(message) break + case "description": + setDescriptionError(message) + break case "roleDefinition": setRoleDefinitionError(message) break @@ -301,6 +316,7 @@ const ModesView = ({ onDone }: ModesViewProps) => { }, [ newModeName, newModeSlug, + newModeDescription, newModeRoleDefinition, newModeWhenToUse, // Add whenToUse dependency newModeCustomInstructions, @@ -348,6 +364,7 @@ const ModesView = ({ onDone }: ModesViewProps) => { } if (customMode) { const source = customMode.source || "global" + updateCustomMode(customMode.slug, { ...customMode, groups: newGroups, @@ -386,7 +403,10 @@ const ModesView = ({ onDone }: ModesViewProps) => { return () => window.removeEventListener("message", handler) }, []) - const handleAgentReset = (modeSlug: string, type: "roleDefinition" | "whenToUse" | "customInstructions") => { + const handleAgentReset = ( + modeSlug: string, + type: "roleDefinition" | "description" | "whenToUse" | "customInstructions", + ) => { // Only reset for built-in modes const existingPrompt = customModePrompts?.[modeSlug] as PromptComponent const updatedPrompt = { ...existingPrompt } @@ -493,10 +513,10 @@ const ModesView = ({ onDone }: ModesViewProps) => { variant="combobox" role="combobox" aria-expanded={open} - className="grow justify-between" + className="justify-between w-60" data-testid="mode-select-trigger">
{getCurrentMode()?.name || t("prompts:modes.selectMode")}
- + @@ -582,6 +602,9 @@ const ModesView = ({ onDone }: ModesViewProps) => { {/* API Configuration - Moved Here */}
{t("prompts:apiConfiguration.title")}
+
+ {t("prompts:apiConfiguration.select")} +
-
- {t("prompts:apiConfiguration.select")} -
+ {/* Name section */}
{/* Only show name and delete for custom modes */} {visualMode && findModeBySlug(visualMode, customModes) && ( @@ -647,6 +668,8 @@ const ModesView = ({ onDone }: ModesViewProps) => {
)} + + {/* Role Definition section */}
{t("prompts:roleDefinition.title")}
@@ -700,11 +723,64 @@ const ModesView = ({ onDone }: ModesViewProps) => { } }} className="w-full" - rows={4} + rows={5} data-testid={`${getCurrentMode()?.slug || "code"}-prompt-textarea`} />
+ {/* Description section */} +
+
+
{t("prompts:description.title")}
+ {!findModeBySlug(visualMode, customModes) && ( + + )} +
+
+ {t("prompts:description.description")} +
+ { + const customMode = findModeBySlug(visualMode, customModes) + const prompt = customModePrompts?.[visualMode] as PromptComponent + return customMode?.description ?? prompt?.description ?? getDescription(visualMode) + })()} + onChange={(e) => { + const value = + (e as unknown as CustomEvent)?.detail?.target?.value || + ((e as any).target as HTMLTextAreaElement).value + const customMode = findModeBySlug(visualMode, customModes) + if (customMode) { + // For custom modes, update the JSON file + updateCustomMode(visualMode, { + ...customMode, + description: value.trim() || undefined, + source: customMode.source || "global", + }) + } else { + // For built-in modes, update the prompts + updateAgentPrompt(visualMode, { + description: value.trim() || undefined, + }) + } + }} + className="w-full" + data-testid={`${getCurrentMode()?.slug || "code"}-description-textfield`} + /> +
+ {/* When to Use section */}
@@ -755,7 +831,7 @@ const ModesView = ({ onDone }: ModesViewProps) => { } }} className="w-full" - rows={3} + rows={4} data-testid={`${getCurrentMode()?.slug || "code"}-when-to-use-textarea`} />
@@ -912,7 +988,7 @@ const ModesView = ({ onDone }: ModesViewProps) => { }) } }} - rows={4} + rows={10} className="w-full" data-testid={`${getCurrentMode()?.slug || "code"}-custom-instructions-textarea`} /> @@ -1189,6 +1265,23 @@ const ModesView = ({ onDone }: ModesViewProps) => { )}
+
+
{t("prompts:createModeDialog.description.label")}
+
+ {t("prompts:createModeDialog.description.description")} +
+ { + setNewModeDescription((e.target as HTMLInputElement).value) + }} + className="w-full" + /> + {descriptionError && ( +
{descriptionError}
+ )} +
+
{t("prompts:createModeDialog.whenToUse.label")}
diff --git a/webview-ui/src/components/modes/__tests__/ModesView.spec.tsx b/webview-ui/src/components/modes/__tests__/ModesView.spec.tsx index 47ff05613c..4ff7f4cf87 100644 --- a/webview-ui/src/components/modes/__tests__/ModesView.spec.tsx +++ b/webview-ui/src/components/modes/__tests__/ModesView.spec.tsx @@ -138,10 +138,13 @@ describe("PromptsView", () => { await fireEvent.click(resetButton) // Verify it only resets role definition + // When resetting a built-in mode's role definition, the field should be removed entirely + // from the customPrompt object, not set to undefined. + // This allows the default role definition from the built-in mode to be used instead. expect(vscode.postMessage).toHaveBeenCalledWith({ type: "updatePrompt", promptMode: "code", - customPrompt: { roleDefinition: undefined }, + customPrompt: {}, // Empty object because the role definition field is removed entirely }) // Cleanup before testing custom mode @@ -159,6 +162,46 @@ describe("PromptsView", () => { expect(screen.queryByTestId("role-definition-reset")).not.toBeInTheDocument() }) + it("description section behavior for different mode types", async () => { + const customMode = { + slug: "custom-mode", + name: "Custom Mode", + roleDefinition: "Custom role", + description: "Custom description", + groups: [], + } + + // Test with built-in mode (code) - description section should be shown with reset button + const { unmount } = render( + + + , + ) + + // Verify description reset button IS present for built-in modes + // because built-in modes can have their descriptions customized and reset + expect(screen.queryByTestId("description-reset")).toBeInTheDocument() + + // Cleanup before testing custom mode + unmount() + + // Test with custom mode - description section should be shown + render( + + + , + ) + + // Verify description section is present for custom modes + // but reset button is NOT present (since custom modes manage their own descriptions) + expect(screen.queryByTestId("description-reset")).not.toBeInTheDocument() + + // Verify the description text field is present for custom modes + expect(screen.getByTestId("custom-mode-description-textfield")).toBeInTheDocument() + }) + it("handles clearing custom instructions correctly", async () => { const setCustomInstructions = vitest.fn() renderPromptsView({ diff --git a/webview-ui/src/components/settings/About.tsx b/webview-ui/src/components/settings/About.tsx index bfffeb1611..5075643e6e 100644 --- a/webview-ui/src/components/settings/About.tsx +++ b/webview-ui/src/components/settings/About.tsx @@ -48,7 +48,12 @@ export const About = ({ telemetrySetting, setTelemetrySetting, className, ...pro {t("settings:footer.telemetry.label")}

- {t("settings:footer.telemetry.description")} + , + }} + />

diff --git a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts index 0d58268d14..5fefabf59e 100644 --- a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts +++ b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts @@ -369,4 +369,76 @@ describe("useSelectedModel", () => { expect(result.current.info).toBeUndefined() }) }) + + describe("claude-code provider", () => { + it("should return claude-code model with supportsImages disabled", () => { + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: {}, + requesty: {}, + glama: {}, + unbound: {}, + litellm: {}, + }, + isLoading: false, + isError: false, + } as any) + + mockUseOpenRouterModelProviders.mockReturnValue({ + data: {}, + isLoading: false, + isError: false, + } as any) + + const apiConfiguration: ProviderSettings = { + apiProvider: "claude-code", + apiModelId: "claude-sonnet-4-20250514", + } + + const wrapper = createWrapper() + const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) + + expect(result.current.provider).toBe("claude-code") + expect(result.current.id).toBe("claude-sonnet-4-20250514") + expect(result.current.info).toBeDefined() + expect(result.current.info?.supportsImages).toBe(false) + expect(result.current.info?.supportsPromptCache).toBe(true) // Claude Code now supports prompt cache + // Verify it inherits other properties from anthropic models + expect(result.current.info?.maxTokens).toBe(64_000) + expect(result.current.info?.contextWindow).toBe(200_000) + expect(result.current.info?.supportsComputerUse).toBe(true) + }) + + it("should use default claude-code model when no modelId is specified", () => { + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: {}, + requesty: {}, + glama: {}, + unbound: {}, + litellm: {}, + }, + isLoading: false, + isError: false, + } as any) + + mockUseOpenRouterModelProviders.mockReturnValue({ + data: {}, + isLoading: false, + isError: false, + } as any) + + const apiConfiguration: ProviderSettings = { + apiProvider: "claude-code", + } + + const wrapper = createWrapper() + const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) + + expect(result.current.provider).toBe("claude-code") + expect(result.current.id).toBe("claude-sonnet-4-20250514") // Default model + expect(result.current.info).toBeDefined() + expect(result.current.info?.supportsImages).toBe(false) + }) + }) }) diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 09cae03e5d..40c1ff2431 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -30,6 +30,8 @@ import { glamaDefaultModelId, unboundDefaultModelId, litellmDefaultModelId, + claudeCodeDefaultModelId, + claudeCodeModels, } from "@roo-code/types" import type { RouterModels } from "@roo/api" @@ -199,6 +201,12 @@ function getSelectedModel({ const info = vscodeLlmModels[modelFamily as keyof typeof vscodeLlmModels] return { id, info: { ...openAiModelInfoSaneDefaults, ...info, supportsImages: false } } // VSCode LM API currently doesn't support images. } + case "claude-code": { + // Claude Code models extend anthropic models but with images and prompt caching disabled + const id = apiConfiguration.apiModelId ?? claudeCodeDefaultModelId + const info = claudeCodeModels[id as keyof typeof claudeCodeModels] + return { id, info: { ...openAiModelInfoSaneDefaults, ...info } } + } // case "anthropic": // case "human-relay": // case "fake-ai": diff --git a/webview-ui/src/components/ui/select-dropdown.tsx b/webview-ui/src/components/ui/select-dropdown.tsx index 3f1906b81e..7fcc6884b7 100644 --- a/webview-ui/src/components/ui/select-dropdown.tsx +++ b/webview-ui/src/components/ui/select-dropdown.tsx @@ -37,6 +37,7 @@ export interface SelectDropdownProps { placeholder?: string shortcutText?: string renderItem?: (option: DropdownOption) => React.ReactNode + disableSearch?: boolean } export const SelectDropdown = React.memo( @@ -56,6 +57,7 @@ export const SelectDropdown = React.memo( placeholder = "", shortcutText = "", renderItem, + disableSearch = false, }, ref, ) => { @@ -117,8 +119,8 @@ export const SelectDropdown = React.memo( // Filter options based on search value using memoized Fzf instance const filteredOptions = React.useMemo(() => { - // If no search value, return all options without filtering - if (!searchValue) return options + // If search is disabled or no search value, return all options without filtering + if (disableSearch || !searchValue) return options // Get fuzzy matching items - only perform search if we have a search value const matchingItems = fzfInstance.find(searchValue).map((result) => result.item.original) @@ -132,7 +134,7 @@ export const SelectDropdown = React.memo( // Include if it's in the matching items return matchingItems.some((item) => item.value === option.value) }) - }, [options, searchValue, fzfInstance]) + }, [options, searchValue, fzfInstance, disableSearch]) // Group options by type and handle separators const groupedOptions = React.useMemo(() => { @@ -209,24 +211,26 @@ export const SelectDropdown = React.memo( className={cn("p-0 overflow-hidden", contentClassName)}>
{/* Search input */} -
- setSearchValue(e.target.value)} - placeholder={t("common:ui.search_placeholder")} - className="w-full h-8 px-2 py-1 text-xs bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded focus:outline-0" - /> - {searchValue.length > 0 && ( -
- -
- )} -
+ {!disableSearch && ( +
+ setSearchValue(e.target.value)} + placeholder={t("common:ui.search_placeholder")} + className="w-full h-8 px-2 py-1 text-xs bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded focus:outline-0" + /> + {searchValue.length > 0 && ( +
+ +
+ )} +
+ )} {/* Dropdown items - Use windowing for large lists */}
diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 313c5170a0..c87ccdb6e9 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -38,6 +38,8 @@ export interface ExtensionStateContextType extends ExtensionState { sharingEnabled: boolean maxConcurrentFileReads?: number mdmCompliant?: boolean + hasOpenedModeSelector: boolean // New property to track if user has opened mode selector + setHasOpenedModeSelector: (value: boolean) => void // Setter for the new property condensingApiConfigId?: string setCondensingApiConfigId: (value: string) => void customCondensingPrompt?: string @@ -180,6 +182,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode enhancementApiConfigId: "", condensingApiConfigId: "", // Default empty string for condensing API config ID customCondensingPrompt: "", // Default empty string for custom condensing prompt + hasOpenedModeSelector: false, // Default to false (not opened yet) autoApprovalEnabled: false, customModes: [], maxOpenTabsContext: 20, @@ -427,6 +430,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode }), setHistoryPreviewCollapsed: (value) => setState((prevState) => ({ ...prevState, historyPreviewCollapsed: value })), + setHasOpenedModeSelector: (value) => setState((prevState) => ({ ...prevState, hasOpenedModeSelector: value })), setAutoCondenseContext: (value) => setState((prevState) => ({ ...prevState, autoCondenseContext: value })), setAutoCondenseContextPercent: (value) => setState((prevState) => ({ ...prevState, autoCondenseContextPercent: value })), diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx index 56dd2ef426..085cd5da71 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx +++ b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx @@ -208,6 +208,7 @@ describe("mergeExtensionState", () => { cloudIsAuthenticated: false, sharingEnabled: false, profileThresholds: {}, + hasOpenedModeSelector: false, // Add the new required property } const prevState: ExtensionState = { diff --git a/webview-ui/src/i18n/locales/ca/account.json b/webview-ui/src/i18n/locales/ca/account.json index 037bfbe306..a94a978b87 100644 --- a/webview-ui/src/i18n/locales/ca/account.json +++ b/webview-ui/src/i18n/locales/ca/account.json @@ -1,8 +1,14 @@ { "title": "Compte", "profilePicture": "Imatge de perfil", - "unknownUser": "Usuari desconegut", "logOut": "Tancar sessió", "testApiAuthentication": "Provar autenticació d'API", - "signIn": "Connecta't a Roo Code Cloud" + "signIn": "Connecta't a Roo Code Cloud", + "connect": "Connecta", + "cloudBenefitsTitle": "Connecta't a Roo Code Cloud", + "cloudBenefitsSubtitle": "Sincronitza els teus prompts i telemetria per habilitar:", + "cloudBenefitHistory": "Historial de tasques en línia", + "cloudBenefitSharing": "Funcions de compartició i col·laboració", + "cloudBenefitMetrics": "Mètriques d'ús basades en tasques, tokens i costos", + "visitCloudWebsite": "Visita Roo Code Cloud" } diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index 1c5ff13e88..4db48e40f3 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -16,7 +16,12 @@ "shareWithOrganization": "Compartir amb l'organització", "shareWithOrganizationDescription": "Només els membres de la teva organització poden accedir", "sharePublicly": "Compartir públicament", - "sharePubliclyDescription": "Qualsevol amb l'enllaç pot accedir" + "sharePubliclyDescription": "Qualsevol amb l'enllaç pot accedir", + "connectToCloud": "Connecta al núvol", + "connectToCloudDescription": "Inicia sessió a Roo Code Cloud per compartir tasques", + "sharingDisabledByOrganization": "Compartició deshabilitada per l'organització", + "shareSuccessOrganization": "Enllaç d'organització copiat al porta-retalls", + "shareSuccessPublic": "Enllaç públic copiat al porta-retalls" }, "unpin": "Desfixar", "pin": "Fixar", @@ -106,6 +111,12 @@ "dragFiles": "manté premut shift per arrossegar fitxers", "dragFilesImages": "manté premut shift per arrossegar fitxers/imatges", "enhancePromptDescription": "El botó 'Millora la sol·licitud' ajuda a millorar la teva sol·licitud proporcionant context addicional, aclariments o reformulacions. Prova d'escriure una sol·licitud aquí i fes clic al botó de nou per veure com funciona.", + "modeSelector": { + "title": "Modes", + "marketplace": "Marketplace de Modes", + "settings": "Configuració de Modes", + "description": "Personalitats especialitzades que adapten el comportament de Roo." + }, "errorReadingFile": "Error en llegir el fitxer:", "noValidImages": "No s'ha processat cap imatge vàlida", "separator": "Separador", diff --git a/webview-ui/src/i18n/locales/ca/marketplace.json b/webview-ui/src/i18n/locales/ca/marketplace.json index 8653762d4f..5af3d1eee6 100644 --- a/webview-ui/src/i18n/locales/ca/marketplace.json +++ b/webview-ui/src/i18n/locales/ca/marketplace.json @@ -91,7 +91,7 @@ "whatNextMode": "Ara pots utilitzar aquest mode. Feu clic a la icona Modes de la barra lateral per canviar de pestanya.", "done": "Fet", "goToMcp": "Anar a la pestanya MCP", - "goToModes": "Anar a la pestanya Modes", + "goToModes": "Anar a la configuració de Modes", "moreInfoMcp": "Veure documentació MCP de {{name}}", "validationRequired": "Si us plau, proporciona un valor per a {{paramName}}", "prerequisites": "Prerequisits" diff --git a/webview-ui/src/i18n/locales/ca/prompts.json b/webview-ui/src/i18n/locales/ca/prompts.json index c9a2be41fb..04359f996a 100644 --- a/webview-ui/src/i18n/locales/ca/prompts.json +++ b/webview-ui/src/i18n/locales/ca/prompts.json @@ -34,6 +34,11 @@ "resetToDefault": "Restablir a valors predeterminats", "description": "Definiu l'experiència i personalitat de Roo per a aquest mode. Aquesta descripció determina com Roo es presenta i aborda les tasques." }, + "description": { + "title": "Descripció curta (per a humans)", + "resetToDefault": "Restablir a la descripció predeterminada", + "description": "Una breu descripció que es mostra al desplegable del selector de mode." + }, "whenToUse": { "title": "Quan utilitzar (opcional)", "description": "Descriviu quan s'hauria d'utilitzar aquest mode. Això ajuda l'Orchestrator a escollir el mode correcte per a una tasca.", @@ -145,6 +150,10 @@ "label": "Eines disponibles", "description": "Seleccioneu quines eines pot utilitzar aquest mode." }, + "description": { + "label": "Descripció curta (per a humans)", + "description": "Una breu descripció que es mostra al desplegable del selector de mode." + }, "customInstructions": { "label": "Instruccions personalitzades (opcional)", "description": "Afegiu directrius de comportament específiques per a aquest mode." diff --git a/webview-ui/src/i18n/locales/de/account.json b/webview-ui/src/i18n/locales/de/account.json index daa3961a04..bd4d71eada 100644 --- a/webview-ui/src/i18n/locales/de/account.json +++ b/webview-ui/src/i18n/locales/de/account.json @@ -1,8 +1,14 @@ { "title": "Konto", "profilePicture": "Profilbild", - "unknownUser": "Unbekannter Benutzer", "logOut": "Abmelden", "testApiAuthentication": "API-Authentifizierung testen", - "signIn": "Mit Roo Code Cloud verbinden" + "signIn": "Mit Roo Code Cloud verbinden", + "connect": "Verbinden", + "cloudBenefitsTitle": "Mit Roo Code Cloud verbinden", + "cloudBenefitsSubtitle": "Synchronisiere deine Prompts und Telemetrie, um folgendes zu aktivieren:", + "cloudBenefitHistory": "Online-Aufgabenverlauf", + "cloudBenefitSharing": "Freigabe- und Kollaborationsfunktionen", + "cloudBenefitMetrics": "Aufgaben-, Token- und kostenbasierte Nutzungsmetriken", + "visitCloudWebsite": "Roo Code Cloud besuchen" } diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 99717acb8e..3ee69f9ae4 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -16,7 +16,12 @@ "shareWithOrganization": "Mit Organisation teilen", "shareWithOrganizationDescription": "Nur Mitglieder deiner Organisation können zugreifen", "sharePublicly": "Öffentlich teilen", - "sharePubliclyDescription": "Jeder mit dem Link kann zugreifen" + "sharePubliclyDescription": "Jeder mit dem Link kann zugreifen", + "connectToCloud": "Mit Cloud verbinden", + "connectToCloudDescription": "Melde dich bei Roo Code Cloud an, um Aufgaben zu teilen", + "sharingDisabledByOrganization": "Freigabe von der Organisation deaktiviert", + "shareSuccessOrganization": "Organisationslink in die Zwischenablage kopiert", + "shareSuccessPublic": "Öffentlicher Link in die Zwischenablage kopiert" }, "unpin": "Lösen von oben", "pin": "Anheften", @@ -106,6 +111,12 @@ "dragFiles": "Shift halten, um Dateien einzufügen", "dragFilesImages": "Shift halten, um Dateien/Bilder einzufügen", "enhancePromptDescription": "Die Schaltfläche 'Prompt verbessern' hilft, deine Anfrage durch zusätzlichen Kontext, Klarstellungen oder Umformulierungen zu verbessern. Versuche, hier eine Anfrage einzugeben und klicke erneut auf die Schaltfläche, um zu sehen, wie es funktioniert.", + "modeSelector": { + "title": "Modi", + "marketplace": "Modus-Marketplace", + "settings": "Modus-Einstellungen", + "description": "Spezialisierte Personas, die Roos Verhalten anpassen." + }, "errorReadingFile": "Fehler beim Lesen der Datei:", "noValidImages": "Keine gültigen Bilder wurden verarbeitet", "separator": "Trennlinie", diff --git a/webview-ui/src/i18n/locales/de/marketplace.json b/webview-ui/src/i18n/locales/de/marketplace.json index c9bc9f9c43..4632ce5fea 100644 --- a/webview-ui/src/i18n/locales/de/marketplace.json +++ b/webview-ui/src/i18n/locales/de/marketplace.json @@ -91,7 +91,7 @@ "whatNextMode": "Du kannst diesen Modus jetzt verwenden. Klicke auf das Modi-Symbol in der Seitenleiste, um die Tabs zu wechseln.", "done": "Fertig", "goToMcp": "Zum MCP-Tab gehen", - "goToModes": "Zum Modi-Tab gehen", + "goToModes": "Zu den Modi-Einstellungen gehen", "moreInfoMcp": "{{name}} MCP-Dokumentation anzeigen", "validationRequired": "Bitte gib einen Wert für {{paramName}} an", "prerequisites": "Voraussetzungen" diff --git a/webview-ui/src/i18n/locales/de/prompts.json b/webview-ui/src/i18n/locales/de/prompts.json index bab48f876f..6e9fd0f47b 100644 --- a/webview-ui/src/i18n/locales/de/prompts.json +++ b/webview-ui/src/i18n/locales/de/prompts.json @@ -34,6 +34,11 @@ "resetToDefault": "Auf Standardwerte zurücksetzen", "description": "Definiere Roos Expertise und Persönlichkeit für diesen Modus. Diese Beschreibung prägt, wie Roo sich präsentiert und an Aufgaben herangeht." }, + "description": { + "title": "Kurzbeschreibung (für Menschen)", + "resetToDefault": "Auf Standardbeschreibung zurücksetzen", + "description": "Eine kurze Beschreibung, die im Dropdown-Menü der Modusauswahl angezeigt wird." + }, "whenToUse": { "title": "Wann zu verwenden (optional)", "description": "Beschreibe, wann dieser Modus verwendet werden sollte. Dies hilft dem Orchestrator, den richtigen Modus für eine Aufgabe auszuwählen.", @@ -145,6 +150,10 @@ "label": "Verfügbare Werkzeuge", "description": "Wähle, welche Werkzeuge dieser Modus verwenden kann." }, + "description": { + "label": "Kurzbeschreibung (für Menschen)", + "description": "Eine kurze Beschreibung, die im Dropdown-Menü der Modusauswahl angezeigt wird." + }, "customInstructions": { "label": "Benutzerdefinierte Anweisungen (optional)", "description": "Fügen Sie verhaltensspezifische Richtlinien für diesen Modus hinzu." diff --git a/webview-ui/src/i18n/locales/en/account.json b/webview-ui/src/i18n/locales/en/account.json index 7833471eb2..f900abb297 100644 --- a/webview-ui/src/i18n/locales/en/account.json +++ b/webview-ui/src/i18n/locales/en/account.json @@ -1,8 +1,14 @@ { "title": "Account", "profilePicture": "Profile picture", - "unknownUser": "Unknown User", "logOut": "Log out", "testApiAuthentication": "Test API Authentication", - "signIn": "Connect to Roo Code Cloud" + "signIn": "Connect to Roo Code Cloud", + "connect": "Connect", + "cloudBenefitsTitle": "Connect to Roo Code Cloud", + "cloudBenefitsSubtitle": "Sync your prompts and telemetry to enable:", + "cloudBenefitHistory": "Online task history", + "cloudBenefitSharing": "Sharing and collaboration features", + "cloudBenefitMetrics": "Task, token, and cost-based usage metrics", + "visitCloudWebsite": "Visit Roo Code Cloud" } diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index 9ecfdde1ff..9ee8e167bd 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -16,7 +16,12 @@ "shareWithOrganization": "Share with Organization", "shareWithOrganizationDescription": "Only members of your organization can access", "sharePublicly": "Share Publicly", - "sharePubliclyDescription": "Anyone with the link can access" + "sharePubliclyDescription": "Anyone with the link can access", + "connectToCloud": "Connect to Cloud", + "connectToCloudDescription": "Sign in to Roo Code Cloud to share tasks", + "sharingDisabledByOrganization": "Sharing disabled by organization", + "shareSuccessOrganization": "Organization link copied to clipboard", + "shareSuccessPublic": "Public link copied to clipboard" }, "unpin": "Unpin", "pin": "Pin", @@ -106,6 +111,12 @@ "selectMode": "Select mode for interaction", "selectApiConfig": "Select API configuration", "enhancePrompt": "Enhance prompt with additional context", + "modeSelector": { + "title": "Modes", + "marketplace": "Mode Marketplace", + "settings": "Mode Settings", + "description": "Specialized personas that tailor Roo's behavior." + }, "enhancePromptDescription": "The 'Enhance Prompt' button helps improve your prompt by providing additional context, clarification, or rephrasing. Try typing a prompt in here and clicking the button again to see how it works.", "addImages": "Add images to message", "sendMessage": "Send message", diff --git a/webview-ui/src/i18n/locales/en/marketplace.json b/webview-ui/src/i18n/locales/en/marketplace.json index 6a5e877b2a..c8c9a2f11a 100644 --- a/webview-ui/src/i18n/locales/en/marketplace.json +++ b/webview-ui/src/i18n/locales/en/marketplace.json @@ -92,7 +92,7 @@ "whatNextMode": "You can now use this mode. Click the Modes icon in the sidebar to switch tabs.", "done": "Done", "goToMcp": "Go to MCP Tab", - "goToModes": "Go to Modes Tab", + "goToModes": "Go to Modes Settings", "moreInfoMcp": "View {{name}} MCP documentation", "validationRequired": "Please provide a value for {{paramName}}" }, diff --git a/webview-ui/src/i18n/locales/en/prompts.json b/webview-ui/src/i18n/locales/en/prompts.json index f03d48ca92..3614d79872 100644 --- a/webview-ui/src/i18n/locales/en/prompts.json +++ b/webview-ui/src/i18n/locales/en/prompts.json @@ -34,9 +34,14 @@ "resetToDefault": "Reset to default", "description": "Define Roo's expertise and personality for this mode. This description shapes how Roo presents itself and approaches tasks." }, + "description": { + "title": "Short description (for humans)", + "resetToDefault": "Reset to default description", + "description": "A brief description shown in the mode selector dropdown." + }, "whenToUse": { "title": "When to Use (optional)", - "description": "Describe when this mode should be used. This helps the Orchestrator choose the right mode for a task.", + "description": "Guidance for Roo for when this mode should be used. This helps the Orchestrator choose the right mode for a task.", "resetToDefault": "Reset to default 'When to Use' description" }, "customInstructions": { @@ -137,9 +142,13 @@ "label": "Role Definition", "description": "Define Roo's expertise and personality for this mode." }, + "description": { + "label": "Short description (for humans)", + "description": "A brief description shown in the mode selector dropdown." + }, "whenToUse": { "label": "When to Use (optional)", - "description": "Provide a clear description of when this mode is most effective and what types of tasks it excels at." + "description": "Guidance for Roo for when this mode should be used. This helps the Orchestrator choose the right mode for a task." }, "tools": { "label": "Available Tools", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 7dc79b8c1a..58cdd2a528 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -610,7 +610,7 @@ "feedback": "If you have any questions or feedback, feel free to open an issue at github.com/RooCodeInc/Roo-Code or join reddit.com/r/RooCode or discord.gg/roocode", "telemetry": { "label": "Allow anonymous error and usage reporting", - "description": "Help improve Roo Code by sending anonymous usage data and error reports. No code, prompts, or personal information is ever sent. See our privacy policy for more details." + "description": "Help improve Roo Code by sending anonymous usage data and error reports. No code, prompts, or personal information is ever sent (unless you connect to Roo Code Cloud). See our privacy policy for more details." }, "settings": { "import": "Import", diff --git a/webview-ui/src/i18n/locales/en/welcome.json b/webview-ui/src/i18n/locales/en/welcome.json index e876717a17..2202f6fd61 100644 --- a/webview-ui/src/i18n/locales/en/welcome.json +++ b/webview-ui/src/i18n/locales/en/welcome.json @@ -17,7 +17,7 @@ "startCustom": "Or you can bring your provider API key:", "telemetry": { "title": "Help Improve Roo Code", - "anonymousTelemetry": "Send anonymous error and usage data to help us fix bugs and improve the extension. No code, prompts, or personal information is ever sent.", + "anonymousTelemetry": "Send anonymous error and usage data to help us fix bugs and improve the extension. No code, prompts, or personal information is ever sent (unless you connect to Roo Code Cloud). See our privacy policy for more details.", "changeSettings": "You can always change this at the bottom of the settings", "settings": "settings", "allow": "Allow", diff --git a/webview-ui/src/i18n/locales/es/account.json b/webview-ui/src/i18n/locales/es/account.json index 64b96cda63..2bda10e82f 100644 --- a/webview-ui/src/i18n/locales/es/account.json +++ b/webview-ui/src/i18n/locales/es/account.json @@ -1,8 +1,14 @@ { "title": "Cuenta", "profilePicture": "Foto de perfil", - "unknownUser": "Usuario desconocido", "logOut": "Cerrar sesión", "testApiAuthentication": "Probar autenticación de API", - "signIn": "Conectar a Roo Code Cloud" + "signIn": "Conectar a Roo Code Cloud", + "connect": "Conectar", + "cloudBenefitsTitle": "Conectar a Roo Code Cloud", + "cloudBenefitsSubtitle": "Sincroniza tus prompts y telemetría para habilitar:", + "cloudBenefitHistory": "Historial de tareas en línea", + "cloudBenefitSharing": "Funciones de compartir y colaboración", + "cloudBenefitMetrics": "Métricas de uso basadas en tareas, tokens y costos", + "visitCloudWebsite": "Visitar Roo Code Cloud" } diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index 433cab868a..7072d0ea16 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -16,7 +16,12 @@ "shareWithOrganization": "Compartir con organización", "shareWithOrganizationDescription": "Solo los miembros de tu organización pueden acceder", "sharePublicly": "Compartir públicamente", - "sharePubliclyDescription": "Cualquiera con el enlace puede acceder" + "sharePubliclyDescription": "Cualquiera con el enlace puede acceder", + "connectToCloud": "Conectar al Cloud", + "connectToCloudDescription": "Inicia sesión en Roo Code Cloud para compartir tareas", + "sharingDisabledByOrganization": "Compartir deshabilitado por la organización", + "shareSuccessOrganization": "Enlace de organización copiado al portapapeles", + "shareSuccessPublic": "Enlace público copiado al portapapeles" }, "unpin": "Desfijar", "pin": "Fijar", @@ -106,6 +111,12 @@ "dragFiles": "mantén shift para arrastrar archivos", "dragFilesImages": "mantén shift para arrastrar archivos/imágenes", "enhancePromptDescription": "El botón 'Mejorar el mensaje' ayuda a mejorar tu petición proporcionando contexto adicional, aclaraciones o reformulaciones. Intenta escribir una petición aquí y haz clic en el botón nuevamente para ver cómo funciona.", + "modeSelector": { + "title": "Modos", + "marketplace": "Marketplace de Modos", + "settings": "Configuración de Modos", + "description": "Personalidades especializadas que adaptan el comportamiento de Roo." + }, "errorReadingFile": "Error al leer el archivo:", "noValidImages": "No se procesaron imágenes válidas", "separator": "Separador", diff --git a/webview-ui/src/i18n/locales/es/marketplace.json b/webview-ui/src/i18n/locales/es/marketplace.json index 38056f32ea..918d10ef8d 100644 --- a/webview-ui/src/i18n/locales/es/marketplace.json +++ b/webview-ui/src/i18n/locales/es/marketplace.json @@ -91,7 +91,7 @@ "whatNextMode": "Ahora puedes usar este modo. Haz clic en el icono Modos en la barra lateral para cambiar de pestaña.", "done": "Hecho", "goToMcp": "Ir a la pestaña MCP", - "goToModes": "Ir a la pestaña Modos", + "goToModes": "Ir a la configuración de Modos", "moreInfoMcp": "Ver documentación MCP de {{name}}", "validationRequired": "Por favor proporciona un valor para {{paramName}}", "prerequisites": "Requisitos previos" diff --git a/webview-ui/src/i18n/locales/es/prompts.json b/webview-ui/src/i18n/locales/es/prompts.json index c96fc5a602..54b5c1bd2d 100644 --- a/webview-ui/src/i18n/locales/es/prompts.json +++ b/webview-ui/src/i18n/locales/es/prompts.json @@ -34,6 +34,11 @@ "resetToDefault": "Restablecer a valores predeterminados", "description": "Define la experiencia y personalidad de Roo para este modo. Esta descripción determina cómo Roo se presenta y aborda las tareas." }, + "description": { + "title": "Descripción breve (para humanos)", + "resetToDefault": "Restablecer a la descripción predeterminada", + "description": "Una breve descripción que se muestra en el menú desplegable del selector de modo." + }, "whenToUse": { "title": "Cuándo usar (opcional)", "description": "Describe cuándo se debe usar este modo. Esto ayuda al Orchestrator a elegir el modo correcto para una tarea.", @@ -145,6 +150,10 @@ "label": "Herramientas disponibles", "description": "Selecciona qué herramientas puede usar este modo." }, + "description": { + "label": "Descripción breve (para humanos)", + "description": "Una breve descripción que se muestra en el menú desplegable del selector de modo." + }, "customInstructions": { "label": "Instrucciones personalizadas (opcional)", "description": "Agrega directrices de comportamiento específicas para este modo." diff --git a/webview-ui/src/i18n/locales/fr/account.json b/webview-ui/src/i18n/locales/fr/account.json index 7f09815113..1af4483c5c 100644 --- a/webview-ui/src/i18n/locales/fr/account.json +++ b/webview-ui/src/i18n/locales/fr/account.json @@ -1,8 +1,14 @@ { "title": "Compte", "profilePicture": "Photo de profil", - "unknownUser": "Utilisateur inconnu", "logOut": "Déconnexion", "testApiAuthentication": "Tester l'authentification API", - "signIn": "Se connecter à Roo Code Cloud" + "signIn": "Se connecter à Roo Code Cloud", + "connect": "Se connecter", + "cloudBenefitsTitle": "Se connecter à Roo Code Cloud", + "cloudBenefitsSubtitle": "Synchronise tes prompts et télémétrie pour activer :", + "cloudBenefitHistory": "Historique des tâches en ligne", + "cloudBenefitSharing": "Fonctionnalités de partage et collaboration", + "cloudBenefitMetrics": "Métriques d'utilisation basées sur les tâches, tokens et coûts", + "visitCloudWebsite": "Visiter Roo Code Cloud" } diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index a8691f2550..25d5074f45 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -16,7 +16,12 @@ "shareWithOrganization": "Partager avec l'organisation", "shareWithOrganizationDescription": "Seuls les membres de ton organisation peuvent accéder", "sharePublicly": "Partager publiquement", - "sharePubliclyDescription": "Toute personne avec le lien peut accéder" + "sharePubliclyDescription": "Toute personne avec le lien peut accéder", + "connectToCloud": "Se connecter au Cloud", + "connectToCloudDescription": "Connecte-toi à Roo Code Cloud pour partager des tâches", + "sharingDisabledByOrganization": "Partage désactivé par l'organisation", + "shareSuccessOrganization": "Lien d'organisation copié dans le presse-papiers", + "shareSuccessPublic": "Lien public copié dans le presse-papiers" }, "unpin": "Désépingler", "pin": "Épingler", @@ -106,6 +111,12 @@ "dragFiles": "maintenir Maj pour glisser des fichiers", "dragFilesImages": "maintenir Maj pour glisser des fichiers/images", "enhancePromptDescription": "Le bouton 'Améliorer la requête' aide à améliorer votre demande en fournissant un contexte supplémentaire, des clarifications ou des reformulations. Essayez de taper une demande ici et cliquez à nouveau sur le bouton pour voir comment cela fonctionne.", + "modeSelector": { + "title": "Modes", + "marketplace": "Marketplace de Modes", + "settings": "Paramètres des Modes", + "description": "Personas spécialisés qui adaptent le comportement de Roo." + }, "errorReadingFile": "Erreur lors de la lecture du fichier :", "noValidImages": "Aucune image valide n'a été traitée", "separator": "Séparateur", diff --git a/webview-ui/src/i18n/locales/fr/marketplace.json b/webview-ui/src/i18n/locales/fr/marketplace.json index cabac260ef..942d06aa49 100644 --- a/webview-ui/src/i18n/locales/fr/marketplace.json +++ b/webview-ui/src/i18n/locales/fr/marketplace.json @@ -91,7 +91,7 @@ "whatNextMode": "Vous pouvez maintenant utiliser ce mode. Cliquez sur l'icône Modes dans la barre latérale pour changer d'onglet.", "done": "Terminé", "goToMcp": "Aller à l'onglet MCP", - "goToModes": "Aller à l'onglet Modes", + "goToModes": "Aller aux paramètres des Modes", "moreInfoMcp": "Voir la documentation MCP de {{name}}", "validationRequired": "Veuillez fournir une valeur pour {{paramName}}", "prerequisites": "Prérequis" diff --git a/webview-ui/src/i18n/locales/fr/prompts.json b/webview-ui/src/i18n/locales/fr/prompts.json index ea9fdd5f3a..39bc67e482 100644 --- a/webview-ui/src/i18n/locales/fr/prompts.json +++ b/webview-ui/src/i18n/locales/fr/prompts.json @@ -34,6 +34,11 @@ "resetToDefault": "Réinitialiser aux valeurs par défaut", "description": "Définissez l'expertise et la personnalité de Roo pour ce mode. Cette description façonne la manière dont Roo se présente et aborde les tâches." }, + "description": { + "title": "Description courte (pour humains)", + "resetToDefault": "Réinitialiser à la description par défaut", + "description": "Une brève description affichée dans le menu déroulant du sélecteur de mode." + }, "whenToUse": { "title": "Quand utiliser (optionnel)", "description": "Décrivez quand ce mode doit être utilisé. Cela aide l'Orchestrateur à choisir le mode approprié pour une tâche.", @@ -145,6 +150,10 @@ "label": "Outils disponibles", "description": "Sélectionnez quels outils ce mode peut utiliser." }, + "description": { + "label": "Description courte (pour humains)", + "description": "Une brève description affichée dans le menu déroulant du sélecteur de mode." + }, "customInstructions": { "label": "Instructions personnalisées (optionnel)", "description": "Ajoutez des directives comportementales spécifiques à ce mode." diff --git a/webview-ui/src/i18n/locales/hi/account.json b/webview-ui/src/i18n/locales/hi/account.json index c24c980b59..be6ea00d88 100644 --- a/webview-ui/src/i18n/locales/hi/account.json +++ b/webview-ui/src/i18n/locales/hi/account.json @@ -1,8 +1,14 @@ { "title": "खाता", "profilePicture": "प्रोफाइल चित्र", - "unknownUser": "अज्ञात उपयोगकर्ता", "logOut": "लॉग आउट", "testApiAuthentication": "API प्रमाणीकरण का परीक्षण करें", - "signIn": "Roo Code Cloud से कनेक्ट करें" + "signIn": "Roo Code Cloud से कनेक्ट करें", + "connect": "कनेक्ट करें", + "cloudBenefitsTitle": "Roo Code Cloud से कनेक्ट करें", + "cloudBenefitsSubtitle": "निम्नलिखित को सक्षम करने के लिए अपने prompts और telemetry को sync करें:", + "cloudBenefitHistory": "ऑनलाइन कार्य इतिहास", + "cloudBenefitSharing": "साझाकरण और सहयोग सुविधाएं", + "cloudBenefitMetrics": "कार्य, token और लागत आधारित उपयोग मेट्रिक्स", + "visitCloudWebsite": "Roo Code Cloud पर जाएं" } diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index 34f06b375c..e67068d090 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -16,7 +16,12 @@ "shareWithOrganization": "संगठन के साथ साझा करें", "shareWithOrganizationDescription": "केवल आपके संगठन के सदस्य पहुंच सकते हैं", "sharePublicly": "सार्वजनिक रूप से साझा करें", - "sharePubliclyDescription": "लिंक वाला कोई भी व्यक्ति पहुंच सकता है" + "sharePubliclyDescription": "लिंक वाला कोई भी व्यक्ति पहुंच सकता है", + "connectToCloud": "Cloud से कनेक्ट करें", + "connectToCloudDescription": "कार्य साझा करने के लिए Roo Code Cloud में साइन इन करें", + "sharingDisabledByOrganization": "संगठन द्वारा साझाकरण अक्षम किया गया", + "shareSuccessOrganization": "संगठन लिंक क्लिपबोर्ड में कॉपी किया गया", + "shareSuccessPublic": "सार्वजनिक लिंक क्लिपबोर्ड में कॉपी किया गया" }, "unpin": "पिन करें", "pin": "अवपिन करें", @@ -106,6 +111,12 @@ "dragFiles": "फ़ाइलें खींचने के लिए shift दबाकर रखें", "dragFilesImages": "फ़ाइलें/चित्र खींचने के लिए shift दबाकर रखें", "enhancePromptDescription": "'प्रॉम्प्ट बढ़ाएँ' बटन अतिरिक्त संदर्भ, स्पष्टीकरण या पुनर्विचार प्रदान करके आपके अनुरोध को बेहतर बनाने में मदद करता है। यहां अनुरोध लिखकर देखें और यह कैसे काम करता है यह देखने के लिए बटन पर फिर से क्लिक करें।", + "modeSelector": { + "title": "मोड्स", + "marketplace": "मोड मार्केटप्लेस", + "settings": "मोड सेटिंग्स", + "description": "विशेष व्यक्तित्व जो Roo के व्यवहार को अनुकूलित करते हैं।" + }, "errorReadingFile": "फ़ाइल पढ़ने में त्रुटि:", "noValidImages": "कोई मान्य चित्र प्रोसेस नहीं किया गया", "separator": "विभाजक", diff --git a/webview-ui/src/i18n/locales/hi/marketplace.json b/webview-ui/src/i18n/locales/hi/marketplace.json index 34924d3686..b9c9857b54 100644 --- a/webview-ui/src/i18n/locales/hi/marketplace.json +++ b/webview-ui/src/i18n/locales/hi/marketplace.json @@ -91,7 +91,7 @@ "whatNextMode": "अब आप इस मोड का उपयोग कर सकते हैं। टैब स्विच करने के लिए साइडबार में मोड आइकन पर क्लिक करें।", "done": "पूर्ण", "goToMcp": "MCP टैब पर जाएं", - "goToModes": "मोड टैब पर जाएं", + "goToModes": "मोड सेटिंग्स पर जाएं", "moreInfoMcp": "{{name}} MCP दस्तावेज़ देखें", "validationRequired": "कृपया {{paramName}} के लिए एक मान प्रदान करें", "prerequisites": "आवश्यकताएं" diff --git a/webview-ui/src/i18n/locales/hi/prompts.json b/webview-ui/src/i18n/locales/hi/prompts.json index e4f939562e..9633b02953 100644 --- a/webview-ui/src/i18n/locales/hi/prompts.json +++ b/webview-ui/src/i18n/locales/hi/prompts.json @@ -34,6 +34,11 @@ "resetToDefault": "डिफ़ॉल्ट पर रीसेट करें", "description": "इस मोड के लिए Roo की विशेषज्ञता और व्यक्तित्व परिभाषित करें। यह विवरण Roo के स्वयं को प्रस्तुत करने और कार्यों से निपटने के तरीके को आकार देता है।" }, + "description": { + "title": "संक्षिप्त विवरण (मनुष्यों के लिए)", + "resetToDefault": "डिफ़ॉल्ट विवरण पर रीसेट करें", + "description": "मोड सेलेक्टर ड्रॉपडाउन में दिखाया गया संक्षिप्त विवरण।" + }, "whenToUse": { "title": "कब उपयोग करें (वैकल्पिक)", "description": "बताएं कि इस मोड का उपयोग कब किया जाना चाहिए। यह Orchestrator को किसी कार्य के लिए सही मोड चुनने में मदद करता है।", @@ -145,6 +150,10 @@ "label": "उपलब्ध टूल्स", "description": "चुनें कि यह मोड कौन से टूल्स उपयोग कर सकता है।" }, + "description": { + "label": "संक्षिप्त विवरण (मनुष्यों के लिए)", + "description": "मोड सेलेक्टर ड्रॉपडाउन में दिखाया गया संक्षिप्त विवरण।" + }, "customInstructions": { "label": "कस्टम निर्देश (वैकल्पिक)", "description": "इस मोड के लिए विशिष्ट व्यवहार दिशानिर्देश जोड़ें।" diff --git a/webview-ui/src/i18n/locales/id/account.json b/webview-ui/src/i18n/locales/id/account.json index f01be9eb8e..57f3fec0df 100644 --- a/webview-ui/src/i18n/locales/id/account.json +++ b/webview-ui/src/i18n/locales/id/account.json @@ -1,8 +1,14 @@ { "title": "Akun", "profilePicture": "Foto profil", - "unknownUser": "Pengguna Tidak Dikenal", "logOut": "Keluar", "testApiAuthentication": "Uji Autentikasi API", - "signIn": "Hubungkan ke Roo Code Cloud" + "signIn": "Hubungkan ke Roo Code Cloud", + "connect": "Hubungkan", + "cloudBenefitsTitle": "Hubungkan ke Roo Code Cloud", + "cloudBenefitsSubtitle": "Sinkronkan prompt dan telemetri kamu untuk mengaktifkan:", + "cloudBenefitHistory": "Riwayat tugas online", + "cloudBenefitSharing": "Fitur berbagi dan kolaborasi", + "cloudBenefitMetrics": "Metrik penggunaan berdasarkan tugas, token, dan biaya", + "visitCloudWebsite": "Kunjungi Roo Code Cloud" } diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index add1d8a694..259665a406 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -16,7 +16,12 @@ "shareWithOrganization": "Bagikan dengan organisasi", "shareWithOrganizationDescription": "Hanya anggota organisasi Anda yang dapat mengakses", "sharePublicly": "Bagikan secara publik", - "sharePubliclyDescription": "Siapa pun dengan tautan dapat mengakses" + "sharePubliclyDescription": "Siapa pun dengan tautan dapat mengakses", + "connectToCloud": "Hubungkan ke Cloud", + "connectToCloudDescription": "Masuk ke Roo Code Cloud untuk berbagi tugas", + "sharingDisabledByOrganization": "Berbagi dinonaktifkan oleh organisasi", + "shareSuccessOrganization": "Tautan organisasi disalin ke clipboard", + "shareSuccessPublic": "Tautan publik disalin ke clipboard" }, "history": { "title": "Riwayat" @@ -113,6 +118,12 @@ "selectApiConfig": "Pilih konfigurasi API", "enhancePrompt": "Tingkatkan prompt dengan konteks tambahan", "enhancePromptDescription": "Tombol 'Tingkatkan Prompt' membantu memperbaiki prompt kamu dengan memberikan konteks tambahan, klarifikasi, atau penyusunan ulang. Coba ketik prompt di sini dan klik tombol lagi untuk melihat cara kerjanya.", + "modeSelector": { + "title": "Mode", + "marketplace": "Marketplace Mode", + "settings": "Pengaturan Mode", + "description": "Persona khusus yang menyesuaikan perilaku Roo." + }, "addImages": "Tambahkan gambar ke pesan", "sendMessage": "Kirim pesan", "typeMessage": "Ketik pesan...", diff --git a/webview-ui/src/i18n/locales/id/marketplace.json b/webview-ui/src/i18n/locales/id/marketplace.json index 9d80ebe326..153747fdf8 100644 --- a/webview-ui/src/i18n/locales/id/marketplace.json +++ b/webview-ui/src/i18n/locales/id/marketplace.json @@ -92,7 +92,7 @@ "whatNextMode": "Anda sekarang dapat menggunakan mode ini. Klik ikon Mode di sidebar untuk beralih tab.", "done": "Selesai", "goToMcp": "Ke Tab MCP", - "goToModes": "Ke Tab Mode", + "goToModes": "Ke Pengaturan Mode", "moreInfoMcp": "Lihat dokumentasi MCP {{name}}", "validationRequired": "Silakan berikan nilai untuk {{paramName}}" }, diff --git a/webview-ui/src/i18n/locales/id/prompts.json b/webview-ui/src/i18n/locales/id/prompts.json index d94c85a8a4..a77a6e5376 100644 --- a/webview-ui/src/i18n/locales/id/prompts.json +++ b/webview-ui/src/i18n/locales/id/prompts.json @@ -34,6 +34,11 @@ "resetToDefault": "Reset ke default", "description": "Tentukan keahlian dan kepribadian Roo untuk mode ini. Deskripsi ini membentuk bagaimana Roo mempresentasikan dirinya dan mendekati tugas." }, + "description": { + "title": "Deskripsi singkat (untuk manusia)", + "resetToDefault": "Setel ulang ke deskripsi default", + "description": "Deskripsi singkat yang ditampilkan di dropdown pemilih mode." + }, "whenToUse": { "title": "Kapan Menggunakan (opsional)", "description": "Jelaskan kapan mode ini harus digunakan. Ini membantu Orchestrator memilih mode yang tepat untuk suatu tugas.", @@ -145,6 +150,10 @@ "label": "Tools yang Tersedia", "description": "Pilih tools mana yang dapat digunakan mode ini." }, + "description": { + "label": "Deskripsi singkat (untuk manusia)", + "description": "Deskripsi singkat yang ditampilkan di dropdown pemilih mode." + }, "customInstructions": { "label": "Instruksi Kustom (opsional)", "description": "Tambahkan panduan perilaku khusus untuk mode ini." diff --git a/webview-ui/src/i18n/locales/it/account.json b/webview-ui/src/i18n/locales/it/account.json index 4812f36b51..fda13f563c 100644 --- a/webview-ui/src/i18n/locales/it/account.json +++ b/webview-ui/src/i18n/locales/it/account.json @@ -1,8 +1,14 @@ { "title": "Account", "profilePicture": "Immagine del profilo", - "unknownUser": "Utente sconosciuto", "logOut": "Disconnetti", "testApiAuthentication": "Verifica autenticazione API", - "signIn": "Connetti a Roo Code Cloud" + "signIn": "Connetti a Roo Code Cloud", + "connect": "Connetti", + "cloudBenefitsTitle": "Connetti a Roo Code Cloud", + "cloudBenefitsSubtitle": "Sincronizza i tuoi prompt e telemetria per abilitare:", + "cloudBenefitHistory": "Cronologia attività online", + "cloudBenefitSharing": "Funzionalità di condivisione e collaborazione", + "cloudBenefitMetrics": "Metriche di utilizzo basate su attività, token e costi", + "visitCloudWebsite": "Visita Roo Code Cloud" } diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index 36d79324e1..bbf8c8be6a 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -16,7 +16,12 @@ "shareWithOrganization": "Condividi con l'organizzazione", "shareWithOrganizationDescription": "Solo i membri della tua organizzazione possono accedere", "sharePublicly": "Condividi pubblicamente", - "sharePubliclyDescription": "Chiunque con il link può accedere" + "sharePubliclyDescription": "Chiunque con il link può accedere", + "connectToCloud": "Connetti al Cloud", + "connectToCloudDescription": "Accedi a Roo Code Cloud per condividere attività", + "sharingDisabledByOrganization": "Condivisione disabilitata dall'organizzazione", + "shareSuccessOrganization": "Link organizzazione copiato negli appunti", + "shareSuccessPublic": "Link pubblico copiato negli appunti" }, "unpin": "Rilascia", "pin": "Fissa", @@ -106,6 +111,12 @@ "dragFiles": "tieni premuto shift per trascinare file", "dragFilesImages": "tieni premuto shift per trascinare file/immagini", "enhancePromptDescription": "Il pulsante 'Migliora prompt' aiuta a migliorare la tua richiesta fornendo contesto aggiuntivo, chiarimenti o riformulazioni. Prova a digitare una richiesta qui e fai di nuovo clic sul pulsante per vedere come funziona.", + "modeSelector": { + "title": "Modalità", + "marketplace": "Marketplace delle Modalità", + "settings": "Impostazioni Modalità", + "description": "Personalità specializzate che adattano il comportamento di Roo." + }, "errorReadingFile": "Errore nella lettura del file:", "noValidImages": "Nessuna immagine valida è stata elaborata", "separator": "Separatore", diff --git a/webview-ui/src/i18n/locales/it/marketplace.json b/webview-ui/src/i18n/locales/it/marketplace.json index 875db2685c..ea9845277b 100644 --- a/webview-ui/src/i18n/locales/it/marketplace.json +++ b/webview-ui/src/i18n/locales/it/marketplace.json @@ -91,7 +91,7 @@ "whatNextMode": "Ora puoi utilizzare questa modalità. Clicca sull'icona delle modalità nella barra laterale per cambiare scheda.", "done": "Fatto", "goToMcp": "Vai alla scheda MCP", - "goToModes": "Vai alla scheda Modalità", + "goToModes": "Vai alle impostazioni Modalità", "moreInfoMcp": "Visualizza documentazione MCP {{name}}", "validationRequired": "Fornisci un valore per {{paramName}}", "prerequisites": "Prerequisiti" diff --git a/webview-ui/src/i18n/locales/it/prompts.json b/webview-ui/src/i18n/locales/it/prompts.json index 2ac95bf8e2..c556a18aac 100644 --- a/webview-ui/src/i18n/locales/it/prompts.json +++ b/webview-ui/src/i18n/locales/it/prompts.json @@ -34,6 +34,11 @@ "resetToDefault": "Ripristina predefiniti", "description": "Definisci l'esperienza e la personalità di Roo per questa modalità. Questa descrizione modella come Roo si presenta e affronta i compiti." }, + "description": { + "title": "Descrizione breve (per umani)", + "resetToDefault": "Ripristina alla descrizione predefinita", + "description": "Una breve descrizione mostrata nel menu a discesa del selettore di modalità." + }, "whenToUse": { "title": "Quando utilizzare (opzionale)", "description": "Descrivi quando questa modalità dovrebbe essere utilizzata. Questo aiuta l'Orchestrator a scegliere la modalità giusta per un compito.", @@ -137,6 +142,10 @@ "label": "Definizione del ruolo", "description": "Definisci l'esperienza e la personalità di Roo per questa modalità." }, + "description": { + "label": "Descrizione breve (per umani)", + "description": "Una breve descrizione mostrata nel menu a discesa del selettore di modalità." + }, "whenToUse": { "label": "Quando utilizzare (opzionale)", "description": "Fornisci una chiara descrizione di quando questa modalità è più efficace e per quali tipi di compiti eccelle." diff --git a/webview-ui/src/i18n/locales/ja/account.json b/webview-ui/src/i18n/locales/ja/account.json index 459d5dd5ab..b41eaf7895 100644 --- a/webview-ui/src/i18n/locales/ja/account.json +++ b/webview-ui/src/i18n/locales/ja/account.json @@ -1,8 +1,14 @@ { "title": "アカウント", "profilePicture": "プロフィール画像", - "unknownUser": "不明なユーザー", "logOut": "ログアウト", "testApiAuthentication": "API認証をテスト", - "signIn": "Roo Code Cloud に接続" + "signIn": "Roo Code Cloud に接続", + "connect": "接続", + "cloudBenefitsTitle": "Roo Code Cloudに接続", + "cloudBenefitsSubtitle": "プロンプトとテレメトリを同期して以下を有効にする:", + "cloudBenefitHistory": "オンラインタスク履歴", + "cloudBenefitSharing": "共有とコラボレーション機能", + "cloudBenefitMetrics": "タスク、Token、コストベースの使用メトリクス", + "visitCloudWebsite": "Roo Code Cloudを訪問" } diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 42923a6a1d..0278edd4b6 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -16,7 +16,12 @@ "shareWithOrganization": "組織と共有", "shareWithOrganizationDescription": "組織のメンバーのみがアクセスできます", "sharePublicly": "公開で共有", - "sharePubliclyDescription": "リンクを持つ誰でもアクセスできます" + "sharePubliclyDescription": "リンクを持つ誰でもアクセスできます", + "connectToCloud": "クラウドに接続", + "connectToCloudDescription": "タスクを共有するためにRoo Code Cloudにサインイン", + "sharingDisabledByOrganization": "組織により共有が無効化されています", + "shareSuccessOrganization": "組織リンクをクリップボードにコピーしました", + "shareSuccessPublic": "公開リンクをクリップボードにコピーしました" }, "unpin": "ピン留めを解除", "pin": "ピン留め", @@ -106,6 +111,12 @@ "dragFiles": "ファイルをドラッグするにはShiftキーを押したまま", "dragFilesImages": "ファイル/画像をドラッグするにはShiftキーを押したまま", "enhancePromptDescription": "「プロンプトを強化」ボタンは、追加コンテキスト、説明、または言い換えを提供することで、リクエストを改善します。ここにリクエストを入力し、ボタンを再度クリックして動作を確認してください。", + "modeSelector": { + "title": "モード", + "marketplace": "モードマーケットプレイス", + "settings": "モード設定", + "description": "Rooの動作をカスタマイズする専門的なペルソナ。" + }, "errorReadingFile": "ファイル読み込みエラー:", "noValidImages": "有効な画像が処理されませんでした", "separator": "区切り", diff --git a/webview-ui/src/i18n/locales/ja/marketplace.json b/webview-ui/src/i18n/locales/ja/marketplace.json index c8fe42d8cf..d3343eee22 100644 --- a/webview-ui/src/i18n/locales/ja/marketplace.json +++ b/webview-ui/src/i18n/locales/ja/marketplace.json @@ -91,7 +91,7 @@ "whatNextMode": "このモードを使用できるようになりました。サイドバーのモードアイコンをクリックしてタブを切り替えてください。", "done": "完了", "goToMcp": "MCPタブに移動", - "goToModes": "モードタブに移動", + "goToModes": "モード設定に移動", "moreInfoMcp": "{{name}} MCPドキュメントを表示", "validationRequired": "{{paramName}}の値を入力してください", "prerequisites": "前提条件" diff --git a/webview-ui/src/i18n/locales/ja/prompts.json b/webview-ui/src/i18n/locales/ja/prompts.json index 4af25fa11d..8049a82d31 100644 --- a/webview-ui/src/i18n/locales/ja/prompts.json +++ b/webview-ui/src/i18n/locales/ja/prompts.json @@ -34,6 +34,11 @@ "resetToDefault": "デフォルトにリセット", "description": "このモードのRooの専門知識と個性を定義します。この説明は、Rooが自身をどのように表現し、タスクにどのように取り組むかを形作ります。" }, + "description": { + "title": "短い説明(人間向け)", + "resetToDefault": "デフォルトの説明にリセット", + "description": "モードセレクタのドロップダウンに表示される簡単な説明。" + }, "whenToUse": { "title": "使用タイミング(オプション)", "description": "このモードをいつ使用すべきかを説明します。これはOrchestratorがタスクに適切なモードを選択するのに役立ちます。", @@ -145,6 +150,10 @@ "label": "利用可能なツール", "description": "このモードが使用できるツールを選択します。" }, + "description": { + "label": "短い説明(人間向け)", + "description": "モードセレクタのドロップダウンに表示される簡単な説明。" + }, "customInstructions": { "label": "カスタム指示(オプション)", "description": "このモードに特化した行動ガイドラインを追加します。" diff --git a/webview-ui/src/i18n/locales/ko/account.json b/webview-ui/src/i18n/locales/ko/account.json index 667d607da8..6ad06d43fa 100644 --- a/webview-ui/src/i18n/locales/ko/account.json +++ b/webview-ui/src/i18n/locales/ko/account.json @@ -1,8 +1,14 @@ { "title": "계정", "profilePicture": "프로필 사진", - "unknownUser": "알 수 없는 사용자", "logOut": "로그아웃", "testApiAuthentication": "API 인증 테스트", - "signIn": "Roo Code Cloud에 연결" + "signIn": "Roo Code Cloud에 연결", + "connect": "연결", + "cloudBenefitsTitle": "Roo Code Cloud에 연결", + "cloudBenefitsSubtitle": "프롬프트와 텔레메트리를 동기화하여 다음을 활성화:", + "cloudBenefitHistory": "온라인 작업 기록", + "cloudBenefitSharing": "공유 및 협업 기능", + "cloudBenefitMetrics": "작업, 토큰, 비용 기반 사용 메트릭", + "visitCloudWebsite": "Roo Code Cloud 방문" } diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index 0330e5088a..5373831426 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -16,7 +16,12 @@ "shareWithOrganization": "조직과 공유", "shareWithOrganizationDescription": "조직 구성원만 액세스할 수 있습니다", "sharePublicly": "공개적으로 공유", - "sharePubliclyDescription": "링크가 있는 누구나 액세스할 수 있습니다" + "sharePubliclyDescription": "링크가 있는 누구나 액세스할 수 있습니다", + "connectToCloud": "클라우드에 연결", + "connectToCloudDescription": "작업을 공유하려면 Roo Code Cloud에 로그인하세요", + "sharingDisabledByOrganization": "조직에서 공유가 비활성화됨", + "shareSuccessOrganization": "조직 링크가 클립보드에 복사되었습니다", + "shareSuccessPublic": "공개 링크가 클립보드에 복사되었습니다" }, "unpin": "고정 해제하기", "pin": "고정하기", @@ -106,6 +111,12 @@ "dragFiles": "파일을 드래그하려면 shift 키 누르기", "dragFilesImages": "파일/이미지를 드래그하려면 shift 키 누르기", "enhancePromptDescription": "'프롬프트 향상' 버튼은 추가 컨텍스트, 명확화 또는 재구성을 제공하여 요청을 개선합니다. 여기에 요청을 입력한 다음 버튼을 다시 클릭하여 작동 방식을 확인해보세요.", + "modeSelector": { + "title": "모드", + "marketplace": "모드 마켓플레이스", + "settings": "모드 설정", + "description": "Roo의 행동을 맞춤화하는 전문화된 페르소나." + }, "errorReadingFile": "파일 읽기 오류:", "noValidImages": "처리된 유효한 이미지가 없습니다", "separator": "구분자", diff --git a/webview-ui/src/i18n/locales/ko/marketplace.json b/webview-ui/src/i18n/locales/ko/marketplace.json index 004b90cb31..b4a0b64980 100644 --- a/webview-ui/src/i18n/locales/ko/marketplace.json +++ b/webview-ui/src/i18n/locales/ko/marketplace.json @@ -91,7 +91,7 @@ "whatNextMode": "이제 이 모드를 사용할 수 있습니다. 사이드바의 모드 아이콘을 클릭하여 탭을 전환하세요.", "done": "완료", "goToMcp": "MCP 탭으로 이동", - "goToModes": "모드 탭으로 이동", + "goToModes": "모드 설정으로 이동", "moreInfoMcp": "{{name}} MCP 문서 보기", "validationRequired": "{{paramName}}에 대한 값을 입력해주세요", "prerequisites": "전제 조건" diff --git a/webview-ui/src/i18n/locales/ko/prompts.json b/webview-ui/src/i18n/locales/ko/prompts.json index 6e4eb12c8b..990ee67f03 100644 --- a/webview-ui/src/i18n/locales/ko/prompts.json +++ b/webview-ui/src/i18n/locales/ko/prompts.json @@ -34,6 +34,11 @@ "resetToDefault": "기본값으로 재설정", "description": "이 모드에 대한 Roo의 전문성과 성격을 정의하세요. 이 설명은 Roo가 자신을 어떻게 표현하고 작업에 접근하는지 형성합니다." }, + "description": { + "title": "짧은 설명 (사람용)", + "resetToDefault": "기본 설명으로 재설정", + "description": "모드 선택기 드롭다운에 표시되는 간단한 설명입니다." + }, "whenToUse": { "title": "사용 시기 (선택 사항)", "description": "이 모드를 언제 사용해야 하는지 설명합니다. 이는 Orchestrator가 작업에 적합한 모드를 선택하는 데 도움이 됩니다.", @@ -145,6 +150,10 @@ "label": "사용 가능한 도구", "description": "이 모드가 사용할 수 있는 도구를 선택하세요." }, + "description": { + "label": "짧은 설명 (사람용)", + "description": "모드 선택기 드롭다운에 표시되는 간단한 설명입니다." + }, "customInstructions": { "label": "사용자 지정 지침 (선택 사항)", "description": "이 모드에 대한 특정 행동 지침을 추가하세요." diff --git a/webview-ui/src/i18n/locales/nl/account.json b/webview-ui/src/i18n/locales/nl/account.json index 0a3ca89381..15ceb1865b 100644 --- a/webview-ui/src/i18n/locales/nl/account.json +++ b/webview-ui/src/i18n/locales/nl/account.json @@ -1,8 +1,14 @@ { "title": "Account", "profilePicture": "Profielfoto", - "unknownUser": "Onbekende gebruiker", "logOut": "Uitloggen", "testApiAuthentication": "API-authenticatie testen", - "signIn": "Verbind met Roo Code Cloud" + "signIn": "Verbind met Roo Code Cloud", + "connect": "Verbinden", + "cloudBenefitsTitle": "Verbind met Roo Code Cloud", + "cloudBenefitsSubtitle": "Synchroniseer je prompts en telemetrie om het volgende in te schakelen:", + "cloudBenefitHistory": "Online taakgeschiedenis", + "cloudBenefitSharing": "Deel- en samenwerkingsfuncties", + "cloudBenefitMetrics": "Taak-, token- en kostengebaseerde gebruiksstatistieken", + "visitCloudWebsite": "Bezoek Roo Code Cloud" } diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index 8be76bf55e..2dc60da09e 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -16,7 +16,12 @@ "shareWithOrganization": "Delen met organisatie", "shareWithOrganizationDescription": "Alleen leden van je organisatie kunnen toegang krijgen", "sharePublicly": "Openbaar delen", - "sharePubliclyDescription": "Iedereen met de link kan toegang krijgen" + "sharePubliclyDescription": "Iedereen met de link kan toegang krijgen", + "connectToCloud": "Verbind met Cloud", + "connectToCloudDescription": "Meld je aan bij Roo Code Cloud om taken te delen", + "sharingDisabledByOrganization": "Delen uitgeschakeld door organisatie", + "shareSuccessOrganization": "Organisatielink gekopieerd naar klembord", + "shareSuccessPublic": "Openbare link gekopieerd naar klembord" }, "unpin": "Losmaken", "pin": "Vastmaken", @@ -99,6 +104,12 @@ "selectApiConfig": "Selecteer API-configuratie", "enhancePrompt": "Prompt verbeteren met extra context", "enhancePromptDescription": "De knop 'Prompt verbeteren' helpt je prompt te verbeteren door extra context, verduidelijking of herformulering te bieden. Probeer hier een prompt te typen en klik opnieuw op de knop om te zien hoe het werkt.", + "modeSelector": { + "title": "Modi", + "marketplace": "Modus Marktplaats", + "settings": "Modus Instellingen", + "description": "Gespecialiseerde persona's die het gedrag van Roo aanpassen." + }, "addImages": "Afbeeldingen toevoegen aan bericht", "sendMessage": "Bericht verzenden", "typeMessage": "Typ een bericht...", diff --git a/webview-ui/src/i18n/locales/nl/marketplace.json b/webview-ui/src/i18n/locales/nl/marketplace.json index b9effed30f..c2fe7cd610 100644 --- a/webview-ui/src/i18n/locales/nl/marketplace.json +++ b/webview-ui/src/i18n/locales/nl/marketplace.json @@ -91,7 +91,7 @@ "whatNextMode": "Je kunt deze modus nu gebruiken. Klik op het modi-pictogram in de zijbalk om van tabblad te wisselen.", "done": "Gereed", "goToMcp": "Ga naar MCP-tabblad", - "goToModes": "Ga naar Modi-tabblad", + "goToModes": "Ga naar Modi-instellingen", "moreInfoMcp": "{{name}} MCP-documentatie bekijken", "validationRequired": "Geef een waarde op voor {{paramName}}", "prerequisites": "Vereisten" diff --git a/webview-ui/src/i18n/locales/nl/prompts.json b/webview-ui/src/i18n/locales/nl/prompts.json index e275f1a5a0..2aa09a5a15 100644 --- a/webview-ui/src/i18n/locales/nl/prompts.json +++ b/webview-ui/src/i18n/locales/nl/prompts.json @@ -34,6 +34,11 @@ "resetToDefault": "Terugzetten naar standaard", "description": "Definieer Roo's expertise en persoonlijkheid voor deze modus. Deze beschrijving bepaalt hoe Roo zich presenteert en taken benadert." }, + "description": { + "title": "Korte beschrijving (voor mensen)", + "resetToDefault": "Terugzetten naar standaardbeschrijving", + "description": "Een korte beschrijving die wordt getoond in de modusselectie dropdown." + }, "whenToUse": { "title": "Wanneer te gebruiken (optioneel)", "description": "Beschrijf wanneer deze modus gebruikt moet worden. Dit helpt de Orchestrator om de juiste modus voor een taak te kiezen.", @@ -145,6 +150,10 @@ "label": "Beschikbare tools", "description": "Selecteer welke tools deze modus kan gebruiken." }, + "description": { + "label": "Korte beschrijving (voor mensen)", + "description": "Een korte beschrijving die wordt getoond in de modusselectie dropdown." + }, "customInstructions": { "label": "Aangepaste instructies (optioneel)", "description": "Voeg gedragsrichtlijnen toe die specifiek zijn voor deze modus." diff --git a/webview-ui/src/i18n/locales/pl/account.json b/webview-ui/src/i18n/locales/pl/account.json index 175b4da696..fdb0e4d894 100644 --- a/webview-ui/src/i18n/locales/pl/account.json +++ b/webview-ui/src/i18n/locales/pl/account.json @@ -1,8 +1,14 @@ { "title": "Konto", "profilePicture": "Zdjęcie profilowe", - "unknownUser": "Nieznany użytkownik", "logOut": "Wyloguj", "testApiAuthentication": "Testuj uwierzytelnianie API", - "signIn": "Połącz z Roo Code Cloud" + "signIn": "Połącz z Roo Code Cloud", + "connect": "Połącz", + "cloudBenefitsTitle": "Połącz z Roo Code Cloud", + "cloudBenefitsSubtitle": "Synchronizuj swoje prompty i telemetrię, aby włączyć:", + "cloudBenefitHistory": "Historia zadań online", + "cloudBenefitSharing": "Funkcje udostępniania i współpracy", + "cloudBenefitMetrics": "Metryki użycia oparte na zadaniach, tokenach i kosztach", + "visitCloudWebsite": "Odwiedź Roo Code Cloud" } diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index 435c003179..0af70c595b 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -16,7 +16,12 @@ "shareWithOrganization": "Udostępnij organizacji", "shareWithOrganizationDescription": "Tylko członkowie twojej organizacji mogą uzyskać dostęp", "sharePublicly": "Udostępnij publicznie", - "sharePubliclyDescription": "Każdy z linkiem może uzyskać dostęp" + "sharePubliclyDescription": "Każdy z linkiem może uzyskać dostęp", + "connectToCloud": "Połącz z chmurą", + "connectToCloudDescription": "Zaloguj się do Roo Code Cloud, aby udostępniać zadania", + "sharingDisabledByOrganization": "Udostępnianie wyłączone przez organizację", + "shareSuccessOrganization": "Link organizacji skopiowany do schowka", + "shareSuccessPublic": "Link publiczny skopiowany do schowka" }, "unpin": "Odepnij", "pin": "Przypnij", @@ -106,6 +111,12 @@ "dragFiles": "przytrzymaj shift, aby przeciągnąć pliki", "dragFilesImages": "przytrzymaj shift, aby przeciągnąć pliki/obrazy", "enhancePromptDescription": "Przycisk 'Ulepsz podpowiedź' pomaga ulepszyć Twoją prośbę, dostarczając dodatkowy kontekst, wyjaśnienia lub przeformułowania. Spróbuj wpisać prośbę tutaj i kliknij przycisk ponownie, aby zobaczyć, jak to działa.", + "modeSelector": { + "title": "Tryby", + "marketplace": "Marketplace Trybów", + "settings": "Ustawienia Trybów", + "description": "Wyspecjalizowane persony, które dostosowują zachowanie Roo." + }, "errorReadingFile": "Błąd odczytu pliku:", "noValidImages": "Nie przetworzono żadnych prawidłowych obrazów", "separator": "Separator", diff --git a/webview-ui/src/i18n/locales/pl/marketplace.json b/webview-ui/src/i18n/locales/pl/marketplace.json index fe663c7e31..9acb67ed03 100644 --- a/webview-ui/src/i18n/locales/pl/marketplace.json +++ b/webview-ui/src/i18n/locales/pl/marketplace.json @@ -91,7 +91,7 @@ "whatNextMode": "Możesz teraz używać tego trybu. Kliknij ikonę Tryby na pasku bocznym, aby przełączyć zakładki.", "done": "Gotowe", "goToMcp": "Przejdź do zakładki MCP", - "goToModes": "Przejdź do zakładki Tryby", + "goToModes": "Przejdź do ustawień Trybów", "moreInfoMcp": "Zobacz dokumentację MCP {{name}}", "validationRequired": "Podaj wartość dla {{paramName}}", "prerequisites": "Wymagania wstępne" diff --git a/webview-ui/src/i18n/locales/pl/prompts.json b/webview-ui/src/i18n/locales/pl/prompts.json index 0cd4f665f7..b4a1bdcc50 100644 --- a/webview-ui/src/i18n/locales/pl/prompts.json +++ b/webview-ui/src/i18n/locales/pl/prompts.json @@ -34,6 +34,11 @@ "resetToDefault": "Przywróć domyślne", "description": "Zdefiniuj wiedzę specjalistyczną i osobowość Roo dla tego trybu. Ten opis kształtuje, jak Roo prezentuje się i podchodzi do zadań." }, + "description": { + "title": "Krótki opis (dla ludzi)", + "resetToDefault": "Przywróć domyślny opis", + "description": "Krótki opis wyświetlany w rozwijanej liście wyboru trybu." + }, "whenToUse": { "title": "Kiedy używać (opcjonalne)", "description": "Opisz, kiedy ten tryb powinien być używany. Pomaga to Orchestratorowi wybrać odpowiedni tryb dla zadania.", @@ -145,6 +150,10 @@ "label": "Dostępne narzędzia", "description": "Wybierz, których narzędzi może używać ten tryb." }, + "description": { + "label": "Krótki opis (dla ludzi)", + "description": "Krótki opis wyświetlany w rozwijanej liście wyboru trybu." + }, "customInstructions": { "label": "Niestandardowe instrukcje (opcjonalne)", "description": "Dodaj wytyczne dotyczące zachowania specyficzne dla tego trybu." diff --git a/webview-ui/src/i18n/locales/pt-BR/account.json b/webview-ui/src/i18n/locales/pt-BR/account.json index e02ec55178..5492ca7520 100644 --- a/webview-ui/src/i18n/locales/pt-BR/account.json +++ b/webview-ui/src/i18n/locales/pt-BR/account.json @@ -1,8 +1,14 @@ { "title": "Conta", "profilePicture": "Foto de perfil", - "unknownUser": "Usuário desconhecido", "logOut": "Sair", "testApiAuthentication": "Testar Autenticação de API", - "signIn": "Conectar ao Roo Code Cloud" + "signIn": "Conectar ao Roo Code Cloud", + "connect": "Conectar", + "cloudBenefitsTitle": "Conectar ao Roo Code Cloud", + "cloudBenefitsSubtitle": "Sincronize seus prompts e telemetria para habilitar:", + "cloudBenefitHistory": "Histórico de tarefas online", + "cloudBenefitSharing": "Recursos de compartilhamento e colaboração", + "cloudBenefitMetrics": "Métricas de uso baseadas em tarefas, tokens e custos", + "visitCloudWebsite": "Visitar Roo Code Cloud" } diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index ada704ac04..a09f8174f6 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -16,7 +16,12 @@ "shareWithOrganization": "Compartilhar com organização", "shareWithOrganizationDescription": "Apenas membros da sua organização podem acessar", "sharePublicly": "Compartilhar publicamente", - "sharePubliclyDescription": "Qualquer pessoa com o link pode acessar" + "sharePubliclyDescription": "Qualquer pessoa com o link pode acessar", + "connectToCloud": "Conectar ao Cloud", + "connectToCloudDescription": "Entre no Roo Code Cloud para compartilhar tarefas", + "sharingDisabledByOrganization": "Compartilhamento desabilitado pela organização", + "shareSuccessOrganization": "Link da organização copiado para a área de transferência", + "shareSuccessPublic": "Link público copiado para a área de transferência" }, "unpin": "Desfixar", "pin": "Fixar", @@ -106,6 +111,12 @@ "dragFiles": "segure shift para arrastar arquivos", "dragFilesImages": "segure shift para arrastar arquivos/imagens", "enhancePromptDescription": "O botão 'Aprimorar prompt' ajuda a melhorar seu pedido fornecendo contexto adicional, esclarecimentos ou reformulações. Tente digitar um pedido aqui e clique no botão novamente para ver como funciona.", + "modeSelector": { + "title": "Modos", + "marketplace": "Marketplace de Modos", + "settings": "Configurações de Modos", + "description": "Personas especializadas que adaptam o comportamento do Roo." + }, "errorReadingFile": "Erro ao ler arquivo:", "noValidImages": "Nenhuma imagem válida foi processada", "separator": "Separador", diff --git a/webview-ui/src/i18n/locales/pt-BR/marketplace.json b/webview-ui/src/i18n/locales/pt-BR/marketplace.json index 088adc850a..b634291eeb 100644 --- a/webview-ui/src/i18n/locales/pt-BR/marketplace.json +++ b/webview-ui/src/i18n/locales/pt-BR/marketplace.json @@ -91,7 +91,7 @@ "whatNextMode": "Agora você pode usar este modo. Clique no ícone Modos na barra lateral para trocar de aba.", "done": "Concluído", "goToMcp": "Ir para aba MCP", - "goToModes": "Ir para aba Modos", + "goToModes": "Ir para configurações de Modos", "moreInfoMcp": "Ver documentação MCP do {{name}}", "validationRequired": "Por favor, forneça um valor para {{paramName}}", "prerequisites": "Pré-requisitos" diff --git a/webview-ui/src/i18n/locales/pt-BR/prompts.json b/webview-ui/src/i18n/locales/pt-BR/prompts.json index e6abbb6bf6..c2a88d4eaa 100644 --- a/webview-ui/src/i18n/locales/pt-BR/prompts.json +++ b/webview-ui/src/i18n/locales/pt-BR/prompts.json @@ -34,6 +34,11 @@ "resetToDefault": "Restaurar para padrão", "description": "Defina a expertise e personalidade do Roo para este modo. Esta descrição molda como o Roo se apresenta e aborda tarefas." }, + "description": { + "title": "Descrição curta (para humanos)", + "resetToDefault": "Redefinir para descrição padrão", + "description": "Uma breve descrição exibida no menu suspenso do seletor de modo." + }, "whenToUse": { "title": "Quando usar (opcional)", "description": "Descreva quando este modo deve ser usado. Isso ajuda o Orchestrator a escolher o modo certo para uma tarefa.", @@ -145,6 +150,10 @@ "label": "Ferramentas disponíveis", "description": "Selecione quais ferramentas este modo pode usar." }, + "description": { + "label": "Descrição curta (para humanos)", + "description": "Uma breve descrição exibida no menu suspenso do seletor de modo." + }, "customInstructions": { "label": "Instruções personalizadas (opcional)", "description": "Adicione diretrizes comportamentais específicas para este modo." diff --git a/webview-ui/src/i18n/locales/ru/account.json b/webview-ui/src/i18n/locales/ru/account.json index c5e08619aa..1c8dcf5289 100644 --- a/webview-ui/src/i18n/locales/ru/account.json +++ b/webview-ui/src/i18n/locales/ru/account.json @@ -1,8 +1,14 @@ { "title": "Учетная запись", "profilePicture": "Фото профиля", - "unknownUser": "Неизвестный пользователь", "logOut": "Выход", "testApiAuthentication": "Проверить аутентификацию API", - "signIn": "Подключиться к Roo Code Cloud" + "signIn": "Подключиться к Roo Code Cloud", + "connect": "Подключиться", + "cloudBenefitsTitle": "Подключиться к Roo Code Cloud", + "cloudBenefitsSubtitle": "Синхронизируй свои промпты и телеметрию, чтобы включить:", + "cloudBenefitHistory": "Онлайн-история задач", + "cloudBenefitSharing": "Функции обмена и совместной работы", + "cloudBenefitMetrics": "Метрики использования на основе задач, токенов и затрат", + "visitCloudWebsite": "Посетить Roo Code Cloud" } diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index c30e0103c9..89d35f322a 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -16,7 +16,12 @@ "shareWithOrganization": "Поделиться с организацией", "shareWithOrganizationDescription": "Только члены вашей организации могут получить доступ", "sharePublicly": "Поделиться публично", - "sharePubliclyDescription": "Любой, у кого есть ссылка, может получить доступ" + "sharePubliclyDescription": "Любой, у кого есть ссылка, может получить доступ", + "connectToCloud": "Подключиться к облаку", + "connectToCloudDescription": "Войди в Roo Code Cloud, чтобы делиться задачами", + "sharingDisabledByOrganization": "Обмен отключен организацией", + "shareSuccessOrganization": "Ссылка организации скопирована в буфер обмена", + "shareSuccessPublic": "Публичная ссылка скопирована в буфер обмена" }, "unpin": "Открепить", "pin": "Закрепить", @@ -99,6 +104,12 @@ "selectApiConfig": "Выберите конфигурацию API", "enhancePrompt": "Улучшить запрос с дополнительным контекстом", "enhancePromptDescription": "Кнопка 'Улучшить запрос' помогает сделать ваш запрос лучше, предоставляя дополнительный контекст, уточнения или переформулировку. Попробуйте ввести запрос и снова нажать кнопку, чтобы увидеть, как это работает.", + "modeSelector": { + "title": "Режимы", + "marketplace": "Маркетплейс режимов", + "settings": "Настройки режимов", + "description": "Специализированные персоны, которые настраивают поведение Roo." + }, "addImages": "Добавить изображения к сообщению", "sendMessage": "Отправить сообщение", "typeMessage": "Введите сообщение...", diff --git a/webview-ui/src/i18n/locales/ru/marketplace.json b/webview-ui/src/i18n/locales/ru/marketplace.json index 4f87737722..7a33014cf5 100644 --- a/webview-ui/src/i18n/locales/ru/marketplace.json +++ b/webview-ui/src/i18n/locales/ru/marketplace.json @@ -91,7 +91,7 @@ "whatNextMode": "Теперь вы можете использовать этот режим. Нажмите на иконку Режимы в боковой панели для переключения вкладок.", "done": "Готово", "goToMcp": "Перейти во вкладку MCP", - "goToModes": "Перейти во вкладку Режимы", + "goToModes": "Перейти в настройки Режимов", "moreInfoMcp": "Просмотреть документацию MCP {{name}}", "validationRequired": "Пожалуйста, укажите значение для {{paramName}}", "prerequisites": "Предварительные требования" diff --git a/webview-ui/src/i18n/locales/ru/prompts.json b/webview-ui/src/i18n/locales/ru/prompts.json index 2254f03979..07e9f91db8 100644 --- a/webview-ui/src/i18n/locales/ru/prompts.json +++ b/webview-ui/src/i18n/locales/ru/prompts.json @@ -34,6 +34,11 @@ "resetToDefault": "Сбросить по умолчанию", "description": "Определите экспертность и личность Roo для этого режима. Это описание формирует, как Roo будет себя вести и выполнять задачи." }, + "description": { + "title": "Краткое описание (для людей)", + "resetToDefault": "Сбросить до описания по умолчанию", + "description": "Краткое описание, отображаемое в выпадающем списке выбора режима." + }, "whenToUse": { "title": "Когда использовать (необязательно)", "description": "Опишите, когда следует использовать этот режим. Это помогает Orchestrator выбрать правильный режим для задачи.", @@ -145,6 +150,10 @@ "label": "Доступные инструменты", "description": "Выберите, какие инструменты может использовать этот режим." }, + "description": { + "label": "Краткое описание (для людей)", + "description": "Краткое описание, отображаемое в выпадающем списке выбора режима." + }, "customInstructions": { "label": "Пользовательские инструкции (необязательно)", "description": "Добавьте рекомендации по поведению, специфичные для этого режима." diff --git a/webview-ui/src/i18n/locales/tr/account.json b/webview-ui/src/i18n/locales/tr/account.json index 6e7300a0cf..a344ce940f 100644 --- a/webview-ui/src/i18n/locales/tr/account.json +++ b/webview-ui/src/i18n/locales/tr/account.json @@ -1,8 +1,14 @@ { "title": "Hesap", "profilePicture": "Profil resmi", - "unknownUser": "Bilinmeyen kullanıcı", "logOut": "Çıkış yap", "testApiAuthentication": "API Kimlik Doğrulamayı Test Et", - "signIn": "Roo Code Cloud'a bağlan" + "signIn": "Roo Code Cloud'a bağlan", + "connect": "Bağlan", + "cloudBenefitsTitle": "Roo Code Cloud'a bağlan", + "cloudBenefitsSubtitle": "Aşağıdakileri etkinleştirmek için promptlarını ve telemetriyi senkronize et:", + "cloudBenefitHistory": "Çevrimiçi görev geçmişi", + "cloudBenefitSharing": "Paylaşım ve işbirliği özellikleri", + "cloudBenefitMetrics": "Görev, token ve maliyet tabanlı kullanım metrikleri", + "visitCloudWebsite": "Roo Code Cloud'u ziyaret et" } diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index 0082501eaf..f12fa62bbb 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -16,7 +16,12 @@ "shareWithOrganization": "Kuruluşla paylaş", "shareWithOrganizationDescription": "Sadece kuruluşunuzun üyeleri erişebilir", "sharePublicly": "Herkese açık paylaş", - "sharePubliclyDescription": "Bağlantıya sahip herkes erişebilir" + "sharePubliclyDescription": "Bağlantıya sahip herkes erişebilir", + "connectToCloud": "Buluta bağlan", + "connectToCloudDescription": "Görevleri paylaşmak için Roo Code Cloud'a giriş yap", + "sharingDisabledByOrganization": "Paylaşım kuruluş tarafından devre dışı bırakıldı", + "shareSuccessOrganization": "Organizasyon bağlantısı panoya kopyalandı", + "shareSuccessPublic": "Genel bağlantı panoya kopyalandı" }, "unpin": "Sabitlemeyi iptal et", "pin": "Sabitle", @@ -106,6 +111,12 @@ "dragFiles": "dosyaları sürüklemek için shift tuşuna basılı tutun", "dragFilesImages": "dosyaları/resimleri sürüklemek için shift tuşuna basılı tutun", "enhancePromptDescription": "'İstemi geliştir' düğmesi, ek bağlam, açıklama veya yeniden ifade sağlayarak isteğinizi iyileştirmeye yardımcı olur. Buraya bir istek yazıp düğmeye tekrar tıklayarak nasıl çalıştığını görebilirsiniz.", + "modeSelector": { + "title": "Modlar", + "marketplace": "Mod Pazaryeri", + "settings": "Mod Ayarları", + "description": "Roo'nun davranışını özelleştiren uzmanlaşmış kişilikler." + }, "errorReadingFile": "Dosya okuma hatası:", "noValidImages": "Hiçbir geçerli resim işlenmedi", "separator": "Ayırıcı", diff --git a/webview-ui/src/i18n/locales/tr/marketplace.json b/webview-ui/src/i18n/locales/tr/marketplace.json index a034f7876f..6c3f857a5f 100644 --- a/webview-ui/src/i18n/locales/tr/marketplace.json +++ b/webview-ui/src/i18n/locales/tr/marketplace.json @@ -91,7 +91,7 @@ "whatNextMode": "Artık bu modu kullanabilirsiniz. Sekmeleri değiştirmek için kenar çubuğundaki Modlar simgesine tıklayın.", "done": "Tamamlandı", "goToMcp": "MCP Sekmesine Git", - "goToModes": "Modlar Sekmesine Git", + "goToModes": "Modlar Ayarlarına Git", "moreInfoMcp": "{{name}} MCP belgelerini görüntüle", "validationRequired": "Lütfen {{paramName}} için bir değer sağlayın", "prerequisites": "Ön koşullar" diff --git a/webview-ui/src/i18n/locales/tr/prompts.json b/webview-ui/src/i18n/locales/tr/prompts.json index e90aa25f65..d091456e43 100644 --- a/webview-ui/src/i18n/locales/tr/prompts.json +++ b/webview-ui/src/i18n/locales/tr/prompts.json @@ -34,6 +34,11 @@ "resetToDefault": "Varsayılana sıfırla", "description": "Bu mod için Roo'nun uzmanlığını ve kişiliğini tanımlayın. Bu açıklama, Roo'nun kendini nasıl sunduğunu ve görevlere nasıl yaklaştığını şekillendirir." }, + "description": { + "title": "Kısa açıklama (insanlar için)", + "resetToDefault": "Varsayılan açıklamaya sıfırla", + "description": "Mod seçici açılır menüsünde gösterilen kısa bir açıklama." + }, "whenToUse": { "title": "Ne zaman kullanılmalı (isteğe bağlı)", "description": "Bu modun ne zaman kullanılması gerektiğini açıklayın. Bu, Orchestrator'ın bir görev için doğru modu seçmesine yardımcı olur.", @@ -145,6 +150,10 @@ "label": "Kullanılabilir Araçlar", "description": "Bu modun hangi araçları kullanabileceğini seçin." }, + "description": { + "label": "Kısa açıklama (insanlar için)", + "description": "Mod seçici açılır menüsünde gösterilen kısa bir açıklama." + }, "customInstructions": { "label": "Özel Talimatlar (isteğe bağlı)", "description": "Bu mod için özel davranış yönergeleri ekleyin." diff --git a/webview-ui/src/i18n/locales/vi/account.json b/webview-ui/src/i18n/locales/vi/account.json index 4a1d697998..0e826b75ad 100644 --- a/webview-ui/src/i18n/locales/vi/account.json +++ b/webview-ui/src/i18n/locales/vi/account.json @@ -1,8 +1,14 @@ { "title": "Tài khoản", "profilePicture": "Ảnh hồ sơ", - "unknownUser": "Người dùng không xác định", "logOut": "Đăng xuất", "testApiAuthentication": "Kiểm tra xác thực API", - "signIn": "Kết nối với Roo Code Cloud" + "signIn": "Kết nối với Roo Code Cloud", + "connect": "Kết nối", + "cloudBenefitsTitle": "Kết nối với Roo Code Cloud", + "cloudBenefitsSubtitle": "Đồng bộ prompts và telemetry của bạn để kích hoạt:", + "cloudBenefitHistory": "Lịch sử tác vụ trực tuyến", + "cloudBenefitSharing": "Tính năng chia sẻ và cộng tác", + "cloudBenefitMetrics": "Số liệu sử dụng dựa trên tác vụ, token và chi phí", + "visitCloudWebsite": "Truy cập Roo Code Cloud" } diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index 3e0fa241a2..c2338e33aa 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -16,7 +16,12 @@ "shareWithOrganization": "Chia sẻ với tổ chức", "shareWithOrganizationDescription": "Chỉ thành viên tổ chức của bạn mới có thể truy cập", "sharePublicly": "Chia sẻ công khai", - "sharePubliclyDescription": "Bất kỳ ai có liên kết đều có thể truy cập" + "sharePubliclyDescription": "Bất kỳ ai có liên kết đều có thể truy cập", + "connectToCloud": "Kết nối với Cloud", + "connectToCloudDescription": "Đăng nhập vào Roo Code Cloud để chia sẻ tác vụ", + "sharingDisabledByOrganization": "Chia sẻ bị tổ chức vô hiệu hóa", + "shareSuccessOrganization": "Liên kết tổ chức đã được sao chép vào clipboard", + "shareSuccessPublic": "Liên kết công khai đã được sao chép vào clipboard" }, "unpin": "Bỏ ghim khỏi đầu", "pin": "Ghim lên đầu", @@ -106,6 +111,12 @@ "dragFiles": "giữ shift để kéo tệp", "dragFilesImages": "giữ shift để kéo tệp/hình ảnh", "enhancePromptDescription": "Nút 'Nâng cao yêu cầu' giúp cải thiện yêu cầu của bạn bằng cách cung cấp ngữ cảnh bổ sung, làm rõ hoặc diễn đạt lại. Hãy thử nhập yêu cầu tại đây và nhấp vào nút một lần nữa để xem cách thức hoạt động.", + "modeSelector": { + "title": "Chế độ", + "marketplace": "Chợ Chế độ", + "settings": "Cài đặt Chế độ", + "description": "Các nhân cách chuyên biệt điều chỉnh hành vi của Roo." + }, "errorReadingFile": "Lỗi khi đọc tệp:", "noValidImages": "Không có hình ảnh hợp lệ nào được xử lý", "separator": "Dấu phân cách", diff --git a/webview-ui/src/i18n/locales/vi/marketplace.json b/webview-ui/src/i18n/locales/vi/marketplace.json index 6539177161..d84f2d0e1f 100644 --- a/webview-ui/src/i18n/locales/vi/marketplace.json +++ b/webview-ui/src/i18n/locales/vi/marketplace.json @@ -91,7 +91,7 @@ "whatNextMode": "Bây giờ bạn có thể sử dụng chế độ này. Nhấp vào biểu tượng Chế độ trong thanh bên để chuyển tab.", "done": "Hoàn thành", "goToMcp": "Đi đến Tab MCP", - "goToModes": "Đi đến Tab Chế độ", + "goToModes": "Đi đến Cài đặt Chế độ", "moreInfoMcp": "Xem tài liệu MCP {{name}}", "validationRequired": "Vui lòng cung cấp giá trị cho {{paramName}}", "prerequisites": "Điều kiện tiên quyết" diff --git a/webview-ui/src/i18n/locales/vi/prompts.json b/webview-ui/src/i18n/locales/vi/prompts.json index e2573711fa..7a0b311a02 100644 --- a/webview-ui/src/i18n/locales/vi/prompts.json +++ b/webview-ui/src/i18n/locales/vi/prompts.json @@ -34,6 +34,11 @@ "resetToDefault": "Đặt lại về mặc định", "description": "Xác định chuyên môn và tính cách của Roo cho chế độ này. Mô tả này định hình cách Roo giới thiệu bản thân và tiếp cận nhiệm vụ." }, + "description": { + "title": "Mô tả ngắn (cho con người)", + "resetToDefault": "Đặt lại về mô tả mặc định", + "description": "Mô tả ngắn gọn hiển thị trong menu thả xuống bộ chọn chế độ." + }, "whenToUse": { "title": "Khi nào nên sử dụng (tùy chọn)", "description": "Mô tả khi nào nên sử dụng chế độ này. Điều này giúp Orchestrator chọn chế độ phù hợp cho một nhiệm vụ.", @@ -145,6 +150,10 @@ "label": "Công cụ có sẵn", "description": "Chọn công cụ nào chế độ này có thể sử dụng." }, + "description": { + "label": "Mô tả ngắn (cho con người)", + "description": "Mô tả ngắn gọn hiển thị trong menu thả xuống bộ chọn chế độ." + }, "customInstructions": { "label": "Hướng dẫn tùy chỉnh (tùy chọn)", "description": "Thêm hướng dẫn hành vi dành riêng cho chế độ này." diff --git a/webview-ui/src/i18n/locales/zh-CN/account.json b/webview-ui/src/i18n/locales/zh-CN/account.json index fee797d962..65a4c1d221 100644 --- a/webview-ui/src/i18n/locales/zh-CN/account.json +++ b/webview-ui/src/i18n/locales/zh-CN/account.json @@ -1,8 +1,14 @@ { "title": "账户", "profilePicture": "头像", - "unknownUser": "未知用户", "logOut": "退出登录", "testApiAuthentication": "测试 API 认证", - "signIn": "连接到 Roo Code Cloud" + "signIn": "连接到 Roo Code Cloud", + "connect": "连接", + "cloudBenefitsTitle": "连接到 Roo Code Cloud", + "cloudBenefitsSubtitle": "同步你的提示词和遥测数据以启用:", + "cloudBenefitHistory": "在线任务历史", + "cloudBenefitSharing": "共享和协作功能", + "cloudBenefitMetrics": "基于任务、Token 和成本的使用指标", + "visitCloudWebsite": "访问 Roo Code Cloud" } diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index a65a7ed4da..f3fb857f06 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -16,7 +16,12 @@ "shareWithOrganization": "与组织分享", "shareWithOrganizationDescription": "仅组织成员可访问", "sharePublicly": "公开分享", - "sharePubliclyDescription": "任何拥有链接的人都可访问" + "sharePubliclyDescription": "任何拥有链接的人都可访问", + "connectToCloud": "连接到云端", + "connectToCloudDescription": "登录 Roo Code Cloud 以分享任务", + "sharingDisabledByOrganization": "组织已禁用分享功能", + "shareSuccessOrganization": "组织链接已复制到剪贴板", + "shareSuccessPublic": "公开链接已复制到剪贴板" }, "unpin": "取消置顶", "pin": "置顶", @@ -106,6 +111,12 @@ "dragFiles": "Shift+拖拽文件", "dragFilesImages": "Shift+拖拽文件/图片", "enhancePromptDescription": "'增强提示'按钮通过提供额外上下文、澄清或重新表述来帮助改进您的请求。尝试在此处输入请求,然后再次点击按钮查看其工作原理。", + "modeSelector": { + "title": "模式", + "marketplace": "模式市场", + "settings": "模式设置", + "description": "专门定制Roo行为的角色。" + }, "errorReadingFile": "读取文件时出错:", "noValidImages": "没有处理有效图片", "separator": "分隔符", diff --git a/webview-ui/src/i18n/locales/zh-CN/marketplace.json b/webview-ui/src/i18n/locales/zh-CN/marketplace.json index ccf1873ca6..ff94aaabcc 100644 --- a/webview-ui/src/i18n/locales/zh-CN/marketplace.json +++ b/webview-ui/src/i18n/locales/zh-CN/marketplace.json @@ -91,7 +91,7 @@ "whatNextMode": "现在您可以使用此模式。点击侧边栏中的模式图标切换标签页。", "done": "完成", "goToMcp": "转到 MCP 标签页", - "goToModes": "转到模式标签页", + "goToModes": "转到模式设置", "moreInfoMcp": "查看 {{name}} MCP 文档", "validationRequired": "请为 {{paramName}} 提供值", "prerequisites": "前置条件" diff --git a/webview-ui/src/i18n/locales/zh-CN/prompts.json b/webview-ui/src/i18n/locales/zh-CN/prompts.json index 2ebc57855d..2abf922b14 100644 --- a/webview-ui/src/i18n/locales/zh-CN/prompts.json +++ b/webview-ui/src/i18n/locales/zh-CN/prompts.json @@ -34,6 +34,11 @@ "resetToDefault": "重置为默认值", "description": "设定专业领域和应答风格" }, + "description": { + "title": "简短描述(给人看的)", + "resetToDefault": "重置为默认描述", + "description": "在模式选择下拉菜单中显示的简短描述。" + }, "whenToUse": { "title": "使用场景(可选)", "description": "描述何时应该使用此模式。这有助于 Orchestrator 为任务选择合适的模式。", @@ -145,6 +150,10 @@ "label": "可用工具", "description": "选择可用工具" }, + "description": { + "label": "简短描述(给人看的)", + "description": "在模式选择下拉菜单中显示的简短描述。" + }, "customInstructions": { "label": "自定义指令(可选)", "description": "设置专属规则" diff --git a/webview-ui/src/i18n/locales/zh-TW/account.json b/webview-ui/src/i18n/locales/zh-TW/account.json index d1b15f5752..dca8d3231c 100644 --- a/webview-ui/src/i18n/locales/zh-TW/account.json +++ b/webview-ui/src/i18n/locales/zh-TW/account.json @@ -1,8 +1,14 @@ { "title": "帳戶", "profilePicture": "個人圖片", - "unknownUser": "未知使用者", "logOut": "登出", "testApiAuthentication": "測試 API 認證", - "signIn": "連接到 Roo Code Cloud" + "signIn": "連接到 Roo Code Cloud", + "connect": "連接", + "cloudBenefitsTitle": "連接到 Roo Code Cloud", + "cloudBenefitsSubtitle": "同步你的提示詞和遙測資料以啟用:", + "cloudBenefitHistory": "線上工作歷史", + "cloudBenefitSharing": "分享和協作功能", + "cloudBenefitMetrics": "基於工作、Token 和成本的使用指標", + "visitCloudWebsite": "造訪 Roo Code Cloud" } diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 44742d4460..9a20f129a3 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -16,7 +16,12 @@ "shareWithOrganization": "與組織分享", "shareWithOrganizationDescription": "僅組織成員可存取", "sharePublicly": "公開分享", - "sharePubliclyDescription": "任何擁有連結的人都可存取" + "sharePubliclyDescription": "任何擁有連結的人都可存取", + "connectToCloud": "連接到雲端", + "connectToCloudDescription": "登入 Roo Code Cloud 以分享工作", + "sharingDisabledByOrganization": "組織已停用分享功能", + "shareSuccessOrganization": "組織連結已複製到剪貼簿", + "shareSuccessPublic": "公開連結已複製到剪貼簿" }, "unpin": "取消置頂", "pin": "置頂", @@ -106,6 +111,12 @@ "dragFiles": "按住 Shift 鍵拖曳檔案", "dragFilesImages": "按住 Shift 鍵拖曳檔案/圖片", "enhancePromptDescription": "「增強提示」按鈕透過提供額外內容、說明或重新表述來幫助改進您的請求。嘗試在此處輸入請求,然後再次點選按鈕以了解其運作方式。", + "modeSelector": { + "title": "模式", + "marketplace": "模式市集", + "settings": "模式設定", + "description": "專門定制Roo行為的角色。" + }, "errorReadingFile": "讀取檔案時發生錯誤:", "noValidImages": "未處理到任何有效圖片", "separator": "分隔符號", diff --git a/webview-ui/src/i18n/locales/zh-TW/marketplace.json b/webview-ui/src/i18n/locales/zh-TW/marketplace.json index 201d3b2bb0..3ca00585ca 100644 --- a/webview-ui/src/i18n/locales/zh-TW/marketplace.json +++ b/webview-ui/src/i18n/locales/zh-TW/marketplace.json @@ -91,7 +91,7 @@ "whatNextMode": "現在您可以使用此模式。點擊側邊欄中的模式圖示以切換標籤頁。", "done": "完成", "goToMcp": "前往 MCP 標籤頁", - "goToModes": "前往模式標籤頁", + "goToModes": "前往模式設定", "moreInfoMcp": "檢視 {{name}} MCP 文件", "validationRequired": "請為 {{paramName}} 提供值", "prerequisites": "前置條件" diff --git a/webview-ui/src/i18n/locales/zh-TW/prompts.json b/webview-ui/src/i18n/locales/zh-TW/prompts.json index 2620af2caf..e853a5d91d 100644 --- a/webview-ui/src/i18n/locales/zh-TW/prompts.json +++ b/webview-ui/src/i18n/locales/zh-TW/prompts.json @@ -34,6 +34,11 @@ "resetToDefault": "重設為預設值", "description": "定義此模式下 Roo 的專業知識和個性。此描述會形塑 Roo 如何展現自己並處理工作。" }, + "description": { + "title": "簡短描述(給人看的)", + "resetToDefault": "重置為預設描述", + "description": "在模式選擇下拉選單中顯示的簡短描述。" + }, "whenToUse": { "title": "使用時機(選用)", "description": "描述何時應使用此模式。這有助於 Orchestrator 為任務選擇適當的模式。", @@ -145,6 +150,10 @@ "label": "可用工具", "description": "選擇此模式可使用的工具。" }, + "description": { + "label": "簡短描述(給人看的)", + "description": "在模式選擇下拉選單中顯示的簡短描述。" + }, "customInstructions": { "label": "自訂指令(選用)", "description": "為此模式新增特定的行為指南。" diff --git a/webview-ui/src/utils/context-mentions.ts b/webview-ui/src/utils/context-mentions.ts index 5df3404b23..3299bc4e08 100644 --- a/webview-ui/src/utils/context-mentions.ts +++ b/webview-ui/src/utils/context-mentions.ts @@ -143,7 +143,7 @@ export function getContextMenuOptions( type: ContextMenuOptionType.Mode, value: mode.slug, label: mode.name, - description: (mode.whenToUse || mode.roleDefinition).split("\n")[0], + description: (mode.description || mode.whenToUse || mode.roleDefinition).split("\n")[0], })) return matchingModes.length > 0 ? matchingModes : [{ type: ContextMenuOptionType.NoResults }] diff --git a/webview-ui/vitest.setup.ts b/webview-ui/vitest.setup.ts index 3e7f39cd54..afa37bd96d 100644 --- a/webview-ui/vitest.setup.ts +++ b/webview-ui/vitest.setup.ts @@ -42,6 +42,9 @@ Object.defineProperty(window, "matchMedia", { })), }) +// Mock scrollIntoView which is not available in jsdom +Element.prototype.scrollIntoView = vi.fn() + // Suppress console.log during tests to reduce noise. // Keep console.error for actual errors. const originalConsoleLog = console.log