mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-03 08:46:15 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0362ac9b3d | |||
| 835ba06335 |
@@ -1,42 +0,0 @@
|
||||
---
|
||||
description: Extract non-obvious learnings from session to AGENTS.md files to build codebase understanding
|
||||
---
|
||||
|
||||
Analyze this session and extract non-obvious learnings to add to AGENTS.md files.
|
||||
|
||||
AGENTS.md files can exist at any directory level, not just the project root. When an agent reads a file, any AGENTS.md in parent directories are automatically loaded into the context of the tool read. Place learnings as close to the relevant code as possible:
|
||||
|
||||
- Project-wide learnings → root AGENTS.md
|
||||
- Package/module-specific → packages/foo/AGENTS.md
|
||||
- Feature-specific → src/auth/AGENTS.md
|
||||
|
||||
What counts as a learning (non-obvious discoveries only):
|
||||
|
||||
- Hidden relationships between files or modules
|
||||
- Execution paths that differ from how code appears
|
||||
- Non-obvious configuration, env vars, or flags
|
||||
- Debugging breakthroughs when error messages were misleading
|
||||
- API/tool quirks and workarounds
|
||||
- Build/test commands not in README
|
||||
- Architectural decisions and constraints
|
||||
- Files that must change together
|
||||
|
||||
What NOT to include:
|
||||
|
||||
- Obvious facts from documentation
|
||||
- Standard language/framework behavior
|
||||
- Things already in an AGENTS.md
|
||||
- Verbose explanations
|
||||
- Session-specific details
|
||||
|
||||
Process:
|
||||
|
||||
1. Review session for discoveries, errors that took multiple attempts, unexpected connections
|
||||
2. Determine scope - what directory does each learning apply to?
|
||||
3. Read existing AGENTS.md files at relevant levels
|
||||
4. Create or update AGENTS.md at the appropriate level
|
||||
5. Keep entries to 1-3 lines per insight
|
||||
|
||||
After updating, summarize which AGENTS.md files were created/updated and how many learnings per file.
|
||||
|
||||
$ARGUMENTS
|
||||
@@ -10,7 +10,6 @@
|
||||
- Avoid using the `any` type
|
||||
- Prefer single word variable names where possible
|
||||
- Use Bun APIs when possible, like `Bun.file()`
|
||||
- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity
|
||||
|
||||
### Avoid let statements
|
||||
|
||||
|
||||
+13
-13
@@ -15,20 +15,20 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> |
|
||||
<a href="README.zh.md">简体中文</a> |
|
||||
<a href="README.zht.md">繁體中文</a> |
|
||||
<a href="README.ko.md">한국어</a> |
|
||||
<a href="README.de.md">Deutsch</a> |
|
||||
<a href="README.es.md">Español</a> |
|
||||
<a href="README.fr.md">Français</a> |
|
||||
<a href="README.da.md">Dansk</a> |
|
||||
<a href="README.ja.md">日本語</a> |
|
||||
<a href="README.pl.md">Polski</a> |
|
||||
<a href="README.ru.md">Русский</a> |
|
||||
<a href="README.md">الإنجليزية</a> |
|
||||
<a href="README.zh.md">الصينية (المبسطة)</a> |
|
||||
<a href="README.zht.md">الصينية (التقليدية)</a> |
|
||||
<a href="README.ko.md">الكورية</a> |
|
||||
<a href="README.de.md">الألمانية</a> |
|
||||
<a href="README.es.md">الإسبانية</a> |
|
||||
<a href="README.fr.md">الفرنسية</a> |
|
||||
<a href="README.da.md">الدنماركية</a> |
|
||||
<a href="README.ja.md">اليابانية</a> |
|
||||
<a href="README.pl.md">البولندية</a> |
|
||||
<a href="README.ru.md">الروسية</a> |
|
||||
<a href="README.ar.md">العربية</a> |
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a>
|
||||
<a href="README.no.md">النرويجية</a> |
|
||||
<a href="README.br.md">البرتغالية (البرازيل)</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
|
||||
+13
-13
@@ -15,19 +15,19 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> |
|
||||
<a href="README.zh.md">简体中文</a> |
|
||||
<a href="README.zht.md">繁體中文</a> |
|
||||
<a href="README.ko.md">한국어</a> |
|
||||
<a href="README.de.md">Deutsch</a> |
|
||||
<a href="README.es.md">Español</a> |
|
||||
<a href="README.fr.md">Français</a> |
|
||||
<a href="README.da.md">Dansk</a> |
|
||||
<a href="README.ja.md">日本語</a> |
|
||||
<a href="README.pl.md">Polski</a> |
|
||||
<a href="README.ru.md">Русский</a> |
|
||||
<a href="README.ar.md">العربية</a> |
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.md">Inglês</a> |
|
||||
<a href="README.zh.md">Chinês (Simplificado)</a> |
|
||||
<a href="README.zht.md">Chinês (Tradicional)</a> |
|
||||
<a href="README.ko.md">Coreano</a> |
|
||||
<a href="README.de.md">Alemão</a> |
|
||||
<a href="README.es.md">Espanhol</a> |
|
||||
<a href="README.fr.md">Francês</a> |
|
||||
<a href="README.da.md">Dinamarquês</a> |
|
||||
<a href="README.ja.md">Japonês</a> |
|
||||
<a href="README.pl.md">Polonês</a> |
|
||||
<a href="README.ru.md">Russo</a> |
|
||||
<a href="README.ar.md">Árabe</a> |
|
||||
<a href="README.no.md">Norueguês</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a>
|
||||
</p>
|
||||
|
||||
|
||||
+12
-12
@@ -15,20 +15,20 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> |
|
||||
<a href="README.zh.md">简体中文</a> |
|
||||
<a href="README.zht.md">繁體中文</a> |
|
||||
<a href="README.ko.md">한국어</a> |
|
||||
<a href="README.de.md">Deutsch</a> |
|
||||
<a href="README.es.md">Español</a> |
|
||||
<a href="README.fr.md">Français</a> |
|
||||
<a href="README.md">Engelsk</a> |
|
||||
<a href="README.zh.md">Kinesisk (forenklet)</a> |
|
||||
<a href="README.zht.md">Kinesisk (traditionelt)</a> |
|
||||
<a href="README.ko.md">Koreansk</a> |
|
||||
<a href="README.de.md">Tysk</a> |
|
||||
<a href="README.es.md">Spansk</a> |
|
||||
<a href="README.fr.md">Fransk</a> |
|
||||
<a href="README.da.md">Dansk</a> |
|
||||
<a href="README.ja.md">日本語</a> |
|
||||
<a href="README.pl.md">Polski</a> |
|
||||
<a href="README.ru.md">Русский</a> |
|
||||
<a href="README.ar.md">العربية</a> |
|
||||
<a href="README.ja.md">Japansk</a> |
|
||||
<a href="README.pl.md">Polsk</a> |
|
||||
<a href="README.ru.md">Russisk</a> |
|
||||
<a href="README.ar.md">Arabisk</a> |
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a>
|
||||
<a href="README.br.md">Portugisisk (Brasilien)</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
|
||||
+13
-13
@@ -15,20 +15,20 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> |
|
||||
<a href="README.zh.md">简体中文</a> |
|
||||
<a href="README.zht.md">繁體中文</a> |
|
||||
<a href="README.ko.md">한국어</a> |
|
||||
<a href="README.md">Englisch</a> |
|
||||
<a href="README.zh.md">Chinesisch (vereinfacht)</a> |
|
||||
<a href="README.zht.md">Chinesisch (traditionell)</a> |
|
||||
<a href="README.ko.md">Koreanisch</a> |
|
||||
<a href="README.de.md">Deutsch</a> |
|
||||
<a href="README.es.md">Español</a> |
|
||||
<a href="README.fr.md">Français</a> |
|
||||
<a href="README.da.md">Dansk</a> |
|
||||
<a href="README.ja.md">日本語</a> |
|
||||
<a href="README.pl.md">Polski</a> |
|
||||
<a href="README.ru.md">Русский</a> |
|
||||
<a href="README.ar.md">العربية</a> |
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a>
|
||||
<a href="README.es.md">Spanisch</a> |
|
||||
<a href="README.fr.md">Französisch</a> |
|
||||
<a href="README.da.md">Dänisch</a> |
|
||||
<a href="README.ja.md">Japanisch</a> |
|
||||
<a href="README.pl.md">Polnisch</a> |
|
||||
<a href="README.ru.md">Russisch</a> |
|
||||
<a href="README.ar.md">Arabisch</a> |
|
||||
<a href="README.no.md">Norwegisch</a> |
|
||||
<a href="README.br.md">Portugiesisch (Brasilien)</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
|
||||
+13
-13
@@ -15,20 +15,20 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> |
|
||||
<a href="README.zh.md">简体中文</a> |
|
||||
<a href="README.zht.md">繁體中文</a> |
|
||||
<a href="README.ko.md">한국어</a> |
|
||||
<a href="README.de.md">Deutsch</a> |
|
||||
<a href="README.md">Inglés</a> |
|
||||
<a href="README.zh.md">Chino (simplificado)</a> |
|
||||
<a href="README.zht.md">Chino (tradicional)</a> |
|
||||
<a href="README.ko.md">Coreano</a> |
|
||||
<a href="README.de.md">Alemán</a> |
|
||||
<a href="README.es.md">Español</a> |
|
||||
<a href="README.fr.md">Français</a> |
|
||||
<a href="README.da.md">Dansk</a> |
|
||||
<a href="README.ja.md">日本語</a> |
|
||||
<a href="README.pl.md">Polski</a> |
|
||||
<a href="README.ru.md">Русский</a> |
|
||||
<a href="README.ar.md">العربية</a> |
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a>
|
||||
<a href="README.fr.md">Francés</a> |
|
||||
<a href="README.da.md">Danés</a> |
|
||||
<a href="README.ja.md">Japonés</a> |
|
||||
<a href="README.pl.md">Polaco</a> |
|
||||
<a href="README.ru.md">Ruso</a> |
|
||||
<a href="README.ar.md">Árabe</a> |
|
||||
<a href="README.no.md">Noruego</a> |
|
||||
<a href="README.br.md">Portugués (Brasil)</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
|
||||
+13
-13
@@ -15,20 +15,20 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> |
|
||||
<a href="README.zh.md">简体中文</a> |
|
||||
<a href="README.zht.md">繁體中文</a> |
|
||||
<a href="README.ko.md">한국어</a> |
|
||||
<a href="README.de.md">Deutsch</a> |
|
||||
<a href="README.es.md">Español</a> |
|
||||
<a href="README.md">Anglais</a> |
|
||||
<a href="README.zh.md">Chinois (simplifié)</a> |
|
||||
<a href="README.zht.md">Chinois (traditionnel)</a> |
|
||||
<a href="README.ko.md">Coréen</a> |
|
||||
<a href="README.de.md">Allemand</a> |
|
||||
<a href="README.es.md">Espagnol</a> |
|
||||
<a href="README.fr.md">Français</a> |
|
||||
<a href="README.da.md">Dansk</a> |
|
||||
<a href="README.ja.md">日本語</a> |
|
||||
<a href="README.pl.md">Polski</a> |
|
||||
<a href="README.ru.md">Русский</a> |
|
||||
<a href="README.ar.md">العربية</a> |
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a>
|
||||
<a href="README.da.md">Danois</a> |
|
||||
<a href="README.ja.md">Japonais</a> |
|
||||
<a href="README.pl.md">Polonais</a> |
|
||||
<a href="README.ru.md">Russe</a> |
|
||||
<a href="README.ar.md">Arabe</a> |
|
||||
<a href="README.no.md">Norvégien</a> |
|
||||
<a href="README.br.md">Portugais (Brésil)</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
|
||||
+13
-13
@@ -15,20 +15,20 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> |
|
||||
<a href="README.zh.md">简体中文</a> |
|
||||
<a href="README.zht.md">繁體中文</a> |
|
||||
<a href="README.ko.md">한국어</a> |
|
||||
<a href="README.de.md">Deutsch</a> |
|
||||
<a href="README.es.md">Español</a> |
|
||||
<a href="README.fr.md">Français</a> |
|
||||
<a href="README.da.md">Dansk</a> |
|
||||
<a href="README.md">英語</a> |
|
||||
<a href="README.zh.md">中国語(簡体字)</a> |
|
||||
<a href="README.zht.md">中国語(繁体字)</a> |
|
||||
<a href="README.ko.md">韓国語</a> |
|
||||
<a href="README.de.md">ドイツ語</a> |
|
||||
<a href="README.es.md">スペイン語</a> |
|
||||
<a href="README.fr.md">フランス語</a> |
|
||||
<a href="README.da.md">デンマーク語</a> |
|
||||
<a href="README.ja.md">日本語</a> |
|
||||
<a href="README.pl.md">Polski</a> |
|
||||
<a href="README.ru.md">Русский</a> |
|
||||
<a href="README.ar.md">العربية</a> |
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a>
|
||||
<a href="README.pl.md">ポーランド語</a> |
|
||||
<a href="README.ru.md">ロシア語</a> |
|
||||
<a href="README.ar.md">アラビア語</a> |
|
||||
<a href="README.no.md">ノルウェー語</a> |
|
||||
<a href="README.br.md">ポルトガル語(ブラジル)</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
|
||||
+13
-13
@@ -15,20 +15,20 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> |
|
||||
<a href="README.zh.md">简体中文</a> |
|
||||
<a href="README.zht.md">繁體中文</a> |
|
||||
<a href="README.md">영어</a> |
|
||||
<a href="README.zh.md">중국어(간체)</a> |
|
||||
<a href="README.zht.md">중국어(번체)</a> |
|
||||
<a href="README.ko.md">한국어</a> |
|
||||
<a href="README.de.md">Deutsch</a> |
|
||||
<a href="README.es.md">Español</a> |
|
||||
<a href="README.fr.md">Français</a> |
|
||||
<a href="README.da.md">Dansk</a> |
|
||||
<a href="README.ja.md">日本語</a> |
|
||||
<a href="README.pl.md">Polski</a> |
|
||||
<a href="README.ru.md">Русский</a> |
|
||||
<a href="README.ar.md">العربية</a> |
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a>
|
||||
<a href="README.de.md">독일어</a> |
|
||||
<a href="README.es.md">스페인어</a> |
|
||||
<a href="README.fr.md">프랑스어</a> |
|
||||
<a href="README.da.md">덴마크어</a> |
|
||||
<a href="README.ja.md">일본어</a> |
|
||||
<a href="README.pl.md">폴란드어</a> |
|
||||
<a href="README.ru.md">러시아어</a> |
|
||||
<a href="README.ar.md">아랍어</a> |
|
||||
<a href="README.no.md">노르웨이어</a> |
|
||||
<a href="README.br.md">포르투갈어(브라질)</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
|
||||
@@ -16,19 +16,19 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> |
|
||||
<a href="README.zh.md">简体中文</a> |
|
||||
<a href="README.zht.md">繁體中文</a> |
|
||||
<a href="README.ko.md">한국어</a> |
|
||||
<a href="README.de.md">Deutsch</a> |
|
||||
<a href="README.es.md">Español</a> |
|
||||
<a href="README.fr.md">Français</a> |
|
||||
<a href="README.da.md">Dansk</a> |
|
||||
<a href="README.ja.md">日本語</a> |
|
||||
<a href="README.pl.md">Polski</a> |
|
||||
<a href="README.ru.md">Русский</a> |
|
||||
<a href="README.ar.md">العربية</a> |
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a>
|
||||
<a href="README.zh.md">Chinese (Simplified)</a> |
|
||||
<a href="README.zht.md">Chinese (Traditional)</a> |
|
||||
<a href="README.ko.md">Korean</a> |
|
||||
<a href="README.de.md">German</a> |
|
||||
<a href="README.es.md">Spanish</a> |
|
||||
<a href="README.fr.md">French</a> |
|
||||
<a href="README.da.md">Danish</a> |
|
||||
<a href="README.ja.md">Japanese</a> |
|
||||
<a href="README.pl.md">Polish</a> |
|
||||
<a href="README.ru.md">Russian</a> |
|
||||
<a href="README.ar.md">Arabic</a> |
|
||||
<a href="README.no.md">Norwegian</a> |
|
||||
<a href="README.br.md">Portuguese (Brazil)</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
|
||||
+12
-12
@@ -15,20 +15,20 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> |
|
||||
<a href="README.zh.md">简体中文</a> |
|
||||
<a href="README.zht.md">繁體中文</a> |
|
||||
<a href="README.ko.md">한국어</a> |
|
||||
<a href="README.de.md">Deutsch</a> |
|
||||
<a href="README.es.md">Español</a> |
|
||||
<a href="README.fr.md">Français</a> |
|
||||
<a href="README.md">Engelsk</a> |
|
||||
<a href="README.zh.md">Kinesisk (forenklet)</a> |
|
||||
<a href="README.zht.md">Kinesisk (tradisjonell)</a> |
|
||||
<a href="README.ko.md">Koreansk</a> |
|
||||
<a href="README.de.md">Tysk</a> |
|
||||
<a href="README.es.md">Spansk</a> |
|
||||
<a href="README.fr.md">Fransk</a> |
|
||||
<a href="README.da.md">Dansk</a> |
|
||||
<a href="README.ja.md">日本語</a> |
|
||||
<a href="README.pl.md">Polski</a> |
|
||||
<a href="README.ru.md">Русский</a> |
|
||||
<a href="README.ar.md">العربية</a> |
|
||||
<a href="README.ja.md">Japansk</a> |
|
||||
<a href="README.pl.md">Polsk</a> |
|
||||
<a href="README.ru.md">Russisk</a> |
|
||||
<a href="README.ar.md">Arabisk</a> |
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a>
|
||||
<a href="README.br.md">Portugisisk (Brasil)</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
|
||||
+13
-13
@@ -15,20 +15,20 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> |
|
||||
<a href="README.zh.md">简体中文</a> |
|
||||
<a href="README.zht.md">繁體中文</a> |
|
||||
<a href="README.ko.md">한국어</a> |
|
||||
<a href="README.de.md">Deutsch</a> |
|
||||
<a href="README.es.md">Español</a> |
|
||||
<a href="README.fr.md">Français</a> |
|
||||
<a href="README.da.md">Dansk</a> |
|
||||
<a href="README.ja.md">日本語</a> |
|
||||
<a href="README.md">Angielski</a> |
|
||||
<a href="README.zh.md">Chiński (uproszczony)</a> |
|
||||
<a href="README.zht.md">Chiński (tradycyjny)</a> |
|
||||
<a href="README.ko.md">Koreański</a> |
|
||||
<a href="README.de.md">Niemiecki</a> |
|
||||
<a href="README.es.md">Hiszpański</a> |
|
||||
<a href="README.fr.md">Francuski</a> |
|
||||
<a href="README.da.md">Duński</a> |
|
||||
<a href="README.ja.md">Japoński</a> |
|
||||
<a href="README.pl.md">Polski</a> |
|
||||
<a href="README.ru.md">Русский</a> |
|
||||
<a href="README.ar.md">العربية</a> |
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a>
|
||||
<a href="README.ru.md">Rosyjski</a> |
|
||||
<a href="README.ar.md">Arabski</a> |
|
||||
<a href="README.no.md">Norweski</a> |
|
||||
<a href="README.br.md">Portugalski (Brazylia)</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
|
||||
+13
-13
@@ -15,20 +15,20 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> |
|
||||
<a href="README.zh.md">简体中文</a> |
|
||||
<a href="README.zht.md">繁體中文</a> |
|
||||
<a href="README.ko.md">한국어</a> |
|
||||
<a href="README.de.md">Deutsch</a> |
|
||||
<a href="README.es.md">Español</a> |
|
||||
<a href="README.fr.md">Français</a> |
|
||||
<a href="README.da.md">Dansk</a> |
|
||||
<a href="README.ja.md">日本語</a> |
|
||||
<a href="README.pl.md">Polski</a> |
|
||||
<a href="README.md">Английский</a> |
|
||||
<a href="README.zh.md">Китайский (упрощенный)</a> |
|
||||
<a href="README.zht.md">Китайский (традиционный)</a> |
|
||||
<a href="README.ko.md">Корейский</a> |
|
||||
<a href="README.de.md">Немецкий</a> |
|
||||
<a href="README.es.md">Испанский</a> |
|
||||
<a href="README.fr.md">Французский</a> |
|
||||
<a href="README.da.md">Датский</a> |
|
||||
<a href="README.ja.md">Японский</a> |
|
||||
<a href="README.pl.md">Польский</a> |
|
||||
<a href="README.ru.md">Русский</a> |
|
||||
<a href="README.ar.md">العربية</a> |
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a>
|
||||
<a href="README.ar.md">Арабский</a> |
|
||||
<a href="README.no.md">Норвежский</a> |
|
||||
<a href="README.br.md">Португальский (Бразилия)</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
|
||||
+13
-13
@@ -15,20 +15,20 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> |
|
||||
<a href="README.md">英语</a> |
|
||||
<a href="README.zh.md">简体中文</a> |
|
||||
<a href="README.zht.md">繁體中文</a> |
|
||||
<a href="README.ko.md">한국어</a> |
|
||||
<a href="README.de.md">Deutsch</a> |
|
||||
<a href="README.es.md">Español</a> |
|
||||
<a href="README.fr.md">Français</a> |
|
||||
<a href="README.da.md">Dansk</a> |
|
||||
<a href="README.ja.md">日本語</a> |
|
||||
<a href="README.pl.md">Polski</a> |
|
||||
<a href="README.ru.md">Русский</a> |
|
||||
<a href="README.ar.md">العربية</a> |
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a>
|
||||
<a href="README.zht.md">繁体中文</a> |
|
||||
<a href="README.ko.md">韩语</a> |
|
||||
<a href="README.de.md">德语</a> |
|
||||
<a href="README.es.md">西班牙语</a> |
|
||||
<a href="README.fr.md">法语</a> |
|
||||
<a href="README.da.md">丹麦语</a> |
|
||||
<a href="README.ja.md">日语</a> |
|
||||
<a href="README.pl.md">波兰语</a> |
|
||||
<a href="README.ru.md">俄语</a> |
|
||||
<a href="README.ar.md">阿拉伯语</a> |
|
||||
<a href="README.no.md">挪威语</a> |
|
||||
<a href="README.br.md">葡萄牙语(巴西)</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
|
||||
+13
-13
@@ -15,20 +15,20 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> |
|
||||
<a href="README.zh.md">简体中文</a> |
|
||||
<a href="README.md">英語</a> |
|
||||
<a href="README.zh.md">簡體中文</a> |
|
||||
<a href="README.zht.md">繁體中文</a> |
|
||||
<a href="README.ko.md">한국어</a> |
|
||||
<a href="README.de.md">Deutsch</a> |
|
||||
<a href="README.es.md">Español</a> |
|
||||
<a href="README.fr.md">Français</a> |
|
||||
<a href="README.da.md">Dansk</a> |
|
||||
<a href="README.ja.md">日本語</a> |
|
||||
<a href="README.pl.md">Polski</a> |
|
||||
<a href="README.ru.md">Русский</a> |
|
||||
<a href="README.ar.md">العربية</a> |
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a>
|
||||
<a href="README.ko.md">韓語</a> |
|
||||
<a href="README.de.md">德語</a> |
|
||||
<a href="README.es.md">西班牙語</a> |
|
||||
<a href="README.fr.md">法語</a> |
|
||||
<a href="README.da.md">丹麥語</a> |
|
||||
<a href="README.ja.md">日語</a> |
|
||||
<a href="README.pl.md">波蘭語</a> |
|
||||
<a href="README.ru.md">俄語</a> |
|
||||
<a href="README.ar.md">阿拉伯語</a> |
|
||||
<a href="README.no.md">挪威語</a> |
|
||||
<a href="README.br.md">葡萄牙語(巴西)</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
|
||||
@@ -211,4 +211,3 @@
|
||||
| 2026-01-23 | 6,096,236 (+329,896) | 2,096,235 (+66,748) | 8,192,471 (+396,644) |
|
||||
| 2026-01-24 | 6,371,019 (+274,783) | 2,156,870 (+60,635) | 8,527,889 (+335,418) |
|
||||
| 2026-01-25 | 6,639,082 (+268,063) | 2,187,853 (+30,983) | 8,826,935 (+299,046) |
|
||||
| 2026-01-26 | 6,941,620 (+302,538) | 2,232,115 (+44,262) | 9,173,735 (+346,800) |
|
||||
|
||||
@@ -296,8 +296,8 @@
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "1.5.2",
|
||||
"@opentui/core": "0.1.75",
|
||||
"@opentui/solid": "0.1.75",
|
||||
"@opentui/core": "0.1.74",
|
||||
"@opentui/solid": "0.1.74",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
@@ -1227,21 +1227,21 @@
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
"@opentui/core": ["@opentui/core@0.1.75", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.75", "@opentui/core-darwin-x64": "0.1.75", "@opentui/core-linux-arm64": "0.1.75", "@opentui/core-linux-x64": "0.1.75", "@opentui/core-win32-arm64": "0.1.75", "@opentui/core-win32-x64": "0.1.75", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-8ARRZxSG+BXkJmEVtM2DQ4se7DAF1ZCKD07d+AklgTr2mxCzmdxxPbOwRzboSQ6FM7qGuTVPVbV4O2W9DpUmoA=="],
|
||||
"@opentui/core": ["@opentui/core@0.1.74", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.74", "@opentui/core-darwin-x64": "0.1.74", "@opentui/core-linux-arm64": "0.1.74", "@opentui/core-linux-x64": "0.1.74", "@opentui/core-win32-arm64": "0.1.74", "@opentui/core-win32-x64": "0.1.74", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-g4W16ymv12JdgZ+9B4t7mpIICvzWy2+eHERfmDf80ALduOQCUedKQdULcBFhVCYUXIkDRtIy6CID5thMAah3FA=="],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.75", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gGaGZjkFpqcXJk6321JzhRl66pM2VxBlI470L8W4DQUW4S6iDT1R9L7awSzGB4Cn9toUl7DTV8BemaXZYXV4SA=="],
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.74", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rfmlDLtm/u17CnuhJgCxPeYMvOST+A2MOdVOk46IurtHO849bdYqK6iudKNlFRs1FOrymgSKF9GlWBHAOKeRjg=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.75", "", { "os": "darwin", "cpu": "x64" }, "sha512-tPlvqQI0whZ76amHydpJs5kN+QeWAIcFbI8RAtlAo9baj2EbxTDC+JGwgb9Fnt0/YQx831humbtaNDhV2Jt1bw=="],
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.74", "", { "os": "darwin", "cpu": "x64" }, "sha512-WAD8orsDV0ZdW/5GwjOOB4FY96772xbkz+rcV7WRzEFUVaqoBaC04IuqYzS9d5s+cjkbT5Cpj47hrVYkkVQKng=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.75", "", { "os": "linux", "cpu": "arm64" }, "sha512-nVxIQ4Hqf84uBergDpWiVzU6pzpjy6tqBHRQpySxZ2flkJ/U6/aMEizVrQ1jcgIdxZtvqWDETZhzxhG0yDx+cw=="],
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.74", "", { "os": "linux", "cpu": "arm64" }, "sha512-lgmHzrzLy4e+rgBS+lhtsMLLgIMLbtLNMm6EzVPyYVDlLDGjM7+ulXMem7AtpaRrWrUUl4REiG9BoQUsCFDwYA=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.75", "", { "os": "linux", "cpu": "x64" }, "sha512-1CnApef4kxA+ORyLfbuCLgZfEjp4wr3HjFnt7FAfOb73kIZH82cb7JYixeqRyy9eOcKfKqxLmBYy3o8IDkc4Rg=="],
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.74", "", { "os": "linux", "cpu": "x64" }, "sha512-8Mn2WbdBQ29xCThuPZezjDhd1N3+fXwKkGvCBOdTI0le6h2A/vCNbfUVjwfr/EGZSRXxCG+Yapol34BAULGpOA=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.75", "", { "os": "win32", "cpu": "arm64" }, "sha512-j0UB95nmkYGNzmOrs6GqaddO1S90R0YC6IhbKnbKBdjchFPNVLz9JpexAs6MBDXPZwdKAywMxtwG2h3aTJtxng=="],
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.74", "", { "os": "win32", "cpu": "arm64" }, "sha512-dvYUXz03avnI6ZluyLp00HPmR0UT/IE/6QS97XBsgJlUTtpnbKkBtB5jD1NHwWkElaRj1Qv2QP36ngFoJqbl9g=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.75", "", { "os": "win32", "cpu": "x64" }, "sha512-ESpVZVGewe3JkB2TwrG3VRbkxT909iPdtvgNT7xTCIYH2VB4jqZomJfvERPTE0tvqAZJm19mHECzJFI8asSJgQ=="],
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.74", "", { "os": "win32", "cpu": "x64" }, "sha512-3wfWXaAKOIlDQz6ZZIESf2M+YGZ7uFHijjTEM8w/STRlLw8Y6+QyGYi1myHSM4d6RSO+/s2EMDxvjDf899W9vQ=="],
|
||||
|
||||
"@opentui/solid": ["@opentui/solid@0.1.75", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.75", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-WjKsZIfrm29znfRlcD9w3uUn/+uvoy2MmeoDwTvg1YOa0OjCTCmjZ43L9imp0m9S4HmVU8ma6o2bR4COzcyDdg=="],
|
||||
"@opentui/solid": ["@opentui/solid@0.1.74", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.74", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-Vz82cI8T9YeJjGsVg4ULp6ral4N+xyt1j9A6Tbu3aaQgEKiB74LW03EXREehfjPr1irOFxtKfWPbx5NKH0Upag=="],
|
||||
|
||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-AkI3guNjnE+bLZQVfzm0z14UENOECv2QBqMo5Lzkvt8=",
|
||||
"aarch64-linux": "sha256-dBfdyVTqW+fBZKCxC9Ld+1m3cP+nIbS6UDo0tUfPOSk=",
|
||||
"aarch64-darwin": "sha256-tOw31AMnHkW2cEDi+iqT3P93lU3SiMve26TEIqPz97k=",
|
||||
"x86_64-darwin": "sha256-wL/DmdZmxCmh+r4dsS1XGXuj8VPwR4pUqy5VIA76jl0="
|
||||
"x86_64-linux": "sha256-olTZ+tKugAY3LxizsJMlbK3TW78HZUoM03PigvQLP4A=",
|
||||
"aarch64-linux": "sha256-xdKDeqMEnYM2+vGySfb8pbcYyo/xMmgxG/ZhPCKaZEg=",
|
||||
"aarch64-darwin": "sha256-fihCTrHIiUG+py4vuqdr+YshqSKm2/B5onY50b97sPM=",
|
||||
"x86_64-darwin": "sha256-inlQQPNAOdkmKK6HQAMI2bG/ZFlfwmUQu9a6vm6Q0jQ="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,14 +14,13 @@ import { GlobalSyncProvider } from "@/context/global-sync"
|
||||
import { PermissionProvider } from "@/context/permission"
|
||||
import { LayoutProvider } from "@/context/layout"
|
||||
import { GlobalSDKProvider } from "@/context/global-sdk"
|
||||
import { normalizeServerUrl, ServerProvider, useServer } from "@/context/server"
|
||||
import { ServerProvider, useServer } from "@/context/server"
|
||||
import { SettingsProvider } from "@/context/settings"
|
||||
import { TerminalProvider } from "@/context/terminal"
|
||||
import { PromptProvider } from "@/context/prompt"
|
||||
import { FileProvider } from "@/context/file"
|
||||
import { CommentsProvider } from "@/context/comments"
|
||||
import { NotificationProvider } from "@/context/notification"
|
||||
import { ModelsProvider } from "@/context/models"
|
||||
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
|
||||
import { CommandProvider } from "@/context/command"
|
||||
import { LanguageProvider, useLanguage } from "@/context/language"
|
||||
@@ -86,19 +85,8 @@ function ServerKey(props: ParentProps) {
|
||||
}
|
||||
|
||||
export function AppInterface(props: { defaultUrl?: string }) {
|
||||
const platform = usePlatform()
|
||||
|
||||
const stored = (() => {
|
||||
if (platform.platform !== "web") return
|
||||
const result = platform.getDefaultServerUrl?.()
|
||||
if (result instanceof Promise) return
|
||||
if (!result) return
|
||||
return normalizeServerUrl(result)
|
||||
})()
|
||||
|
||||
const defaultServerUrl = () => {
|
||||
if (props.defaultUrl) return props.defaultUrl
|
||||
if (stored) return stored
|
||||
if (location.hostname.includes("opencode.ai")) return "http://localhost:4096"
|
||||
if (import.meta.env.DEV)
|
||||
return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
|
||||
@@ -117,11 +105,9 @@ export function AppInterface(props: { defaultUrl?: string }) {
|
||||
<PermissionProvider>
|
||||
<LayoutProvider>
|
||||
<NotificationProvider>
|
||||
<ModelsProvider>
|
||||
<CommandProvider>
|
||||
<Layout>{props.children}</Layout>
|
||||
</CommandProvider>
|
||||
</ModelsProvider>
|
||||
<CommandProvider>
|
||||
<Layout>{props.children}</Layout>
|
||||
</CommandProvider>
|
||||
</NotificationProvider>
|
||||
</LayoutProvider>
|
||||
</PermissionProvider>
|
||||
|
||||
@@ -155,7 +155,7 @@ export function DialogSelectServer() {
|
||||
},
|
||||
{ initialValue: null },
|
||||
)
|
||||
const canDefault = createMemo(() => !!platform.getDefaultServerUrl && !!platform.setDefaultServerUrl)
|
||||
const isDesktop = platform.platform === "desktop"
|
||||
|
||||
const looksComplete = (value: string) => {
|
||||
const normalized = normalizeServerUrl(value)
|
||||
@@ -505,7 +505,7 @@ export function DialogSelectServer() {
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<Show when={canDefault() && defaultUrl() !== i}>
|
||||
<Show when={isDesktop && defaultUrl() !== i}>
|
||||
<DropdownMenu.Item
|
||||
onSelect={async () => {
|
||||
await platform.setDefaultServerUrl?.(i)
|
||||
@@ -517,7 +517,7 @@ export function DialogSelectServer() {
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<Show when={canDefault() && defaultUrl() === i}>
|
||||
<Show when={isDesktop && defaultUrl() === i}>
|
||||
<DropdownMenu.Item
|
||||
onSelect={async () => {
|
||||
await platform.setDefaultServerUrl?.(null)
|
||||
|
||||
@@ -6,8 +6,12 @@ import { useLanguage } from "@/context/language"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { SettingsGeneral } from "./settings-general"
|
||||
import { SettingsKeybinds } from "./settings-keybinds"
|
||||
import { SettingsPermissions } from "./settings-permissions"
|
||||
import { SettingsProviders } from "./settings-providers"
|
||||
import { SettingsModels } from "./settings-models"
|
||||
import { SettingsAgents } from "./settings-agents"
|
||||
import { SettingsCommands } from "./settings-commands"
|
||||
import { SettingsMcp } from "./settings-mcp"
|
||||
|
||||
export const DialogSettings: Component = () => {
|
||||
const language = useLanguage()
|
||||
@@ -17,36 +21,34 @@ export const DialogSettings: Component = () => {
|
||||
<Dialog size="x-large">
|
||||
<Tabs orientation="vertical" variant="settings" defaultValue="general" class="h-full settings-dialog">
|
||||
<Tabs.List>
|
||||
<div class="flex flex-col justify-between h-full w-full">
|
||||
<div class="flex flex-col gap-3 w-full pt-3">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<Tabs.SectionTitle>{language.t("settings.section.desktop")}</Tabs.SectionTitle>
|
||||
<div class="flex flex-col gap-1.5 w-full">
|
||||
<Tabs.Trigger value="general">
|
||||
<Icon name="sliders" />
|
||||
{language.t("settings.tab.general")}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="shortcuts">
|
||||
<Icon name="keyboard" />
|
||||
{language.t("settings.tab.shortcuts")}
|
||||
</Tabs.Trigger>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<Tabs.SectionTitle>{language.t("settings.section.server")}</Tabs.SectionTitle>
|
||||
<div class="flex flex-col gap-1.5 w-full">
|
||||
<Tabs.Trigger value="providers">
|
||||
<Icon name="server" />
|
||||
{language.t("settings.providers.title")}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="models">
|
||||
<Icon name="server" />
|
||||
{language.t("settings.models.title")}
|
||||
</Tabs.Trigger>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
"flex-direction": "column",
|
||||
"justify-content": "space-between",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
"flex-direction": "column",
|
||||
gap: "12px",
|
||||
width: "100%",
|
||||
"padding-top": "12px",
|
||||
}}
|
||||
>
|
||||
<Tabs.SectionTitle>{language.t("settings.section.desktop")}</Tabs.SectionTitle>
|
||||
<div style={{ display: "flex", "flex-direction": "column", gap: "6px", width: "100%" }}>
|
||||
<Tabs.Trigger value="general">
|
||||
<Icon name="sliders" />
|
||||
{language.t("settings.tab.general")}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="shortcuts">
|
||||
<Icon name="keyboard" />
|
||||
{language.t("settings.tab.shortcuts")}
|
||||
</Tabs.Trigger>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 pl-1 py-1 text-12-medium text-text-weak">
|
||||
@@ -54,6 +56,31 @@ export const DialogSettings: Component = () => {
|
||||
<span class="text-11-regular">v{platform.version}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* <Tabs.SectionTitle>Server</Tabs.SectionTitle> */}
|
||||
{/* <Tabs.Trigger value="permissions"> */}
|
||||
{/* <Icon name="checklist" /> */}
|
||||
{/* Permissions */}
|
||||
{/* </Tabs.Trigger> */}
|
||||
{/* <Tabs.Trigger value="providers"> */}
|
||||
{/* <Icon name="server" /> */}
|
||||
{/* Providers */}
|
||||
{/* </Tabs.Trigger> */}
|
||||
{/* <Tabs.Trigger value="models"> */}
|
||||
{/* <Icon name="brain" /> */}
|
||||
{/* Models */}
|
||||
{/* </Tabs.Trigger> */}
|
||||
{/* <Tabs.Trigger value="agents"> */}
|
||||
{/* <Icon name="task" /> */}
|
||||
{/* Agents */}
|
||||
{/* </Tabs.Trigger> */}
|
||||
{/* <Tabs.Trigger value="commands"> */}
|
||||
{/* <Icon name="console" /> */}
|
||||
{/* Commands */}
|
||||
{/* </Tabs.Trigger> */}
|
||||
{/* <Tabs.Trigger value="mcp"> */}
|
||||
{/* <Icon name="mcp" /> */}
|
||||
{/* MCP */}
|
||||
{/* </Tabs.Trigger> */}
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="general" class="no-scrollbar">
|
||||
<SettingsGeneral />
|
||||
@@ -61,12 +88,15 @@ export const DialogSettings: Component = () => {
|
||||
<Tabs.Content value="shortcuts" class="no-scrollbar">
|
||||
<SettingsKeybinds />
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="providers" class="no-scrollbar">
|
||||
<SettingsProviders />
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="models" class="no-scrollbar">
|
||||
<SettingsModels />
|
||||
</Tabs.Content>
|
||||
{/* <Tabs.Content value="permissions" class="no-scrollbar"> */}
|
||||
{/* <SettingsPermissions /> */}
|
||||
{/* </Tabs.Content> */}
|
||||
{/* <Tabs.Content value="providers" class="no-scrollbar"> */}
|
||||
{/* <SettingsProviders /> */}
|
||||
{/* </Tabs.Content> */}
|
||||
{/* <Tabs.Content value="models" class="no-scrollbar"> */}
|
||||
{/* <SettingsModels /> */}
|
||||
{/* </Tabs.Content> */}
|
||||
{/* <Tabs.Content value="agents" class="no-scrollbar"> */}
|
||||
{/* <SettingsAgents /> */}
|
||||
{/* </Tabs.Content> */}
|
||||
|
||||
@@ -131,7 +131,7 @@ export function SessionHeader() {
|
||||
<Portal mount={mount()}>
|
||||
<button
|
||||
type="button"
|
||||
class="hidden md:flex w-[320px] p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus-visible:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
|
||||
class="hidden md:flex w-[320px] p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
|
||||
onClick={() => command.trigger("file.open")}
|
||||
aria-label={language.t("session.header.searchFiles")}
|
||||
>
|
||||
|
||||
@@ -1,135 +1,14 @@
|
||||
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { Switch } from "@opencode-ai/ui/switch"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import type { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { type Component, For, Show } from "solid-js"
|
||||
import { Component } from "solid-js"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useModels } from "@/context/models"
|
||||
import { popularProviders } from "@/hooks/use-providers"
|
||||
|
||||
type ModelItem = ReturnType<ReturnType<typeof useModels>["list"]>[number]
|
||||
|
||||
export const SettingsModels: Component = () => {
|
||||
const language = useLanguage()
|
||||
const models = useModels()
|
||||
|
||||
const list = useFilteredList<ModelItem>({
|
||||
items: (_filter) => models.list(),
|
||||
key: (x) => `${x.provider.id}:${x.id}`,
|
||||
filterKeys: ["provider.name", "name", "id"],
|
||||
sortBy: (a, b) => a.name.localeCompare(b.name),
|
||||
groupBy: (x) => x.provider.id,
|
||||
sortGroupsBy: (a, b) => {
|
||||
const aIndex = popularProviders.indexOf(a.category)
|
||||
const bIndex = popularProviders.indexOf(b.category)
|
||||
const aPopular = aIndex >= 0
|
||||
const bPopular = bIndex >= 0
|
||||
|
||||
if (aPopular && !bPopular) return -1
|
||||
if (!aPopular && bPopular) return 1
|
||||
if (aPopular && bPopular) return aIndex - bIndex
|
||||
|
||||
const aName = a.items[0].provider.name
|
||||
const bName = b.items[0].provider.name
|
||||
return aName.localeCompare(bName)
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar" style={{ padding: "0 40px 40px 40px" }}>
|
||||
<div
|
||||
class="sticky top-0 z-10"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(to bottom, var(--surface-raised-stronger-non-alpha) calc(100% - 24px), transparent)",
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
|
||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.models.title")}</h2>
|
||||
<div class="flex items-center gap-2 px-3 h-9 rounded-lg bg-surface-base">
|
||||
<Icon name="magnifying-glass" class="text-icon-weak-base flex-shrink-0" />
|
||||
<TextField
|
||||
variant="ghost"
|
||||
type="text"
|
||||
value={list.filter()}
|
||||
onChange={list.onInput}
|
||||
placeholder={language.t("dialog.model.search.placeholder")}
|
||||
spellcheck={false}
|
||||
autocorrect="off"
|
||||
autocomplete="off"
|
||||
autocapitalize="off"
|
||||
class="flex-1"
|
||||
/>
|
||||
<Show when={list.filter()}>
|
||||
<IconButton icon="circle-x" variant="ghost" onClick={list.clear} />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-8 max-w-[720px]">
|
||||
<Show
|
||||
when={!list.grouped.loading}
|
||||
fallback={
|
||||
<div class="flex flex-col items-center justify-center py-12 text-center">
|
||||
<span class="text-14-regular text-text-weak">
|
||||
{language.t("common.loading")}
|
||||
{language.t("common.loading.ellipsis")}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={list.flat().length > 0}
|
||||
fallback={
|
||||
<div class="flex flex-col items-center justify-center py-12 text-center">
|
||||
<span class="text-14-regular text-text-weak">{language.t("dialog.model.empty")}</span>
|
||||
<Show when={list.filter()}>
|
||||
<span class="text-14-regular text-text-strong mt-1">"{list.filter()}"</span>
|
||||
</Show>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<For each={list.grouped.latest}>
|
||||
{(group) => (
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex items-center gap-2 pb-2">
|
||||
<ProviderIcon id={group.category as IconName} class="size-5 shrink-0 icon-strong-base" />
|
||||
<span class="text-14-medium text-text-strong">{group.items[0].provider.name}</span>
|
||||
</div>
|
||||
<div class="bg-surface-raised-base px-4 rounded-lg">
|
||||
<For each={group.items}>
|
||||
{(item) => {
|
||||
const key = { providerID: item.provider.id, modelID: item.id }
|
||||
return (
|
||||
<div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
|
||||
<div class="min-w-0">
|
||||
<span class="text-14-regular text-text-strong truncate block">{item.name}</span>
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<Switch
|
||||
checked={models.visible(key)}
|
||||
onChange={(checked) => {
|
||||
models.setVisibility(key, checked)
|
||||
}}
|
||||
hideLabel
|
||||
>
|
||||
{item.name}
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</Show>
|
||||
<div class="flex flex-col h-full overflow-y-auto">
|
||||
<div class="flex flex-col gap-6 p-6 max-w-[600px]">
|
||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.models.title")}</h2>
|
||||
<p class="text-14-regular text-text-weak">{language.t("settings.models.description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,154 +1,14 @@
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { Tag } from "@opencode-ai/ui/tag"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import type { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
||||
import { createMemo, type Component, For, Show } from "solid-js"
|
||||
import { Component } from "solid-js"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { DialogConnectProvider } from "./dialog-connect-provider"
|
||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||
|
||||
type ProviderSource = "env" | "api" | "config" | "custom"
|
||||
type ProviderMeta = { source?: ProviderSource }
|
||||
|
||||
export const SettingsProviders: Component = () => {
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
const globalSDK = useGlobalSDK()
|
||||
const providers = useProviders()
|
||||
|
||||
const connected = createMemo(() => providers.connected())
|
||||
const popular = createMemo(() => {
|
||||
const connectedIDs = new Set(connected().map((p) => p.id))
|
||||
const items = providers
|
||||
.popular()
|
||||
.filter((p) => !connectedIDs.has(p.id))
|
||||
.slice()
|
||||
items.sort((a, b) => popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id))
|
||||
return items
|
||||
})
|
||||
|
||||
const source = (item: unknown) => (item as ProviderMeta).source
|
||||
|
||||
const type = (item: unknown) => {
|
||||
const current = source(item)
|
||||
if (current === "env") return language.t("settings.providers.tag.environment")
|
||||
if (current === "api") return language.t("provider.connect.method.apiKey")
|
||||
if (current === "config") return language.t("settings.providers.tag.config")
|
||||
if (current === "custom") return language.t("settings.providers.tag.custom")
|
||||
return language.t("settings.providers.tag.other")
|
||||
}
|
||||
|
||||
const canDisconnect = (item: unknown) => source(item) !== "env"
|
||||
|
||||
const disconnect = async (providerID: string, name: string) => {
|
||||
await globalSDK.client.auth
|
||||
.remove({ providerID })
|
||||
.then(async () => {
|
||||
await globalSDK.client.global.dispose()
|
||||
showToast({
|
||||
variant: "success",
|
||||
icon: "circle-check",
|
||||
title: language.t("provider.disconnect.toast.disconnected.title", { provider: name }),
|
||||
description: language.t("provider.disconnect.toast.disconnected.description", { provider: name }),
|
||||
})
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
showToast({ title: language.t("common.requestFailed"), description: message })
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar" style={{ padding: "0 40px 40px 40px" }}>
|
||||
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
||||
<div class="flex flex-col gap-1 pt-6 pb-8 max-w-[720px]">
|
||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-8 max-w-[720px]">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.providers.section.connected")}</h3>
|
||||
<div class="bg-surface-raised-base px-4 rounded-lg">
|
||||
<Show
|
||||
when={connected().length > 0}
|
||||
fallback={
|
||||
<div class="py-4 text-14-regular text-text-weak">
|
||||
{language.t("settings.providers.connected.empty")}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<For each={connected()}>
|
||||
{(item) => (
|
||||
<div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<ProviderIcon id={item.id as IconName} class="size-5 shrink-0 icon-strong-base" />
|
||||
<span class="text-14-regular text-text-strong truncate">{item.name}</span>
|
||||
<Tag>{type(item)}</Tag>
|
||||
</div>
|
||||
<Show when={canDisconnect(item)}>
|
||||
<Button size="small" variant="ghost" onClick={() => void disconnect(item.id, item.name)}>
|
||||
{language.t("common.disconnect")}
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.providers.section.popular")}</h3>
|
||||
<div class="bg-surface-raised-base px-4 rounded-lg">
|
||||
<For each={popular()}>
|
||||
{(item) => (
|
||||
<div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
|
||||
<div class="flex items-center gap-x-3 min-w-0">
|
||||
<ProviderIcon id={item.id as IconName} class="size-5 shrink-0 icon-strong-base" />
|
||||
<span class="text-14-regular text-text-strong">{item.name}</span>
|
||||
<Show when={item.id === "opencode"}>
|
||||
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
||||
</Show>
|
||||
<Show when={item.id === "anthropic"}>
|
||||
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div>
|
||||
</Show>
|
||||
<Show when={item.id === "openai"}>
|
||||
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.openai.note")}</div>
|
||||
</Show>
|
||||
<Show when={item.id.startsWith("github-copilot")}>
|
||||
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.copilot.note")}</div>
|
||||
</Show>
|
||||
</div>
|
||||
<Button
|
||||
size="large"
|
||||
variant="secondary"
|
||||
icon="plus-small"
|
||||
onClick={() => {
|
||||
dialog.show(() => <DialogConnectProvider provider={item.id} />)
|
||||
}}
|
||||
>
|
||||
{language.t("common.connect")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="px-0 py-0 text-14-medium text-text-strong text-left justify-start hover:bg-transparent active:bg-transparent"
|
||||
onClick={() => {
|
||||
dialog.show(() => <DialogSelectProvider />)
|
||||
}}
|
||||
>
|
||||
{language.t("dialog.provider.viewAll")}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-col h-full overflow-y-auto">
|
||||
<div class="flex flex-col gap-6 p-6 max-w-[600px]">
|
||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
|
||||
<p class="text-14-regular text-text-weak">{language.t("settings.providers.description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -125,21 +125,13 @@ export function StatusPopover() {
|
||||
|
||||
const [defaultServerUrl, setDefaultServerUrl] = createSignal<string | undefined>()
|
||||
|
||||
const refreshDefaultServerUrl = () => {
|
||||
createEffect(() => {
|
||||
const result = platform.getDefaultServerUrl?.()
|
||||
if (!result) {
|
||||
setDefaultServerUrl(undefined)
|
||||
return
|
||||
}
|
||||
if (result instanceof Promise) {
|
||||
result.then((url) => setDefaultServerUrl(url ? normalizeServerUrl(url) : undefined))
|
||||
return
|
||||
}
|
||||
setDefaultServerUrl(normalizeServerUrl(result))
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
refreshDefaultServerUrl()
|
||||
if (result) setDefaultServerUrl(normalizeServerUrl(result))
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -161,7 +153,7 @@ export function StatusPopover() {
|
||||
"bg-border-weak-base": server.healthy() === undefined,
|
||||
}}
|
||||
/>
|
||||
<span class="text-12-regular text-text-strong">{language.t("status.popover.trigger")}</span>
|
||||
<span class="text-12-regular text-text-strong">Status</span>
|
||||
</div>
|
||||
}
|
||||
class="[&_[data-slot=popover-body]]:p-0 w-[360px] max-w-[calc(100vw-40px)] bg-transparent border-0 shadow-none rounded-xl"
|
||||
@@ -174,7 +166,7 @@ export function StatusPopover() {
|
||||
style={{ "box-shadow": "var(--shadow-lg-border-base)" }}
|
||||
>
|
||||
<Tabs
|
||||
aria-label={language.t("status.popover.ariaLabel")}
|
||||
aria-label="Server Configurations"
|
||||
class="tabs"
|
||||
data-component="tabs"
|
||||
data-active="servers"
|
||||
@@ -197,20 +189,16 @@ export function StatusPopover() {
|
||||
}}
|
||||
>
|
||||
<Tabs.Trigger value="servers" data-slot="tab" class="text-12-regular">
|
||||
{serverCount() > 0 ? `${serverCount()} ` : ""}
|
||||
{language.t("status.popover.tab.servers")}
|
||||
{serverCount() > 0 ? `${serverCount()} ` : ""}Servers
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="mcp" data-slot="tab" class="text-12-regular">
|
||||
{mcpConnected() > 0 ? `${mcpConnected()} ` : ""}
|
||||
{language.t("status.popover.tab.mcp")}
|
||||
{mcpConnected() > 0 ? `${mcpConnected()} ` : ""}MCP
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="lsp" data-slot="tab" class="text-12-regular">
|
||||
{lspCount() > 0 ? `${lspCount()} ` : ""}
|
||||
{language.t("status.popover.tab.lsp")}
|
||||
{lspCount() > 0 ? `${lspCount()} ` : ""}LSP
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="plugins" data-slot="tab" class="text-12-regular">
|
||||
{pluginCount() > 0 ? `${pluginCount()} ` : ""}
|
||||
{language.t("status.popover.tab.plugins")}
|
||||
{pluginCount() > 0 ? `${pluginCount()} ` : ""}Plugins
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
@@ -286,7 +274,7 @@ export function StatusPopover() {
|
||||
</Show>
|
||||
<Show when={isDefault()}>
|
||||
<span class="text-11-regular text-text-base bg-surface-base px-1.5 py-0.5 rounded-md">
|
||||
{language.t("common.default")}
|
||||
Default
|
||||
</span>
|
||||
</Show>
|
||||
<div class="flex-1" />
|
||||
@@ -302,9 +290,9 @@ export function StatusPopover() {
|
||||
<Button
|
||||
variant="secondary"
|
||||
class="mt-3 self-start h-8 px-3 py-1.5"
|
||||
onClick={() => dialog.show(() => <DialogSelectServer />, refreshDefaultServerUrl)}
|
||||
onClick={() => dialog.show(() => <DialogSelectServer />)}
|
||||
>
|
||||
{language.t("status.popover.action.manageServers")}
|
||||
Manage servers
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -316,9 +304,7 @@ export function StatusPopover() {
|
||||
<Show
|
||||
when={mcpItems().length > 0}
|
||||
fallback={
|
||||
<div class="text-14-regular text-text-base text-center my-auto">
|
||||
{language.t("dialog.mcp.empty")}
|
||||
</div>
|
||||
<div class="text-14-regular text-text-base text-center my-auto">No MCP servers configured</div>
|
||||
}
|
||||
>
|
||||
<For each={mcpItems()}>
|
||||
@@ -365,7 +351,7 @@ export function StatusPopover() {
|
||||
when={lspItems().length > 0}
|
||||
fallback={
|
||||
<div class="text-14-regular text-text-base text-center my-auto">
|
||||
{language.t("dialog.lsp.empty")}
|
||||
LSPs auto-detected from file types
|
||||
</div>
|
||||
}
|
||||
>
|
||||
@@ -395,19 +381,8 @@ export function StatusPopover() {
|
||||
when={plugins().length > 0}
|
||||
fallback={
|
||||
<div class="text-14-regular text-text-base text-center my-auto">
|
||||
{(() => {
|
||||
const value = language.t("dialog.plugins.empty")
|
||||
const file = "opencode.json"
|
||||
const parts = value.split(file)
|
||||
if (parts.length === 1) return value
|
||||
return (
|
||||
<>
|
||||
{parts[0]}
|
||||
<code class="bg-surface-raised-base px-1.5 py-0.5 rounded-sm text-text-base">{file}</code>
|
||||
{parts.slice(1).join(file)}
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
Plugins configured in{" "}
|
||||
<code class="bg-surface-raised-base px-1.5 py-0.5 rounded-sm text-text-base">opencode.json</code>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -24,15 +24,6 @@ function normalizeKey(key: string) {
|
||||
return key.toLowerCase()
|
||||
}
|
||||
|
||||
function signature(key: string, ctrl: boolean, meta: boolean, shift: boolean, alt: boolean) {
|
||||
const mask = (ctrl ? 1 : 0) | (meta ? 2 : 0) | (shift ? 4 : 0) | (alt ? 8 : 0)
|
||||
return `${key}:${mask}`
|
||||
}
|
||||
|
||||
function signatureFromEvent(event: KeyboardEvent) {
|
||||
return signature(normalizeKey(event.key), event.ctrlKey, event.metaKey, event.shiftKey, event.altKey)
|
||||
}
|
||||
|
||||
export type KeybindConfig = string
|
||||
|
||||
export interface Keybind {
|
||||
@@ -232,30 +223,6 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
||||
|
||||
const suspended = () => suspendCount() > 0
|
||||
|
||||
const palette = createMemo(() => {
|
||||
const config = settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND
|
||||
const keybinds = parseKeybind(config)
|
||||
return new Set(keybinds.map((kb) => signature(kb.key, kb.ctrl, kb.meta, kb.shift, kb.alt)))
|
||||
})
|
||||
|
||||
const keymap = createMemo(() => {
|
||||
const map = new Map<string, CommandOption>()
|
||||
for (const option of options()) {
|
||||
if (option.id.startsWith(SUGGESTED_PREFIX)) continue
|
||||
if (option.disabled) continue
|
||||
if (!option.keybind) continue
|
||||
|
||||
const keybinds = parseKeybind(option.keybind)
|
||||
for (const kb of keybinds) {
|
||||
if (!kb.key) continue
|
||||
const sig = signature(kb.key, kb.ctrl, kb.meta, kb.shift, kb.alt)
|
||||
if (map.has(sig)) continue
|
||||
map.set(sig, option)
|
||||
}
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
const run = (id: string, source?: "palette" | "keybind" | "slash") => {
|
||||
for (const option of options()) {
|
||||
if (option.id === id || option.id === "suggested." + id) {
|
||||
@@ -272,18 +239,24 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (suspended() || dialog.active) return
|
||||
|
||||
const sig = signatureFromEvent(event)
|
||||
|
||||
if (palette().has(sig)) {
|
||||
const paletteKeybinds = parseKeybind(settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND)
|
||||
if (matchKeybind(paletteKeybinds, event)) {
|
||||
event.preventDefault()
|
||||
showPalette()
|
||||
return
|
||||
}
|
||||
|
||||
const option = keymap().get(sig)
|
||||
if (!option) return
|
||||
event.preventDefault()
|
||||
option.onSelect?.("keybind")
|
||||
for (const option of options()) {
|
||||
if (option.disabled) continue
|
||||
if (!option.keybind) continue
|
||||
|
||||
const keybinds = parseKeybind(option.keybind)
|
||||
if (matchKeybind(keybinds, event)) {
|
||||
event.preventDefault()
|
||||
option.onSelect?.("keybind")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
|
||||
@@ -57,62 +57,6 @@ function stripQueryAndHash(input: string) {
|
||||
return input
|
||||
}
|
||||
|
||||
function unquoteGitPath(input: string) {
|
||||
if (!input.startsWith('"')) return input
|
||||
if (!input.endsWith('"')) return input
|
||||
const body = input.slice(1, -1)
|
||||
const bytes: number[] = []
|
||||
|
||||
for (let i = 0; i < body.length; i++) {
|
||||
const char = body[i]!
|
||||
if (char !== "\\") {
|
||||
bytes.push(char.charCodeAt(0))
|
||||
continue
|
||||
}
|
||||
|
||||
const next = body[i + 1]
|
||||
if (!next) {
|
||||
bytes.push("\\".charCodeAt(0))
|
||||
continue
|
||||
}
|
||||
|
||||
if (next >= "0" && next <= "7") {
|
||||
const chunk = body.slice(i + 1, i + 4)
|
||||
const match = chunk.match(/^[0-7]{1,3}/)
|
||||
if (!match) {
|
||||
bytes.push(next.charCodeAt(0))
|
||||
i++
|
||||
continue
|
||||
}
|
||||
bytes.push(parseInt(match[0], 8))
|
||||
i += match[0].length
|
||||
continue
|
||||
}
|
||||
|
||||
const escaped =
|
||||
next === "n"
|
||||
? "\n"
|
||||
: next === "r"
|
||||
? "\r"
|
||||
: next === "t"
|
||||
? "\t"
|
||||
: next === "b"
|
||||
? "\b"
|
||||
: next === "f"
|
||||
? "\f"
|
||||
: next === "v"
|
||||
? "\v"
|
||||
: next === "\\" || next === '"'
|
||||
? next
|
||||
: undefined
|
||||
|
||||
bytes.push((escaped ?? next).charCodeAt(0))
|
||||
i++
|
||||
}
|
||||
|
||||
return new TextDecoder().decode(new Uint8Array(bytes))
|
||||
}
|
||||
|
||||
export function selectionFromLines(range: SelectedLineRange): FileSelection {
|
||||
const startLine = Math.min(range.start, range.end)
|
||||
const endLine = Math.max(range.start, range.end)
|
||||
@@ -253,7 +197,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
const root = directory()
|
||||
const prefix = root.endsWith("/") ? root : root + "/"
|
||||
|
||||
let path = unquoteGitPath(stripQueryAndHash(stripFileProtocol(input)))
|
||||
let path = stripQueryAndHash(stripFileProtocol(input))
|
||||
|
||||
if (path.startsWith(prefix)) {
|
||||
path = path.slice(prefix.length)
|
||||
|
||||
@@ -218,7 +218,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
}
|
||||
|
||||
function enrich(project: { worktree: string; expanded: boolean }) {
|
||||
const [childStore] = globalSync.child(project.worktree, { bootstrap: false })
|
||||
const [childStore] = globalSync.child(project.worktree)
|
||||
const projectID = childStore.project
|
||||
const metadata = projectID
|
||||
? globalSync.data.project.find((x) => x.id === projectID)
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { batch, createEffect, createMemo, onCleanup } from "solid-js"
|
||||
import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda"
|
||||
import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { useSDK } from "./sdk"
|
||||
import { useSync } from "./sync"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
import { useProviders } from "@/hooks/use-providers"
|
||||
import { useModels } from "@/context/models"
|
||||
import { DateTime } from "luxon"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
@@ -110,7 +112,18 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
})()
|
||||
|
||||
const model = (() => {
|
||||
const models = useModels()
|
||||
const [store, setStore, _, modelReady] = persisted(
|
||||
Persist.global("model", ["model.v1"]),
|
||||
createStore<{
|
||||
user: (ModelKey & { visibility: "show" | "hide"; favorite?: boolean })[]
|
||||
recent: ModelKey[]
|
||||
variant?: Record<string, string | undefined>
|
||||
}>({
|
||||
user: [],
|
||||
recent: [],
|
||||
variant: {},
|
||||
}),
|
||||
)
|
||||
|
||||
const [ephemeral, setEphemeral] = createStore<{
|
||||
model: Record<string, ModelKey | undefined>
|
||||
@@ -118,6 +131,57 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
model: {},
|
||||
})
|
||||
|
||||
const available = createMemo(() =>
|
||||
providers.connected().flatMap((p) =>
|
||||
Object.values(p.models).map((m) => ({
|
||||
...m,
|
||||
provider: p,
|
||||
})),
|
||||
),
|
||||
)
|
||||
|
||||
const latest = createMemo(() =>
|
||||
pipe(
|
||||
available(),
|
||||
filter((x) => Math.abs(DateTime.fromISO(x.release_date).diffNow().as("months")) < 6),
|
||||
groupBy((x) => x.provider.id),
|
||||
mapValues((models) =>
|
||||
pipe(
|
||||
models,
|
||||
groupBy((x) => x.family),
|
||||
values(),
|
||||
(groups) =>
|
||||
groups.flatMap((g) => {
|
||||
const first = firstBy(g, [(x) => x.release_date, "desc"])
|
||||
return first ? [{ modelID: first.id, providerID: first.provider.id }] : []
|
||||
}),
|
||||
),
|
||||
),
|
||||
values(),
|
||||
flat(),
|
||||
),
|
||||
)
|
||||
|
||||
const latestSet = createMemo(() => new Set(latest().map((x) => `${x.providerID}:${x.modelID}`)))
|
||||
|
||||
const userVisibilityMap = createMemo(() => {
|
||||
const map = new Map<string, "show" | "hide">()
|
||||
for (const item of store.user) {
|
||||
map.set(`${item.providerID}:${item.modelID}`, item.visibility)
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
const list = createMemo(() =>
|
||||
available().map((m) => ({
|
||||
...m,
|
||||
name: m.name.replace("(latest)", "").trim(),
|
||||
latest: m.name.includes("(latest)"),
|
||||
})),
|
||||
)
|
||||
|
||||
const find = (key: ModelKey) => list().find((m) => m.id === key?.modelID && m.provider.id === key.providerID)
|
||||
|
||||
const fallbackModel = createMemo<ModelKey | undefined>(() => {
|
||||
if (sync.data.config.model) {
|
||||
const [providerID, modelID] = sync.data.config.model.split("/")
|
||||
@@ -129,7 +193,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of models.recent.list()) {
|
||||
for (const item of store.recent) {
|
||||
if (isModelValid(item)) {
|
||||
return item
|
||||
}
|
||||
@@ -161,10 +225,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
fallbackModel,
|
||||
)
|
||||
if (!key) return undefined
|
||||
return models.find(key)
|
||||
return find(key)
|
||||
})
|
||||
|
||||
const recent = createMemo(() => models.recent.list().map(models.find).filter(Boolean))
|
||||
const recent = createMemo(() => store.recent.map(find).filter(Boolean))
|
||||
|
||||
const cycle = (direction: 1 | -1) => {
|
||||
const recentList = recent()
|
||||
@@ -189,32 +253,54 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
})
|
||||
}
|
||||
|
||||
function updateVisibility(model: ModelKey, visibility: "show" | "hide") {
|
||||
const index = store.user.findIndex((x) => x.modelID === model.modelID && x.providerID === model.providerID)
|
||||
if (index >= 0) {
|
||||
setStore("user", index, { visibility })
|
||||
} else {
|
||||
setStore("user", store.user.length, { ...model, visibility })
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ready: models.ready,
|
||||
ready: modelReady,
|
||||
current,
|
||||
recent,
|
||||
list: models.list,
|
||||
list,
|
||||
cycle,
|
||||
set(model: ModelKey | undefined, options?: { recent?: boolean }) {
|
||||
batch(() => {
|
||||
const currentAgent = agent.current()
|
||||
const next = model ?? fallbackModel()
|
||||
if (currentAgent) setEphemeral("model", currentAgent.name, next)
|
||||
if (model) models.setVisibility(model, true)
|
||||
if (options?.recent && model) models.recent.push(model)
|
||||
if (model) updateVisibility(model, "show")
|
||||
if (options?.recent && model) {
|
||||
const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID)
|
||||
if (uniq.length > 5) uniq.pop()
|
||||
setStore("recent", uniq)
|
||||
}
|
||||
})
|
||||
},
|
||||
visible(model: ModelKey) {
|
||||
return models.visible(model)
|
||||
const key = `${model.providerID}:${model.modelID}`
|
||||
const visibility = userVisibilityMap().get(key)
|
||||
if (visibility === "hide") return false
|
||||
if (visibility === "show") return true
|
||||
if (latestSet().has(key)) return true
|
||||
// For models without valid release_date (e.g. custom models), show by default
|
||||
const m = find(model)
|
||||
if (!m?.release_date || !DateTime.fromISO(m.release_date).isValid) return true
|
||||
return false
|
||||
},
|
||||
setVisibility(model: ModelKey, visible: boolean) {
|
||||
models.setVisibility(model, visible)
|
||||
updateVisibility(model, visible ? "show" : "hide")
|
||||
},
|
||||
variant: {
|
||||
current() {
|
||||
const m = current()
|
||||
if (!m) return undefined
|
||||
return models.variant.get({ providerID: m.provider.id, modelID: m.id })
|
||||
const key = `${m.provider.id}/${m.id}`
|
||||
return store.variant?.[key]
|
||||
},
|
||||
list() {
|
||||
const m = current()
|
||||
@@ -225,7 +311,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
set(value: string | undefined) {
|
||||
const m = current()
|
||||
if (!m) return
|
||||
models.variant.set({ providerID: m.provider.id, modelID: m.id }, value)
|
||||
const key = `${m.provider.id}/${m.id}`
|
||||
if (!store.variant) {
|
||||
setStore("variant", { [key]: value })
|
||||
} else {
|
||||
setStore("variant", key, value)
|
||||
}
|
||||
},
|
||||
cycle() {
|
||||
const variants = this.list()
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
import { createMemo } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { DateTime } from "luxon"
|
||||
import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { useProviders } from "@/hooks/use-providers"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
|
||||
export type ModelKey = { providerID: string; modelID: string }
|
||||
|
||||
type Visibility = "show" | "hide"
|
||||
type User = ModelKey & { visibility: Visibility; favorite?: boolean }
|
||||
type Store = {
|
||||
user: User[]
|
||||
recent: ModelKey[]
|
||||
variant?: Record<string, string | undefined>
|
||||
}
|
||||
|
||||
export const { use: useModels, provider: ModelsProvider } = createSimpleContext({
|
||||
name: "Models",
|
||||
init: () => {
|
||||
const providers = useProviders()
|
||||
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
Persist.global("model", ["model.v1"]),
|
||||
createStore<Store>({
|
||||
user: [],
|
||||
recent: [],
|
||||
variant: {},
|
||||
}),
|
||||
)
|
||||
|
||||
const available = createMemo(() =>
|
||||
providers.connected().flatMap((p) =>
|
||||
Object.values(p.models).map((m) => ({
|
||||
...m,
|
||||
provider: p,
|
||||
})),
|
||||
),
|
||||
)
|
||||
|
||||
const latest = createMemo(() =>
|
||||
pipe(
|
||||
available(),
|
||||
filter((x) => Math.abs(DateTime.fromISO(x.release_date).diffNow().as("months")) < 6),
|
||||
groupBy((x) => x.provider.id),
|
||||
mapValues((models) =>
|
||||
pipe(
|
||||
models,
|
||||
groupBy((x) => x.family),
|
||||
values(),
|
||||
(groups) =>
|
||||
groups.flatMap((g) => {
|
||||
const first = firstBy(g, [(x) => x.release_date, "desc"])
|
||||
return first ? [{ modelID: first.id, providerID: first.provider.id }] : []
|
||||
}),
|
||||
),
|
||||
),
|
||||
values(),
|
||||
flat(),
|
||||
),
|
||||
)
|
||||
|
||||
const latestSet = createMemo(() => new Set(latest().map((x) => `${x.providerID}:${x.modelID}`)))
|
||||
|
||||
const visibility = createMemo(() => {
|
||||
const map = new Map<string, Visibility>()
|
||||
for (const item of store.user) map.set(`${item.providerID}:${item.modelID}`, item.visibility)
|
||||
return map
|
||||
})
|
||||
|
||||
const list = createMemo(() =>
|
||||
available().map((m) => ({
|
||||
...m,
|
||||
name: m.name.replace("(latest)", "").trim(),
|
||||
latest: m.name.includes("(latest)"),
|
||||
})),
|
||||
)
|
||||
|
||||
const find = (key: ModelKey) => list().find((m) => m.id === key.modelID && m.provider.id === key.providerID)
|
||||
|
||||
function update(model: ModelKey, state: Visibility) {
|
||||
const index = store.user.findIndex((x) => x.modelID === model.modelID && x.providerID === model.providerID)
|
||||
if (index >= 0) {
|
||||
setStore("user", index, { visibility: state })
|
||||
return
|
||||
}
|
||||
setStore("user", store.user.length, { ...model, visibility: state })
|
||||
}
|
||||
|
||||
const visible = (model: ModelKey) => {
|
||||
const key = `${model.providerID}:${model.modelID}`
|
||||
const state = visibility().get(key)
|
||||
if (state === "hide") return false
|
||||
if (state === "show") return true
|
||||
if (latestSet().has(key)) return true
|
||||
const m = find(model)
|
||||
if (!m?.release_date || !DateTime.fromISO(m.release_date).isValid) return true
|
||||
return false
|
||||
}
|
||||
|
||||
const setVisibility = (model: ModelKey, state: boolean) => {
|
||||
update(model, state ? "show" : "hide")
|
||||
}
|
||||
|
||||
const push = (model: ModelKey) => {
|
||||
const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID)
|
||||
if (uniq.length > 5) uniq.pop()
|
||||
setStore("recent", uniq)
|
||||
}
|
||||
|
||||
const variantKey = (model: ModelKey) => `${model.providerID}/${model.modelID}`
|
||||
const getVariant = (model: ModelKey) => store.variant?.[variantKey(model)]
|
||||
|
||||
const setVariant = (model: ModelKey, value: string | undefined) => {
|
||||
const key = variantKey(model)
|
||||
if (!store.variant) {
|
||||
setStore("variant", { [key]: value })
|
||||
return
|
||||
}
|
||||
setStore("variant", key, value)
|
||||
}
|
||||
|
||||
return {
|
||||
ready,
|
||||
list,
|
||||
find,
|
||||
visible,
|
||||
setVisibility,
|
||||
recent: {
|
||||
list: createMemo(() => store.recent),
|
||||
push,
|
||||
},
|
||||
variant: {
|
||||
get: getVariant,
|
||||
set: setVariant,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createEffect, createMemo, onCleanup } from "solid-js"
|
||||
import { createEffect, onCleanup } from "solid-js"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { useGlobalSDK } from "./global-sdk"
|
||||
@@ -52,15 +52,6 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
||||
const settings = useSettings()
|
||||
const language = useLanguage()
|
||||
|
||||
const empty: Notification[] = []
|
||||
|
||||
const currentDirectory = createMemo(() => {
|
||||
if (!params.dir) return
|
||||
return base64Decode(params.dir)
|
||||
})
|
||||
|
||||
const currentSession = createMemo(() => params.id)
|
||||
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
Persist.global("notification", ["notification.v1"]),
|
||||
createStore({
|
||||
@@ -81,59 +72,13 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
||||
setStore("list", (list) => pruneNotifications([...list, notification]))
|
||||
}
|
||||
|
||||
const index = createMemo(() => {
|
||||
const sessionAll = new Map<string, Notification[]>()
|
||||
const sessionUnseen = new Map<string, Notification[]>()
|
||||
const projectAll = new Map<string, Notification[]>()
|
||||
const projectUnseen = new Map<string, Notification[]>()
|
||||
|
||||
for (const notification of store.list) {
|
||||
const session = notification.session
|
||||
if (session) {
|
||||
const list = sessionAll.get(session)
|
||||
if (list) list.push(notification)
|
||||
else sessionAll.set(session, [notification])
|
||||
if (!notification.viewed) {
|
||||
const unseen = sessionUnseen.get(session)
|
||||
if (unseen) unseen.push(notification)
|
||||
else sessionUnseen.set(session, [notification])
|
||||
}
|
||||
}
|
||||
|
||||
const directory = notification.directory
|
||||
if (directory) {
|
||||
const list = projectAll.get(directory)
|
||||
if (list) list.push(notification)
|
||||
else projectAll.set(directory, [notification])
|
||||
if (!notification.viewed) {
|
||||
const unseen = projectUnseen.get(directory)
|
||||
if (unseen) unseen.push(notification)
|
||||
else projectUnseen.set(directory, [notification])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
session: {
|
||||
all: sessionAll,
|
||||
unseen: sessionUnseen,
|
||||
},
|
||||
project: {
|
||||
all: projectAll,
|
||||
unseen: projectUnseen,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const unsub = globalSDK.event.listen((e) => {
|
||||
const event = e.details
|
||||
if (event.type !== "session.idle" && event.type !== "session.error") return
|
||||
|
||||
const directory = e.name
|
||||
const event = e.details
|
||||
const time = Date.now()
|
||||
const activeDirectory = params.dir ? base64Decode(params.dir) : undefined
|
||||
const activeSession = params.id
|
||||
const viewed = (sessionID?: string) => {
|
||||
const activeDirectory = currentDirectory()
|
||||
const activeSession = currentSession()
|
||||
if (!activeDirectory) return false
|
||||
if (!activeSession) return false
|
||||
if (!sessionID) return false
|
||||
@@ -143,7 +88,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
||||
switch (event.type) {
|
||||
case "session.idle": {
|
||||
const sessionID = event.properties.sessionID
|
||||
const [syncStore] = globalSync.child(directory, { bootstrap: false })
|
||||
const [syncStore] = globalSync.child(directory)
|
||||
const match = Binary.search(syncStore.session, sessionID, (s) => s.id)
|
||||
const session = match.found ? syncStore.session[match.index] : undefined
|
||||
if (session?.parentID) break
|
||||
@@ -170,7 +115,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
||||
}
|
||||
case "session.error": {
|
||||
const sessionID = event.properties.sessionID
|
||||
const [syncStore] = globalSync.child(directory, { bootstrap: false })
|
||||
const [syncStore] = globalSync.child(directory)
|
||||
const match = sessionID ? Binary.search(syncStore.session, sessionID, (s) => s.id) : undefined
|
||||
const session = sessionID && match?.found ? syncStore.session[match.index] : undefined
|
||||
if (session?.parentID) break
|
||||
@@ -203,10 +148,10 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
||||
ready,
|
||||
session: {
|
||||
all(session: string) {
|
||||
return index().session.all.get(session) ?? empty
|
||||
return store.list.filter((n) => n.session === session)
|
||||
},
|
||||
unseen(session: string) {
|
||||
return index().session.unseen.get(session) ?? empty
|
||||
return store.list.filter((n) => n.session === session && !n.viewed)
|
||||
},
|
||||
markViewed(session: string) {
|
||||
setStore("list", (n) => n.session === session, "viewed", true)
|
||||
@@ -214,10 +159,10 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
||||
},
|
||||
project: {
|
||||
all(directory: string) {
|
||||
return index().project.all.get(directory) ?? empty
|
||||
return store.list.filter((n) => n.directory === directory)
|
||||
},
|
||||
unseen(directory: string) {
|
||||
return index().project.unseen.get(directory) ?? empty
|
||||
return store.list.filter((n) => n.directory === directory && !n.viewed)
|
||||
},
|
||||
markViewed(directory: string) {
|
||||
setStore("list", (n) => n.directory === directory, "viewed", true)
|
||||
|
||||
@@ -41,11 +41,11 @@ export type Platform = {
|
||||
/** Fetch override */
|
||||
fetch?: typeof fetch
|
||||
|
||||
/** Get the configured default server URL (platform-specific) */
|
||||
getDefaultServerUrl?(): Promise<string | null> | string | null
|
||||
/** Get the configured default server URL (desktop only) */
|
||||
getDefaultServerUrl?(): Promise<string | null>
|
||||
|
||||
/** Set the default server URL to use on app startup (platform-specific) */
|
||||
setDefaultServerUrl?(url: string | null): Promise<void> | void
|
||||
/** Set the default server URL to use on app startup (desktop only) */
|
||||
setDefaultServerUrl?(url: string | null): Promise<void>
|
||||
|
||||
/** Parse markdown to HTML using native parser (desktop only, returns unprocessed code blocks) */
|
||||
parseMarkdown?(markdown: string): Promise<string>
|
||||
|
||||
@@ -6,8 +6,6 @@ import { dict as en } from "@/i18n/en"
|
||||
import { dict as zh } from "@/i18n/zh"
|
||||
import pkg from "../package.json"
|
||||
|
||||
const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl"
|
||||
|
||||
const root = document.getElementById("root")
|
||||
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
||||
const locale = (() => {
|
||||
@@ -64,26 +62,6 @@ const platform: Platform = {
|
||||
})
|
||||
.catch(() => undefined)
|
||||
},
|
||||
getDefaultServerUrl: () => {
|
||||
if (typeof localStorage === "undefined") return null
|
||||
try {
|
||||
return localStorage.getItem(DEFAULT_SERVER_URL_KEY)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
},
|
||||
setDefaultServerUrl: (url) => {
|
||||
if (typeof localStorage === "undefined") return
|
||||
try {
|
||||
if (url) {
|
||||
localStorage.setItem(DEFAULT_SERVER_URL_KEY, url)
|
||||
return
|
||||
}
|
||||
localStorage.removeItem(DEFAULT_SERVER_URL_KEY)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
render(
|
||||
|
||||
+14
-22
@@ -100,7 +100,7 @@ export const dict = {
|
||||
"dialog.model.unpaid.freeModels.title": "نماذج مجانية مقدمة من OpenCode",
|
||||
"dialog.model.unpaid.addMore.title": "إضافة المزيد من النماذج من موفرين مشهورين",
|
||||
|
||||
"dialog.provider.viewAll": "عرض المزيد من الموفرين",
|
||||
"dialog.provider.viewAll": "عرض جميع الموفرين",
|
||||
|
||||
"provider.connect.title": "اتصال {{provider}}",
|
||||
"provider.connect.title.anthropicProMax": "تسجيل الدخول باستخدام Claude Pro/Max",
|
||||
@@ -304,20 +304,20 @@ export const dict = {
|
||||
"context.usage.clickToView": "انقر لعرض السياق",
|
||||
"context.usage.view": "عرض استخدام السياق",
|
||||
|
||||
"language.en": "English",
|
||||
"language.zh": "简体中文",
|
||||
"language.zht": "繁體中文",
|
||||
"language.ko": "한국어",
|
||||
"language.de": "Deutsch",
|
||||
"language.es": "Español",
|
||||
"language.fr": "Français",
|
||||
"language.da": "Dansk",
|
||||
"language.ja": "日本語",
|
||||
"language.pl": "Polski",
|
||||
"language.ru": "Русский",
|
||||
"language.en": "الإنجليزية",
|
||||
"language.zh": "الصينية (المبسطة)",
|
||||
"language.zht": "الصينية (التقليدية)",
|
||||
"language.ko": "الكورية",
|
||||
"language.de": "الألمانية",
|
||||
"language.es": "الإسبانية",
|
||||
"language.fr": "الفرنسية",
|
||||
"language.ja": "اليابانية",
|
||||
"language.da": "الدانماركية",
|
||||
"language.ru": "الروسية",
|
||||
"language.pl": "البولندية",
|
||||
"language.ar": "العربية",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.no": "النرويجية",
|
||||
"language.br": "البرتغالية (البرازيل)",
|
||||
|
||||
"toast.language.title": "لغة",
|
||||
"toast.language.description": "تم التبديل إلى {{language}}",
|
||||
@@ -426,14 +426,6 @@ export const dict = {
|
||||
"session.header.search.placeholder": "بحث {{project}}",
|
||||
"session.header.searchFiles": "بحث عن الملفات",
|
||||
|
||||
"status.popover.trigger": "الحالة",
|
||||
"status.popover.ariaLabel": "إعدادات الخوادم",
|
||||
"status.popover.tab.servers": "الخوادم",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "الإضافات",
|
||||
"status.popover.action.manageServers": "إدارة الخوادم",
|
||||
|
||||
"session.share.popover.title": "نشر على الويب",
|
||||
"session.share.popover.description.shared": "هذه الجلسة عامة على الويب. يمكن لأي شخص لديه الرابط الوصول إليها.",
|
||||
"session.share.popover.description.unshared": "شارك الجلسة علنًا على الويب. ستكون متاحة لأي شخص لديه الرابط.",
|
||||
|
||||
+14
-22
@@ -100,7 +100,7 @@ export const dict = {
|
||||
"dialog.model.unpaid.freeModels.title": "Modelos gratuitos fornecidos pelo OpenCode",
|
||||
"dialog.model.unpaid.addMore.title": "Adicionar mais modelos de provedores populares",
|
||||
|
||||
"dialog.provider.viewAll": "Ver mais provedores",
|
||||
"dialog.provider.viewAll": "Ver todos os provedores",
|
||||
|
||||
"provider.connect.title": "Conectar {{provider}}",
|
||||
"provider.connect.title.anthropicProMax": "Entrar com Claude Pro/Max",
|
||||
@@ -298,19 +298,19 @@ export const dict = {
|
||||
"context.usage.clickToView": "Clique para ver o contexto",
|
||||
"context.usage.view": "Ver uso do contexto",
|
||||
|
||||
"language.en": "English",
|
||||
"language.zh": "简体中文",
|
||||
"language.zht": "繁體中文",
|
||||
"language.ko": "한국어",
|
||||
"language.de": "Deutsch",
|
||||
"language.es": "Español",
|
||||
"language.fr": "Français",
|
||||
"language.da": "Dansk",
|
||||
"language.ja": "日本語",
|
||||
"language.pl": "Polski",
|
||||
"language.ru": "Русский",
|
||||
"language.ar": "العربية",
|
||||
"language.no": "Norsk",
|
||||
"language.en": "Inglês",
|
||||
"language.zh": "Chinês (Simplificado)",
|
||||
"language.zht": "Chinês (Tradicional)",
|
||||
"language.ko": "Coreano",
|
||||
"language.de": "Alemão",
|
||||
"language.es": "Espanhol",
|
||||
"language.fr": "Francês",
|
||||
"language.ja": "Japonês",
|
||||
"language.da": "Dinamarquês",
|
||||
"language.ru": "Russo",
|
||||
"language.pl": "Polonês",
|
||||
"language.ar": "Árabe",
|
||||
"language.no": "Norueguês",
|
||||
"language.br": "Português (Brasil)",
|
||||
|
||||
"toast.language.title": "Idioma",
|
||||
@@ -422,14 +422,6 @@ export const dict = {
|
||||
"session.header.search.placeholder": "Buscar {{project}}",
|
||||
"session.header.searchFiles": "Buscar arquivos",
|
||||
|
||||
"status.popover.trigger": "Status",
|
||||
"status.popover.ariaLabel": "Configurações de servidores",
|
||||
"status.popover.tab.servers": "Servidores",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "Plugins",
|
||||
"status.popover.action.manageServers": "Gerenciar servidores",
|
||||
|
||||
"session.share.popover.title": "Publicar na web",
|
||||
"session.share.popover.description.shared":
|
||||
"Esta sessão é pública na web. Está acessível para qualquer pessoa com o link.",
|
||||
|
||||
+13
-21
@@ -98,7 +98,7 @@ export const dict = {
|
||||
"dialog.model.unpaid.freeModels.title": "Gratis modeller leveret af OpenCode",
|
||||
"dialog.model.unpaid.addMore.title": "Tilføj flere modeller fra populære udbydere",
|
||||
|
||||
"dialog.provider.viewAll": "Vis flere udbydere",
|
||||
"dialog.provider.viewAll": "Vis alle udbydere",
|
||||
|
||||
"provider.connect.title": "Forbind {{provider}}",
|
||||
"provider.connect.title.anthropicProMax": "Log ind med Claude Pro/Max",
|
||||
@@ -287,20 +287,20 @@ export const dict = {
|
||||
"context.usage.clickToView": "Klik for at se kontekst",
|
||||
"context.usage.view": "Se kontekstforbrug",
|
||||
|
||||
"language.en": "English",
|
||||
"language.zh": "简体中文",
|
||||
"language.zht": "繁體中文",
|
||||
"language.ko": "한국어",
|
||||
"language.de": "Deutsch",
|
||||
"language.es": "Español",
|
||||
"language.fr": "Français",
|
||||
"language.en": "Engelsk",
|
||||
"language.zh": "Kinesisk (forenklet)",
|
||||
"language.zht": "Kinesisk (traditionelt)",
|
||||
"language.ko": "Koreansk",
|
||||
"language.de": "Tysk",
|
||||
"language.es": "Spansk",
|
||||
"language.fr": "Fransk",
|
||||
"language.ja": "Japansk",
|
||||
"language.da": "Dansk",
|
||||
"language.ja": "日本語",
|
||||
"language.pl": "Polski",
|
||||
"language.ru": "Русский",
|
||||
"language.ar": "العربية",
|
||||
"language.ru": "Russisk",
|
||||
"language.pl": "Polsk",
|
||||
"language.ar": "Arabisk",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.br": "Portugisisk (Brasilien)",
|
||||
|
||||
"toast.language.title": "Sprog",
|
||||
"toast.language.description": "Skiftede til {{language}}",
|
||||
@@ -409,14 +409,6 @@ export const dict = {
|
||||
"session.header.search.placeholder": "Søg {{project}}",
|
||||
"session.header.searchFiles": "Søg efter filer",
|
||||
|
||||
"status.popover.trigger": "Status",
|
||||
"status.popover.ariaLabel": "Serverkonfigurationer",
|
||||
"status.popover.tab.servers": "Servere",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "Plugins",
|
||||
"status.popover.action.manageServers": "Administrer servere",
|
||||
|
||||
"session.share.popover.title": "Udgiv på nettet",
|
||||
"session.share.popover.description.shared":
|
||||
"Denne session er offentlig på nettet. Den er tilgængelig for alle med linket.",
|
||||
|
||||
+14
-22
@@ -102,7 +102,7 @@ export const dict = {
|
||||
"dialog.model.unpaid.freeModels.title": "Kostenlose Modelle von OpenCode",
|
||||
"dialog.model.unpaid.addMore.title": "Weitere Modelle von beliebten Anbietern hinzufügen",
|
||||
|
||||
"dialog.provider.viewAll": "Mehr Anbieter anzeigen",
|
||||
"dialog.provider.viewAll": "Alle Anbieter anzeigen",
|
||||
|
||||
"provider.connect.title": "{{provider}} verbinden",
|
||||
"provider.connect.title.anthropicProMax": "Mit Claude Pro/Max anmelden",
|
||||
@@ -292,20 +292,20 @@ export const dict = {
|
||||
"context.usage.clickToView": "Klicken, um Kontext anzuzeigen",
|
||||
"context.usage.view": "Kontextnutzung anzeigen",
|
||||
|
||||
"language.en": "English",
|
||||
"language.zh": "简体中文",
|
||||
"language.zht": "繁體中文",
|
||||
"language.ko": "한국어",
|
||||
"language.en": "Englisch",
|
||||
"language.zh": "Chinesisch (Vereinfacht)",
|
||||
"language.zht": "Chinesisch (Traditionell)",
|
||||
"language.ko": "Koreanisch",
|
||||
"language.de": "Deutsch",
|
||||
"language.es": "Español",
|
||||
"language.fr": "Français",
|
||||
"language.da": "Dansk",
|
||||
"language.ja": "日本語",
|
||||
"language.pl": "Polski",
|
||||
"language.ru": "Русский",
|
||||
"language.ar": "العربية",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.es": "Spanisch",
|
||||
"language.fr": "Französisch",
|
||||
"language.ja": "Japanisch",
|
||||
"language.da": "Dänisch",
|
||||
"language.ru": "Russisch",
|
||||
"language.pl": "Polnisch",
|
||||
"language.ar": "Arabisch",
|
||||
"language.no": "Norwegisch",
|
||||
"language.br": "Portugiesisch (Brasilien)",
|
||||
|
||||
"toast.language.title": "Sprache",
|
||||
"toast.language.description": "Zu {{language}} gewechselt",
|
||||
@@ -416,14 +416,6 @@ export const dict = {
|
||||
"session.header.search.placeholder": "{{project}} durchsuchen",
|
||||
"session.header.searchFiles": "Dateien suchen",
|
||||
|
||||
"status.popover.trigger": "Status",
|
||||
"status.popover.ariaLabel": "Serverkonfigurationen",
|
||||
"status.popover.tab.servers": "Server",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "Plugins",
|
||||
"status.popover.action.manageServers": "Server verwalten",
|
||||
|
||||
"session.share.popover.title": "Im Web veröffentlichen",
|
||||
"session.share.popover.description.shared":
|
||||
"Diese Sitzung ist öffentlich im Web. Sie ist für jeden mit dem Link zugänglich.",
|
||||
|
||||
+14
-35
@@ -100,7 +100,7 @@ export const dict = {
|
||||
"dialog.model.unpaid.freeModels.title": "Free models provided by OpenCode",
|
||||
"dialog.model.unpaid.addMore.title": "Add more models from popular providers",
|
||||
|
||||
"dialog.provider.viewAll": "Show more providers",
|
||||
"dialog.provider.viewAll": "View all providers",
|
||||
|
||||
"provider.connect.title": "Connect {{provider}}",
|
||||
"provider.connect.title.anthropicProMax": "Login with Claude Pro/Max",
|
||||
@@ -137,9 +137,6 @@ export const dict = {
|
||||
"provider.connect.toast.connected.title": "{{provider}} connected",
|
||||
"provider.connect.toast.connected.description": "{{provider}} models are now available to use.",
|
||||
|
||||
"provider.disconnect.toast.disconnected.title": "{{provider}} disconnected",
|
||||
"provider.disconnect.toast.disconnected.description": "{{provider}} models are no longer available.",
|
||||
|
||||
"model.tag.free": "Free",
|
||||
"model.tag.latest": "Latest",
|
||||
"model.provider.anthropic": "Anthropic",
|
||||
@@ -162,8 +159,6 @@ export const dict = {
|
||||
"common.loading": "Loading",
|
||||
"common.loading.ellipsis": "...",
|
||||
"common.cancel": "Cancel",
|
||||
"common.connect": "Connect",
|
||||
"common.disconnect": "Disconnect",
|
||||
"common.submit": "Submit",
|
||||
"common.save": "Save",
|
||||
"common.saving": "Saving...",
|
||||
@@ -313,19 +308,19 @@ export const dict = {
|
||||
"context.usage.view": "View context usage",
|
||||
|
||||
"language.en": "English",
|
||||
"language.zh": "简体中文",
|
||||
"language.zht": "繁體中文",
|
||||
"language.ko": "한국어",
|
||||
"language.de": "Deutsch",
|
||||
"language.es": "Español",
|
||||
"language.fr": "Français",
|
||||
"language.da": "Dansk",
|
||||
"language.ja": "日本語",
|
||||
"language.pl": "Polski",
|
||||
"language.ru": "Русский",
|
||||
"language.ar": "العربية",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.zh": "Chinese (Simplified)",
|
||||
"language.zht": "Chinese (Traditional)",
|
||||
"language.ko": "Korean",
|
||||
"language.de": "German",
|
||||
"language.es": "Spanish",
|
||||
"language.fr": "French",
|
||||
"language.ja": "Japanese",
|
||||
"language.da": "Danish",
|
||||
"language.ru": "Russian",
|
||||
"language.pl": "Polish",
|
||||
"language.ar": "Arabic",
|
||||
"language.no": "Norwegian",
|
||||
"language.br": "Portuguese (Brazil)",
|
||||
|
||||
"toast.language.title": "Language",
|
||||
"toast.language.description": "Switched to {{language}}",
|
||||
@@ -435,14 +430,6 @@ export const dict = {
|
||||
"session.header.search.placeholder": "Search {{project}}",
|
||||
"session.header.searchFiles": "Search files",
|
||||
|
||||
"status.popover.trigger": "Status",
|
||||
"status.popover.ariaLabel": "Server configurations",
|
||||
"status.popover.tab.servers": "Servers",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "Plugins",
|
||||
"status.popover.action.manageServers": "Manage servers",
|
||||
|
||||
"session.share.popover.title": "Publish on web",
|
||||
"session.share.popover.description.shared":
|
||||
"This session is public on the web. It is accessible to anyone with the link.",
|
||||
@@ -496,7 +483,6 @@ export const dict = {
|
||||
"sidebar.project.viewAllSessions": "View all sessions",
|
||||
|
||||
"settings.section.desktop": "Desktop",
|
||||
"settings.section.server": "Server",
|
||||
"settings.tab.general": "General",
|
||||
"settings.tab.shortcuts": "Shortcuts",
|
||||
|
||||
@@ -605,13 +591,6 @@ export const dict = {
|
||||
|
||||
"settings.providers.title": "Providers",
|
||||
"settings.providers.description": "Provider settings will be configurable here.",
|
||||
"settings.providers.section.connected": "Connected providers",
|
||||
"settings.providers.connected.empty": "No connected providers",
|
||||
"settings.providers.section.popular": "Popular providers",
|
||||
"settings.providers.tag.environment": "Environment",
|
||||
"settings.providers.tag.config": "Config",
|
||||
"settings.providers.tag.custom": "Custom",
|
||||
"settings.providers.tag.other": "Other",
|
||||
"settings.models.title": "Models",
|
||||
"settings.models.description": "Model settings will be configurable here.",
|
||||
"settings.agents.title": "Agents",
|
||||
|
||||
+14
-22
@@ -98,7 +98,7 @@ export const dict = {
|
||||
"dialog.model.unpaid.freeModels.title": "Modelos gratuitos proporcionados por OpenCode",
|
||||
"dialog.model.unpaid.addMore.title": "Añadir más modelos de proveedores populares",
|
||||
|
||||
"dialog.provider.viewAll": "Ver más proveedores",
|
||||
"dialog.provider.viewAll": "Ver todos los proveedores",
|
||||
|
||||
"provider.connect.title": "Conectar {{provider}}",
|
||||
"provider.connect.title.anthropicProMax": "Iniciar sesión con Claude Pro/Max",
|
||||
@@ -287,20 +287,20 @@ export const dict = {
|
||||
"context.usage.clickToView": "Haz clic para ver contexto",
|
||||
"context.usage.view": "Ver uso del contexto",
|
||||
|
||||
"language.en": "English",
|
||||
"language.zh": "简体中文",
|
||||
"language.zht": "繁體中文",
|
||||
"language.ko": "한국어",
|
||||
"language.de": "Deutsch",
|
||||
"language.en": "Inglés",
|
||||
"language.zh": "Chino (simplificado)",
|
||||
"language.zht": "Chino (tradicional)",
|
||||
"language.ko": "Coreano",
|
||||
"language.de": "Alemán",
|
||||
"language.es": "Español",
|
||||
"language.fr": "Français",
|
||||
"language.da": "Dansk",
|
||||
"language.ja": "日本語",
|
||||
"language.pl": "Polski",
|
||||
"language.ru": "Русский",
|
||||
"language.ar": "العربية",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.fr": "Francés",
|
||||
"language.ja": "Japonés",
|
||||
"language.da": "Danés",
|
||||
"language.ru": "Ruso",
|
||||
"language.pl": "Polaco",
|
||||
"language.ar": "Árabe",
|
||||
"language.no": "Noruego",
|
||||
"language.br": "Portugués (Brasil)",
|
||||
|
||||
"toast.language.title": "Idioma",
|
||||
"toast.language.description": "Cambiado a {{language}}",
|
||||
@@ -410,14 +410,6 @@ export const dict = {
|
||||
"session.header.search.placeholder": "Buscar {{project}}",
|
||||
"session.header.searchFiles": "Buscar archivos",
|
||||
|
||||
"status.popover.trigger": "Estado",
|
||||
"status.popover.ariaLabel": "Configuraciones del servidor",
|
||||
"status.popover.tab.servers": "Servidores",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "Plugins",
|
||||
"status.popover.action.manageServers": "Administrar servidores",
|
||||
|
||||
"session.share.popover.title": "Publicar en web",
|
||||
"session.share.popover.description.shared":
|
||||
"Esta sesión es pública en la web. Es accesible para cualquiera con el enlace.",
|
||||
|
||||
+14
-22
@@ -98,7 +98,7 @@ export const dict = {
|
||||
"dialog.model.unpaid.freeModels.title": "Modèles gratuits fournis par OpenCode",
|
||||
"dialog.model.unpaid.addMore.title": "Ajouter plus de modèles de fournisseurs populaires",
|
||||
|
||||
"dialog.provider.viewAll": "Voir plus de fournisseurs",
|
||||
"dialog.provider.viewAll": "Voir tous les fournisseurs",
|
||||
|
||||
"provider.connect.title": "Connecter {{provider}}",
|
||||
"provider.connect.title.anthropicProMax": "Connexion avec Claude Pro/Max",
|
||||
@@ -287,20 +287,20 @@ export const dict = {
|
||||
"context.usage.clickToView": "Cliquez pour voir le contexte",
|
||||
"context.usage.view": "Voir l'utilisation du contexte",
|
||||
|
||||
"language.en": "English",
|
||||
"language.zh": "简体中文",
|
||||
"language.zht": "繁體中文",
|
||||
"language.ko": "한국어",
|
||||
"language.de": "Deutsch",
|
||||
"language.es": "Español",
|
||||
"language.en": "Anglais",
|
||||
"language.zh": "Chinois (simplifié)",
|
||||
"language.zht": "Chinois (traditionnel)",
|
||||
"language.ko": "Coréen",
|
||||
"language.de": "Allemand",
|
||||
"language.es": "Espagnol",
|
||||
"language.fr": "Français",
|
||||
"language.da": "Dansk",
|
||||
"language.ja": "日本語",
|
||||
"language.pl": "Polski",
|
||||
"language.ru": "Русский",
|
||||
"language.ar": "العربية",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.ja": "Japonais",
|
||||
"language.da": "Danois",
|
||||
"language.ru": "Russe",
|
||||
"language.pl": "Polonais",
|
||||
"language.ar": "Arabe",
|
||||
"language.no": "Norvégien",
|
||||
"language.br": "Portugais (Brésil)",
|
||||
|
||||
"toast.language.title": "Langue",
|
||||
"toast.language.description": "Passé à {{language}}",
|
||||
@@ -415,14 +415,6 @@ export const dict = {
|
||||
"session.header.search.placeholder": "Rechercher {{project}}",
|
||||
"session.header.searchFiles": "Rechercher des fichiers",
|
||||
|
||||
"status.popover.trigger": "Statut",
|
||||
"status.popover.ariaLabel": "Configurations des serveurs",
|
||||
"status.popover.tab.servers": "Serveurs",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "Plugins",
|
||||
"status.popover.action.manageServers": "Gérer les serveurs",
|
||||
|
||||
"session.share.popover.title": "Publier sur le web",
|
||||
"session.share.popover.description.shared":
|
||||
"Cette session est publique sur le web. Elle est accessible à toute personne disposant du lien.",
|
||||
|
||||
+14
-22
@@ -98,7 +98,7 @@ export const dict = {
|
||||
"dialog.model.unpaid.freeModels.title": "OpenCodeが提供する無料モデル",
|
||||
"dialog.model.unpaid.addMore.title": "人気のプロバイダーからモデルを追加",
|
||||
|
||||
"dialog.provider.viewAll": "さらにプロバイダーを表示",
|
||||
"dialog.provider.viewAll": "すべてのプロバイダーを表示",
|
||||
|
||||
"provider.connect.title": "{{provider}}を接続",
|
||||
"provider.connect.title.anthropicProMax": "Claude Pro/Maxでログイン",
|
||||
@@ -285,20 +285,20 @@ export const dict = {
|
||||
"context.usage.clickToView": "クリックしてコンテキストを表示",
|
||||
"context.usage.view": "コンテキスト使用量を表示",
|
||||
|
||||
"language.en": "English",
|
||||
"language.zh": "简体中文",
|
||||
"language.zht": "繁體中文",
|
||||
"language.ko": "한국어",
|
||||
"language.de": "Deutsch",
|
||||
"language.es": "Español",
|
||||
"language.fr": "Français",
|
||||
"language.da": "Dansk",
|
||||
"language.en": "英語",
|
||||
"language.zh": "中国語(簡体字)",
|
||||
"language.zht": "中国語(繁体字)",
|
||||
"language.ko": "韓国語",
|
||||
"language.de": "ドイツ語",
|
||||
"language.es": "スペイン語",
|
||||
"language.fr": "フランス語",
|
||||
"language.ja": "日本語",
|
||||
"language.pl": "Polski",
|
||||
"language.ru": "Русский",
|
||||
"language.ar": "العربية",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.da": "デンマーク語",
|
||||
"language.ru": "ロシア語",
|
||||
"language.pl": "ポーランド語",
|
||||
"language.ar": "アラビア語",
|
||||
"language.no": "ノルウェー語",
|
||||
"language.br": "ポルトガル語(ブラジル)",
|
||||
|
||||
"toast.language.title": "言語",
|
||||
"toast.language.description": "{{language}}に切り替えました",
|
||||
@@ -407,14 +407,6 @@ export const dict = {
|
||||
"session.header.search.placeholder": "{{project}}を検索",
|
||||
"session.header.searchFiles": "ファイルを検索",
|
||||
|
||||
"status.popover.trigger": "ステータス",
|
||||
"status.popover.ariaLabel": "サーバー設定",
|
||||
"status.popover.tab.servers": "サーバー",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "プラグイン",
|
||||
"status.popover.action.manageServers": "サーバーを管理",
|
||||
|
||||
"session.share.popover.title": "ウェブで公開",
|
||||
"session.share.popover.description.shared":
|
||||
"このセッションはウェブで公開されています。リンクを知っている人なら誰でもアクセスできます。",
|
||||
|
||||
+14
-22
@@ -102,7 +102,7 @@ export const dict = {
|
||||
"dialog.model.unpaid.freeModels.title": "OpenCode에서 제공하는 무료 모델",
|
||||
"dialog.model.unpaid.addMore.title": "인기 공급자의 모델 추가",
|
||||
|
||||
"dialog.provider.viewAll": "더 많은 공급자 보기",
|
||||
"dialog.provider.viewAll": "모든 공급자 보기",
|
||||
|
||||
"provider.connect.title": "{{provider}} 연결",
|
||||
"provider.connect.title.anthropicProMax": "Claude Pro/Max로 로그인",
|
||||
@@ -289,20 +289,20 @@ export const dict = {
|
||||
"context.usage.clickToView": "컨텍스트를 보려면 클릭",
|
||||
"context.usage.view": "컨텍스트 사용량 보기",
|
||||
|
||||
"language.en": "English",
|
||||
"language.zh": "简体中文",
|
||||
"language.zht": "繁體中文",
|
||||
"language.en": "영어",
|
||||
"language.zh": "중국어 (간체)",
|
||||
"language.zht": "중국어 (번체)",
|
||||
"language.ko": "한국어",
|
||||
"language.de": "Deutsch",
|
||||
"language.es": "Español",
|
||||
"language.fr": "Français",
|
||||
"language.da": "Dansk",
|
||||
"language.ja": "日本語",
|
||||
"language.pl": "Polski",
|
||||
"language.ru": "Русский",
|
||||
"language.ar": "العربية",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.de": "독일어",
|
||||
"language.es": "스페인어",
|
||||
"language.fr": "프랑스어",
|
||||
"language.ja": "일본어",
|
||||
"language.da": "덴마크어",
|
||||
"language.ru": "러시아어",
|
||||
"language.pl": "폴란드어",
|
||||
"language.ar": "아랍어",
|
||||
"language.no": "노르웨이어",
|
||||
"language.br": "포르투갈어 (브라질)",
|
||||
|
||||
"toast.language.title": "언어",
|
||||
"toast.language.description": "{{language}}(으)로 전환됨",
|
||||
@@ -410,14 +410,6 @@ export const dict = {
|
||||
"session.header.search.placeholder": "{{project}} 검색",
|
||||
"session.header.searchFiles": "파일 검색",
|
||||
|
||||
"status.popover.trigger": "상태",
|
||||
"status.popover.ariaLabel": "서버 구성",
|
||||
"status.popover.tab.servers": "서버",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "플러그인",
|
||||
"status.popover.action.manageServers": "서버 관리",
|
||||
|
||||
"session.share.popover.title": "웹에 게시",
|
||||
"session.share.popover.description.shared": "이 세션은 웹에 공개되었습니다. 링크가 있는 누구나 액세스할 수 있습니다.",
|
||||
"session.share.popover.description.unshared":
|
||||
|
||||
+13
-21
@@ -103,7 +103,7 @@ export const dict = {
|
||||
"dialog.model.unpaid.freeModels.title": "Gratis modeller levert av OpenCode",
|
||||
"dialog.model.unpaid.addMore.title": "Legg til flere modeller fra populære leverandører",
|
||||
|
||||
"dialog.provider.viewAll": "Vis flere leverandører",
|
||||
"dialog.provider.viewAll": "Vis alle leverandører",
|
||||
|
||||
"provider.connect.title": "Koble til {{provider}}",
|
||||
"provider.connect.title.anthropicProMax": "Logg inn med Claude Pro/Max",
|
||||
@@ -307,20 +307,20 @@ export const dict = {
|
||||
"context.usage.clickToView": "Klikk for å se kontekst",
|
||||
"context.usage.view": "Se kontekstforbruk",
|
||||
|
||||
"language.en": "English",
|
||||
"language.zh": "简体中文",
|
||||
"language.zht": "繁體中文",
|
||||
"language.ko": "한국어",
|
||||
"language.de": "Deutsch",
|
||||
"language.es": "Español",
|
||||
"language.fr": "Français",
|
||||
"language.en": "Engelsk",
|
||||
"language.zh": "Kinesisk (forenklet)",
|
||||
"language.zht": "Kinesisk (tradisjonell)",
|
||||
"language.ko": "Koreansk",
|
||||
"language.de": "Tysk",
|
||||
"language.es": "Spansk",
|
||||
"language.fr": "Fransk",
|
||||
"language.ja": "Japansk",
|
||||
"language.da": "Dansk",
|
||||
"language.ja": "日本語",
|
||||
"language.pl": "Polski",
|
||||
"language.ru": "Русский",
|
||||
"language.ar": "العربية",
|
||||
"language.ru": "Russisk",
|
||||
"language.pl": "Polsk",
|
||||
"language.ar": "Arabisk",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.br": "Portugisisk (Brasil)",
|
||||
|
||||
"toast.language.title": "Språk",
|
||||
"toast.language.description": "Byttet til {{language}}",
|
||||
@@ -430,14 +430,6 @@ export const dict = {
|
||||
"session.header.search.placeholder": "Søk i {{project}}",
|
||||
"session.header.searchFiles": "Søk etter filer",
|
||||
|
||||
"status.popover.trigger": "Status",
|
||||
"status.popover.ariaLabel": "Serverkonfigurasjoner",
|
||||
"status.popover.tab.servers": "Servere",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "Plugins",
|
||||
"status.popover.action.manageServers": "Administrer servere",
|
||||
|
||||
"session.share.popover.title": "Publiser på nett",
|
||||
"session.share.popover.description.shared":
|
||||
"Denne sesjonen er offentlig på nettet. Den er tilgjengelig for alle med lenken.",
|
||||
|
||||
+13
-22
@@ -100,7 +100,7 @@ export const dict = {
|
||||
"dialog.model.unpaid.freeModels.title": "Darmowe modele dostarczane przez OpenCode",
|
||||
"dialog.model.unpaid.addMore.title": "Dodaj więcej modeli od popularnych dostawców",
|
||||
|
||||
"dialog.provider.viewAll": "Zobacz więcej dostawców",
|
||||
"dialog.provider.viewAll": "Zobacz wszystkich dostawców",
|
||||
|
||||
"provider.connect.title": "Połącz {{provider}}",
|
||||
"provider.connect.title.anthropicProMax": "Zaloguj się z Claude Pro/Max",
|
||||
@@ -304,20 +304,19 @@ export const dict = {
|
||||
"context.usage.clickToView": "Kliknij, aby zobaczyć kontekst",
|
||||
"context.usage.view": "Pokaż użycie kontekstu",
|
||||
|
||||
"language.en": "English",
|
||||
"language.zh": "简体中文",
|
||||
"language.zht": "繁體中文",
|
||||
"language.ko": "한국어",
|
||||
"language.de": "Deutsch",
|
||||
"language.es": "Español",
|
||||
"language.fr": "Français",
|
||||
"language.da": "Dansk",
|
||||
"language.ja": "日本語",
|
||||
"language.en": "Angielski",
|
||||
"language.zh": "Chiński",
|
||||
"language.ko": "Koreański",
|
||||
"language.de": "Niemiecki",
|
||||
"language.es": "Hiszpański",
|
||||
"language.fr": "Francuski",
|
||||
"language.ja": "Japoński",
|
||||
"language.da": "Duński",
|
||||
"language.pl": "Polski",
|
||||
"language.ru": "Русский",
|
||||
"language.ar": "العربية",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.ru": "Rosyjski",
|
||||
"language.ar": "Arabski",
|
||||
"language.no": "Norweski",
|
||||
"language.br": "Portugalski (Brazylia)",
|
||||
|
||||
"toast.language.title": "Język",
|
||||
"toast.language.description": "Przełączono na {{language}}",
|
||||
@@ -428,14 +427,6 @@ export const dict = {
|
||||
"session.header.search.placeholder": "Szukaj {{project}}",
|
||||
"session.header.searchFiles": "Szukaj plików",
|
||||
|
||||
"status.popover.trigger": "Status",
|
||||
"status.popover.ariaLabel": "Konfiguracje serwerów",
|
||||
"status.popover.tab.servers": "Serwery",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "Wtyczki",
|
||||
"status.popover.action.manageServers": "Zarządzaj serwerami",
|
||||
|
||||
"session.share.popover.title": "Opublikuj w sieci",
|
||||
"session.share.popover.description.shared":
|
||||
"Ta sesja jest publiczna w sieci. Jest dostępna dla każdego, kto posiada link.",
|
||||
|
||||
+12
-22
@@ -100,7 +100,7 @@ export const dict = {
|
||||
"dialog.model.unpaid.freeModels.title": "Бесплатные модели от OpenCode",
|
||||
"dialog.model.unpaid.addMore.title": "Добавьте больше моделей от популярных провайдеров",
|
||||
|
||||
"dialog.provider.viewAll": "Показать больше провайдеров",
|
||||
"dialog.provider.viewAll": "Посмотреть всех провайдеров",
|
||||
|
||||
"provider.connect.title": "Подключить {{provider}}",
|
||||
"provider.connect.title.anthropicProMax": "Войти с помощью Claude Pro/Max",
|
||||
@@ -305,20 +305,18 @@ export const dict = {
|
||||
"context.usage.clickToView": "Нажмите для просмотра контекста",
|
||||
"context.usage.view": "Показать использование контекста",
|
||||
|
||||
"language.en": "English",
|
||||
"language.zh": "简体中文",
|
||||
"language.zht": "繁體中文",
|
||||
"language.ko": "한국어",
|
||||
"language.de": "Deutsch",
|
||||
"language.es": "Español",
|
||||
"language.fr": "Français",
|
||||
"language.da": "Dansk",
|
||||
"language.ja": "日本語",
|
||||
"language.pl": "Polski",
|
||||
"language.en": "Английский",
|
||||
"language.zh": "Китайский",
|
||||
"language.ko": "Корейский",
|
||||
"language.de": "Немецкий",
|
||||
"language.es": "Испанский",
|
||||
"language.fr": "Французский",
|
||||
"language.ja": "Японский",
|
||||
"language.da": "Датский",
|
||||
"language.ru": "Русский",
|
||||
"language.ar": "العربية",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.ar": "Арабский",
|
||||
"language.no": "Норвежский",
|
||||
"language.br": "Португальский (Бразилия)",
|
||||
|
||||
"toast.language.title": "Язык",
|
||||
"toast.language.description": "Переключено на {{language}}",
|
||||
@@ -430,14 +428,6 @@ export const dict = {
|
||||
"session.header.search.placeholder": "Поиск {{project}}",
|
||||
"session.header.searchFiles": "Поиск файлов",
|
||||
|
||||
"status.popover.trigger": "Статус",
|
||||
"status.popover.ariaLabel": "Настройки серверов",
|
||||
"status.popover.tab.servers": "Серверы",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "Плагины",
|
||||
"status.popover.action.manageServers": "Управлять серверами",
|
||||
|
||||
"session.share.popover.title": "Опубликовать в интернете",
|
||||
"session.share.popover.description.shared":
|
||||
"Эта сессия общедоступна. Доступ к ней может получить любой, у кого есть ссылка.",
|
||||
|
||||
+14
-22
@@ -102,7 +102,7 @@ export const dict = {
|
||||
"dialog.model.unpaid.freeModels.title": "OpenCode 提供的免费模型",
|
||||
"dialog.model.unpaid.addMore.title": "从热门提供商添加更多模型",
|
||||
|
||||
"dialog.provider.viewAll": "查看更多提供商",
|
||||
"dialog.provider.viewAll": "查看全部提供商",
|
||||
|
||||
"provider.connect.title": "连接 {{provider}}",
|
||||
"provider.connect.title.anthropicProMax": "使用 Claude Pro/Max 登录",
|
||||
@@ -285,20 +285,20 @@ export const dict = {
|
||||
"context.usage.clickToView": "点击查看上下文",
|
||||
"context.usage.view": "查看上下文用量",
|
||||
|
||||
"language.en": "English",
|
||||
"language.en": "英语",
|
||||
"language.zh": "简体中文",
|
||||
"language.zht": "繁體中文",
|
||||
"language.ko": "한국어",
|
||||
"language.de": "Deutsch",
|
||||
"language.es": "Español",
|
||||
"language.fr": "Français",
|
||||
"language.da": "Dansk",
|
||||
"language.ja": "日本語",
|
||||
"language.pl": "Polski",
|
||||
"language.ru": "Русский",
|
||||
"language.ar": "العربية",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.zht": "繁体中文",
|
||||
"language.ko": "韩语",
|
||||
"language.de": "德语",
|
||||
"language.es": "西班牙语",
|
||||
"language.fr": "法语",
|
||||
"language.ja": "日语",
|
||||
"language.da": "丹麦语",
|
||||
"language.ru": "俄语",
|
||||
"language.pl": "波兰语",
|
||||
"language.ar": "阿拉伯语",
|
||||
"language.no": "挪威语",
|
||||
"language.br": "葡萄牙语(巴西)",
|
||||
|
||||
"toast.language.title": "语言",
|
||||
"toast.language.description": "已切换到{{language}}",
|
||||
@@ -405,14 +405,6 @@ export const dict = {
|
||||
"session.header.search.placeholder": "搜索 {{project}}",
|
||||
"session.header.searchFiles": "搜索文件",
|
||||
|
||||
"status.popover.trigger": "状态",
|
||||
"status.popover.ariaLabel": "服务器配置",
|
||||
"status.popover.tab.servers": "服务器",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "插件",
|
||||
"status.popover.action.manageServers": "管理服务器",
|
||||
|
||||
"session.share.popover.title": "发布到网页",
|
||||
"session.share.popover.description.shared": "此会话已在网页上公开。任何拥有链接的人都可以访问。",
|
||||
"session.share.popover.description.unshared": "在网页上公开分享此会话。任何拥有链接的人都可以访问。",
|
||||
|
||||
@@ -102,7 +102,7 @@ export const dict = {
|
||||
"dialog.model.unpaid.freeModels.title": "OpenCode 提供的免費模型",
|
||||
"dialog.model.unpaid.addMore.title": "從熱門提供者新增更多模型",
|
||||
|
||||
"dialog.provider.viewAll": "查看更多提供者",
|
||||
"dialog.provider.viewAll": "查看全部提供者",
|
||||
|
||||
"provider.connect.title": "連線 {{provider}}",
|
||||
"provider.connect.title.anthropicProMax": "使用 Claude Pro/Max 登入",
|
||||
@@ -287,20 +287,14 @@ export const dict = {
|
||||
"context.usage.clickToView": "點擊查看上下文",
|
||||
"context.usage.view": "檢視上下文用量",
|
||||
|
||||
"language.en": "English",
|
||||
"language.zh": "简体中文",
|
||||
"language.en": "英語",
|
||||
"language.zh": "簡體中文",
|
||||
"language.zht": "繁體中文",
|
||||
"language.ko": "한국어",
|
||||
"language.de": "Deutsch",
|
||||
"language.es": "Español",
|
||||
"language.fr": "Français",
|
||||
"language.da": "Dansk",
|
||||
"language.ja": "日本語",
|
||||
"language.pl": "Polski",
|
||||
"language.ru": "Русский",
|
||||
"language.ar": "العربية",
|
||||
"language.no": "Norsk",
|
||||
"language.br": "Português (Brasil)",
|
||||
"language.ko": "韓語",
|
||||
"language.ru": "俄語",
|
||||
"language.ar": "阿拉伯語",
|
||||
"language.no": "挪威語",
|
||||
"language.br": "葡萄牙語(巴西)",
|
||||
|
||||
"toast.language.title": "語言",
|
||||
"toast.language.description": "已切換到 {{language}}",
|
||||
@@ -407,14 +401,6 @@ export const dict = {
|
||||
"session.header.search.placeholder": "搜尋 {{project}}",
|
||||
"session.header.searchFiles": "搜尋檔案",
|
||||
|
||||
"status.popover.trigger": "狀態",
|
||||
"status.popover.ariaLabel": "伺服器設定",
|
||||
"status.popover.tab.servers": "伺服器",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "外掛程式",
|
||||
"status.popover.action.manageServers": "管理伺服器",
|
||||
|
||||
"session.share.popover.title": "發佈到網頁",
|
||||
"session.share.popover.description.shared": "此工作階段已在網頁上公開。任何擁有連結的人都可以存取。",
|
||||
"session.share.popover.description.unshared": "在網頁上公開分享此工作階段。任何擁有連結的人都可以存取。",
|
||||
|
||||
@@ -23,11 +23,6 @@ export default function Home() {
|
||||
const server = useServer()
|
||||
const language = useLanguage()
|
||||
const homedir = createMemo(() => sync.data.path.home)
|
||||
const recent = createMemo(() => {
|
||||
return sync.data.project
|
||||
.toSorted((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
|
||||
.slice(0, 5)
|
||||
})
|
||||
|
||||
function openProject(directory: string) {
|
||||
layout.projects.open(directory)
|
||||
@@ -89,7 +84,11 @@ export default function Home() {
|
||||
</Button>
|
||||
</div>
|
||||
<ul class="flex flex-col gap-2">
|
||||
<For each={recent()}>
|
||||
<For
|
||||
each={sync.data.project
|
||||
.toSorted((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
|
||||
.slice(0, 5)}
|
||||
>
|
||||
{(project) => (
|
||||
<Button
|
||||
size="large"
|
||||
|
||||
+396
-517
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,6 @@ import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
import { useCodeComponent } from "@opencode-ai/ui/context/code"
|
||||
import { LineComment as LineCommentView, LineCommentEditor } from "@opencode-ai/ui/line-comment"
|
||||
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
||||
import { BasicTool } from "@opencode-ai/ui/basic-tool"
|
||||
import { createAutoScroll } from "@opencode-ai/ui/hooks"
|
||||
import { SessionReview } from "@opencode-ai/ui/session-review"
|
||||
import { Mark } from "@opencode-ai/ui/logo"
|
||||
@@ -185,40 +184,6 @@ export default function Page() {
|
||||
const prompt = usePrompt()
|
||||
const comments = useComments()
|
||||
const permission = usePermission()
|
||||
|
||||
const request = createMemo(() => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
const next = sync.data.permission[sessionID]?.[0]
|
||||
if (!next) return
|
||||
if (next.tool) return
|
||||
return next
|
||||
})
|
||||
|
||||
const [responding, setResponding] = createSignal(false)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => request()?.id,
|
||||
() => setResponding(false),
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
const decide = (response: "once" | "always" | "reject") => {
|
||||
const perm = request()
|
||||
if (!perm) return
|
||||
if (responding()) return
|
||||
|
||||
setResponding(true)
|
||||
sdk.client.permission
|
||||
.respond({ sessionID: perm.sessionID, permissionID: perm.id, response })
|
||||
.catch((err: unknown) => {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
showToast({ title: language.t("common.requestFailed"), description: message })
|
||||
})
|
||||
.finally(() => setResponding(false))
|
||||
}
|
||||
const [pendingMessage, setPendingMessage] = createSignal<string | undefined>(undefined)
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const tabs = createMemo(() => layout.tabs(sessionKey))
|
||||
@@ -1016,7 +981,7 @@ export default function Page() {
|
||||
|
||||
const activeTab = createMemo(() => {
|
||||
const active = tabs().active()
|
||||
if (active) return normalizeTab(active)
|
||||
if (active) return active
|
||||
if (hasReview()) return "review"
|
||||
|
||||
const first = openedTabs()[0]
|
||||
@@ -1725,56 +1690,6 @@ export default function Page() {
|
||||
"md:max-w-200": !showTabs(),
|
||||
}}
|
||||
>
|
||||
<Show when={request()} keyed>
|
||||
{(perm) => (
|
||||
<div data-component="tool-part-wrapper" data-permission="true" class="mb-3">
|
||||
<BasicTool
|
||||
icon="checklist"
|
||||
locked
|
||||
defaultOpen
|
||||
trigger={{
|
||||
title: language.t("notification.permission.title"),
|
||||
subtitle:
|
||||
perm.permission === "doom_loop"
|
||||
? language.t("settings.permissions.tool.doom_loop.title")
|
||||
: perm.permission,
|
||||
}}
|
||||
>
|
||||
<Show when={perm.patterns.length > 0}>
|
||||
<div class="flex flex-col gap-1 py-2 px-3 max-h-40 overflow-y-auto no-scrollbar">
|
||||
<For each={perm.patterns}>
|
||||
{(pattern) => <code class="text-12-regular text-text-base break-all">{pattern}</code>}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={perm.permission === "doom_loop"}>
|
||||
<div class="text-12-regular text-text-weak pb-2 px-3">
|
||||
{language.t("settings.permissions.tool.doom_loop.description")}
|
||||
</div>
|
||||
</Show>
|
||||
</BasicTool>
|
||||
<div data-component="permission-prompt">
|
||||
<div data-slot="permission-actions">
|
||||
<Button variant="ghost" size="small" onClick={() => decide("reject")} disabled={responding()}>
|
||||
{language.t("ui.permission.deny")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={() => decide("always")}
|
||||
disabled={responding()}
|
||||
>
|
||||
{language.t("ui.permission.allowAlways")}
|
||||
</Button>
|
||||
<Button variant="primary" size="small" onClick={() => decide("once")} disabled={responding()}>
|
||||
{language.t("ui.permission.allowOnce")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<Show
|
||||
when={prompt.ready()}
|
||||
fallback={
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
type Release = {
|
||||
tag_name: string
|
||||
name: string
|
||||
body: string
|
||||
published_at: string
|
||||
html_url: string
|
||||
}
|
||||
|
||||
type HighlightMedia = { type: "video"; src: string } | { type: "image"; src: string; width: string; height: string }
|
||||
|
||||
type HighlightItem = {
|
||||
title: string
|
||||
description: string
|
||||
shortDescription?: string
|
||||
media: HighlightMedia
|
||||
}
|
||||
|
||||
type HighlightGroup = {
|
||||
source: string
|
||||
items: HighlightItem[]
|
||||
}
|
||||
|
||||
const ok = "public, max-age=1, s-maxage=300, stale-while-revalidate=86400, stale-if-error=86400"
|
||||
const error = "public, max-age=1, s-maxage=60, stale-while-revalidate=600, stale-if-error=86400"
|
||||
|
||||
function parseHighlights(body: string): HighlightGroup[] {
|
||||
const groups = new Map<string, HighlightItem[]>()
|
||||
const regex = /<highlight\s+source="([^"]+)">([\s\S]*?)<\/highlight>/g
|
||||
let match
|
||||
|
||||
while ((match = regex.exec(body)) !== null) {
|
||||
const source = match[1]
|
||||
const content = match[2]
|
||||
|
||||
const titleMatch = content.match(/<h2>([^<]+)<\/h2>/)
|
||||
const pMatch = content.match(/<p(?:\s+short="([^"]*)")?>([^<]+)<\/p>/)
|
||||
const imgMatch = content.match(/<img\s+width="([^"]+)"\s+height="([^"]+)"\s+alt="[^"]*"\s+src="([^"]+)"/)
|
||||
const videoMatch = content.match(/^\s*(https:\/\/github\.com\/user-attachments\/assets\/[a-f0-9-]+)\s*$/m)
|
||||
|
||||
let media: HighlightMedia | undefined
|
||||
if (videoMatch) {
|
||||
media = { type: "video", src: videoMatch[1] }
|
||||
} else if (imgMatch) {
|
||||
media = { type: "image", src: imgMatch[3], width: imgMatch[1], height: imgMatch[2] }
|
||||
}
|
||||
|
||||
if (titleMatch && media) {
|
||||
const item: HighlightItem = {
|
||||
title: titleMatch[1],
|
||||
description: pMatch?.[2] || "",
|
||||
shortDescription: pMatch?.[1],
|
||||
media,
|
||||
}
|
||||
|
||||
if (!groups.has(source)) {
|
||||
groups.set(source, [])
|
||||
}
|
||||
groups.get(source)!.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(groups.entries()).map(([source, items]) => ({ source, items }))
|
||||
}
|
||||
|
||||
function parseMarkdown(body: string) {
|
||||
const lines = body.split("\n")
|
||||
const sections: { title: string; items: string[] }[] = []
|
||||
let current: { title: string; items: string[] } | null = null
|
||||
let skip = false
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("## ")) {
|
||||
if (current) sections.push(current)
|
||||
const title = line.slice(3).trim()
|
||||
current = { title, items: [] }
|
||||
skip = false
|
||||
} else if (line.startsWith("**Thank you")) {
|
||||
skip = true
|
||||
} else if (line.startsWith("- ") && !skip) {
|
||||
current?.items.push(line.slice(2).trim())
|
||||
}
|
||||
}
|
||||
if (current) sections.push(current)
|
||||
|
||||
const highlights = parseHighlights(body)
|
||||
|
||||
return { sections, highlights }
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const response = await fetch("https://api.github.com/repos/anomalyco/opencode/releases?per_page=20", {
|
||||
headers: {
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
"User-Agent": "OpenCode-Console",
|
||||
},
|
||||
cf: {
|
||||
// best-effort edge caching (ignored outside Cloudflare)
|
||||
cacheTtl: 60 * 5,
|
||||
cacheEverything: true,
|
||||
},
|
||||
} as any).catch(() => undefined)
|
||||
|
||||
const fail = () =>
|
||||
new Response(JSON.stringify({ releases: [] }), {
|
||||
status: 503,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": error,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response?.ok) return fail()
|
||||
|
||||
const data = await response.json().catch(() => undefined)
|
||||
if (!Array.isArray(data)) return fail()
|
||||
|
||||
const releases = data as Release[]
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
releases: releases.map((release) => {
|
||||
const parsed = parseMarkdown(release.body || "")
|
||||
return {
|
||||
tag: release.tag_name,
|
||||
name: release.name,
|
||||
date: release.published_at,
|
||||
url: release.html_url,
|
||||
highlights: parsed.highlights,
|
||||
sections: parsed.sections,
|
||||
}
|
||||
}),
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": ok,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -367,18 +367,11 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
position: sticky;
|
||||
top: 80px;
|
||||
align-self: start;
|
||||
background: var(--color-background);
|
||||
padding: 44px 0 8px;
|
||||
|
||||
@media (max-width: 50rem) {
|
||||
position: static;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
[data-slot="version"] {
|
||||
@@ -409,26 +402,24 @@
|
||||
|
||||
[data-component="section"] {
|
||||
h3 {
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-strong);
|
||||
margin-bottom: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding-left: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
|
||||
li {
|
||||
color: var(--color-text);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
padding-left: 12px;
|
||||
padding-left: 16px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
@@ -440,7 +431,7 @@
|
||||
|
||||
[data-slot="author"] {
|
||||
color: var(--color-text-weak);
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
margin-left: 4px;
|
||||
text-decoration: none;
|
||||
|
||||
@@ -474,120 +465,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="highlights"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
[data-component="collapsible-sections"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
[data-component="collapsible-section"] {
|
||||
[data-slot="toggle"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 6px 0;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-weak);
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
[data-slot="icon"] {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding-left: 16px;
|
||||
padding-bottom: 8px;
|
||||
|
||||
li {
|
||||
color: var(--color-text);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
padding-left: 12px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: "-";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--color-text-weak);
|
||||
}
|
||||
|
||||
[data-slot="author"] {
|
||||
color: var(--color-text-weak);
|
||||
font-size: 12px;
|
||||
margin-left: 4px;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
text-decoration-thickness: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="highlight"] {
|
||||
h4 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-strong);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--color-border-weak);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
[data-slot="highlight-item"] {
|
||||
margin-bottom: 48px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
p[data-slot="title"] {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
img,
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
|
||||
@@ -1,46 +1,35 @@
|
||||
import "./index.css"
|
||||
import { Title, Meta, Link } from "@solidjs/meta"
|
||||
import { createAsync } from "@solidjs/router"
|
||||
import { createAsync, query } from "@solidjs/router"
|
||||
import { Header } from "~/component/header"
|
||||
import { Footer } from "~/component/footer"
|
||||
import { Legal } from "~/component/legal"
|
||||
import { config } from "~/config"
|
||||
import { For, Show, createSignal } from "solid-js"
|
||||
import { getRequestEvent } from "solid-js/web"
|
||||
import { For, Show } from "solid-js"
|
||||
|
||||
type HighlightMedia = { type: "video"; src: string } | { type: "image"; src: string; width: string; height: string }
|
||||
|
||||
type HighlightItem = {
|
||||
title: string
|
||||
description: string
|
||||
shortDescription?: string
|
||||
media: HighlightMedia
|
||||
}
|
||||
|
||||
type HighlightGroup = {
|
||||
source: string
|
||||
items: HighlightItem[]
|
||||
}
|
||||
|
||||
type ChangelogRelease = {
|
||||
tag: string
|
||||
type Release = {
|
||||
tag_name: string
|
||||
name: string
|
||||
date: string
|
||||
url: string
|
||||
highlights: HighlightGroup[]
|
||||
sections: { title: string; items: string[] }[]
|
||||
body: string
|
||||
published_at: string
|
||||
html_url: string
|
||||
}
|
||||
|
||||
async function getReleases() {
|
||||
const event = getRequestEvent()
|
||||
const url = event ? new URL("/changelog.json", event.request.url).toString() : "/changelog.json"
|
||||
|
||||
const response = await fetch(url).catch(() => undefined)
|
||||
if (!response?.ok) return []
|
||||
|
||||
const json = await response.json().catch(() => undefined)
|
||||
return Array.isArray(json?.releases) ? (json.releases as ChangelogRelease[]) : []
|
||||
}
|
||||
const getReleases = query(async () => {
|
||||
"use server"
|
||||
const response = await fetch("https://api.github.com/repos/anomalyco/opencode/releases?per_page=20", {
|
||||
headers: {
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
"User-Agent": "OpenCode-Console",
|
||||
},
|
||||
cf: {
|
||||
cacheTtl: 60 * 5,
|
||||
cacheEverything: true,
|
||||
},
|
||||
} as any)
|
||||
if (!response.ok) return []
|
||||
return response.json() as Promise<Release[]>
|
||||
}, "releases.get")
|
||||
|
||||
function formatDate(dateString: string) {
|
||||
const date = new Date(dateString)
|
||||
@@ -51,6 +40,29 @@ function formatDate(dateString: string) {
|
||||
})
|
||||
}
|
||||
|
||||
function parseMarkdown(body: string) {
|
||||
const lines = body.split("\n")
|
||||
const sections: { title: string; items: string[] }[] = []
|
||||
let current: { title: string; items: string[] } | null = null
|
||||
let skip = false
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("## ")) {
|
||||
if (current) sections.push(current)
|
||||
const title = line.slice(3).trim()
|
||||
current = { title, items: [] }
|
||||
skip = false
|
||||
} else if (line.startsWith("**Thank you")) {
|
||||
skip = true
|
||||
} else if (line.startsWith("- ") && !skip) {
|
||||
current?.items.push(line.slice(2).trim())
|
||||
}
|
||||
}
|
||||
if (current) sections.push(current)
|
||||
|
||||
return { sections }
|
||||
}
|
||||
|
||||
function ReleaseItem(props: { item: string }) {
|
||||
const parts = () => {
|
||||
const match = props.item.match(/^(.+?)(\s*\(@([\w-]+)\))?$/)
|
||||
@@ -75,60 +87,6 @@ function ReleaseItem(props: { item: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
function HighlightSection(props: { group: HighlightGroup }) {
|
||||
return (
|
||||
<div data-component="highlight">
|
||||
<h4>{props.group.source}</h4>
|
||||
<hr />
|
||||
<For each={props.group.items}>
|
||||
{(item) => (
|
||||
<div data-slot="highlight-item">
|
||||
<p data-slot="title">{item.title}</p>
|
||||
<p>{item.description}</p>
|
||||
<Show when={item.media.type === "video"}>
|
||||
<video src={item.media.src} controls autoplay loop muted playsinline />
|
||||
</Show>
|
||||
<Show when={item.media.type === "image"}>
|
||||
<img
|
||||
src={item.media.src}
|
||||
alt={item.title}
|
||||
width={(item.media as { width: string }).width}
|
||||
height={(item.media as { height: string }).height}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CollapsibleSection(props: { section: { title: string; items: string[] } }) {
|
||||
const [open, setOpen] = createSignal(false)
|
||||
|
||||
return (
|
||||
<div data-component="collapsible-section">
|
||||
<button data-slot="toggle" onClick={() => setOpen(!open())}>
|
||||
<span data-slot="icon">{open() ? "▾" : "▸"}</span>
|
||||
<span>{props.section.title}</span>
|
||||
</button>
|
||||
<Show when={open()}>
|
||||
<ul>
|
||||
<For each={props.section.items}>{(item) => <ReleaseItem item={item} />}</For>
|
||||
</ul>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CollapsibleSections(props: { sections: { title: string; items: string[] }[] }) {
|
||||
return (
|
||||
<div data-component="collapsible-sections">
|
||||
<For each={props.sections}>{(section) => <CollapsibleSection section={section} />}</For>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Changelog() {
|
||||
const releases = createAsync(() => getReleases())
|
||||
|
||||
@@ -150,46 +108,37 @@ export default function Changelog() {
|
||||
<section data-component="releases">
|
||||
<For each={releases()}>
|
||||
{(release) => {
|
||||
const parsed = () => parseMarkdown(release.body || "")
|
||||
return (
|
||||
<article data-component="release">
|
||||
<header>
|
||||
<div data-slot="version">
|
||||
<a href={release.url} target="_blank" rel="noopener noreferrer">
|
||||
{release.tag}
|
||||
<a href={release.html_url} target="_blank" rel="noopener noreferrer">
|
||||
{release.tag_name}
|
||||
</a>
|
||||
</div>
|
||||
<time dateTime={release.date}>{formatDate(release.date)}</time>
|
||||
<time dateTime={release.published_at}>{formatDate(release.published_at)}</time>
|
||||
</header>
|
||||
<div data-slot="content">
|
||||
<Show when={release.highlights.length > 0}>
|
||||
<div data-component="highlights">
|
||||
<For each={release.highlights}>{(group) => <HighlightSection group={group} />}</For>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={release.highlights.length > 0 && release.sections.length > 0}>
|
||||
<CollapsibleSections sections={release.sections} />
|
||||
</Show>
|
||||
<Show when={release.highlights.length === 0}>
|
||||
<For each={release.sections}>
|
||||
{(section) => (
|
||||
<div data-component="section">
|
||||
<h3>{section.title}</h3>
|
||||
<ul>
|
||||
<For each={section.items}>{(item) => <ReleaseItem item={item} />}</For>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
<For each={parsed().sections}>
|
||||
{(section) => (
|
||||
<div data-component="section">
|
||||
<h3>{section.title}</h3>
|
||||
<ul>
|
||||
<For each={section.items}>{(item) => <ReleaseItem item={item} />}</For>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Legal />
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</head>
|
||||
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root" class="flex flex-col h-dvh p-px"></div>
|
||||
<div id="root" class="flex flex-col h-dvh"></div>
|
||||
<div data-tauri-decorum-tb class="w-0 h-0 hidden" />
|
||||
<script src="/src/index.tsx" type="module"></script>
|
||||
</body>
|
||||
|
||||
@@ -328,23 +328,18 @@ render(() => {
|
||||
const [serverPassword, setServerPassword] = createSignal<string | null>(null)
|
||||
const platform = createPlatform(() => serverPassword())
|
||||
|
||||
onMount(() => {
|
||||
// Handle external links - open in system browser instead of webview
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement
|
||||
const link = target.closest("a") as HTMLAnchorElement | null
|
||||
|
||||
if (link?.href && !link.href.startsWith("javascript:") && !link.href.startsWith("#")) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.stopImmediatePropagation()
|
||||
void shellOpen(link.href).catch(() => undefined)
|
||||
}
|
||||
function handleClick(e: MouseEvent) {
|
||||
const link = (e.target as HTMLElement).closest("a.external-link") as HTMLAnchorElement | null
|
||||
if (link?.href) {
|
||||
e.preventDefault()
|
||||
platform.openLink(link.href)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("click", handleClick, true)
|
||||
onMount(() => {
|
||||
document.addEventListener("click", handleClick)
|
||||
onCleanup(() => {
|
||||
document.removeEventListener("click", handleClick, true)
|
||||
document.removeEventListener("click", handleClick)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -82,8 +82,8 @@
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "1.5.2",
|
||||
"@opentui/core": "0.1.75",
|
||||
"@opentui/solid": "0.1.75",
|
||||
"@opentui/core": "0.1.74",
|
||||
"@opentui/solid": "0.1.74",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
|
||||
@@ -73,7 +73,6 @@ export namespace Agent {
|
||||
const result: Record<string, Info> = {
|
||||
build: {
|
||||
name: "build",
|
||||
description: "The default agent. Executes tools based on configured permissions.",
|
||||
options: {},
|
||||
permission: PermissionNext.merge(
|
||||
defaults,
|
||||
@@ -88,7 +87,6 @@ export namespace Agent {
|
||||
},
|
||||
plan: {
|
||||
name: "plan",
|
||||
description: "Plan mode. Disallows all edit tools.",
|
||||
options: {},
|
||||
permission: PermissionNext.merge(
|
||||
defaults,
|
||||
|
||||
@@ -153,7 +153,6 @@ async function createToolContext(agent: Agent.Info) {
|
||||
callID: Identifier.ascending("part"),
|
||||
agent: agent.name,
|
||||
abort: new AbortController().signal,
|
||||
messages: [],
|
||||
metadata: () => {},
|
||||
async ask(req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) {
|
||||
for (const pattern of req.patterns) {
|
||||
|
||||
@@ -488,6 +488,15 @@ function App() {
|
||||
},
|
||||
category: "System",
|
||||
},
|
||||
{
|
||||
title: "Open WebUI",
|
||||
value: "webui.open",
|
||||
onSelect: () => {
|
||||
open(sdk.url).catch(() => {})
|
||||
dialog.clear()
|
||||
},
|
||||
category: "System",
|
||||
},
|
||||
{
|
||||
title: "Exit the app",
|
||||
value: "app.exit",
|
||||
@@ -570,16 +579,6 @@ function App() {
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: kv.get("diff_wrap_mode", "word") === "word" ? "Disable diff wrapping" : "Enable diff wrapping",
|
||||
value: "app.toggle.diffwrap",
|
||||
category: "System",
|
||||
onSelect: (dialog) => {
|
||||
const current = kv.get("diff_wrap_mode", "word")
|
||||
kv.set("diff_wrap_mode", current === "word" ? "none" : "word")
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
createEffect(() => {
|
||||
|
||||
@@ -333,57 +333,32 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
const start = Date.now() - 30 * 24 * 60 * 60 * 1000
|
||||
const sessionListPromise = sdk.client.session
|
||||
.list({ start: start })
|
||||
.then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)))
|
||||
.then((x) => setStore("session", reconcile((x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)))))
|
||||
|
||||
// blocking - include session.list when continuing a session
|
||||
const providersPromise = sdk.client.config.providers({}, { throwOnError: true })
|
||||
const providerListPromise = sdk.client.provider.list({}, { throwOnError: true })
|
||||
const agentsPromise = sdk.client.app.agents({}, { throwOnError: true })
|
||||
const configPromise = sdk.client.config.get({}, { throwOnError: true })
|
||||
const blockingRequests: Promise<unknown>[] = [
|
||||
providersPromise,
|
||||
providerListPromise,
|
||||
agentsPromise,
|
||||
configPromise,
|
||||
sdk.client.config.providers({}, { throwOnError: true }).then((x) => {
|
||||
batch(() => {
|
||||
setStore("provider", reconcile(x.data!.providers))
|
||||
setStore("provider_default", reconcile(x.data!.default))
|
||||
})
|
||||
}),
|
||||
sdk.client.provider.list({}, { throwOnError: true }).then((x) => {
|
||||
batch(() => {
|
||||
setStore("provider_next", reconcile(x.data!))
|
||||
})
|
||||
}),
|
||||
sdk.client.app.agents({}, { throwOnError: true }).then((x) => setStore("agent", reconcile(x.data ?? []))),
|
||||
sdk.client.config.get({}, { throwOnError: true }).then((x) => setStore("config", reconcile(x.data!))),
|
||||
...(args.continue ? [sessionListPromise] : []),
|
||||
]
|
||||
|
||||
await Promise.all(blockingRequests)
|
||||
.then(() => {
|
||||
const providersResponse = providersPromise.then((x) => x.data!)
|
||||
const providerListResponse = providerListPromise.then((x) => x.data!)
|
||||
const agentsResponse = agentsPromise.then((x) => x.data ?? [])
|
||||
const configResponse = configPromise.then((x) => x.data!)
|
||||
const sessionListResponse = args.continue ? sessionListPromise : undefined
|
||||
|
||||
return Promise.all([
|
||||
providersResponse,
|
||||
providerListResponse,
|
||||
agentsResponse,
|
||||
configResponse,
|
||||
...(sessionListResponse ? [sessionListResponse] : []),
|
||||
]).then((responses) => {
|
||||
const providers = responses[0]
|
||||
const providerList = responses[1]
|
||||
const agents = responses[2]
|
||||
const config = responses[3]
|
||||
const sessions = responses[4]
|
||||
|
||||
batch(() => {
|
||||
setStore("provider", reconcile(providers.providers))
|
||||
setStore("provider_default", reconcile(providers.default))
|
||||
setStore("provider_next", reconcile(providerList))
|
||||
setStore("agent", reconcile(agents))
|
||||
setStore("config", reconcile(config))
|
||||
if (sessions !== undefined) setStore("session", reconcile(sessions))
|
||||
})
|
||||
})
|
||||
})
|
||||
.then(() => {
|
||||
if (store.status !== "complete") setStore("status", "partial")
|
||||
// non-blocking
|
||||
Promise.all([
|
||||
...(args.continue ? [] : [sessionListPromise.then((sessions) => setStore("session", reconcile(sessions)))]),
|
||||
...(args.continue ? [] : [sessionListPromise]),
|
||||
sdk.client.command.list().then((x) => setStore("command", reconcile(x.data ?? []))),
|
||||
sdk.client.lsp.status().then((x) => setStore("lsp", reconcile(x.data!))),
|
||||
sdk.client.mcp.status().then((x) => setStore("mcp", reconcile(x.data!))),
|
||||
|
||||
@@ -145,7 +145,7 @@ export function Session() {
|
||||
const [showDetails, setShowDetails] = kv.signal("tool_details_visibility", true)
|
||||
const [showAssistantMetadata, setShowAssistantMetadata] = kv.signal("assistant_metadata_visibility", true)
|
||||
const [showScrollbar, setShowScrollbar] = kv.signal("scrollbar_visible", false)
|
||||
const [diffWrapMode] = kv.signal<"word" | "none">("diff_wrap_mode", "word")
|
||||
const [diffWrapMode, setDiffWrapMode] = createSignal<"word" | "none">("word")
|
||||
const [animationsEnabled, setAnimationsEnabled] = kv.signal("animations_enabled", true)
|
||||
|
||||
const wide = createMemo(() => dimensions().width > 120)
|
||||
@@ -503,7 +503,7 @@ export function Session() {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: conceal() ? "Disable code concealment" : "Enable code concealment",
|
||||
title: "Toggle code concealment",
|
||||
value: "session.toggle.conceal",
|
||||
keybind: "messages_toggle_conceal" as any,
|
||||
category: "Session",
|
||||
@@ -538,6 +538,18 @@ export function Session() {
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Toggle diff wrapping",
|
||||
value: "session.toggle.diffwrap",
|
||||
category: "Session",
|
||||
slash: {
|
||||
name: "diffwrap",
|
||||
},
|
||||
onSelect: (dialog) => {
|
||||
setDiffWrapMode((prev) => (prev === "word" ? "none" : "word"))
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: showDetails() ? "Hide tool details" : "Show tool details",
|
||||
value: "session.toggle.actions",
|
||||
@@ -1693,29 +1705,10 @@ function Glob(props: ToolProps<typeof GlobTool>) {
|
||||
}
|
||||
|
||||
function Read(props: ToolProps<typeof ReadTool>) {
|
||||
const { theme } = useTheme()
|
||||
const loaded = createMemo(() => {
|
||||
if (props.part.state.status !== "completed") return []
|
||||
if (props.part.state.time.compacted) return []
|
||||
const value = props.metadata.loaded
|
||||
if (!value || !Array.isArray(value)) return []
|
||||
return value.filter((p): p is string => typeof p === "string")
|
||||
})
|
||||
return (
|
||||
<>
|
||||
<InlineTool icon="→" pending="Reading file..." complete={props.input.filePath} part={props.part}>
|
||||
Read {normalizePath(props.input.filePath!)} {input(props.input, ["filePath"])}
|
||||
</InlineTool>
|
||||
<For each={loaded()}>
|
||||
{(filepath) => (
|
||||
<box paddingLeft={3}>
|
||||
<text paddingLeft={3} fg={theme.textMuted}>
|
||||
↳ Loaded {normalizePath(filepath)}
|
||||
</text>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
</>
|
||||
<InlineTool icon="→" pending="Reading file..." complete={props.input.filePath} part={props.part}>
|
||||
Read {normalizePath(props.input.filePath!)} {input(props.input, ["filePath"])}
|
||||
</InlineTool>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -206,11 +206,7 @@ export namespace File {
|
||||
const project = Instance.project
|
||||
if (project.vcs !== "git") return []
|
||||
|
||||
const diffOutput = await $`git -c core.quotepath=false diff --numstat HEAD`
|
||||
.cwd(Instance.directory)
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.text()
|
||||
const diffOutput = await $`git diff --numstat HEAD`.cwd(Instance.directory).quiet().nothrow().text()
|
||||
|
||||
const changedFiles: Info[] = []
|
||||
|
||||
@@ -227,7 +223,7 @@ export namespace File {
|
||||
}
|
||||
}
|
||||
|
||||
const untrackedOutput = await $`git -c core.quotepath=false ls-files --others --exclude-standard`
|
||||
const untrackedOutput = await $`git ls-files --others --exclude-standard`
|
||||
.cwd(Instance.directory)
|
||||
.quiet()
|
||||
.nothrow()
|
||||
@@ -252,7 +248,7 @@ export namespace File {
|
||||
}
|
||||
|
||||
// Get deleted files
|
||||
const deletedOutput = await $`git -c core.quotepath=false diff --name-only --diff-filter=D HEAD`
|
||||
const deletedOutput = await $`git diff --name-only --diff-filter=D HEAD`
|
||||
.cwd(Instance.directory)
|
||||
.quiet()
|
||||
.nothrow()
|
||||
|
||||
@@ -854,9 +854,10 @@ export namespace Provider {
|
||||
// Load for the main provider if auth exists
|
||||
if (auth) {
|
||||
const options = await plugin.auth.loader(() => Auth.get(providerID) as any, database[plugin.auth.provider])
|
||||
const opts = options ?? {}
|
||||
const patch: Partial<Info> = providers[providerID] ? { options: opts } : { source: "custom", options: opts }
|
||||
mergeProvider(providerID, patch)
|
||||
mergeProvider(plugin.auth.provider, {
|
||||
source: "custom",
|
||||
options: options,
|
||||
})
|
||||
}
|
||||
|
||||
// If this is github-copilot plugin, also register for github-copilot-enterprise if auth exists
|
||||
@@ -869,11 +870,10 @@ export namespace Provider {
|
||||
() => Auth.get(enterpriseProviderID) as any,
|
||||
database[enterpriseProviderID],
|
||||
)
|
||||
const opts = enterpriseOptions ?? {}
|
||||
const patch: Partial<Info> = providers[enterpriseProviderID]
|
||||
? { options: opts }
|
||||
: { source: "custom", options: opts }
|
||||
mergeProvider(enterpriseProviderID, patch)
|
||||
mergeProvider(enterpriseProviderID, {
|
||||
source: "custom",
|
||||
options: enterpriseOptions,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -889,9 +889,10 @@ export namespace Provider {
|
||||
const result = await fn(data)
|
||||
if (result && (result.autoload || providers[providerID])) {
|
||||
if (result.getModel) modelLoaders[providerID] = result.getModel
|
||||
const opts = result.options ?? {}
|
||||
const patch: Partial<Info> = providers[providerID] ? { options: opts } : { source: "custom", options: opts }
|
||||
mergeProvider(providerID, patch)
|
||||
mergeProvider(providerID, {
|
||||
source: "custom",
|
||||
options: result.options,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -441,36 +441,6 @@ export namespace Server {
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.delete(
|
||||
"/auth/:providerID",
|
||||
describeRoute({
|
||||
summary: "Remove auth credentials",
|
||||
description: "Remove authentication credentials",
|
||||
operationId: "auth.remove",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successfully removed authentication credentials",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.boolean()),
|
||||
},
|
||||
},
|
||||
},
|
||||
...errors(400),
|
||||
},
|
||||
}),
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
providerID: z.string(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const providerID = c.req.valid("param").providerID
|
||||
await Auth.remove(providerID)
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/event",
|
||||
describeRoute({
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import { Global } from "../global"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { Config } from "../config/config"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { Log } from "../util/log"
|
||||
import type { MessageV2 } from "./message-v2"
|
||||
|
||||
const log = Log.create({ service: "instruction" })
|
||||
|
||||
const FILES = [
|
||||
"AGENTS.md",
|
||||
"CLAUDE.md",
|
||||
"CONTEXT.md", // deprecated
|
||||
]
|
||||
|
||||
function globalFiles() {
|
||||
const files = [path.join(Global.Path.config, "AGENTS.md")]
|
||||
if (!Flag.OPENCODE_DISABLE_CLAUDE_CODE_PROMPT) {
|
||||
files.push(path.join(os.homedir(), ".claude", "CLAUDE.md"))
|
||||
}
|
||||
if (Flag.OPENCODE_CONFIG_DIR) {
|
||||
files.push(path.join(Flag.OPENCODE_CONFIG_DIR, "AGENTS.md"))
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
async function resolveRelative(instruction: string): Promise<string[]> {
|
||||
if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
|
||||
return Filesystem.globUp(instruction, Instance.directory, Instance.worktree).catch(() => [])
|
||||
}
|
||||
if (!Flag.OPENCODE_CONFIG_DIR) {
|
||||
log.warn(
|
||||
`Skipping relative instruction "${instruction}" - no OPENCODE_CONFIG_DIR set while project config is disabled`,
|
||||
)
|
||||
return []
|
||||
}
|
||||
return Filesystem.globUp(instruction, Flag.OPENCODE_CONFIG_DIR, Flag.OPENCODE_CONFIG_DIR).catch(() => [])
|
||||
}
|
||||
|
||||
export namespace InstructionPrompt {
|
||||
export async function systemPaths() {
|
||||
const config = await Config.get()
|
||||
const paths = new Set<string>()
|
||||
|
||||
if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
|
||||
for (const file of FILES) {
|
||||
const matches = await Filesystem.findUp(file, Instance.directory, Instance.worktree)
|
||||
if (matches.length > 0) {
|
||||
matches.forEach((p) => paths.add(path.resolve(p)))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const file of globalFiles()) {
|
||||
if (await Bun.file(file).exists()) {
|
||||
paths.add(path.resolve(file))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (config.instructions) {
|
||||
for (let instruction of config.instructions) {
|
||||
if (instruction.startsWith("https://") || instruction.startsWith("http://")) continue
|
||||
if (instruction.startsWith("~/")) {
|
||||
instruction = path.join(os.homedir(), instruction.slice(2))
|
||||
}
|
||||
const matches = path.isAbsolute(instruction)
|
||||
? await Array.fromAsync(
|
||||
new Bun.Glob(path.basename(instruction)).scan({
|
||||
cwd: path.dirname(instruction),
|
||||
absolute: true,
|
||||
onlyFiles: true,
|
||||
}),
|
||||
).catch(() => [])
|
||||
: await resolveRelative(instruction)
|
||||
matches.forEach((p) => paths.add(path.resolve(p)))
|
||||
}
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
export async function system() {
|
||||
const config = await Config.get()
|
||||
const paths = await systemPaths()
|
||||
|
||||
const files = Array.from(paths).map(async (p) => {
|
||||
const content = await Bun.file(p)
|
||||
.text()
|
||||
.catch(() => "")
|
||||
return content ? "Instructions from: " + p + "\n" + content : ""
|
||||
})
|
||||
|
||||
const urls: string[] = []
|
||||
if (config.instructions) {
|
||||
for (const instruction of config.instructions) {
|
||||
if (instruction.startsWith("https://") || instruction.startsWith("http://")) {
|
||||
urls.push(instruction)
|
||||
}
|
||||
}
|
||||
}
|
||||
const fetches = urls.map((url) =>
|
||||
fetch(url, { signal: AbortSignal.timeout(5000) })
|
||||
.then((res) => (res.ok ? res.text() : ""))
|
||||
.catch(() => "")
|
||||
.then((x) => (x ? "Instructions from: " + url + "\n" + x : "")),
|
||||
)
|
||||
|
||||
return Promise.all([...files, ...fetches]).then((result) => result.filter(Boolean))
|
||||
}
|
||||
|
||||
export function loaded(messages: MessageV2.WithParts[]) {
|
||||
const paths = new Set<string>()
|
||||
for (const msg of messages) {
|
||||
for (const part of msg.parts) {
|
||||
if (part.type === "tool" && part.tool === "read" && part.state.status === "completed") {
|
||||
if (part.state.time.compacted) continue
|
||||
const loaded = part.state.metadata?.loaded
|
||||
if (!loaded || !Array.isArray(loaded)) continue
|
||||
for (const p of loaded) {
|
||||
if (typeof p === "string") paths.add(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
export async function find(dir: string) {
|
||||
for (const file of FILES) {
|
||||
const filepath = path.resolve(path.join(dir, file))
|
||||
if (await Bun.file(filepath).exists()) return filepath
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolve(messages: MessageV2.WithParts[], filepath: string) {
|
||||
const system = await systemPaths()
|
||||
const already = loaded(messages)
|
||||
const results: { filepath: string; content: string }[] = []
|
||||
|
||||
let current = path.dirname(path.resolve(filepath))
|
||||
const root = path.resolve(Instance.directory)
|
||||
|
||||
while (current.startsWith(root)) {
|
||||
const found = await find(current)
|
||||
if (found && !system.has(found) && !already.has(found)) {
|
||||
const content = await Bun.file(found)
|
||||
.text()
|
||||
.catch(() => undefined)
|
||||
if (content) {
|
||||
results.push({ filepath: found, content: "Instructions from: " + found + "\n" + content })
|
||||
}
|
||||
}
|
||||
if (current === root) break
|
||||
current = path.dirname(current)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
}
|
||||
@@ -631,7 +631,7 @@ export namespace MessageV2 {
|
||||
sessionID: Identifier.schema("session"),
|
||||
messageID: Identifier.schema("message"),
|
||||
}),
|
||||
async (input): Promise<WithParts> => {
|
||||
async (input) => {
|
||||
return {
|
||||
info: await Storage.read<MessageV2.Info>(["message", input.sessionID, input.messageID]),
|
||||
parts: await parts(input.messageID),
|
||||
|
||||
@@ -15,7 +15,6 @@ import { Instance } from "../project/instance"
|
||||
import { Bus } from "../bus"
|
||||
import { ProviderTransform } from "../provider/transform"
|
||||
import { SystemPrompt } from "./system"
|
||||
import { InstructionPrompt } from "./instruction"
|
||||
import { Plugin } from "../plugin"
|
||||
import PROMPT_PLAN from "../session/prompt/plan.txt"
|
||||
import BUILD_SWITCH from "../session/prompt/build-switch.txt"
|
||||
@@ -387,7 +386,6 @@ export namespace SessionPrompt {
|
||||
abort,
|
||||
callID: part.callID,
|
||||
extra: { bypassAgentCheck: true },
|
||||
messages: msgs,
|
||||
async metadata(input) {
|
||||
await Session.updatePart({
|
||||
...part,
|
||||
@@ -563,7 +561,6 @@ export namespace SessionPrompt {
|
||||
tools: lastUser.tools,
|
||||
processor,
|
||||
bypassAgentCheck,
|
||||
messages: msgs,
|
||||
})
|
||||
|
||||
if (step === 1) {
|
||||
@@ -601,7 +598,7 @@ export namespace SessionPrompt {
|
||||
agent,
|
||||
abort,
|
||||
sessionID,
|
||||
system: [...(await SystemPrompt.environment(model)), ...(await InstructionPrompt.system())],
|
||||
system: [...(await SystemPrompt.environment(model)), ...(await SystemPrompt.custom())],
|
||||
messages: [
|
||||
...MessageV2.toModelMessages(sessionMessages, model),
|
||||
...(isLastStep
|
||||
@@ -653,7 +650,6 @@ export namespace SessionPrompt {
|
||||
tools?: Record<string, boolean>
|
||||
processor: SessionProcessor.Info
|
||||
bypassAgentCheck: boolean
|
||||
messages: MessageV2.WithParts[]
|
||||
}) {
|
||||
using _ = log.time("resolveTools")
|
||||
const tools: Record<string, AITool> = {}
|
||||
@@ -665,7 +661,6 @@ export namespace SessionPrompt {
|
||||
callID: options.toolCallId,
|
||||
extra: { model: input.model, bypassAgentCheck: input.bypassAgentCheck },
|
||||
agent: input.agent.name,
|
||||
messages: input.messages,
|
||||
metadata: async (val: { title?: string; metadata?: any }) => {
|
||||
const match = input.processor.partFromToolCall(options.toolCallId)
|
||||
if (match && match.state.status === "running") {
|
||||
@@ -1013,7 +1008,6 @@ export namespace SessionPrompt {
|
||||
agent: input.agent!,
|
||||
messageID: info.id,
|
||||
extra: { bypassCwdCheck: true, model },
|
||||
messages: [],
|
||||
metadata: async () => {},
|
||||
ask: async () => {},
|
||||
}
|
||||
@@ -1075,7 +1069,6 @@ export namespace SessionPrompt {
|
||||
agent: input.agent!,
|
||||
messageID: info.id,
|
||||
extra: { bypassCwdCheck: true },
|
||||
messages: [],
|
||||
metadata: async () => {},
|
||||
ask: async () => {},
|
||||
}
|
||||
@@ -1356,7 +1349,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
|
||||
const session = await Session.get(input.sessionID)
|
||||
if (session.revert) {
|
||||
await SessionRevert.cleanup(session)
|
||||
SessionRevert.cleanup(session)
|
||||
}
|
||||
const agent = await Agent.get(input.agent)
|
||||
const model = input.model ?? agent.model ?? (await lastModel(input.sessionID))
|
||||
|
||||
@@ -20,62 +20,6 @@ import { Agent } from "@/agent/agent"
|
||||
export namespace SessionSummary {
|
||||
const log = Log.create({ service: "session.summary" })
|
||||
|
||||
function unquoteGitPath(input: string) {
|
||||
if (!input.startsWith('"')) return input
|
||||
if (!input.endsWith('"')) return input
|
||||
const body = input.slice(1, -1)
|
||||
const bytes: number[] = []
|
||||
|
||||
for (let i = 0; i < body.length; i++) {
|
||||
const char = body[i]!
|
||||
if (char !== "\\") {
|
||||
bytes.push(char.charCodeAt(0))
|
||||
continue
|
||||
}
|
||||
|
||||
const next = body[i + 1]
|
||||
if (!next) {
|
||||
bytes.push("\\".charCodeAt(0))
|
||||
continue
|
||||
}
|
||||
|
||||
if (next >= "0" && next <= "7") {
|
||||
const chunk = body.slice(i + 1, i + 4)
|
||||
const match = chunk.match(/^[0-7]{1,3}/)
|
||||
if (!match) {
|
||||
bytes.push(next.charCodeAt(0))
|
||||
i++
|
||||
continue
|
||||
}
|
||||
bytes.push(parseInt(match[0], 8))
|
||||
i += match[0].length
|
||||
continue
|
||||
}
|
||||
|
||||
const escaped =
|
||||
next === "n"
|
||||
? "\n"
|
||||
: next === "r"
|
||||
? "\r"
|
||||
: next === "t"
|
||||
? "\t"
|
||||
: next === "b"
|
||||
? "\b"
|
||||
: next === "f"
|
||||
? "\f"
|
||||
: next === "v"
|
||||
? "\v"
|
||||
: next === "\\" || next === '"'
|
||||
? next
|
||||
: undefined
|
||||
|
||||
bytes.push((escaped ?? next).charCodeAt(0))
|
||||
i++
|
||||
}
|
||||
|
||||
return Buffer.from(bytes).toString()
|
||||
}
|
||||
|
||||
export const summarize = fn(
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
@@ -172,18 +116,7 @@ export namespace SessionSummary {
|
||||
messageID: Identifier.schema("message").optional(),
|
||||
}),
|
||||
async (input) => {
|
||||
const diffs = await Storage.read<Snapshot.FileDiff[]>(["session_diff", input.sessionID]).catch(() => [])
|
||||
const next = diffs.map((item) => {
|
||||
const file = unquoteGitPath(item.file)
|
||||
if (file === item.file) return item
|
||||
return {
|
||||
...item,
|
||||
file,
|
||||
}
|
||||
})
|
||||
const changed = next.some((item, i) => item.file !== diffs[i]?.file)
|
||||
if (changed) Storage.write(["session_diff", input.sessionID], next).catch(() => {})
|
||||
return next
|
||||
return Storage.read<Snapshot.FileDiff[]>(["session_diff", input.sessionID]).catch(() => [])
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -1,14 +1,37 @@
|
||||
import { Ripgrep } from "../file/ripgrep"
|
||||
import { Global } from "../global"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { Config } from "../config/config"
|
||||
import { Log } from "../util/log"
|
||||
|
||||
import { Instance } from "../project/instance"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
|
||||
import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
|
||||
import PROMPT_ANTHROPIC_WITHOUT_TODO from "./prompt/qwen.txt"
|
||||
import PROMPT_BEAST from "./prompt/beast.txt"
|
||||
import PROMPT_GEMINI from "./prompt/gemini.txt"
|
||||
import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"
|
||||
|
||||
import PROMPT_CODEX from "./prompt/codex_header.txt"
|
||||
import type { Provider } from "@/provider/provider"
|
||||
import { Flag } from "@/flag/flag"
|
||||
|
||||
const log = Log.create({ service: "system-prompt" })
|
||||
|
||||
async function resolveRelativeInstruction(instruction: string): Promise<string[]> {
|
||||
if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
|
||||
return Filesystem.globUp(instruction, Instance.directory, Instance.worktree).catch(() => [])
|
||||
}
|
||||
if (!Flag.OPENCODE_CONFIG_DIR) {
|
||||
log.warn(
|
||||
`Skipping relative instruction "${instruction}" - no OPENCODE_CONFIG_DIR set while project config is disabled`,
|
||||
)
|
||||
return []
|
||||
}
|
||||
return Filesystem.globUp(instruction, Flag.OPENCODE_CONFIG_DIR, Flag.OPENCODE_CONFIG_DIR).catch(() => [])
|
||||
}
|
||||
|
||||
export namespace SystemPrompt {
|
||||
export function instructions() {
|
||||
@@ -49,4 +72,81 @@ export namespace SystemPrompt {
|
||||
].join("\n"),
|
||||
]
|
||||
}
|
||||
|
||||
const LOCAL_RULE_FILES = [
|
||||
"AGENTS.md",
|
||||
"CLAUDE.md",
|
||||
"CONTEXT.md", // deprecated
|
||||
]
|
||||
const GLOBAL_RULE_FILES = [path.join(Global.Path.config, "AGENTS.md")]
|
||||
if (!Flag.OPENCODE_DISABLE_CLAUDE_CODE_PROMPT) {
|
||||
GLOBAL_RULE_FILES.push(path.join(os.homedir(), ".claude", "CLAUDE.md"))
|
||||
}
|
||||
|
||||
if (Flag.OPENCODE_CONFIG_DIR) {
|
||||
GLOBAL_RULE_FILES.push(path.join(Flag.OPENCODE_CONFIG_DIR, "AGENTS.md"))
|
||||
}
|
||||
|
||||
export async function custom() {
|
||||
const config = await Config.get()
|
||||
const paths = new Set<string>()
|
||||
|
||||
// Only scan local rule files when project discovery is enabled
|
||||
if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
|
||||
for (const localRuleFile of LOCAL_RULE_FILES) {
|
||||
const matches = await Filesystem.findUp(localRuleFile, Instance.directory, Instance.worktree)
|
||||
if (matches.length > 0) {
|
||||
matches.forEach((path) => paths.add(path))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const globalRuleFile of GLOBAL_RULE_FILES) {
|
||||
if (await Bun.file(globalRuleFile).exists()) {
|
||||
paths.add(globalRuleFile)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const urls: string[] = []
|
||||
if (config.instructions) {
|
||||
for (let instruction of config.instructions) {
|
||||
if (instruction.startsWith("https://") || instruction.startsWith("http://")) {
|
||||
urls.push(instruction)
|
||||
continue
|
||||
}
|
||||
if (instruction.startsWith("~/")) {
|
||||
instruction = path.join(os.homedir(), instruction.slice(2))
|
||||
}
|
||||
let matches: string[] = []
|
||||
if (path.isAbsolute(instruction)) {
|
||||
matches = await Array.fromAsync(
|
||||
new Bun.Glob(path.basename(instruction)).scan({
|
||||
cwd: path.dirname(instruction),
|
||||
absolute: true,
|
||||
onlyFiles: true,
|
||||
}),
|
||||
).catch(() => [])
|
||||
} else {
|
||||
matches = await resolveRelativeInstruction(instruction)
|
||||
}
|
||||
matches.forEach((path) => paths.add(path))
|
||||
}
|
||||
}
|
||||
|
||||
const foundFiles = Array.from(paths).map((p) =>
|
||||
Bun.file(p)
|
||||
.text()
|
||||
.catch(() => "")
|
||||
.then((x) => "Instructions from: " + p + "\n" + x),
|
||||
)
|
||||
const foundUrls = urls.map((url) =>
|
||||
fetch(url, { signal: AbortSignal.timeout(5000) })
|
||||
.then((res) => (res.ok ? res.text() : ""))
|
||||
.catch(() => "")
|
||||
.then((x) => (x ? "Instructions from: " + url + "\n" + x : "")),
|
||||
)
|
||||
return Promise.all([...foundFiles, ...foundUrls]).then((result) => result.filter(Boolean))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ export namespace Snapshot {
|
||||
const git = gitdir()
|
||||
await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow()
|
||||
const result =
|
||||
await $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff ${hash} -- .`
|
||||
await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff ${hash} -- .`
|
||||
.quiet()
|
||||
.cwd(Instance.worktree)
|
||||
.nothrow()
|
||||
@@ -196,7 +196,7 @@ export namespace Snapshot {
|
||||
export async function diffFull(from: string, to: string): Promise<FileDiff[]> {
|
||||
const git = gitdir()
|
||||
const result: FileDiff[] = []
|
||||
for await (const line of $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --no-renames --numstat ${from} ${to} -- .`
|
||||
for await (const line of $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --no-renames --numstat ${from} ${to} -- .`
|
||||
.quiet()
|
||||
.cwd(Instance.directory)
|
||||
.nothrow()
|
||||
|
||||
@@ -8,7 +8,6 @@ import DESCRIPTION from "./read.txt"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Identifier } from "../id/id"
|
||||
import { assertExternalDirectory } from "./external-directory"
|
||||
import { InstructionPrompt } from "../session/instruction"
|
||||
|
||||
const DEFAULT_READ_LIMIT = 2000
|
||||
const MAX_LINE_LENGTH = 2000
|
||||
@@ -60,8 +59,6 @@ export const ReadTool = Tool.define("read", {
|
||||
throw new Error(`File not found: ${filepath}`)
|
||||
}
|
||||
|
||||
const instructions = await InstructionPrompt.resolve(ctx.messages, filepath)
|
||||
|
||||
// Exclude SVG (XML-based) and vnd.fastbidsheet (.fbs extension, commonly FlatBuffers schema files)
|
||||
const isImage =
|
||||
file.type.startsWith("image/") && file.type !== "image/svg+xml" && file.type !== "image/vnd.fastbidsheet"
|
||||
@@ -75,7 +72,6 @@ export const ReadTool = Tool.define("read", {
|
||||
metadata: {
|
||||
preview: msg,
|
||||
truncated: false,
|
||||
...(instructions.length > 0 && { loaded: instructions.map((i) => i.filepath) }),
|
||||
},
|
||||
attachments: [
|
||||
{
|
||||
@@ -137,17 +133,12 @@ export const ReadTool = Tool.define("read", {
|
||||
LSP.touchFile(filepath, false)
|
||||
FileTime.read(ctx.sessionID, filepath)
|
||||
|
||||
if (instructions.length > 0) {
|
||||
output += `\n\n<system-reminder>\n${instructions.map((i) => i.content).join("\n\n")}\n</system-reminder>`
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
output,
|
||||
metadata: {
|
||||
preview,
|
||||
truncated,
|
||||
...(instructions.length > 0 && { loaded: instructions.map((i) => i.filepath) }),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -20,7 +20,6 @@ export namespace Tool {
|
||||
abort: AbortSignal
|
||||
callID?: string
|
||||
extra?: { [key: string]: any }
|
||||
messages: MessageV2.WithParts[]
|
||||
metadata(input: { title?: string; metadata?: M }): void
|
||||
ask(input: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">): Promise<void>
|
||||
}
|
||||
|
||||
@@ -46,10 +46,9 @@ test("provider loaded from env variable", async () => {
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers["anthropic"]).toBeDefined()
|
||||
// Provider should retain its connection source even if custom loaders
|
||||
// merge additional options.
|
||||
expect(providers["anthropic"].source).toBe("env")
|
||||
expect(providers["anthropic"].options.headers["anthropic-beta"]).toBeDefined()
|
||||
// Note: source becomes "custom" because CUSTOM_LOADERS run after env loading
|
||||
// and anthropic has a custom loader that merges additional options
|
||||
expect(providers["anthropic"].source).toBe("custom")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import path from "path"
|
||||
import { InstructionPrompt } from "../../src/session/instruction"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
describe("InstructionPrompt.resolve", () => {
|
||||
test("returns empty when AGENTS.md is at project root (already in systemPaths)", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, "AGENTS.md"), "# Root Instructions")
|
||||
await Bun.write(path.join(dir, "src", "file.ts"), "const x = 1")
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const system = await InstructionPrompt.systemPaths()
|
||||
expect(system.has(path.join(tmp.path, "AGENTS.md"))).toBe(true)
|
||||
|
||||
const results = await InstructionPrompt.resolve([], path.join(tmp.path, "src", "file.ts"))
|
||||
expect(results).toEqual([])
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("returns AGENTS.md from subdirectory (not in systemPaths)", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, "subdir", "AGENTS.md"), "# Subdir Instructions")
|
||||
await Bun.write(path.join(dir, "subdir", "nested", "file.ts"), "const x = 1")
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const system = await InstructionPrompt.systemPaths()
|
||||
expect(system.has(path.join(tmp.path, "subdir", "AGENTS.md"))).toBe(false)
|
||||
|
||||
const results = await InstructionPrompt.resolve([], path.join(tmp.path, "subdir", "nested", "file.ts"))
|
||||
expect(results.length).toBe(1)
|
||||
expect(results[0].filepath).toBe(path.join(tmp.path, "subdir", "AGENTS.md"))
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -11,7 +11,6 @@ const baseCtx = {
|
||||
callID: "",
|
||||
agent: "build",
|
||||
abort: AbortSignal.any([]),
|
||||
messages: [],
|
||||
metadata: () => {},
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ const ctx = {
|
||||
callID: "",
|
||||
agent: "build",
|
||||
abort: AbortSignal.any([]),
|
||||
messages: [],
|
||||
metadata: () => {},
|
||||
ask: async () => {},
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ const baseCtx: Omit<Tool.Context, "ask"> = {
|
||||
callID: "",
|
||||
agent: "build",
|
||||
abort: AbortSignal.any([]),
|
||||
messages: [],
|
||||
metadata: () => {},
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ const ctx = {
|
||||
callID: "",
|
||||
agent: "build",
|
||||
abort: AbortSignal.any([]),
|
||||
messages: [],
|
||||
metadata: () => {},
|
||||
ask: async () => {},
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ const ctx = {
|
||||
callID: "test-call",
|
||||
agent: "test-agent",
|
||||
abort: AbortSignal.any([]),
|
||||
messages: [],
|
||||
metadata: () => {},
|
||||
ask: async () => {},
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ const ctx = {
|
||||
callID: "",
|
||||
agent: "build",
|
||||
abort: AbortSignal.any([]),
|
||||
messages: [],
|
||||
metadata: () => {},
|
||||
ask: async () => {},
|
||||
}
|
||||
@@ -331,26 +330,3 @@ root_type Monster;`
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("tool.read loaded instructions", () => {
|
||||
test("loads AGENTS.md from parent directory and includes in metadata", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, "subdir", "AGENTS.md"), "# Test Instructions\nDo something special.")
|
||||
await Bun.write(path.join(dir, "subdir", "nested", "test.txt"), "test content")
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const read = await ReadTool.init()
|
||||
const result = await read.execute({ filePath: path.join(tmp.path, "subdir", "nested", "test.txt") }, ctx)
|
||||
expect(result.output).toContain("test content")
|
||||
expect(result.output).toContain("system-reminder")
|
||||
expect(result.output).toContain("Test Instructions")
|
||||
expect(result.metadata.loaded).toBeDefined()
|
||||
expect(result.metadata.loaded).toContain(path.join(tmp.path, "subdir", "AGENTS.md"))
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,8 +9,6 @@ import type {
|
||||
AppLogResponses,
|
||||
AppSkillsResponses,
|
||||
Auth as Auth3,
|
||||
AuthRemoveErrors,
|
||||
AuthRemoveResponses,
|
||||
AuthSetErrors,
|
||||
AuthSetResponses,
|
||||
CommandListResponses,
|
||||
@@ -3056,36 +3054,6 @@ export class Formatter extends HeyApiClient {
|
||||
}
|
||||
|
||||
export class Auth2 extends HeyApiClient {
|
||||
/**
|
||||
* Remove auth credentials
|
||||
*
|
||||
* Remove authentication credentials
|
||||
*/
|
||||
public remove<ThrowOnError extends boolean = false>(
|
||||
parameters: {
|
||||
providerID: string
|
||||
directory?: string
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
const params = buildClientParams(
|
||||
[parameters],
|
||||
[
|
||||
{
|
||||
args: [
|
||||
{ in: "path", key: "providerID" },
|
||||
{ in: "query", key: "directory" },
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
return (options?.client ?? this.client).delete<AuthRemoveResponses, AuthRemoveErrors, ThrowOnError>({
|
||||
url: "/auth/{providerID}",
|
||||
...options,
|
||||
...params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Set auth credentials
|
||||
*
|
||||
|
||||
@@ -4867,35 +4867,6 @@ export type FormatterStatusResponses = {
|
||||
|
||||
export type FormatterStatusResponse = FormatterStatusResponses[keyof FormatterStatusResponses]
|
||||
|
||||
export type AuthRemoveData = {
|
||||
body?: never
|
||||
path: {
|
||||
providerID: string
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
}
|
||||
url: "/auth/{providerID}"
|
||||
}
|
||||
|
||||
export type AuthRemoveErrors = {
|
||||
/**
|
||||
* Bad request
|
||||
*/
|
||||
400: BadRequestError
|
||||
}
|
||||
|
||||
export type AuthRemoveError = AuthRemoveErrors[keyof AuthRemoveErrors]
|
||||
|
||||
export type AuthRemoveResponses = {
|
||||
/**
|
||||
* Successfully removed authentication credentials
|
||||
*/
|
||||
200: boolean
|
||||
}
|
||||
|
||||
export type AuthRemoveResponse = AuthRemoveResponses[keyof AuthRemoveResponses]
|
||||
|
||||
export type AuthSetData = {
|
||||
body?: Auth
|
||||
path: {
|
||||
|
||||
@@ -5709,56 +5709,6 @@
|
||||
"source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.auth.set({\n ...\n})"
|
||||
}
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"operationId": "auth.remove",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "directory",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "path",
|
||||
"name": "providerID",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"summary": "Remove auth credentials",
|
||||
"description": "Remove authentication credentials",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successfully removed authentication credentials",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/BadRequestError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-codeSamples": [
|
||||
{
|
||||
"lang": "js",
|
||||
"source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.auth.remove({\n ...\n})"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/event": {
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
}
|
||||
&:focus-visible:not(:disabled) {
|
||||
&:focus:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
}
|
||||
&:active:not(:disabled) {
|
||||
|
||||
@@ -91,19 +91,6 @@ export function Code<T>(props: CodeProps<T>) {
|
||||
return root
|
||||
}
|
||||
|
||||
const applyScheme = () => {
|
||||
const host = container.querySelector("diffs-container")
|
||||
if (!(host instanceof HTMLElement)) return
|
||||
|
||||
const scheme = document.documentElement.dataset.colorScheme
|
||||
if (scheme === "dark" || scheme === "light") {
|
||||
host.dataset.colorScheme = scheme
|
||||
return
|
||||
}
|
||||
|
||||
host.removeAttribute("data-color-scheme")
|
||||
}
|
||||
|
||||
const applyCommentedLines = (ranges: SelectedLineRange[]) => {
|
||||
const root = getRoot()
|
||||
if (!root) return
|
||||
@@ -128,56 +115,20 @@ export function Code<T>(props: CodeProps<T>) {
|
||||
}
|
||||
}
|
||||
|
||||
const lineCount = () => {
|
||||
const text = local.file.contents
|
||||
const total = text.split("\n").length - (text.endsWith("\n") ? 1 : 0)
|
||||
return Math.max(1, total)
|
||||
}
|
||||
|
||||
const applySelection = (range: SelectedLineRange | null) => {
|
||||
const root = getRoot()
|
||||
if (!root) return false
|
||||
|
||||
const lines = lineCount()
|
||||
if (root.querySelectorAll("[data-line]").length < lines) return false
|
||||
|
||||
if (!range) {
|
||||
file().setSelectedLines(null)
|
||||
return true
|
||||
}
|
||||
|
||||
const start = Math.min(range.start, range.end)
|
||||
const end = Math.max(range.start, range.end)
|
||||
|
||||
if (start < 1 || end > lines) {
|
||||
file().setSelectedLines(null)
|
||||
return true
|
||||
}
|
||||
|
||||
if (!root.querySelector(`[data-line="${start}"]`) || !root.querySelector(`[data-line="${end}"]`)) {
|
||||
file().setSelectedLines(null)
|
||||
return true
|
||||
}
|
||||
|
||||
const normalized = (() => {
|
||||
if (range.endSide != null) return { start: range.start, end: range.end }
|
||||
if (range.side !== "deletions") return range
|
||||
if (root.querySelector("[data-deletions]") != null) return range
|
||||
return { start: range.start, end: range.end }
|
||||
})()
|
||||
|
||||
file().setSelectedLines(normalized)
|
||||
return true
|
||||
}
|
||||
|
||||
const notifyRendered = () => {
|
||||
if (!local.onRendered) return
|
||||
|
||||
observer?.disconnect()
|
||||
observer = undefined
|
||||
renderToken++
|
||||
|
||||
const token = renderToken
|
||||
|
||||
const lines = lineCount()
|
||||
const lines = (() => {
|
||||
const text = local.file.contents
|
||||
const total = text.split("\n").length - (text.endsWith("\n") ? 1 : 0)
|
||||
return Math.max(1, total)
|
||||
})()
|
||||
|
||||
const isReady = (root: ShadowRoot) => root.querySelectorAll("[data-line]").length >= lines
|
||||
|
||||
@@ -188,7 +139,6 @@ export function Code<T>(props: CodeProps<T>) {
|
||||
observer = undefined
|
||||
requestAnimationFrame(() => {
|
||||
if (token !== renderToken) return
|
||||
applySelection(lastSelection)
|
||||
local.onRendered?.()
|
||||
})
|
||||
}
|
||||
@@ -278,7 +228,7 @@ export function Code<T>(props: CodeProps<T>) {
|
||||
|
||||
const setSelectedLines = (range: SelectedLineRange | null) => {
|
||||
lastSelection = range
|
||||
applySelection(range)
|
||||
file().setSelectedLines(range)
|
||||
}
|
||||
|
||||
const scheduleSelectionUpdate = () => {
|
||||
@@ -419,24 +369,10 @@ export function Code<T>(props: CodeProps<T>) {
|
||||
containerWrapper: container,
|
||||
})
|
||||
|
||||
applyScheme()
|
||||
|
||||
setRendered((value) => value + 1)
|
||||
notifyRendered()
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (typeof document === "undefined") return
|
||||
if (typeof MutationObserver === "undefined") return
|
||||
|
||||
const root = document.documentElement
|
||||
const monitor = new MutationObserver(() => applyScheme())
|
||||
monitor.observe(root, { attributes: true, attributeFilter: ["data-color-scheme"] })
|
||||
applyScheme()
|
||||
|
||||
onCleanup(() => monitor.disconnect())
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
rendered()
|
||||
const ranges = local.commentedLines ?? []
|
||||
|
||||
@@ -28,87 +28,6 @@ export function Diff<T>(props: SSRDiffProps<T>) {
|
||||
|
||||
const getRoot = () => fileDiffRef?.shadowRoot ?? undefined
|
||||
|
||||
const applyScheme = () => {
|
||||
const scheme = document.documentElement.dataset.colorScheme
|
||||
if (scheme === "dark" || scheme === "light") {
|
||||
fileDiffRef.dataset.colorScheme = scheme
|
||||
return
|
||||
}
|
||||
|
||||
fileDiffRef.removeAttribute("data-color-scheme")
|
||||
}
|
||||
|
||||
const lineIndex = (split: boolean, element: HTMLElement) => {
|
||||
const raw = element.dataset.lineIndex
|
||||
if (!raw) return
|
||||
const values = raw
|
||||
.split(",")
|
||||
.map((value) => parseInt(value, 10))
|
||||
.filter((value) => !Number.isNaN(value))
|
||||
if (values.length === 0) return
|
||||
if (!split) return values[0]
|
||||
if (values.length === 2) return values[1]
|
||||
return values[0]
|
||||
}
|
||||
|
||||
const rowIndex = (root: ShadowRoot, split: boolean, line: number, side: "additions" | "deletions" | undefined) => {
|
||||
const nodes = Array.from(root.querySelectorAll(`[data-line="${line}"], [data-alt-line="${line}"]`)).filter(
|
||||
(node): node is HTMLElement => node instanceof HTMLElement,
|
||||
)
|
||||
if (nodes.length === 0) return
|
||||
|
||||
const targetSide = side ?? "additions"
|
||||
|
||||
for (const node of nodes) {
|
||||
if (findSide(node) === targetSide) return lineIndex(split, node)
|
||||
if (parseInt(node.dataset.altLine ?? "", 10) === line) return lineIndex(split, node)
|
||||
}
|
||||
}
|
||||
|
||||
const fixSelection = (range: SelectedLineRange | null) => {
|
||||
if (!range) return range
|
||||
const root = getRoot()
|
||||
if (!root) return
|
||||
|
||||
const diffs = root.querySelector("[data-diffs]")
|
||||
if (!(diffs instanceof HTMLElement)) return
|
||||
|
||||
const split = diffs.dataset.type === "split"
|
||||
|
||||
const start = rowIndex(root, split, range.start, range.side)
|
||||
const end = rowIndex(root, split, range.end, range.endSide ?? range.side)
|
||||
|
||||
if (start === undefined || end === undefined) {
|
||||
if (root.querySelector("[data-line], [data-alt-line]") == null) return
|
||||
return null
|
||||
}
|
||||
if (start <= end) return range
|
||||
|
||||
const side = range.endSide ?? range.side
|
||||
const swapped: SelectedLineRange = {
|
||||
start: range.end,
|
||||
end: range.start,
|
||||
}
|
||||
if (side) swapped.side = side
|
||||
if (range.endSide && range.side) swapped.endSide = range.side
|
||||
|
||||
return swapped
|
||||
}
|
||||
|
||||
const setSelectedLines = (range: SelectedLineRange | null, attempt = 0) => {
|
||||
const diff = fileDiffInstance
|
||||
if (!diff) return
|
||||
|
||||
const fixed = fixSelection(range)
|
||||
if (fixed === undefined) {
|
||||
if (attempt >= 120) return
|
||||
requestAnimationFrame(() => setSelectedLines(range, attempt + 1))
|
||||
return
|
||||
}
|
||||
|
||||
diff.setSelectedLines(fixed)
|
||||
}
|
||||
|
||||
const findSide = (element: HTMLElement): "additions" | "deletions" => {
|
||||
const line = element.closest("[data-line], [data-alt-line]")
|
||||
if (line instanceof HTMLElement) {
|
||||
@@ -202,16 +121,6 @@ export function Diff<T>(props: SSRDiffProps<T>) {
|
||||
|
||||
onMount(() => {
|
||||
if (isServer || !props.preloadedDiff) return
|
||||
|
||||
applyScheme()
|
||||
|
||||
if (typeof MutationObserver !== "undefined") {
|
||||
const root = document.documentElement
|
||||
const monitor = new MutationObserver(() => applyScheme())
|
||||
monitor.observe(root, { attributes: true, attributeFilter: ["data-color-scheme"] })
|
||||
onCleanup(() => monitor.disconnect())
|
||||
}
|
||||
|
||||
fileDiffInstance = new FileDiff<T>(
|
||||
{
|
||||
...createDefaultOptions(props.diffStyle),
|
||||
@@ -230,14 +139,14 @@ export function Diff<T>(props: SSRDiffProps<T>) {
|
||||
containerWrapper: container,
|
||||
})
|
||||
|
||||
setSelectedLines(local.selectedLines ?? null)
|
||||
fileDiffInstance.setSelectedLines(local.selectedLines ?? null)
|
||||
|
||||
createEffect(() => {
|
||||
fileDiffInstance?.setLineAnnotations(local.annotations ?? [])
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
setSelectedLines(local.selectedLines ?? null)
|
||||
fileDiffInstance?.setSelectedLines(local.selectedLines ?? null)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
|
||||
@@ -102,77 +102,9 @@ export function Diff<T>(props: DiffProps<T>) {
|
||||
return root
|
||||
}
|
||||
|
||||
const applyScheme = () => {
|
||||
const host = container.querySelector("diffs-container")
|
||||
if (!(host instanceof HTMLElement)) return
|
||||
|
||||
const scheme = document.documentElement.dataset.colorScheme
|
||||
if (scheme === "dark" || scheme === "light") {
|
||||
host.dataset.colorScheme = scheme
|
||||
return
|
||||
}
|
||||
|
||||
host.removeAttribute("data-color-scheme")
|
||||
}
|
||||
|
||||
const lineIndex = (split: boolean, element: HTMLElement) => {
|
||||
const raw = element.dataset.lineIndex
|
||||
if (!raw) return
|
||||
const values = raw
|
||||
.split(",")
|
||||
.map((value) => parseInt(value, 10))
|
||||
.filter((value) => !Number.isNaN(value))
|
||||
if (values.length === 0) return
|
||||
if (!split) return values[0]
|
||||
if (values.length === 2) return values[1]
|
||||
return values[0]
|
||||
}
|
||||
|
||||
const rowIndex = (root: ShadowRoot, split: boolean, line: number, side: SelectionSide | undefined) => {
|
||||
const nodes = Array.from(root.querySelectorAll(`[data-line="${line}"], [data-alt-line="${line}"]`)).filter(
|
||||
(node): node is HTMLElement => node instanceof HTMLElement,
|
||||
)
|
||||
if (nodes.length === 0) return
|
||||
|
||||
const targetSide = side ?? "additions"
|
||||
|
||||
for (const node of nodes) {
|
||||
if (findSide(node) === targetSide) return lineIndex(split, node)
|
||||
if (parseInt(node.dataset.altLine ?? "", 10) === line) return lineIndex(split, node)
|
||||
}
|
||||
}
|
||||
|
||||
const fixSelection = (range: SelectedLineRange | null) => {
|
||||
if (!range) return range
|
||||
const root = getRoot()
|
||||
if (!root) return
|
||||
|
||||
const diffs = root.querySelector("[data-diffs]")
|
||||
if (!(diffs instanceof HTMLElement)) return
|
||||
|
||||
const split = diffs.dataset.type === "split"
|
||||
|
||||
const start = rowIndex(root, split, range.start, range.side)
|
||||
const end = rowIndex(root, split, range.end, range.endSide ?? range.side)
|
||||
if (start === undefined || end === undefined) {
|
||||
if (root.querySelector("[data-line], [data-alt-line]") == null) return
|
||||
return null
|
||||
}
|
||||
if (start <= end) return range
|
||||
|
||||
const side = range.endSide ?? range.side
|
||||
const swapped: SelectedLineRange = {
|
||||
start: range.end,
|
||||
end: range.start,
|
||||
}
|
||||
|
||||
if (side) swapped.side = side
|
||||
if (range.endSide && range.side) swapped.endSide = range.side
|
||||
|
||||
return swapped
|
||||
}
|
||||
|
||||
const notifyRendered = () => {
|
||||
if (!local.onRendered) return
|
||||
|
||||
observer?.disconnect()
|
||||
observer = undefined
|
||||
renderToken++
|
||||
@@ -189,7 +121,6 @@ export function Diff<T>(props: DiffProps<T>) {
|
||||
observer = undefined
|
||||
requestAnimationFrame(() => {
|
||||
if (token !== renderToken) return
|
||||
setSelectedLines(lastSelection)
|
||||
local.onRendered?.()
|
||||
})
|
||||
}
|
||||
@@ -229,8 +160,7 @@ export function Diff<T>(props: DiffProps<T>) {
|
||||
const root = getRoot()
|
||||
if (typeof MutationObserver === "undefined") {
|
||||
if (!root || !isReady(root)) return
|
||||
setSelectedLines(lastSelection)
|
||||
local.onRendered?.()
|
||||
local.onRendered()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -271,14 +201,41 @@ export function Diff<T>(props: DiffProps<T>) {
|
||||
)
|
||||
if (code.length === 0) return
|
||||
|
||||
const lineIndex = (element: HTMLElement) => {
|
||||
const raw = element.dataset.lineIndex
|
||||
if (!raw) return
|
||||
const values = raw
|
||||
.split(",")
|
||||
.map((value) => parseInt(value, 10))
|
||||
.filter((value) => !Number.isNaN(value))
|
||||
if (values.length === 0) return
|
||||
if (!split) return values[0]
|
||||
if (values.length === 2) return values[1]
|
||||
return values[0]
|
||||
}
|
||||
|
||||
const rowIndex = (line: number, side: SelectionSide | undefined) => {
|
||||
const nodes = Array.from(root.querySelectorAll(`[data-line="${line}"], [data-alt-line="${line}"]`)).filter(
|
||||
(node): node is HTMLElement => node instanceof HTMLElement,
|
||||
)
|
||||
if (nodes.length === 0) return
|
||||
|
||||
const targetSide = side ?? "additions"
|
||||
|
||||
for (const node of nodes) {
|
||||
if (findSide(node) === targetSide) return lineIndex(node)
|
||||
if (parseInt(node.dataset.altLine ?? "", 10) === line) return lineIndex(node)
|
||||
}
|
||||
}
|
||||
|
||||
for (const range of ranges) {
|
||||
const start = rowIndex(root, split, range.start, range.side)
|
||||
const start = rowIndex(range.start, range.side)
|
||||
if (start === undefined) continue
|
||||
|
||||
const end = (() => {
|
||||
const same = range.end === range.start && (range.endSide == null || range.endSide === range.side)
|
||||
if (same) return start
|
||||
return rowIndex(root, split, range.end, range.endSide ?? range.side)
|
||||
return rowIndex(range.end, range.endSide ?? range.side)
|
||||
})()
|
||||
if (end === undefined) continue
|
||||
|
||||
@@ -288,7 +245,7 @@ export function Diff<T>(props: DiffProps<T>) {
|
||||
for (const block of code) {
|
||||
for (const element of Array.from(block.children)) {
|
||||
if (!(element instanceof HTMLElement)) continue
|
||||
const idx = lineIndex(split, element)
|
||||
const idx = lineIndex(element)
|
||||
if (idx === undefined) continue
|
||||
if (idx > last) break
|
||||
if (idx < first) continue
|
||||
@@ -305,15 +262,8 @@ export function Diff<T>(props: DiffProps<T>) {
|
||||
const setSelectedLines = (range: SelectedLineRange | null) => {
|
||||
const active = current()
|
||||
if (!active) return
|
||||
|
||||
const fixed = fixSelection(range)
|
||||
if (fixed === undefined) {
|
||||
lastSelection = range
|
||||
return
|
||||
}
|
||||
|
||||
lastSelection = fixed
|
||||
active.setSelectedLines(fixed)
|
||||
lastSelection = range
|
||||
active.setSelectedLines(range)
|
||||
}
|
||||
|
||||
const updateSelection = () => {
|
||||
@@ -538,24 +488,10 @@ export function Diff<T>(props: DiffProps<T>) {
|
||||
containerWrapper: container,
|
||||
})
|
||||
|
||||
applyScheme()
|
||||
|
||||
setRendered((value) => value + 1)
|
||||
notifyRendered()
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (typeof document === "undefined") return
|
||||
if (typeof MutationObserver === "undefined") return
|
||||
|
||||
const root = document.documentElement
|
||||
const monitor = new MutationObserver(() => applyScheme())
|
||||
monitor.observe(root, { attributes: true, attributeFilter: ["data-color-scheme"] })
|
||||
applyScheme()
|
||||
|
||||
onCleanup(() => monitor.disconnect())
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
rendered()
|
||||
const ranges = local.commentedLines ?? []
|
||||
|
||||
@@ -3,20 +3,19 @@ import { ComponentProps, JSXElement, ParentProps, splitProps } from "solid-js"
|
||||
|
||||
export interface HoverCardProps extends ParentProps, Omit<ComponentProps<typeof Kobalte>, "children"> {
|
||||
trigger: JSXElement
|
||||
mount?: HTMLElement
|
||||
class?: ComponentProps<"div">["class"]
|
||||
classList?: ComponentProps<"div">["classList"]
|
||||
}
|
||||
|
||||
export function HoverCard(props: HoverCardProps) {
|
||||
const [local, rest] = splitProps(props, ["trigger", "mount", "class", "classList", "children"])
|
||||
const [local, rest] = splitProps(props, ["trigger", "class", "classList", "children"])
|
||||
|
||||
return (
|
||||
<Kobalte gutter={4} {...rest}>
|
||||
<Kobalte.Trigger as="div" data-slot="hover-card-trigger">
|
||||
{local.trigger}
|
||||
</Kobalte.Trigger>
|
||||
<Kobalte.Portal mount={local.mount}>
|
||||
<Kobalte.Portal>
|
||||
<Kobalte.Content
|
||||
data-component="hover-card-content"
|
||||
classList={{
|
||||
|
||||
@@ -90,8 +90,8 @@
|
||||
/* color: var(--icon-hover); */
|
||||
/* } */
|
||||
}
|
||||
&:focus-visible:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
&:focus:not(:disabled) {
|
||||
background-color: var(--surface-focus);
|
||||
}
|
||||
&:active:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-active);
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
&:hover:not(:disabled),
|
||||
&:focus-visible:not(:disabled),
|
||||
&:focus:not(:disabled),
|
||||
&:active:not(:disabled) {
|
||||
background-color: transparent;
|
||||
opacity: 0.7;
|
||||
@@ -91,7 +91,7 @@
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
&:hover:not(:disabled),
|
||||
&:focus-visible:not(:disabled),
|
||||
&:focus:not(:disabled),
|
||||
&:active:not(:disabled) {
|
||||
background-color: transparent;
|
||||
opacity: 0.7;
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
&:not([data-expanded]):focus-visible {
|
||||
&:not([data-expanded]):focus {
|
||||
&[data-variant="secondary"] {
|
||||
background-color: var(--button-secondary-base);
|
||||
}
|
||||
|
||||
@@ -239,7 +239,6 @@
|
||||
background-color: transparent;
|
||||
|
||||
[data-slot="tabs-trigger"] {
|
||||
height: 100%;
|
||||
padding: 0 8px;
|
||||
gap: 8px;
|
||||
justify-content: flex-start;
|
||||
@@ -334,7 +333,6 @@
|
||||
gap: 12px;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
[data-component="icon"] {
|
||||
|
||||
@@ -35,22 +35,9 @@ const unsafeCSS = `
|
||||
--diffs-selection-base: var(--surface-warning-strong);
|
||||
--diffs-selection-border: var(--border-warning-base);
|
||||
--diffs-selection-number-fg: #1c1917;
|
||||
/* Use explicit alpha instead of color-mix(..., transparent) to avoid Safari's non-premultiplied interpolation bugs. */
|
||||
--diffs-bg-selection: var(--diffs-bg-selection-override, rgb(from var(--surface-warning-base) r g b / 0.65));
|
||||
--diffs-bg-selection-number: var(
|
||||
--diffs-bg-selection-number-override,
|
||||
rgb(from var(--surface-warning-base) r g b / 0.85)
|
||||
);
|
||||
--diffs-bg-selection-text: rgb(from var(--surface-warning-strong) r g b / 0.2);
|
||||
}
|
||||
|
||||
:host([data-color-scheme='dark']) [data-diffs] {
|
||||
--diffs-selection-number-fg: #fdfbfb;
|
||||
--diffs-bg-selection: var(--diffs-bg-selection-override, rgb(from var(--solaris-dark-6) r g b / 0.65));
|
||||
--diffs-bg-selection-number: var(
|
||||
--diffs-bg-selection-number-override,
|
||||
rgb(from var(--solaris-dark-6) r g b / 0.85)
|
||||
);
|
||||
--diffs-bg-selection: var(--diffs-bg-selection-override, color-mix(in oklch, var(--surface-warning-base) 65%, transparent));
|
||||
--diffs-bg-selection-number: var(--diffs-bg-selection-number-override, color-mix(in oklch, var(--surface-warning-base) 85%, transparent));
|
||||
--diffs-bg-selection-text: color-mix(in oklch, var(--surface-warning-strong) 20%, transparent);
|
||||
}
|
||||
|
||||
[data-diffs] ::selection {
|
||||
@@ -91,6 +78,12 @@ const unsafeCSS = `
|
||||
);
|
||||
}
|
||||
|
||||
:host-context([data-color-scheme='dark']) [data-diffs] {
|
||||
--diffs-selection-number-fg: #fdfbfb;
|
||||
--diffs-bg-selection: var(--diffs-bg-selection-override, color-mix(in oklch, var(--solaris-dark-6) 65%, transparent));
|
||||
--diffs-bg-selection-number: var(--diffs-bg-selection-number-override, color-mix(in oklch, var(--solaris-dark-6) 85%, transparent));
|
||||
}
|
||||
|
||||
[data-diffs-header],
|
||||
[data-diffs] {
|
||||
[data-separator-wrapper] {
|
||||
|
||||
@@ -33,18 +33,18 @@ export const auraTheme = auraThemeJson as DesktopTheme
|
||||
|
||||
export const DEFAULT_THEMES: Record<string, DesktopTheme> = {
|
||||
"oc-1": oc1Theme,
|
||||
aura: auraTheme,
|
||||
ayu: ayuTheme,
|
||||
carbonfox: carbonfoxTheme,
|
||||
catppuccin: catppuccinTheme,
|
||||
tokyonight: tokyonightTheme,
|
||||
dracula: draculaTheme,
|
||||
gruvbox: gruvboxTheme,
|
||||
monokai: monokaiTheme,
|
||||
nightowl: nightowlTheme,
|
||||
solarized: solarizedTheme,
|
||||
nord: nordTheme,
|
||||
catppuccin: catppuccinTheme,
|
||||
ayu: ayuTheme,
|
||||
onedarkpro: oneDarkProTheme,
|
||||
shadesofpurple: shadesOfPurpleTheme,
|
||||
solarized: solarizedTheme,
|
||||
tokyonight: tokyonightTheme,
|
||||
nightowl: nightowlTheme,
|
||||
vesper: vesperTheme,
|
||||
carbonfox: carbonfoxTheme,
|
||||
gruvbox: gruvboxTheme,
|
||||
aura: auraTheme,
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ export OPENCODE_DISABLE_CLAUDE_CODE_SKILLS=1 # Disable only .claude/skills
|
||||
|
||||
When opencode starts, it looks for rule files in this order:
|
||||
|
||||
1. **Local files** by traversing up from the current directory (`AGENTS.md`, `CLAUDE.md`)
|
||||
1. **Local files** by traversing up from the current directory (`AGENTS.md`, `CLAUDE.md`, or `CONTEXT.md`)
|
||||
2. **Global file** at `~/.config/opencode/AGENTS.md`
|
||||
3. **Claude Code file** at `~/.claude/CLAUDE.md` (unless disabled)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user