Compare commits
110
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5422f7d6af | ||
|
|
213c0e18ab | ||
|
|
a8c18dba82 | ||
|
|
6cf2c3e3db | ||
|
|
b59aec6f04 | ||
|
|
d9e8b2b65d | ||
|
|
7655f51e10 | ||
|
|
e7c6267323 | ||
|
|
64a3661a32 | ||
|
|
bf463aee04 | ||
|
|
b24fd90fe8 | ||
|
|
6897bb7d02 | ||
|
|
8371ba5aec | ||
|
|
36577479c5 | ||
|
|
bb178e9352 | ||
|
|
4075f9e1ab | ||
|
|
021d9d105e | ||
|
|
b07d7cdb71 | ||
|
|
77f11dfabe | ||
|
|
45b09c1465 | ||
|
|
ccc7aa49c3 | ||
|
|
53ac394c68 | ||
|
|
8b6484ac1a | ||
|
|
c1e840b9b2 | ||
|
|
a77df3c174 | ||
|
|
9d1cf98192 | ||
|
|
7b3d5f1d68 | ||
|
|
b21f82f5b0 | ||
|
|
d82e94c209 | ||
|
|
cbe8f265b9 | ||
|
|
8b5dde5536 | ||
|
|
6a62b44593 | ||
|
|
c700b928e4 | ||
|
|
ae815cca3a | ||
|
|
5a16d99b60 | ||
|
|
3f9b59c798 | ||
|
|
c6febd8ddd | ||
|
|
010ed55590 | ||
|
|
2be4598011 | ||
|
|
99cd7f3468 | ||
|
|
fca0825b71 | ||
|
|
9babdb80cf | ||
|
|
f4392e023a | ||
|
|
0e08c6c9fd | ||
|
|
0dcb850a7a | ||
|
|
d9a61cd94c | ||
|
|
0f4a10f4a3 | ||
|
|
c551f7e47b | ||
|
|
7962ff38b0 | ||
|
|
3ac11df66a | ||
|
|
7caf59b433 | ||
|
|
92229b44f8 | ||
|
|
0a572afd46 | ||
|
|
ecd04a118a | ||
|
|
6f3d413472 | ||
|
|
7c96d704d3 | ||
|
|
9346c1ae3f | ||
|
|
e0e97e9d93 | ||
|
|
810bc012b6 | ||
|
|
36b832880d | ||
|
|
d4e3acf17e | ||
|
|
04337f6202 | ||
|
|
97aec21cb3 | ||
|
|
319ad2a391 | ||
|
|
de3b654dcd | ||
|
|
8b17ac656c | ||
|
|
18bfc740c8 | ||
|
|
837037cd04 | ||
|
|
b0f865eae5 | ||
|
|
ac53a372b0 | ||
|
|
7795cae0b5 | ||
|
|
4c9d879624 | ||
|
|
3fdd08d66e | ||
|
|
ec2ab639bb | ||
|
|
d05ed5ca83 | ||
|
|
37f1a1a4ef | ||
|
|
b8e8d82323 | ||
|
|
801eb5d2cb | ||
|
|
ebeed03115 | ||
|
|
d9eed4c6ca | ||
|
|
7e34d27b77 | ||
|
|
783121c06e | ||
|
|
984518b1c0 | ||
|
|
7fcdbd155b | ||
|
|
5856ea4e75 | ||
|
|
32a0dcedcb | ||
|
|
f48784d152 | ||
|
|
3dce6a6608 | ||
|
|
39a73d4894 | ||
|
|
b1fbfa7e94 | ||
|
|
805ae19c9a | ||
|
|
fcea7e18a5 | ||
|
|
7c34319b19 | ||
|
|
cd4676171b | ||
|
|
7016be0739 | ||
|
|
ff35db0360 | ||
|
|
af3d8c383e | ||
|
|
7f75f71f6b | ||
|
|
84b12a8fb7 | ||
|
|
1934ee13d8 | ||
|
|
6c1e18f111 | ||
|
|
3296b90372 | ||
|
|
0d651eab3b | ||
|
|
0edd304f42 | ||
|
|
6b83b172ae | ||
|
|
444934a4c1 | ||
|
|
c4f1087e58 | ||
|
|
c87232d5df | ||
|
|
d03c5f6b3f | ||
|
|
9a33b1ec88 |
@@ -0,0 +1,83 @@
|
|||||||
|
name: Close stale PRs
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
dryRun:
|
||||||
|
description: "Log actions without closing PRs"
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
schedule:
|
||||||
|
- cron: "0 6 * * *"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
close-stale-prs:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Close inactive PRs
|
||||||
|
uses: actions/github-script@v8
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
const DAYS_INACTIVE = 60
|
||||||
|
const cutoff = new Date(Date.now() - DAYS_INACTIVE * 24 * 60 * 60 * 1000)
|
||||||
|
const { owner, repo } = context.repo
|
||||||
|
const dryRun = context.payload.inputs?.dryRun === "true"
|
||||||
|
const stalePrs = []
|
||||||
|
|
||||||
|
core.info(`Dry run mode: ${dryRun}`)
|
||||||
|
|
||||||
|
const prs = await github.paginate(github.rest.pulls.list, {
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
state: "open",
|
||||||
|
per_page: 100,
|
||||||
|
sort: "updated",
|
||||||
|
direction: "asc",
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const pr of prs) {
|
||||||
|
const lastUpdated = new Date(pr.updated_at)
|
||||||
|
if (lastUpdated > cutoff) {
|
||||||
|
core.info(`PR ${pr.number} is fresh`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
stalePrs.push(pr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stalePrs.length) {
|
||||||
|
core.info("No stale pull requests found.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const pr of stalePrs) {
|
||||||
|
const issue_number = pr.number
|
||||||
|
const closeComment = `Closing this pull request because it has had no updates for more than ${DAYS_INACTIVE} days. If you plan to continue working on it, feel free to reopen or open a new PR.`
|
||||||
|
|
||||||
|
if (dryRun) {
|
||||||
|
core.info(`[dry-run] Would close PR #${issue_number} from ${pr.user.login}`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number,
|
||||||
|
body: closeComment,
|
||||||
|
})
|
||||||
|
|
||||||
|
await github.rest.pulls.update({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
pull_number: issue_number,
|
||||||
|
state: "closed",
|
||||||
|
})
|
||||||
|
|
||||||
|
core.info(`Closed PR #${issue_number} from ${pr.user.login}`)
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ Please read @package.json and @packages/opencode/package.json.
|
|||||||
Your job is to look into AI SDK dependencies, figure out if they have versions that can be upgraded (minor or patch versions ONLY no major ignore major changes).
|
Your job is to look into AI SDK dependencies, figure out if they have versions that can be upgraded (minor or patch versions ONLY no major ignore major changes).
|
||||||
|
|
||||||
I want a report of every dependency and the version that can be upgraded to.
|
I want a report of every dependency and the version that can be upgraded to.
|
||||||
What would be even better is if you can give me links to the changelog for each dependency, or at least some reference info so I can see what bugs were fixed or new features were added.
|
What would be even better is if you can give me brief summary of the changes for each dep and a link to the changelog for each dependency, or at least some reference info so I can see what bugs were fixed or new features were added.
|
||||||
|
|
||||||
Consider using subagents for each dep to save your context window.
|
Consider using subagents for each dep to save your context window.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
<a href="README.de.md">Deutsch</a> |
|
<a href="README.de.md">Deutsch</a> |
|
||||||
<a href="README.es.md">Español</a> |
|
<a href="README.es.md">Español</a> |
|
||||||
<a href="README.fr.md">Français</a> |
|
<a href="README.fr.md">Français</a> |
|
||||||
|
<a href="README.it.md">Italiano</a> |
|
||||||
<a href="README.da.md">Dansk</a> |
|
<a href="README.da.md">Dansk</a> |
|
||||||
<a href="README.ja.md">日本語</a> |
|
<a href="README.ja.md">日本語</a> |
|
||||||
<a href="README.pl.md">Polski</a> |
|
<a href="README.pl.md">Polski</a> |
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
<a href="README.de.md">Deutsch</a> |
|
<a href="README.de.md">Deutsch</a> |
|
||||||
<a href="README.es.md">Español</a> |
|
<a href="README.es.md">Español</a> |
|
||||||
<a href="README.fr.md">Français</a> |
|
<a href="README.fr.md">Français</a> |
|
||||||
|
<a href="README.it.md">Italiano</a> |
|
||||||
<a href="README.da.md">Dansk</a> |
|
<a href="README.da.md">Dansk</a> |
|
||||||
<a href="README.ja.md">日本語</a> |
|
<a href="README.ja.md">日本語</a> |
|
||||||
<a href="README.pl.md">Polski</a> |
|
<a href="README.pl.md">Polski</a> |
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
<a href="README.de.md">Deutsch</a> |
|
<a href="README.de.md">Deutsch</a> |
|
||||||
<a href="README.es.md">Español</a> |
|
<a href="README.es.md">Español</a> |
|
||||||
<a href="README.fr.md">Français</a> |
|
<a href="README.fr.md">Français</a> |
|
||||||
|
<a href="README.it.md">Italiano</a> |
|
||||||
<a href="README.da.md">Dansk</a> |
|
<a href="README.da.md">Dansk</a> |
|
||||||
<a href="README.ja.md">日本語</a> |
|
<a href="README.ja.md">日本語</a> |
|
||||||
<a href="README.pl.md">Polski</a> |
|
<a href="README.pl.md">Polski</a> |
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
<a href="README.de.md">Deutsch</a> |
|
<a href="README.de.md">Deutsch</a> |
|
||||||
<a href="README.es.md">Español</a> |
|
<a href="README.es.md">Español</a> |
|
||||||
<a href="README.fr.md">Français</a> |
|
<a href="README.fr.md">Français</a> |
|
||||||
|
<a href="README.it.md">Italiano</a> |
|
||||||
<a href="README.da.md">Dansk</a> |
|
<a href="README.da.md">Dansk</a> |
|
||||||
<a href="README.ja.md">日本語</a> |
|
<a href="README.ja.md">日本語</a> |
|
||||||
<a href="README.pl.md">Polski</a> |
|
<a href="README.pl.md">Polski</a> |
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
<a href="README.de.md">Deutsch</a> |
|
<a href="README.de.md">Deutsch</a> |
|
||||||
<a href="README.es.md">Español</a> |
|
<a href="README.es.md">Español</a> |
|
||||||
<a href="README.fr.md">Français</a> |
|
<a href="README.fr.md">Français</a> |
|
||||||
|
<a href="README.it.md">Italiano</a> |
|
||||||
<a href="README.da.md">Dansk</a> |
|
<a href="README.da.md">Dansk</a> |
|
||||||
<a href="README.ja.md">日本語</a> |
|
<a href="README.ja.md">日本語</a> |
|
||||||
<a href="README.pl.md">Polski</a> |
|
<a href="README.pl.md">Polski</a> |
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
<a href="README.de.md">Deutsch</a> |
|
<a href="README.de.md">Deutsch</a> |
|
||||||
<a href="README.es.md">Español</a> |
|
<a href="README.es.md">Español</a> |
|
||||||
<a href="README.fr.md">Français</a> |
|
<a href="README.fr.md">Français</a> |
|
||||||
|
<a href="README.it.md">Italiano</a> |
|
||||||
<a href="README.da.md">Dansk</a> |
|
<a href="README.da.md">Dansk</a> |
|
||||||
<a href="README.ja.md">日本語</a> |
|
<a href="README.ja.md">日本語</a> |
|
||||||
<a href="README.pl.md">Polski</a> |
|
<a href="README.pl.md">Polski</a> |
|
||||||
|
|||||||
+133
@@ -0,0 +1,133 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="https://opencode.ai">
|
||||||
|
<picture>
|
||||||
|
<source srcset="packages/console/app/src/asset/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
|
||||||
|
<source srcset="packages/console/app/src/asset/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
|
||||||
|
<img src="packages/console/app/src/asset/logo-ornate-light.svg" alt="Logo OpenCode">
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">L’agente di coding AI open source.</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
|
||||||
|
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
|
||||||
|
<a href="https://github.com/anomalyco/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/anomalyco/opencode/publish.yml?style=flat-square&branch=dev" /></a>
|
||||||
|
</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.it.md">Italiano</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>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[](https://opencode.ai)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Installazione
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# YOLO
|
||||||
|
curl -fsSL https://opencode.ai/install | bash
|
||||||
|
|
||||||
|
# Package manager
|
||||||
|
npm i -g opencode-ai@latest # oppure bun/pnpm/yarn
|
||||||
|
scoop install opencode # Windows
|
||||||
|
choco install opencode # Windows
|
||||||
|
brew install anomalyco/tap/opencode # macOS e Linux (consigliato, sempre aggiornato)
|
||||||
|
brew install opencode # macOS e Linux (formula brew ufficiale, aggiornata meno spesso)
|
||||||
|
paru -S opencode-bin # Arch Linux
|
||||||
|
mise use -g opencode # Qualsiasi OS
|
||||||
|
nix run nixpkgs#opencode # oppure github:anomalyco/opencode per l’ultima branch di sviluppo
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> Rimuovi le versioni precedenti alla 0.1.x prima di installare.
|
||||||
|
|
||||||
|
### App Desktop (BETA)
|
||||||
|
|
||||||
|
OpenCode è disponibile anche come applicazione desktop. Puoi scaricarla direttamente dalla [pagina delle release](https://github.com/anomalyco/opencode/releases) oppure da [opencode.ai/download](https://opencode.ai/download).
|
||||||
|
|
||||||
|
| Piattaforma | Download |
|
||||||
|
| --------------------- | ------------------------------------- |
|
||||||
|
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||||
|
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||||
|
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||||
|
| Linux | `.deb`, `.rpm`, oppure AppImage |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# macOS (Homebrew)
|
||||||
|
brew install --cask opencode-desktop
|
||||||
|
# Windows (Scoop)
|
||||||
|
scoop bucket add extras; scoop install extras/opencode-desktop
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Directory di installazione
|
||||||
|
|
||||||
|
Lo script di installazione rispetta il seguente ordine di priorità per il percorso di installazione:
|
||||||
|
|
||||||
|
1. `$OPENCODE_INSTALL_DIR` – Directory di installazione personalizzata
|
||||||
|
2. `$XDG_BIN_DIR` – Percorso conforme alla XDG Base Directory Specification
|
||||||
|
3. `$HOME/bin` – Directory binaria standard dell’utente (se esiste o può essere creata)
|
||||||
|
4. `$HOME/.opencode/bin` – Fallback predefinito
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Esempi
|
||||||
|
OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
|
||||||
|
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
### Agenti
|
||||||
|
|
||||||
|
OpenCode include due agenti integrati tra cui puoi passare usando il tasto `Tab`.
|
||||||
|
|
||||||
|
- **build** – Predefinito, agente con accesso completo per il lavoro di sviluppo
|
||||||
|
- **plan** – Agente in sola lettura per analisi ed esplorazione del codice
|
||||||
|
- Nega le modifiche ai file per impostazione predefinita
|
||||||
|
- Chiede il permesso prima di eseguire comandi bash
|
||||||
|
- Ideale per esplorare codebase sconosciute o pianificare modifiche
|
||||||
|
|
||||||
|
È inoltre incluso un sotto-agente **general** per ricerche complesse e attività multi-step.
|
||||||
|
Viene utilizzato internamente e può essere invocato usando `@general` nei messaggi.
|
||||||
|
|
||||||
|
Scopri di più sugli [agenti](https://opencode.ai/docs/agents).
|
||||||
|
|
||||||
|
### Documentazione
|
||||||
|
|
||||||
|
Per maggiori informazioni su come configurare OpenCode, [**consulta la nostra documentazione**](https://opencode.ai/docs).
|
||||||
|
|
||||||
|
### Contribuire
|
||||||
|
|
||||||
|
Se sei interessato a contribuire a OpenCode, leggi la nostra [guida alla contribuzione](./CONTRIBUTING.md) prima di inviare una pull request.
|
||||||
|
|
||||||
|
### Costruire su OpenCode
|
||||||
|
|
||||||
|
Se stai lavorando a un progetto correlato a OpenCode e che utilizza “opencode” come parte del nome (ad esempio “opencode-dashboard” o “opencode-mobile”), aggiungi una nota nel tuo README per chiarire che non è sviluppato dal team OpenCode e che non è affiliato in alcun modo con noi.
|
||||||
|
|
||||||
|
### FAQ
|
||||||
|
|
||||||
|
#### In cosa è diverso da Claude Code?
|
||||||
|
|
||||||
|
È molto simile a Claude Code in termini di funzionalità. Ecco le principali differenze:
|
||||||
|
|
||||||
|
- 100% open source
|
||||||
|
- Non è legato a nessun provider. Anche se consigliamo i modelli forniti tramite [OpenCode Zen](https://opencode.ai/zen), OpenCode può essere utilizzato con Claude, OpenAI, Google o persino modelli locali. Con l’evoluzione dei modelli, le differenze tra di essi si ridurranno e i prezzi scenderanno, quindi essere indipendenti dal provider è importante.
|
||||||
|
- Supporto LSP pronto all’uso
|
||||||
|
- Forte attenzione alla TUI. OpenCode è sviluppato da utenti neovim e dai creatori di [terminal.shop](https://terminal.shop); spingeremo al limite ciò che è possibile fare nel terminale.
|
||||||
|
- Architettura client/server. Questo, ad esempio, permette a OpenCode di girare sul tuo computer mentre lo controlli da remoto tramite un’app mobile. La frontend TUI è quindi solo uno dei possibili client.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Unisciti alla nostra community** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
<a href="README.de.md">Deutsch</a> |
|
<a href="README.de.md">Deutsch</a> |
|
||||||
<a href="README.es.md">Español</a> |
|
<a href="README.es.md">Español</a> |
|
||||||
<a href="README.fr.md">Français</a> |
|
<a href="README.fr.md">Français</a> |
|
||||||
|
<a href="README.it.md">Italiano</a> |
|
||||||
<a href="README.da.md">Dansk</a> |
|
<a href="README.da.md">Dansk</a> |
|
||||||
<a href="README.ja.md">日本語</a> |
|
<a href="README.ja.md">日本語</a> |
|
||||||
<a href="README.pl.md">Polski</a> |
|
<a href="README.pl.md">Polski</a> |
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
<a href="README.de.md">Deutsch</a> |
|
<a href="README.de.md">Deutsch</a> |
|
||||||
<a href="README.es.md">Español</a> |
|
<a href="README.es.md">Español</a> |
|
||||||
<a href="README.fr.md">Français</a> |
|
<a href="README.fr.md">Français</a> |
|
||||||
|
<a href="README.it.md">Italiano</a> |
|
||||||
<a href="README.da.md">Dansk</a> |
|
<a href="README.da.md">Dansk</a> |
|
||||||
<a href="README.ja.md">日本語</a> |
|
<a href="README.ja.md">日本語</a> |
|
||||||
<a href="README.pl.md">Polski</a> |
|
<a href="README.pl.md">Polski</a> |
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
<a href="README.de.md">Deutsch</a> |
|
<a href="README.de.md">Deutsch</a> |
|
||||||
<a href="README.es.md">Español</a> |
|
<a href="README.es.md">Español</a> |
|
||||||
<a href="README.fr.md">Français</a> |
|
<a href="README.fr.md">Français</a> |
|
||||||
|
<a href="README.it.md">Italiano</a> |
|
||||||
<a href="README.da.md">Dansk</a> |
|
<a href="README.da.md">Dansk</a> |
|
||||||
<a href="README.ja.md">日本語</a> |
|
<a href="README.ja.md">日本語</a> |
|
||||||
<a href="README.pl.md">Polski</a> |
|
<a href="README.pl.md">Polski</a> |
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
<a href="README.de.md">Deutsch</a> |
|
<a href="README.de.md">Deutsch</a> |
|
||||||
<a href="README.es.md">Español</a> |
|
<a href="README.es.md">Español</a> |
|
||||||
<a href="README.fr.md">Français</a> |
|
<a href="README.fr.md">Français</a> |
|
||||||
|
<a href="README.it.md">Italiano</a> |
|
||||||
<a href="README.da.md">Dansk</a> |
|
<a href="README.da.md">Dansk</a> |
|
||||||
<a href="README.ja.md">日本語</a> |
|
<a href="README.ja.md">日本語</a> |
|
||||||
<a href="README.pl.md">Polski</a> |
|
<a href="README.pl.md">Polski</a> |
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
<a href="README.de.md">Deutsch</a> |
|
<a href="README.de.md">Deutsch</a> |
|
||||||
<a href="README.es.md">Español</a> |
|
<a href="README.es.md">Español</a> |
|
||||||
<a href="README.fr.md">Français</a> |
|
<a href="README.fr.md">Français</a> |
|
||||||
|
<a href="README.it.md">Italiano</a> |
|
||||||
<a href="README.da.md">Dansk</a> |
|
<a href="README.da.md">Dansk</a> |
|
||||||
<a href="README.ja.md">日本語</a> |
|
<a href="README.ja.md">日本語</a> |
|
||||||
<a href="README.pl.md">Polski</a> |
|
<a href="README.pl.md">Polski</a> |
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
<a href="README.de.md">Deutsch</a> |
|
<a href="README.de.md">Deutsch</a> |
|
||||||
<a href="README.es.md">Español</a> |
|
<a href="README.es.md">Español</a> |
|
||||||
<a href="README.fr.md">Français</a> |
|
<a href="README.fr.md">Français</a> |
|
||||||
|
<a href="README.it.md">Italiano</a> |
|
||||||
<a href="README.da.md">Dansk</a> |
|
<a href="README.da.md">Dansk</a> |
|
||||||
<a href="README.ja.md">日本語</a> |
|
<a href="README.ja.md">日本語</a> |
|
||||||
<a href="README.pl.md">Polski</a> |
|
<a href="README.pl.md">Polski</a> |
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
<a href="README.de.md">Deutsch</a> |
|
<a href="README.de.md">Deutsch</a> |
|
||||||
<a href="README.es.md">Español</a> |
|
<a href="README.es.md">Español</a> |
|
||||||
<a href="README.fr.md">Français</a> |
|
<a href="README.fr.md">Français</a> |
|
||||||
|
<a href="README.it.md">Italiano</a> |
|
||||||
<a href="README.da.md">Dansk</a> |
|
<a href="README.da.md">Dansk</a> |
|
||||||
<a href="README.ja.md">日本語</a> |
|
<a href="README.ja.md">日本語</a> |
|
||||||
<a href="README.pl.md">Polski</a> |
|
<a href="README.pl.md">Polski</a> |
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
<a href="README.de.md">Deutsch</a> |
|
<a href="README.de.md">Deutsch</a> |
|
||||||
<a href="README.es.md">Español</a> |
|
<a href="README.es.md">Español</a> |
|
||||||
<a href="README.fr.md">Français</a> |
|
<a href="README.fr.md">Français</a> |
|
||||||
|
<a href="README.it.md">Italiano</a> |
|
||||||
<a href="README.da.md">Dansk</a> |
|
<a href="README.da.md">Dansk</a> |
|
||||||
<a href="README.ja.md">日本語</a> |
|
<a href="README.ja.md">日本語</a> |
|
||||||
<a href="README.pl.md">Polski</a> |
|
<a href="README.pl.md">Polski</a> |
|
||||||
|
|||||||
@@ -211,3 +211,4 @@
|
|||||||
| 2026-01-23 | 6,096,236 (+329,896) | 2,096,235 (+66,748) | 8,192,471 (+396,644) |
|
| 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-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-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/sdk": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
"@openrouter/ai-sdk-provider": "1.5.2",
|
"@openrouter/ai-sdk-provider": "1.5.2",
|
||||||
"@opentui/core": "0.1.74",
|
"@opentui/core": "0.1.75",
|
||||||
"@opentui/solid": "0.1.74",
|
"@opentui/solid": "0.1.75",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
@@ -1227,21 +1227,21 @@
|
|||||||
|
|
||||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||||
|
|
||||||
"@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": ["@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-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.74", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rfmlDLtm/u17CnuhJgCxPeYMvOST+A2MOdVOk46IurtHO849bdYqK6iudKNlFRs1FOrymgSKF9GlWBHAOKeRjg=="],
|
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.75", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gGaGZjkFpqcXJk6321JzhRl66pM2VxBlI470L8W4DQUW4S6iDT1R9L7awSzGB4Cn9toUl7DTV8BemaXZYXV4SA=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.74", "", { "os": "darwin", "cpu": "x64" }, "sha512-WAD8orsDV0ZdW/5GwjOOB4FY96772xbkz+rcV7WRzEFUVaqoBaC04IuqYzS9d5s+cjkbT5Cpj47hrVYkkVQKng=="],
|
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.75", "", { "os": "darwin", "cpu": "x64" }, "sha512-tPlvqQI0whZ76amHydpJs5kN+QeWAIcFbI8RAtlAo9baj2EbxTDC+JGwgb9Fnt0/YQx831humbtaNDhV2Jt1bw=="],
|
||||||
|
|
||||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.74", "", { "os": "linux", "cpu": "arm64" }, "sha512-lgmHzrzLy4e+rgBS+lhtsMLLgIMLbtLNMm6EzVPyYVDlLDGjM7+ulXMem7AtpaRrWrUUl4REiG9BoQUsCFDwYA=="],
|
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.75", "", { "os": "linux", "cpu": "arm64" }, "sha512-nVxIQ4Hqf84uBergDpWiVzU6pzpjy6tqBHRQpySxZ2flkJ/U6/aMEizVrQ1jcgIdxZtvqWDETZhzxhG0yDx+cw=="],
|
||||||
|
|
||||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.74", "", { "os": "linux", "cpu": "x64" }, "sha512-8Mn2WbdBQ29xCThuPZezjDhd1N3+fXwKkGvCBOdTI0le6h2A/vCNbfUVjwfr/EGZSRXxCG+Yapol34BAULGpOA=="],
|
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.75", "", { "os": "linux", "cpu": "x64" }, "sha512-1CnApef4kxA+ORyLfbuCLgZfEjp4wr3HjFnt7FAfOb73kIZH82cb7JYixeqRyy9eOcKfKqxLmBYy3o8IDkc4Rg=="],
|
||||||
|
|
||||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.74", "", { "os": "win32", "cpu": "arm64" }, "sha512-dvYUXz03avnI6ZluyLp00HPmR0UT/IE/6QS97XBsgJlUTtpnbKkBtB5jD1NHwWkElaRj1Qv2QP36ngFoJqbl9g=="],
|
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.75", "", { "os": "win32", "cpu": "arm64" }, "sha512-j0UB95nmkYGNzmOrs6GqaddO1S90R0YC6IhbKnbKBdjchFPNVLz9JpexAs6MBDXPZwdKAywMxtwG2h3aTJtxng=="],
|
||||||
|
|
||||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.74", "", { "os": "win32", "cpu": "x64" }, "sha512-3wfWXaAKOIlDQz6ZZIESf2M+YGZ7uFHijjTEM8w/STRlLw8Y6+QyGYi1myHSM4d6RSO+/s2EMDxvjDf899W9vQ=="],
|
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.75", "", { "os": "win32", "cpu": "x64" }, "sha512-ESpVZVGewe3JkB2TwrG3VRbkxT909iPdtvgNT7xTCIYH2VB4jqZomJfvERPTE0tvqAZJm19mHECzJFI8asSJgQ=="],
|
||||||
|
|
||||||
"@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=="],
|
"@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=="],
|
||||||
|
|
||||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-olTZ+tKugAY3LxizsJMlbK3TW78HZUoM03PigvQLP4A=",
|
"x86_64-linux": "sha256-AkI3guNjnE+bLZQVfzm0z14UENOECv2QBqMo5Lzkvt8=",
|
||||||
"aarch64-linux": "sha256-xdKDeqMEnYM2+vGySfb8pbcYyo/xMmgxG/ZhPCKaZEg=",
|
"aarch64-linux": "sha256-dBfdyVTqW+fBZKCxC9Ld+1m3cP+nIbS6UDo0tUfPOSk=",
|
||||||
"aarch64-darwin": "sha256-fihCTrHIiUG+py4vuqdr+YshqSKm2/B5onY50b97sPM=",
|
"aarch64-darwin": "sha256-tOw31AMnHkW2cEDi+iqT3P93lU3SiMve26TEIqPz97k=",
|
||||||
"x86_64-darwin": "sha256-inlQQPNAOdkmKK6HQAMI2bG/ZFlfwmUQu9a6vm6Q0jQ="
|
"x86_64-darwin": "sha256-wL/DmdZmxCmh+r4dsS1XGXuj8VPwR4pUqy5VIA76jl0="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { test, expect } from "./fixtures"
|
||||||
|
|
||||||
|
test("file tree can expand folders and open a file", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Toggle file tree" }).click()
|
||||||
|
|
||||||
|
const treeTabs = page.locator('[data-component="tabs"][data-variant="pill"][data-scope="filetree"]')
|
||||||
|
await expect(treeTabs).toBeVisible()
|
||||||
|
|
||||||
|
await treeTabs.locator('[data-slot="tabs-trigger"]').nth(1).click()
|
||||||
|
|
||||||
|
const node = (name: string) => treeTabs.getByRole("button", { name, exact: true })
|
||||||
|
|
||||||
|
await expect(node("packages")).toBeVisible()
|
||||||
|
await node("packages").click()
|
||||||
|
|
||||||
|
await expect(node("app")).toBeVisible()
|
||||||
|
await node("app").click()
|
||||||
|
|
||||||
|
await expect(node("src")).toBeVisible()
|
||||||
|
await node("src").click()
|
||||||
|
|
||||||
|
await expect(node("components")).toBeVisible()
|
||||||
|
await node("components").click()
|
||||||
|
|
||||||
|
await expect(node("file-tree.tsx")).toBeVisible()
|
||||||
|
await node("file-tree.tsx").click()
|
||||||
|
|
||||||
|
const tab = page.getByRole("tab", { name: "file-tree.tsx" })
|
||||||
|
await expect(tab).toBeVisible()
|
||||||
|
await tab.click()
|
||||||
|
|
||||||
|
const code = page.locator('[data-component="code"]').first()
|
||||||
|
await expect(code.getByText("export default function FileTree")).toBeVisible()
|
||||||
|
})
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { test as base, expect } from "@playwright/test"
|
import { test as base, expect } from "@playwright/test"
|
||||||
import { createSdk, dirSlug, getWorktree, promptSelector, sessionPath } from "./utils"
|
import { createSdk, dirSlug, getWorktree, promptSelector, serverUrl, sessionPath } from "./utils"
|
||||||
|
|
||||||
type TestFixtures = {
|
type TestFixtures = {
|
||||||
sdk: ReturnType<typeof createSdk>
|
sdk: ReturnType<typeof createSdk>
|
||||||
@@ -29,6 +29,55 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
|
|||||||
await use(createSdk(directory))
|
await use(createSdk(directory))
|
||||||
},
|
},
|
||||||
gotoSession: async ({ page, directory }, use) => {
|
gotoSession: async ({ page, directory }, use) => {
|
||||||
|
await page.addInitScript(
|
||||||
|
(input: { directory: string; serverUrl: string }) => {
|
||||||
|
const key = "opencode.global.dat:server"
|
||||||
|
const raw = localStorage.getItem(key)
|
||||||
|
const parsed = (() => {
|
||||||
|
if (!raw) return undefined
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw) as unknown
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
const store = parsed && typeof parsed === "object" ? (parsed as Record<string, unknown>) : {}
|
||||||
|
const list = Array.isArray(store.list) ? store.list : []
|
||||||
|
const lastProject = store.lastProject && typeof store.lastProject === "object" ? store.lastProject : {}
|
||||||
|
const projects = store.projects && typeof store.projects === "object" ? store.projects : {}
|
||||||
|
const nextProjects = { ...(projects as Record<string, unknown>) }
|
||||||
|
|
||||||
|
const add = (origin: string) => {
|
||||||
|
const current = nextProjects[origin]
|
||||||
|
const items = Array.isArray(current) ? current : []
|
||||||
|
const existing = items.filter(
|
||||||
|
(p): p is { worktree: string; expanded?: boolean } =>
|
||||||
|
!!p &&
|
||||||
|
typeof p === "object" &&
|
||||||
|
"worktree" in p &&
|
||||||
|
typeof (p as { worktree?: unknown }).worktree === "string",
|
||||||
|
)
|
||||||
|
|
||||||
|
if (existing.some((p) => p.worktree === input.directory)) return
|
||||||
|
nextProjects[origin] = [{ worktree: input.directory, expanded: true }, ...existing]
|
||||||
|
}
|
||||||
|
|
||||||
|
add("local")
|
||||||
|
add(input.serverUrl)
|
||||||
|
|
||||||
|
localStorage.setItem(
|
||||||
|
key,
|
||||||
|
JSON.stringify({
|
||||||
|
list,
|
||||||
|
projects: nextProjects,
|
||||||
|
lastProject,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ directory, serverUrl },
|
||||||
|
)
|
||||||
|
|
||||||
const gotoSession = async (sessionID?: string) => {
|
const gotoSession = async (sessionID?: string) => {
|
||||||
await page.goto(sessionPath(directory, sessionID))
|
await page.goto(sessionPath(directory, sessionID))
|
||||||
await expect(page.locator(promptSelector)).toBeVisible()
|
await expect(page.locator(promptSelector)).toBeVisible()
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { test, expect } from "./fixtures"
|
||||||
|
import { modKey, promptSelector } from "./utils"
|
||||||
|
|
||||||
|
type Locator = {
|
||||||
|
first: () => Locator
|
||||||
|
getAttribute: (name: string) => Promise<string | null>
|
||||||
|
scrollIntoViewIfNeeded: () => Promise<void>
|
||||||
|
click: () => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Page = {
|
||||||
|
locator: (selector: string) => Locator
|
||||||
|
keyboard: {
|
||||||
|
press: (key: string) => Promise<void>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Fixtures = {
|
||||||
|
page: Page
|
||||||
|
slug: string
|
||||||
|
sdk: {
|
||||||
|
session: {
|
||||||
|
create: (input: { title: string }) => Promise<{ data?: { id?: string } }>
|
||||||
|
delete: (input: { sessionID: string }) => Promise<unknown>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gotoSession: (sessionID?: string) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
test("sidebar session links navigate to the selected session", async ({ page, slug, sdk, gotoSession }: Fixtures) => {
|
||||||
|
const stamp = Date.now()
|
||||||
|
|
||||||
|
const one = await sdk.session.create({ title: `e2e sidebar nav 1 ${stamp}` }).then((r) => r.data)
|
||||||
|
const two = await sdk.session.create({ title: `e2e sidebar nav 2 ${stamp}` }).then((r) => r.data)
|
||||||
|
|
||||||
|
if (!one?.id) throw new Error("Session create did not return an id")
|
||||||
|
if (!two?.id) throw new Error("Session create did not return an id")
|
||||||
|
|
||||||
|
try {
|
||||||
|
await gotoSession(one.id)
|
||||||
|
|
||||||
|
const main = page.locator("main")
|
||||||
|
const collapsed = ((await main.getAttribute("class")) ?? "").includes("xl:border-l")
|
||||||
|
if (collapsed) {
|
||||||
|
await page.keyboard.press(`${modKey}+B`)
|
||||||
|
await expect(main).not.toHaveClass(/xl:border-l/)
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = page.locator(`[data-session-id="${two.id}"] a`).first()
|
||||||
|
await expect(target).toBeVisible()
|
||||||
|
await target.scrollIntoViewIfNeeded()
|
||||||
|
await target.click()
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
|
||||||
|
await expect(page.locator(promptSelector)).toBeVisible()
|
||||||
|
await expect(page.locator(`[data-session-id="${two.id}"] a`).first()).toHaveClass(/\bactive\b/)
|
||||||
|
} finally {
|
||||||
|
await sdk.session.delete({ sessionID: one.id }).catch(() => undefined)
|
||||||
|
await sdk.session.delete({ sessionID: two.id }).catch(() => undefined)
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -14,22 +14,22 @@ import { GlobalSyncProvider } from "@/context/global-sync"
|
|||||||
import { PermissionProvider } from "@/context/permission"
|
import { PermissionProvider } from "@/context/permission"
|
||||||
import { LayoutProvider } from "@/context/layout"
|
import { LayoutProvider } from "@/context/layout"
|
||||||
import { GlobalSDKProvider } from "@/context/global-sdk"
|
import { GlobalSDKProvider } from "@/context/global-sdk"
|
||||||
import { ServerProvider, useServer } from "@/context/server"
|
import { normalizeServerUrl, ServerProvider, useServer } from "@/context/server"
|
||||||
import { SettingsProvider } from "@/context/settings"
|
import { SettingsProvider } from "@/context/settings"
|
||||||
import { TerminalProvider } from "@/context/terminal"
|
import { TerminalProvider } from "@/context/terminal"
|
||||||
import { PromptProvider } from "@/context/prompt"
|
import { PromptProvider } from "@/context/prompt"
|
||||||
import { FileProvider } from "@/context/file"
|
import { FileProvider } from "@/context/file"
|
||||||
import { CommentsProvider } from "@/context/comments"
|
import { CommentsProvider } from "@/context/comments"
|
||||||
import { NotificationProvider } from "@/context/notification"
|
import { NotificationProvider } from "@/context/notification"
|
||||||
|
import { ModelsProvider } from "@/context/models"
|
||||||
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
|
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
|
||||||
import { CommandProvider } from "@/context/command"
|
import { CommandProvider } from "@/context/command"
|
||||||
import { LanguageProvider, useLanguage } from "@/context/language"
|
import { LanguageProvider, useLanguage } from "@/context/language"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { Logo } from "@opencode-ai/ui/logo"
|
import { HighlightsProvider } from "@/context/highlights"
|
||||||
import Layout from "@/pages/layout"
|
import Layout from "@/pages/layout"
|
||||||
import DirectoryLayout from "@/pages/directory-layout"
|
import DirectoryLayout from "@/pages/directory-layout"
|
||||||
import { ErrorPage } from "./pages/error"
|
import { ErrorPage } from "./pages/error"
|
||||||
import { iife } from "@opencode-ai/util/iife"
|
|
||||||
import { Suspense } from "solid-js"
|
import { Suspense } from "solid-js"
|
||||||
|
|
||||||
const Home = lazy(() => import("@/pages/home"))
|
const Home = lazy(() => import("@/pages/home"))
|
||||||
@@ -85,8 +85,19 @@ function ServerKey(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function AppInterface(props: { defaultUrl?: string }) {
|
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 = () => {
|
const defaultServerUrl = () => {
|
||||||
if (props.defaultUrl) return props.defaultUrl
|
if (props.defaultUrl) return props.defaultUrl
|
||||||
|
if (stored) return stored
|
||||||
if (location.hostname.includes("opencode.ai")) return "http://localhost:4096"
|
if (location.hostname.includes("opencode.ai")) return "http://localhost:4096"
|
||||||
if (import.meta.env.DEV)
|
if (import.meta.env.DEV)
|
||||||
return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
|
return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
|
||||||
@@ -105,9 +116,13 @@ export function AppInterface(props: { defaultUrl?: string }) {
|
|||||||
<PermissionProvider>
|
<PermissionProvider>
|
||||||
<LayoutProvider>
|
<LayoutProvider>
|
||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
<CommandProvider>
|
<ModelsProvider>
|
||||||
<Layout>{props.children}</Layout>
|
<CommandProvider>
|
||||||
</CommandProvider>
|
<HighlightsProvider>
|
||||||
|
<Layout>{props.children}</Layout>
|
||||||
|
</HighlightsProvider>
|
||||||
|
</CommandProvider>
|
||||||
|
</ModelsProvider>
|
||||||
</NotificationProvider>
|
</NotificationProvider>
|
||||||
</LayoutProvider>
|
</LayoutProvider>
|
||||||
</PermissionProvider>
|
</PermissionProvider>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { createMemo, createSignal, For, Show } from "solid-js"
|
import { createMemo, For, Show } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
@@ -29,35 +29,34 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
iconUrl: props.project.icon?.override || "",
|
iconUrl: props.project.icon?.override || "",
|
||||||
startup: props.project.commands?.start ?? "",
|
startup: props.project.commands?.start ?? "",
|
||||||
saving: false,
|
saving: false,
|
||||||
|
dragOver: false,
|
||||||
|
iconHover: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const [dragOver, setDragOver] = createSignal(false)
|
|
||||||
const [iconHover, setIconHover] = createSignal(false)
|
|
||||||
|
|
||||||
function handleFileSelect(file: File) {
|
function handleFileSelect(file: File) {
|
||||||
if (!file.type.startsWith("image/")) return
|
if (!file.type.startsWith("image/")) return
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
setStore("iconUrl", e.target?.result as string)
|
setStore("iconUrl", e.target?.result as string)
|
||||||
setIconHover(false)
|
setStore("iconHover", false)
|
||||||
}
|
}
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDrop(e: DragEvent) {
|
function handleDrop(e: DragEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setDragOver(false)
|
setStore("dragOver", false)
|
||||||
const file = e.dataTransfer?.files[0]
|
const file = e.dataTransfer?.files[0]
|
||||||
if (file) handleFileSelect(file)
|
if (file) handleFileSelect(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDragOver(e: DragEvent) {
|
function handleDragOver(e: DragEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setDragOver(true)
|
setStore("dragOver", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDragLeave() {
|
function handleDragLeave() {
|
||||||
setDragOver(false)
|
setStore("dragOver", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleInputChange(e: Event) {
|
function handleInputChange(e: Event) {
|
||||||
@@ -116,19 +115,23 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.icon")}</label>
|
<label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.icon")}</label>
|
||||||
<div class="flex gap-3 items-start">
|
<div class="flex gap-3 items-start">
|
||||||
<div class="relative" onMouseEnter={() => setIconHover(true)} onMouseLeave={() => setIconHover(false)}>
|
<div
|
||||||
|
class="relative"
|
||||||
|
onMouseEnter={() => setStore("iconHover", true)}
|
||||||
|
onMouseLeave={() => setStore("iconHover", false)}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="relative size-16 rounded-md transition-colors cursor-pointer"
|
class="relative size-16 rounded-md transition-colors cursor-pointer"
|
||||||
classList={{
|
classList={{
|
||||||
"border-text-interactive-base bg-surface-info-base/20": dragOver(),
|
"border-text-interactive-base bg-surface-info-base/20": store.dragOver,
|
||||||
"border-border-base hover:border-border-strong": !dragOver(),
|
"border-border-base hover:border-border-strong": !store.dragOver,
|
||||||
"overflow-hidden": !!store.iconUrl,
|
"overflow-hidden": !!store.iconUrl,
|
||||||
}}
|
}}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (store.iconUrl && iconHover()) {
|
if (store.iconUrl && store.iconHover) {
|
||||||
clearIcon()
|
clearIcon()
|
||||||
} else {
|
} else {
|
||||||
document.getElementById("icon-upload")?.click()
|
document.getElementById("icon-upload")?.click()
|
||||||
@@ -166,7 +169,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
"border-radius": "6px",
|
"border-radius": "6px",
|
||||||
"z-index": 10,
|
"z-index": 10,
|
||||||
"pointer-events": "none",
|
"pointer-events": "none",
|
||||||
opacity: iconHover() && !store.iconUrl ? 1 : 0,
|
opacity: store.iconHover && !store.iconUrl ? 1 : 0,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
"align-items": "center",
|
"align-items": "center",
|
||||||
"justify-content": "center",
|
"justify-content": "center",
|
||||||
@@ -185,7 +188,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
"border-radius": "6px",
|
"border-radius": "6px",
|
||||||
"z-index": 10,
|
"z-index": 10,
|
||||||
"pointer-events": "none",
|
"pointer-events": "none",
|
||||||
opacity: iconHover() && store.iconUrl ? 1 : 0,
|
opacity: store.iconHover && store.iconUrl ? 1 : 0,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
"align-items": "center",
|
"align-items": "center",
|
||||||
"justify-content": "center",
|
"justify-content": "center",
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import { createSignal, createEffect, onMount, onCleanup } from "solid-js"
|
||||||
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
|
import { useSettings } from "@/context/settings"
|
||||||
|
|
||||||
|
export type Highlight = {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
media?: {
|
||||||
|
type: "image" | "video"
|
||||||
|
src: string
|
||||||
|
alt?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DialogReleaseNotes(props: { highlights: Highlight[] }) {
|
||||||
|
const dialog = useDialog()
|
||||||
|
const settings = useSettings()
|
||||||
|
const [index, setIndex] = createSignal(0)
|
||||||
|
|
||||||
|
const total = () => props.highlights.length
|
||||||
|
const last = () => Math.max(0, total() - 1)
|
||||||
|
const feature = () => props.highlights[index()] ?? props.highlights[last()]
|
||||||
|
const isFirst = () => index() === 0
|
||||||
|
const isLast = () => index() >= last()
|
||||||
|
const paged = () => total() > 1
|
||||||
|
|
||||||
|
function handleNext() {
|
||||||
|
if (isLast()) return
|
||||||
|
setIndex(index() + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
dialog.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDisable() {
|
||||||
|
settings.general.setReleaseNotes(false)
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
let focusTrap: HTMLDivElement | undefined
|
||||||
|
|
||||||
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
e.preventDefault()
|
||||||
|
handleClose()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!paged()) return
|
||||||
|
if (e.key === "ArrowLeft" && !isFirst()) {
|
||||||
|
e.preventDefault()
|
||||||
|
setIndex(index() - 1)
|
||||||
|
}
|
||||||
|
if (e.key === "ArrowRight" && !isLast()) {
|
||||||
|
e.preventDefault()
|
||||||
|
setIndex(index() + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
focusTrap?.focus()
|
||||||
|
document.addEventListener("keydown", handleKeyDown)
|
||||||
|
onCleanup(() => document.removeEventListener("keydown", handleKeyDown))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Refocus the trap when index changes to ensure escape always works
|
||||||
|
createEffect(() => {
|
||||||
|
index() // track index
|
||||||
|
focusTrap?.focus()
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog class="dialog-release-notes">
|
||||||
|
{/* Hidden element to capture initial focus and handle escape */}
|
||||||
|
<div ref={focusTrap} tabindex="0" class="absolute opacity-0 pointer-events-none" />
|
||||||
|
{/* Left side - Text content */}
|
||||||
|
<div class="flex flex-col flex-1 min-w-0 p-8">
|
||||||
|
{/* Top section - feature content (fixed position from top) */}
|
||||||
|
<div class="flex flex-col gap-2 pt-22">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<h1 class="text-16-medium text-text-strong">{feature()?.title ?? ""}</h1>
|
||||||
|
</div>
|
||||||
|
<p class="text-14-regular text-text-base">{feature()?.description ?? ""}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Spacer to push buttons to bottom */}
|
||||||
|
<div class="flex-1" />
|
||||||
|
|
||||||
|
{/* Bottom section - buttons and indicators (fixed position) */}
|
||||||
|
<div class="flex flex-col gap-12">
|
||||||
|
<div class="flex flex-col items-start gap-3">
|
||||||
|
{isLast() ? (
|
||||||
|
<Button variant="primary" size="large" onClick={handleClose}>
|
||||||
|
Get started
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button variant="secondary" size="large" onClick={handleNext}>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button variant="ghost" size="small" onClick={handleDisable}>
|
||||||
|
Don't show these in the future
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{paged() && (
|
||||||
|
<div class="flex items-center gap-1.5 -my-2.5">
|
||||||
|
{props.highlights.map((_, i) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="h-6 flex items-center cursor-pointer bg-transparent border-none p-0 transition-all duration-200"
|
||||||
|
classList={{
|
||||||
|
"w-8": i === index(),
|
||||||
|
"w-3": i !== index(),
|
||||||
|
}}
|
||||||
|
onClick={() => setIndex(i)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-full h-0.5 rounded-[1px] transition-colors duration-200"
|
||||||
|
classList={{
|
||||||
|
"bg-icon-strong-base": i === index(),
|
||||||
|
"bg-icon-weak-base": i !== index(),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right side - Media content (edge to edge) */}
|
||||||
|
{feature()?.media && (
|
||||||
|
<div class="flex-1 min-w-0 bg-surface-base overflow-hidden rounded-r-xl">
|
||||||
|
{feature()!.media!.type === "image" ? (
|
||||||
|
<img
|
||||||
|
src={feature()!.media!.src}
|
||||||
|
alt={feature()!.media!.alt ?? feature()?.title ?? "Release preview"}
|
||||||
|
class="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<video src={feature()!.media!.src} autoplay loop muted playsinline class="w-full h-full object-cover" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -24,13 +24,16 @@ type Entry = {
|
|||||||
path?: string
|
path?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DialogSelectFile() {
|
type DialogSelectFileMode = "all" | "files"
|
||||||
|
|
||||||
|
export function DialogSelectFile(props: { mode?: DialogSelectFileMode }) {
|
||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
const file = useFile()
|
const file = useFile()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
|
const filesOnly = () => props.mode === "files"
|
||||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
const tabs = createMemo(() => layout.tabs(sessionKey))
|
const tabs = createMemo(() => layout.tabs(sessionKey))
|
||||||
const view = createMemo(() => layout.view(sessionKey))
|
const view = createMemo(() => layout.view(sessionKey))
|
||||||
@@ -46,11 +49,12 @@ export function DialogSelectFile() {
|
|||||||
]
|
]
|
||||||
const limit = 5
|
const limit = 5
|
||||||
|
|
||||||
const allowed = createMemo(() =>
|
const allowed = createMemo(() => {
|
||||||
command.options.filter(
|
if (filesOnly()) return []
|
||||||
|
return command.options.filter(
|
||||||
(option) => !option.disabled && !option.id.startsWith("suggested.") && option.id !== "file.open",
|
(option) => !option.disabled && !option.id.startsWith("suggested.") && option.id !== "file.open",
|
||||||
),
|
)
|
||||||
)
|
})
|
||||||
|
|
||||||
const commandItem = (option: CommandOption): Entry => ({
|
const commandItem = (option: CommandOption): Entry => ({
|
||||||
id: "command:" + option.id,
|
id: "command:" + option.id,
|
||||||
@@ -99,10 +103,50 @@ export function DialogSelectFile() {
|
|||||||
return items.slice(0, limit)
|
return items.slice(0, limit)
|
||||||
})
|
})
|
||||||
|
|
||||||
const items = async (filter: string) => {
|
const root = createMemo(() => {
|
||||||
const query = filter.trim()
|
const nodes = file.tree.children("")
|
||||||
|
const paths = nodes
|
||||||
|
.filter((node) => node.type === "file")
|
||||||
|
.map((node) => node.path)
|
||||||
|
.sort((a, b) => a.localeCompare(b))
|
||||||
|
return paths.slice(0, limit).map(fileItem)
|
||||||
|
})
|
||||||
|
|
||||||
|
const unique = (items: Entry[]) => {
|
||||||
|
const seen = new Set<string>()
|
||||||
|
const out: Entry[] = []
|
||||||
|
for (const item of items) {
|
||||||
|
if (seen.has(item.id)) continue
|
||||||
|
seen.add(item.id)
|
||||||
|
out.push(item)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = async (text: string) => {
|
||||||
|
const query = text.trim()
|
||||||
setGrouped(query.length > 0)
|
setGrouped(query.length > 0)
|
||||||
|
|
||||||
|
if (!query && filesOnly()) {
|
||||||
|
const loaded = file.tree.state("")?.loaded
|
||||||
|
const pending = loaded ? Promise.resolve() : file.tree.list("")
|
||||||
|
const next = unique([...recent(), ...root()])
|
||||||
|
|
||||||
|
if (loaded || next.length > 0) {
|
||||||
|
void pending
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
await pending
|
||||||
|
return unique([...recent(), ...root()])
|
||||||
|
}
|
||||||
|
|
||||||
if (!query) return [...picks(), ...recent()]
|
if (!query) return [...picks(), ...recent()]
|
||||||
|
|
||||||
|
if (filesOnly()) {
|
||||||
|
const files = await file.searchFiles(query)
|
||||||
|
return files.map(fileItem)
|
||||||
|
}
|
||||||
const files = await file.searchFiles(query)
|
const files = await file.searchFiles(query)
|
||||||
const entries = files.map(fileItem)
|
const entries = files.map(fileItem)
|
||||||
return [...list(), ...entries]
|
return [...list(), ...entries]
|
||||||
@@ -143,10 +187,12 @@ export function DialogSelectFile() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog class="pt-3 pb-0 !max-h-[480px]">
|
<Dialog class="pt-3 pb-0 !max-h-[480px]" transition>
|
||||||
<List
|
<List
|
||||||
search={{
|
search={{
|
||||||
placeholder: language.t("palette.search.placeholder"),
|
placeholder: filesOnly()
|
||||||
|
? language.t("session.header.searchFiles")
|
||||||
|
: language.t("palette.search.placeholder"),
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
hideIcon: true,
|
hideIcon: true,
|
||||||
class: "pl-3 pr-2 !mb-0",
|
class: "pl-3 pr-2 !mb-0",
|
||||||
|
|||||||
@@ -110,6 +110,11 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
|
|||||||
setStore("open", false)
|
setStore("open", false)
|
||||||
dialog.show(() => <DialogManageModels />)
|
dialog.show(() => <DialogManageModels />)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleConnectProvider = () => {
|
||||||
|
setStore("open", false)
|
||||||
|
dialog.show(() => <DialogSelectProvider />)
|
||||||
|
}
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
@@ -207,15 +212,26 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
|
|||||||
onSelect={() => setStore("open", false)}
|
onSelect={() => setStore("open", false)}
|
||||||
class="p-1"
|
class="p-1"
|
||||||
action={
|
action={
|
||||||
<IconButton
|
<div class="flex items-center gap-1">
|
||||||
icon="sliders"
|
<IconButton
|
||||||
variant="ghost"
|
icon="plus-small"
|
||||||
iconSize="normal"
|
variant="ghost"
|
||||||
class="size-6"
|
iconSize="normal"
|
||||||
aria-label={language.t("dialog.model.manage")}
|
class="size-6"
|
||||||
title={language.t("dialog.model.manage")}
|
aria-label={language.t("command.provider.connect")}
|
||||||
onClick={handleManage}
|
title={language.t("command.provider.connect")}
|
||||||
/>
|
onClick={handleConnectProvider}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon="sliders"
|
||||||
|
variant="ghost"
|
||||||
|
iconSize="normal"
|
||||||
|
class="size-6"
|
||||||
|
aria-label={language.t("dialog.model.manage")}
|
||||||
|
title={language.t("dialog.model.manage")}
|
||||||
|
onClick={handleManage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Kobalte.Content>
|
</Kobalte.Content>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const DialogSelectProvider: Component = () => {
|
|||||||
const otherGroup = () => language.t("dialog.provider.group.other")
|
const otherGroup = () => language.t("dialog.provider.group.other")
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog title={language.t("command.provider.connect")}>
|
<Dialog title={language.t("command.provider.connect")} transition>
|
||||||
<List
|
<List
|
||||||
search={{ placeholder: language.t("dialog.provider.search.placeholder"), autofocus: true }}
|
search={{ placeholder: language.t("dialog.provider.search.placeholder"), autofocus: true }}
|
||||||
emptyMessage={language.t("dialog.provider.empty")}
|
emptyMessage={language.t("dialog.provider.empty")}
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ export function DialogSelectServer() {
|
|||||||
},
|
},
|
||||||
{ initialValue: null },
|
{ initialValue: null },
|
||||||
)
|
)
|
||||||
const isDesktop = platform.platform === "desktop"
|
const canDefault = createMemo(() => !!platform.getDefaultServerUrl && !!platform.setDefaultServerUrl)
|
||||||
|
|
||||||
const looksComplete = (value: string) => {
|
const looksComplete = (value: string) => {
|
||||||
const normalized = normalizeServerUrl(value)
|
const normalized = normalizeServerUrl(value)
|
||||||
@@ -505,7 +505,7 @@ export function DialogSelectServer() {
|
|||||||
>
|
>
|
||||||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
|
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<Show when={isDesktop && defaultUrl() !== i}>
|
<Show when={canDefault() && defaultUrl() !== i}>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
onSelect={async () => {
|
onSelect={async () => {
|
||||||
await platform.setDefaultServerUrl?.(i)
|
await platform.setDefaultServerUrl?.(i)
|
||||||
@@ -517,7 +517,7 @@ export function DialogSelectServer() {
|
|||||||
</DropdownMenu.ItemLabel>
|
</DropdownMenu.ItemLabel>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={isDesktop && defaultUrl() === i}>
|
<Show when={canDefault() && defaultUrl() === i}>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
onSelect={async () => {
|
onSelect={async () => {
|
||||||
await platform.setDefaultServerUrl?.(null)
|
await platform.setDefaultServerUrl?.(null)
|
||||||
|
|||||||
@@ -6,19 +6,15 @@ import { useLanguage } from "@/context/language"
|
|||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { SettingsGeneral } from "./settings-general"
|
import { SettingsGeneral } from "./settings-general"
|
||||||
import { SettingsKeybinds } from "./settings-keybinds"
|
import { SettingsKeybinds } from "./settings-keybinds"
|
||||||
import { SettingsPermissions } from "./settings-permissions"
|
|
||||||
import { SettingsProviders } from "./settings-providers"
|
import { SettingsProviders } from "./settings-providers"
|
||||||
import { SettingsModels } from "./settings-models"
|
import { SettingsModels } from "./settings-models"
|
||||||
import { SettingsAgents } from "./settings-agents"
|
|
||||||
import { SettingsCommands } from "./settings-commands"
|
|
||||||
import { SettingsMcp } from "./settings-mcp"
|
|
||||||
|
|
||||||
export const DialogSettings: Component = () => {
|
export const DialogSettings: Component = () => {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog size="x-large">
|
<Dialog size="x-large" transition>
|
||||||
<Tabs orientation="vertical" variant="settings" defaultValue="general" class="h-full settings-dialog">
|
<Tabs orientation="vertical" variant="settings" defaultValue="general" class="h-full settings-dialog">
|
||||||
<Tabs.List>
|
<Tabs.List>
|
||||||
<div class="flex flex-col justify-between h-full w-full">
|
<div class="flex flex-col justify-between h-full w-full">
|
||||||
@@ -42,15 +38,19 @@ export const DialogSettings: Component = () => {
|
|||||||
<Tabs.SectionTitle>{language.t("settings.section.server")}</Tabs.SectionTitle>
|
<Tabs.SectionTitle>{language.t("settings.section.server")}</Tabs.SectionTitle>
|
||||||
<div class="flex flex-col gap-1.5 w-full">
|
<div class="flex flex-col gap-1.5 w-full">
|
||||||
<Tabs.Trigger value="providers">
|
<Tabs.Trigger value="providers">
|
||||||
<Icon name="server" />
|
<Icon name="providers" />
|
||||||
{language.t("settings.providers.title")}
|
{language.t("settings.providers.title")}
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger value="models">
|
||||||
|
<Icon name="models" />
|
||||||
|
{language.t("settings.models.title")}
|
||||||
|
</Tabs.Trigger>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-1 pl-1 py-1 text-12-medium text-text-weak">
|
<div class="flex flex-col gap-1 pl-1 py-1 text-12-medium text-text-weak">
|
||||||
<span>OpenCode Desktop</span>
|
<span>{language.t("app.name.desktop")}</span>
|
||||||
<span class="text-11-regular">v{platform.version}</span>
|
<span class="text-11-regular">v{platform.version}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,9 +64,9 @@ export const DialogSettings: Component = () => {
|
|||||||
<Tabs.Content value="providers" class="no-scrollbar">
|
<Tabs.Content value="providers" class="no-scrollbar">
|
||||||
<SettingsProviders />
|
<SettingsProviders />
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
{/* <Tabs.Content value="models" class="no-scrollbar"> */}
|
<Tabs.Content value="models" class="no-scrollbar">
|
||||||
{/* <SettingsModels /> */}
|
<SettingsModels />
|
||||||
{/* </Tabs.Content> */}
|
</Tabs.Content>
|
||||||
{/* <Tabs.Content value="agents" class="no-scrollbar"> */}
|
{/* <Tabs.Content value="agents" class="no-scrollbar"> */}
|
||||||
{/* <SettingsAgents /> */}
|
{/* <SettingsAgents /> */}
|
||||||
{/* </Tabs.Content> */}
|
{/* </Tabs.Content> */}
|
||||||
|
|||||||
@@ -1,111 +1,261 @@
|
|||||||
import { useLocal, type LocalFile } from "@/context/local"
|
import { useFile } from "@/context/file"
|
||||||
import { Collapsible } from "@opencode-ai/ui/collapsible"
|
import { Collapsible } from "@opencode-ai/ui/collapsible"
|
||||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { For, Match, Switch, type ComponentProps, type ParentProps } from "solid-js"
|
import {
|
||||||
|
createEffect,
|
||||||
|
createMemo,
|
||||||
|
For,
|
||||||
|
Match,
|
||||||
|
splitProps,
|
||||||
|
Switch,
|
||||||
|
untrack,
|
||||||
|
type ComponentProps,
|
||||||
|
type ParentProps,
|
||||||
|
} from "solid-js"
|
||||||
import { Dynamic } from "solid-js/web"
|
import { Dynamic } from "solid-js/web"
|
||||||
|
import type { FileNode } from "@opencode-ai/sdk/v2"
|
||||||
|
|
||||||
|
type Filter = {
|
||||||
|
files: Set<string>
|
||||||
|
dirs: Set<string>
|
||||||
|
}
|
||||||
|
|
||||||
export default function FileTree(props: {
|
export default function FileTree(props: {
|
||||||
path: string
|
path: string
|
||||||
class?: string
|
class?: string
|
||||||
nodeClass?: string
|
nodeClass?: string
|
||||||
level?: number
|
level?: number
|
||||||
onFileClick?: (file: LocalFile) => void
|
allowed?: readonly string[]
|
||||||
|
modified?: readonly string[]
|
||||||
|
draggable?: boolean
|
||||||
|
tooltip?: boolean
|
||||||
|
onFileClick?: (file: FileNode) => void
|
||||||
|
|
||||||
|
_filter?: Filter
|
||||||
|
_marks?: Set<string>
|
||||||
|
_deeps?: Map<string, number>
|
||||||
}) {
|
}) {
|
||||||
const local = useLocal()
|
const file = useFile()
|
||||||
const level = props.level ?? 0
|
const level = props.level ?? 0
|
||||||
|
const draggable = () => props.draggable ?? true
|
||||||
|
const tooltip = () => props.tooltip ?? true
|
||||||
|
|
||||||
const Node = (p: ParentProps & ComponentProps<"div"> & { node: LocalFile; as?: "div" | "button" }) => (
|
const filter = createMemo(() => {
|
||||||
<Dynamic
|
if (props._filter) return props._filter
|
||||||
component={p.as ?? "div"}
|
|
||||||
classList={{
|
|
||||||
"p-0.5 w-full flex items-center gap-x-2 hover:bg-background-element": true,
|
|
||||||
// "bg-background-element": local.file.active()?.path === p.node.path,
|
|
||||||
[props.nodeClass ?? ""]: !!props.nodeClass,
|
|
||||||
}}
|
|
||||||
style={`padding-left: ${level * 10}px`}
|
|
||||||
draggable={true}
|
|
||||||
onDragStart={(e: any) => {
|
|
||||||
const evt = e as globalThis.DragEvent
|
|
||||||
evt.dataTransfer!.effectAllowed = "copy"
|
|
||||||
evt.dataTransfer!.setData("text/plain", `file:${p.node.path}`)
|
|
||||||
|
|
||||||
// Create custom drag image without margins
|
const allowed = props.allowed
|
||||||
const dragImage = document.createElement("div")
|
if (!allowed) return
|
||||||
dragImage.className =
|
|
||||||
"flex items-center gap-x-2 px-2 py-1 bg-background-element rounded-md border border-border-1"
|
|
||||||
dragImage.style.position = "absolute"
|
|
||||||
dragImage.style.top = "-1000px"
|
|
||||||
|
|
||||||
// Copy only the icon and text content without padding
|
const files = new Set(allowed)
|
||||||
const icon = e.currentTarget.querySelector("svg")
|
const dirs = new Set<string>()
|
||||||
const text = e.currentTarget.querySelector("span")
|
|
||||||
if (icon && text) {
|
|
||||||
dragImage.innerHTML = icon.outerHTML + text.outerHTML
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.appendChild(dragImage)
|
for (const item of allowed) {
|
||||||
evt.dataTransfer!.setDragImage(dragImage, 0, 12)
|
const parts = item.split("/")
|
||||||
setTimeout(() => document.body.removeChild(dragImage), 0)
|
const parents = parts.slice(0, -1)
|
||||||
}}
|
for (const [idx] of parents.entries()) {
|
||||||
{...p}
|
const dir = parents.slice(0, idx + 1).join("/")
|
||||||
>
|
if (dir) dirs.add(dir)
|
||||||
{p.children}
|
}
|
||||||
<span
|
}
|
||||||
|
|
||||||
|
return { files, dirs }
|
||||||
|
})
|
||||||
|
|
||||||
|
const marks = createMemo(() => {
|
||||||
|
if (props._marks) return props._marks
|
||||||
|
|
||||||
|
const modified = props.modified
|
||||||
|
if (!modified || modified.length === 0) return
|
||||||
|
return new Set(modified)
|
||||||
|
})
|
||||||
|
|
||||||
|
const deeps = createMemo(() => {
|
||||||
|
if (props._deeps) return props._deeps
|
||||||
|
|
||||||
|
const out = new Map<string, number>()
|
||||||
|
|
||||||
|
const visit = (dir: string, lvl: number): number => {
|
||||||
|
const expanded = file.tree.state(dir)?.expanded ?? false
|
||||||
|
if (!expanded) return -1
|
||||||
|
|
||||||
|
const nodes = file.tree.children(dir)
|
||||||
|
const max = nodes.reduce((max, node) => {
|
||||||
|
if (node.type !== "directory") return max
|
||||||
|
const open = file.tree.state(node.path)?.expanded ?? false
|
||||||
|
if (!open) return max
|
||||||
|
return Math.max(max, visit(node.path, lvl + 1))
|
||||||
|
}, lvl)
|
||||||
|
|
||||||
|
out.set(dir, max)
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
|
||||||
|
visit(props.path, level - 1)
|
||||||
|
return out
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const current = filter()
|
||||||
|
if (!current) return
|
||||||
|
if (level !== 0) return
|
||||||
|
|
||||||
|
for (const dir of current.dirs) {
|
||||||
|
const expanded = untrack(() => file.tree.state(dir)?.expanded) ?? false
|
||||||
|
if (expanded) continue
|
||||||
|
file.tree.expand(dir)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const path = props.path
|
||||||
|
untrack(() => void file.tree.list(path))
|
||||||
|
})
|
||||||
|
|
||||||
|
const nodes = createMemo(() => {
|
||||||
|
const nodes = file.tree.children(props.path)
|
||||||
|
const current = filter()
|
||||||
|
if (!current) return nodes
|
||||||
|
return nodes.filter((node) => {
|
||||||
|
if (node.type === "file") return current.files.has(node.path)
|
||||||
|
return current.dirs.has(node.path)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const Node = (
|
||||||
|
p: ParentProps &
|
||||||
|
ComponentProps<"div"> &
|
||||||
|
ComponentProps<"button"> & {
|
||||||
|
node: FileNode
|
||||||
|
as?: "div" | "button"
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const [local, rest] = splitProps(p, ["node", "as", "children", "class", "classList"])
|
||||||
|
return (
|
||||||
|
<Dynamic
|
||||||
|
component={local.as ?? "div"}
|
||||||
classList={{
|
classList={{
|
||||||
"text-xs whitespace-nowrap truncate": true,
|
"w-full min-w-0 h-6 flex items-center justify-start gap-x-1.5 rounded-md px-2 py-0 text-left hover:bg-surface-raised-base-hover active:bg-surface-base-active transition-colors cursor-pointer": true,
|
||||||
"text-text-muted/40": p.node.ignored,
|
...(local.classList ?? {}),
|
||||||
"text-text-muted/80": !p.node.ignored,
|
[local.class ?? ""]: !!local.class,
|
||||||
// "!text-text": local.file.active()?.path === p.node.path,
|
[props.nodeClass ?? ""]: !!props.nodeClass,
|
||||||
// "!text-primary": local.file.changed(p.node.path),
|
|
||||||
}}
|
}}
|
||||||
|
style={`padding-left: ${Math.max(0, 8 + level * 12 - (local.node.type === "file" ? 24 : 4))}px`}
|
||||||
|
draggable={draggable()}
|
||||||
|
onDragStart={(e: DragEvent) => {
|
||||||
|
if (!draggable()) return
|
||||||
|
e.dataTransfer?.setData("text/plain", `file:${local.node.path}`)
|
||||||
|
e.dataTransfer?.setData("text/uri-list", `file://${local.node.path}`)
|
||||||
|
if (e.dataTransfer) e.dataTransfer.effectAllowed = "copy"
|
||||||
|
|
||||||
|
const dragImage = document.createElement("div")
|
||||||
|
dragImage.className =
|
||||||
|
"flex items-center gap-x-2 px-2 py-1 bg-surface-raised-base rounded-md border border-border-base text-12-regular text-text-strong"
|
||||||
|
dragImage.style.position = "absolute"
|
||||||
|
dragImage.style.top = "-1000px"
|
||||||
|
|
||||||
|
const icon =
|
||||||
|
(e.currentTarget as HTMLElement).querySelector('[data-component="file-icon"]') ??
|
||||||
|
(e.currentTarget as HTMLElement).querySelector("svg")
|
||||||
|
const text = (e.currentTarget as HTMLElement).querySelector("span")
|
||||||
|
if (icon && text) {
|
||||||
|
dragImage.innerHTML = (icon as SVGElement).outerHTML + (text as HTMLSpanElement).outerHTML
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.appendChild(dragImage)
|
||||||
|
e.dataTransfer?.setDragImage(dragImage, 0, 12)
|
||||||
|
setTimeout(() => document.body.removeChild(dragImage), 0)
|
||||||
|
}}
|
||||||
|
{...rest}
|
||||||
>
|
>
|
||||||
{p.node.name}
|
{local.children}
|
||||||
</span>
|
<span
|
||||||
{/* <Show when={local.file.changed(p.node.path)}> */}
|
classList={{
|
||||||
{/* <span class="ml-auto mr-1 w-1.5 h-1.5 rounded-full bg-primary/50 shrink-0" /> */}
|
"flex-1 min-w-0 text-12-medium whitespace-nowrap truncate": true,
|
||||||
{/* </Show> */}
|
"text-text-weaker": local.node.ignored,
|
||||||
</Dynamic>
|
"text-text-weak": !local.node.ignored,
|
||||||
)
|
}}
|
||||||
|
>
|
||||||
|
{local.node.name}
|
||||||
|
</span>
|
||||||
|
{local.node.type === "file" && marks()?.has(local.node.path) ? (
|
||||||
|
<div class="shrink-0 size-1.5 rounded-full bg-surface-warning-strong" />
|
||||||
|
) : null}
|
||||||
|
</Dynamic>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={`flex flex-col ${props.class}`}>
|
<div class={`flex flex-col gap-0.5 ${props.class ?? ""}`}>
|
||||||
<For each={local.file.children(props.path)}>
|
<For each={nodes()}>
|
||||||
{(node) => (
|
{(node) => {
|
||||||
<Tooltip forceMount={false} openDelay={2000} value={node.path} placement="right">
|
const expanded = () => file.tree.state(node.path)?.expanded ?? false
|
||||||
|
const deep = () => deeps().get(node.path) ?? -1
|
||||||
|
const Wrapper = (p: ParentProps) => {
|
||||||
|
if (!tooltip()) return p.children
|
||||||
|
return (
|
||||||
|
<Tooltip forceMount={false} openDelay={2000} value={node.path} placement="right" class="w-full">
|
||||||
|
{p.children}
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={node.type === "directory"}>
|
<Match when={node.type === "directory"}>
|
||||||
<Collapsible
|
<Collapsible
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
|
data-scope="filetree"
|
||||||
forceMount={false}
|
forceMount={false}
|
||||||
// open={local.file.node(node.path)?.expanded}
|
open={expanded()}
|
||||||
onOpenChange={(open) => (open ? local.file.expand(node.path) : local.file.collapse(node.path))}
|
onOpenChange={(open) => (open ? file.tree.expand(node.path) : file.tree.collapse(node.path))}
|
||||||
>
|
>
|
||||||
<Collapsible.Trigger>
|
<Collapsible.Trigger>
|
||||||
<Node node={node}>
|
<Wrapper>
|
||||||
<Collapsible.Arrow class="text-text-muted/60 ml-1" />
|
<Node node={node}>
|
||||||
<FileIcon
|
<div class="size-4 flex items-center justify-center text-icon-weak">
|
||||||
node={node}
|
<Icon name={expanded() ? "chevron-down" : "chevron-right"} size="small" />
|
||||||
// expanded={local.file.node(node.path).expanded}
|
</div>
|
||||||
class="text-text-muted/60 -ml-1"
|
</Node>
|
||||||
/>
|
</Wrapper>
|
||||||
</Node>
|
|
||||||
</Collapsible.Trigger>
|
</Collapsible.Trigger>
|
||||||
<Collapsible.Content>
|
<Collapsible.Content class="relative pt-0.5">
|
||||||
<FileTree path={node.path} level={level + 1} onFileClick={props.onFileClick} />
|
<div
|
||||||
|
classList={{
|
||||||
|
"absolute top-0 bottom-0 w-px pointer-events-none bg-border-weak-base opacity-0 transition-opacity duration-150 ease-out motion-reduce:transition-none": true,
|
||||||
|
"group-hover/filetree:opacity-100": expanded() && deep() === level,
|
||||||
|
"group-hover/filetree:opacity-50": !(expanded() && deep() === level),
|
||||||
|
}}
|
||||||
|
style={`left: ${Math.max(0, 8 + level * 12 - 4) + 8}px`}
|
||||||
|
/>
|
||||||
|
<FileTree
|
||||||
|
path={node.path}
|
||||||
|
level={level + 1}
|
||||||
|
allowed={props.allowed}
|
||||||
|
modified={props.modified}
|
||||||
|
draggable={props.draggable}
|
||||||
|
tooltip={props.tooltip}
|
||||||
|
onFileClick={props.onFileClick}
|
||||||
|
_filter={filter()}
|
||||||
|
_marks={marks()}
|
||||||
|
_deeps={deeps()}
|
||||||
|
/>
|
||||||
</Collapsible.Content>
|
</Collapsible.Content>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={node.type === "file"}>
|
<Match when={node.type === "file"}>
|
||||||
<Node node={node} as="button" onClick={() => props.onFileClick?.(node)}>
|
<Wrapper>
|
||||||
<div class="w-4 shrink-0" />
|
<Node node={node} as="button" type="button" onClick={() => props.onFileClick?.(node)}>
|
||||||
<FileIcon node={node} class="text-primary" />
|
<div class="w-4 shrink-0" />
|
||||||
</Node>
|
<FileIcon node={node} class="text-icon-weak size-4" />
|
||||||
|
</Node>
|
||||||
|
</Wrapper>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Tooltip>
|
)
|
||||||
)}
|
}}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1565,7 +1565,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const timeoutMs = 5 * 60 * 1000
|
const timeoutMs = 5 * 60 * 1000
|
||||||
const timeout = new Promise<Awaited<ReturnType<typeof WorktreeState.wait>>>((resolve) => {
|
const timeout = new Promise<Awaited<ReturnType<typeof WorktreeState.wait>>>((resolve) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resolve({ status: "failed", message: "Workspace is still preparing" })
|
resolve({ status: "failed", message: language.t("workspace.error.stillPreparing") })
|
||||||
}, timeoutMs)
|
}, timeoutMs)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1863,9 +1863,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
store.mode === "shell"
|
store.mode === "shell"
|
||||||
? language.t("prompt.placeholder.shell")
|
? language.t("prompt.placeholder.shell")
|
||||||
: commentCount() > 1
|
: commentCount() > 1
|
||||||
? "Summarize comments…"
|
? language.t("prompt.placeholder.summarizeComments")
|
||||||
: commentCount() === 1
|
: commentCount() === 1
|
||||||
? "Summarize comment…"
|
? language.t("prompt.placeholder.summarizeComment")
|
||||||
: language.t("prompt.placeholder.normal", { example: language.t(EXAMPLES[store.placeholder]) })
|
: language.t("prompt.placeholder.normal", { example: language.t(EXAMPLES[store.placeholder]) })
|
||||||
}
|
}
|
||||||
contenteditable="true"
|
contenteditable="true"
|
||||||
@@ -1887,9 +1887,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
{store.mode === "shell"
|
{store.mode === "shell"
|
||||||
? language.t("prompt.placeholder.shell")
|
? language.t("prompt.placeholder.shell")
|
||||||
: commentCount() > 1
|
: commentCount() > 1
|
||||||
? "Summarize comments…"
|
? language.t("prompt.placeholder.summarizeComments")
|
||||||
: commentCount() === 1
|
: commentCount() === 1
|
||||||
? "Summarize comment…"
|
? language.t("prompt.placeholder.summarizeComment")
|
||||||
: language.t("prompt.placeholder.normal", { example: language.t(EXAMPLES[store.placeholder]) })}
|
: language.t("prompt.placeholder.normal", { example: language.t(EXAMPLES[store.placeholder]) })}
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export function SessionHeader() {
|
|||||||
<Portal mount={mount()}>
|
<Portal mount={mount()}>
|
||||||
<button
|
<button
|
||||||
type="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: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-visible:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
|
||||||
onClick={() => command.trigger("file.open")}
|
onClick={() => command.trigger("file.open")}
|
||||||
aria-label={language.t("session.header.searchFiles")}
|
aria-label={language.t("session.header.searchFiles")}
|
||||||
>
|
>
|
||||||
@@ -311,6 +311,32 @@ export function SessionHeader() {
|
|||||||
</Button>
|
</Button>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="hidden md:block shrink-0">
|
||||||
|
<Tooltip value="Toggle file tree" placement="bottom">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="group/file-tree-toggle size-6 p-0"
|
||||||
|
onClick={() => {
|
||||||
|
const opening = !layout.fileTree.opened()
|
||||||
|
if (opening && !view().reviewPanel.opened()) view().reviewPanel.open()
|
||||||
|
layout.fileTree.toggle()
|
||||||
|
}}
|
||||||
|
aria-label="Toggle file tree"
|
||||||
|
aria-expanded={layout.fileTree.opened()}
|
||||||
|
>
|
||||||
|
<div class="relative flex items-center justify-center size-4">
|
||||||
|
<Icon
|
||||||
|
size="small"
|
||||||
|
name="bullet-list"
|
||||||
|
classList={{
|
||||||
|
"text-icon-strong": layout.fileTree.opened(),
|
||||||
|
"text-icon-weak": !layout.fileTree.opened(),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Portal>
|
</Portal>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { JSX } from "solid-js"
|
import type { JSX } from "solid-js"
|
||||||
import { createSignal, Show } from "solid-js"
|
import { Show } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
import { createSortable } from "@thisbeyond/solid-dnd"
|
import { createSortable } from "@thisbeyond/solid-dnd"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
@@ -12,11 +13,13 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
|||||||
const terminal = useTerminal()
|
const terminal = useTerminal()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const sortable = createSortable(props.terminal.id)
|
const sortable = createSortable(props.terminal.id)
|
||||||
const [editing, setEditing] = createSignal(false)
|
const [store, setStore] = createStore({
|
||||||
const [title, setTitle] = createSignal(props.terminal.title)
|
editing: false,
|
||||||
const [menuOpen, setMenuOpen] = createSignal(false)
|
title: props.terminal.title,
|
||||||
const [menuPosition, setMenuPosition] = createSignal({ x: 0, y: 0 })
|
menuOpen: false,
|
||||||
const [blurEnabled, setBlurEnabled] = createSignal(false)
|
menuPosition: { x: 0, y: 0 },
|
||||||
|
blurEnabled: false,
|
||||||
|
})
|
||||||
|
|
||||||
const isDefaultTitle = () => {
|
const isDefaultTitle = () => {
|
||||||
const number = props.terminal.titleNumber
|
const number = props.terminal.titleNumber
|
||||||
@@ -47,7 +50,7 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const focus = () => {
|
const focus = () => {
|
||||||
if (editing()) return
|
if (store.editing) return
|
||||||
|
|
||||||
if (document.activeElement instanceof HTMLElement) {
|
if (document.activeElement instanceof HTMLElement) {
|
||||||
document.activeElement.blur()
|
document.activeElement.blur()
|
||||||
@@ -71,26 +74,26 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
setBlurEnabled(false)
|
setStore("blurEnabled", false)
|
||||||
setTitle(props.terminal.title)
|
setStore("title", props.terminal.title)
|
||||||
setEditing(true)
|
setStore("editing", true)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const input = document.getElementById(`terminal-title-input-${props.terminal.id}`) as HTMLInputElement
|
const input = document.getElementById(`terminal-title-input-${props.terminal.id}`) as HTMLInputElement
|
||||||
if (!input) return
|
if (!input) return
|
||||||
input.focus()
|
input.focus()
|
||||||
input.select()
|
input.select()
|
||||||
setTimeout(() => setBlurEnabled(true), 100)
|
setTimeout(() => setStore("blurEnabled", true), 100)
|
||||||
}, 10)
|
}, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
const save = () => {
|
const save = () => {
|
||||||
if (!blurEnabled()) return
|
if (!store.blurEnabled) return
|
||||||
|
|
||||||
const value = title().trim()
|
const value = store.title.trim()
|
||||||
if (value && value !== props.terminal.title) {
|
if (value && value !== props.terminal.title) {
|
||||||
terminal.update({ id: props.terminal.id, title: value })
|
terminal.update({ id: props.terminal.id, title: value })
|
||||||
}
|
}
|
||||||
setEditing(false)
|
setStore("editing", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const keydown = (e: KeyboardEvent) => {
|
const keydown = (e: KeyboardEvent) => {
|
||||||
@@ -101,14 +104,14 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
|||||||
}
|
}
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setEditing(false)
|
setStore("editing", false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const menu = (e: MouseEvent) => {
|
const menu = (e: MouseEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setMenuPosition({ x: e.clientX, y: e.clientY })
|
setStore("menuPosition", { x: e.clientX, y: e.clientY })
|
||||||
setMenuOpen(true)
|
setStore("menuOpen", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -143,17 +146,17 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span onDblClick={edit} style={{ visibility: editing() ? "hidden" : "visible" }}>
|
<span onDblClick={edit} style={{ visibility: store.editing ? "hidden" : "visible" }}>
|
||||||
{label()}
|
{label()}
|
||||||
</span>
|
</span>
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
<Show when={editing()}>
|
<Show when={store.editing}>
|
||||||
<div class="absolute inset-0 flex items-center px-3 bg-muted z-10 pointer-events-auto">
|
<div class="absolute inset-0 flex items-center px-3 bg-muted z-10 pointer-events-auto">
|
||||||
<input
|
<input
|
||||||
id={`terminal-title-input-${props.terminal.id}`}
|
id={`terminal-title-input-${props.terminal.id}`}
|
||||||
type="text"
|
type="text"
|
||||||
value={title()}
|
value={store.title}
|
||||||
onInput={(e) => setTitle(e.currentTarget.value)}
|
onInput={(e) => setStore("title", e.currentTarget.value)}
|
||||||
onBlur={save}
|
onBlur={save}
|
||||||
onKeyDown={keydown}
|
onKeyDown={keydown}
|
||||||
onMouseDown={(e) => e.stopPropagation()}
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
@@ -161,13 +164,13 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<DropdownMenu open={menuOpen()} onOpenChange={setMenuOpen}>
|
<DropdownMenu open={store.menuOpen} onOpenChange={(open) => setStore("menuOpen", open)}>
|
||||||
<DropdownMenu.Portal>
|
<DropdownMenu.Portal>
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
style={{
|
style={{
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
left: `${menuPosition().x}px`,
|
left: `${store.menuPosition.x}px`,
|
||||||
top: `${menuPosition().y}px`,
|
top: `${store.menuPosition.y}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Item onSelect={edit}>
|
<DropdownMenu.Item onSelect={edit}>
|
||||||
|
|||||||
@@ -214,6 +214,23 @@ export const SettingsGeneral: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Updates Section */}
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.updates")}</h3>
|
||||||
|
|
||||||
|
<div class="bg-surface-raised-base px-4 rounded-lg">
|
||||||
|
<SettingsRow
|
||||||
|
title={language.t("settings.general.row.releaseNotes.title")}
|
||||||
|
description={language.t("settings.general.row.releaseNotes.description")}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.releaseNotes()}
|
||||||
|
onChange={(checked) => settings.general.setReleaseNotes(checked)}
|
||||||
|
/>
|
||||||
|
</SettingsRow>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Sound effects Section */}
|
{/* Sound effects Section */}
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.sounds")}</h3>
|
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.sounds")}</h3>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Component, For, Show, createMemo, createSignal, onCleanup, onMount } from "solid-js"
|
import { Component, For, Show, createMemo, onCleanup, onMount } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
@@ -111,24 +112,26 @@ export const SettingsKeybinds: Component = () => {
|
|||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
|
|
||||||
const [active, setActive] = createSignal<string | null>(null)
|
const [store, setStore] = createStore({
|
||||||
const [filter, setFilter] = createSignal("")
|
active: null as string | null,
|
||||||
|
filter: "",
|
||||||
|
})
|
||||||
|
|
||||||
const stop = () => {
|
const stop = () => {
|
||||||
if (!active()) return
|
if (!store.active) return
|
||||||
setActive(null)
|
setStore("active", null)
|
||||||
command.keybinds(true)
|
command.keybinds(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const start = (id: string) => {
|
const start = (id: string) => {
|
||||||
if (active() === id) {
|
if (store.active === id) {
|
||||||
stop()
|
stop()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (active()) stop()
|
if (store.active) stop()
|
||||||
|
|
||||||
setActive(id)
|
setStore("active", id)
|
||||||
command.keybinds(false)
|
command.keybinds(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +206,7 @@ export const SettingsKeybinds: Component = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const filtered = createMemo(() => {
|
const filtered = createMemo(() => {
|
||||||
const query = filter().toLowerCase().trim()
|
const query = store.filter.toLowerCase().trim()
|
||||||
if (!query) return grouped()
|
if (!query) return grouped()
|
||||||
|
|
||||||
const map = list()
|
const map = list()
|
||||||
@@ -285,7 +288,7 @@ export const SettingsKeybinds: Component = () => {
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const handle = (event: KeyboardEvent) => {
|
const handle = (event: KeyboardEvent) => {
|
||||||
const id = active()
|
const id = store.active
|
||||||
if (!id) return
|
if (!id) return
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -345,7 +348,7 @@ export const SettingsKeybinds: Component = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
if (active()) command.keybinds(true)
|
if (store.active) command.keybinds(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -370,8 +373,8 @@ export const SettingsKeybinds: Component = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
type="text"
|
type="text"
|
||||||
value={filter()}
|
value={store.filter}
|
||||||
onChange={setFilter}
|
onChange={(v) => setStore("filter", v)}
|
||||||
placeholder={language.t("settings.shortcuts.search.placeholder")}
|
placeholder={language.t("settings.shortcuts.search.placeholder")}
|
||||||
spellcheck={false}
|
spellcheck={false}
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
@@ -379,8 +382,8 @@ export const SettingsKeybinds: Component = () => {
|
|||||||
autocapitalize="off"
|
autocapitalize="off"
|
||||||
class="flex-1"
|
class="flex-1"
|
||||||
/>
|
/>
|
||||||
<Show when={filter()}>
|
<Show when={store.filter}>
|
||||||
<IconButton icon="circle-x" variant="ghost" onClick={() => setFilter("")} />
|
<IconButton icon="circle-x" variant="ghost" onClick={() => setStore("filter", "")} />
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -402,13 +405,13 @@ export const SettingsKeybinds: Component = () => {
|
|||||||
classList={{
|
classList={{
|
||||||
"h-8 px-3 rounded-md text-12-regular": true,
|
"h-8 px-3 rounded-md text-12-regular": true,
|
||||||
"bg-surface-base text-text-subtle hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active":
|
"bg-surface-base text-text-subtle hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active":
|
||||||
active() !== id,
|
store.active !== id,
|
||||||
"border border-border-weak-base bg-surface-inset-base text-text-weak": active() === id,
|
"border border-border-weak-base bg-surface-inset-base text-text-weak": store.active === id,
|
||||||
}}
|
}}
|
||||||
onClick={() => start(id)}
|
onClick={() => start(id)}
|
||||||
>
|
>
|
||||||
<Show
|
<Show
|
||||||
when={active() === id}
|
when={store.active === id}
|
||||||
fallback={command.keybind(id) || language.t("settings.shortcuts.unassigned")}
|
fallback={command.keybind(id) || language.t("settings.shortcuts.unassigned")}
|
||||||
>
|
>
|
||||||
{language.t("settings.shortcuts.pressKeys")}
|
{language.t("settings.shortcuts.pressKeys")}
|
||||||
@@ -423,11 +426,11 @@ export const SettingsKeybinds: Component = () => {
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|
||||||
<Show when={filter() && !hasResults()}>
|
<Show when={store.filter && !hasResults()}>
|
||||||
<div class="flex flex-col items-center justify-center py-12 text-center">
|
<div class="flex flex-col items-center justify-center py-12 text-center">
|
||||||
<span class="text-14-regular text-text-weak">{language.t("settings.shortcuts.search.empty")}</span>
|
<span class="text-14-regular text-text-weak">{language.t("settings.shortcuts.search.empty")}</span>
|
||||||
<Show when={filter()}>
|
<Show when={store.filter}>
|
||||||
<span class="text-14-regular text-text-strong mt-1">"{filter()}"</span>
|
<span class="text-14-regular text-text-strong mt-1">"{store.filter}"</span>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -1,14 +1,135 @@
|
|||||||
import { Component } from "solid-js"
|
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 { useLanguage } from "@/context/language"
|
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 = () => {
|
export const SettingsModels: Component = () => {
|
||||||
const language = useLanguage()
|
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 (
|
return (
|
||||||
<div class="flex flex-col h-full overflow-y-auto">
|
<div class="flex flex-col h-full overflow-y-auto no-scrollbar" style={{ padding: "0 40px 40px 40px" }}>
|
||||||
<div class="flex flex-col gap-6 p-6 max-w-[600px]">
|
<div
|
||||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.models.title")}</h2>
|
class="sticky top-0 z-10"
|
||||||
<p class="text-14-regular text-text-weak">{language.t("settings.models.description")}</p>
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,7 +20,12 @@ export const SettingsProviders: Component = () => {
|
|||||||
const globalSDK = useGlobalSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
const providers = useProviders()
|
const providers = useProviders()
|
||||||
|
|
||||||
const connected = createMemo(() => providers.connected())
|
const connected = createMemo(() => {
|
||||||
|
return providers
|
||||||
|
.connected()
|
||||||
|
.filter((p) => p.id !== "opencode" || Object.values(p.models).find((m) => m.cost?.input))
|
||||||
|
})
|
||||||
|
|
||||||
const popular = createMemo(() => {
|
const popular = createMemo(() => {
|
||||||
const connectedIDs = new Set(connected().map((p) => p.id))
|
const connectedIDs = new Set(connected().map((p) => p.id))
|
||||||
const items = providers
|
const items = providers
|
||||||
@@ -84,14 +89,21 @@ export const SettingsProviders: Component = () => {
|
|||||||
>
|
>
|
||||||
<For each={connected()}>
|
<For each={connected()}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
|
<div class="group flex items-center justify-between gap-4 h-16 border-b border-border-weak-base last:border-none">
|
||||||
<div class="flex items-center gap-3 min-w-0">
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
<ProviderIcon id={item.id as IconName} class="size-5 shrink-0 icon-strong-base" />
|
<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>
|
<span class="text-14-medium text-text-strong truncate">{item.name}</span>
|
||||||
<Tag>{type(item)}</Tag>
|
<Tag>{type(item)}</Tag>
|
||||||
</div>
|
</div>
|
||||||
<Show when={canDisconnect(item)}>
|
<Show
|
||||||
<Button size="small" variant="ghost" onClick={() => void disconnect(item.id, item.name)}>
|
when={canDisconnect(item)}
|
||||||
|
fallback={
|
||||||
|
<span class="text-14-regular text-text-base opacity-0 group-hover:opacity-100 transition-opacity duration-200 pr-3 cursor-default">
|
||||||
|
Connected from your environment variables
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button size="large" variant="ghost" onClick={() => void disconnect(item.id, item.name)}>
|
||||||
{language.t("common.disconnect")}
|
{language.t("common.disconnect")}
|
||||||
</Button>
|
</Button>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -107,21 +119,49 @@ export const SettingsProviders: Component = () => {
|
|||||||
<div class="bg-surface-raised-base px-4 rounded-lg">
|
<div class="bg-surface-raised-base px-4 rounded-lg">
|
||||||
<For each={popular()}>
|
<For each={popular()}>
|
||||||
{(item) => (
|
{(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 justify-between gap-4 h-16 border-b border-border-weak-base last:border-none">
|
||||||
<div class="flex items-center gap-x-3 min-w-0">
|
<div class="flex flex-col min-w-0">
|
||||||
<ProviderIcon id={item.id as IconName} class="size-5 shrink-0 icon-strong-base" />
|
<div class="flex items-center gap-x-3">
|
||||||
<span class="text-14-regular text-text-strong">{item.name}</span>
|
<ProviderIcon id={item.id as IconName} class="size-5 shrink-0 icon-strong-base" />
|
||||||
|
<span class="text-14-medium text-text-strong">{item.name}</span>
|
||||||
|
<Show when={item.id === "opencode"}>
|
||||||
|
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
<Show when={item.id === "opencode"}>
|
<Show when={item.id === "opencode"}>
|
||||||
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
<span class="text-12-regular text-text-weak pl-8">
|
||||||
|
{language.t("dialog.provider.opencode.note")}
|
||||||
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={item.id === "anthropic"}>
|
<Show when={item.id === "anthropic"}>
|
||||||
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div>
|
<span class="text-12-regular text-text-weak pl-8">
|
||||||
</Show>
|
{language.t("dialog.provider.anthropic.note")}
|
||||||
<Show when={item.id === "openai"}>
|
</span>
|
||||||
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.openai.note")}</div>
|
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={item.id.startsWith("github-copilot")}>
|
<Show when={item.id.startsWith("github-copilot")}>
|
||||||
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.copilot.note")}</div>
|
<span class="text-12-regular text-text-weak pl-8">
|
||||||
|
{language.t("dialog.provider.copilot.note")}
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
<Show when={item.id === "openai"}>
|
||||||
|
<span class="text-12-regular text-text-weak pl-8">
|
||||||
|
{language.t("dialog.provider.openai.note")}
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
<Show when={item.id === "google"}>
|
||||||
|
<span class="text-12-regular text-text-weak pl-8">
|
||||||
|
{language.t("dialog.provider.google.note")}
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
<Show when={item.id === "openrouter"}>
|
||||||
|
<span class="text-12-regular text-text-weak pl-8">
|
||||||
|
{language.t("dialog.provider.openrouter.note")}
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
<Show when={item.id === "vercel"}>
|
||||||
|
<span class="text-12-regular text-text-weak pl-8">
|
||||||
|
{language.t("dialog.provider.vercel.note")}
|
||||||
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@@ -141,7 +181,7 @@ export const SettingsProviders: Component = () => {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="px-0 py-0 text-14-medium text-text-strong text-left justify-start hover:bg-transparent active:bg-transparent"
|
class="px-0 py-0 mt-5 text-14-medium text-text-interactive-base text-left justify-start hover:bg-transparent active:bg-transparent"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dialog.show(() => <DialogSelectProvider />)
|
dialog.show(() => <DialogSelectProvider />)
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -39,9 +39,10 @@ export function StatusPopover() {
|
|||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const [loading, setLoading] = createSignal<string | null>(null)
|
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
status: {} as Record<string, ServerStatus | undefined>,
|
status: {} as Record<string, ServerStatus | undefined>,
|
||||||
|
loading: null as string | null,
|
||||||
|
defaultServerUrl: undefined as string | undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const servers = createMemo(() => {
|
const servers = createMemo(() => {
|
||||||
@@ -97,8 +98,8 @@ export function StatusPopover() {
|
|||||||
const mcpConnected = createMemo(() => mcpItems().filter((i) => i.status === "connected").length)
|
const mcpConnected = createMemo(() => mcpItems().filter((i) => i.status === "connected").length)
|
||||||
|
|
||||||
const toggleMcp = async (name: string) => {
|
const toggleMcp = async (name: string) => {
|
||||||
if (loading()) return
|
if (store.loading) return
|
||||||
setLoading(name)
|
setStore("loading", name)
|
||||||
const status = sync.data.mcp[name]
|
const status = sync.data.mcp[name]
|
||||||
if (status?.status === "connected") {
|
if (status?.status === "connected") {
|
||||||
await sdk.client.mcp.disconnect({ name })
|
await sdk.client.mcp.disconnect({ name })
|
||||||
@@ -107,7 +108,7 @@ export function StatusPopover() {
|
|||||||
}
|
}
|
||||||
const result = await sdk.client.mcp.status()
|
const result = await sdk.client.mcp.status()
|
||||||
if (result.data) sync.set("mcp", result.data)
|
if (result.data) sync.set("mcp", result.data)
|
||||||
setLoading(null)
|
setStore("loading", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const lspItems = createMemo(() => sync.data.lsp ?? [])
|
const lspItems = createMemo(() => sync.data.lsp ?? [])
|
||||||
@@ -123,15 +124,21 @@ export function StatusPopover() {
|
|||||||
|
|
||||||
const serverCount = createMemo(() => sortedServers().length)
|
const serverCount = createMemo(() => sortedServers().length)
|
||||||
|
|
||||||
const [defaultServerUrl, setDefaultServerUrl] = createSignal<string | undefined>()
|
const refreshDefaultServerUrl = () => {
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const result = platform.getDefaultServerUrl?.()
|
const result = platform.getDefaultServerUrl?.()
|
||||||
if (result instanceof Promise) {
|
if (!result) {
|
||||||
result.then((url) => setDefaultServerUrl(url ? normalizeServerUrl(url) : undefined))
|
setStore("defaultServerUrl", undefined)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (result) setDefaultServerUrl(normalizeServerUrl(result))
|
if (result instanceof Promise) {
|
||||||
|
result.then((url) => setStore("defaultServerUrl", url ? normalizeServerUrl(url) : undefined))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setStore("defaultServerUrl", normalizeServerUrl(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
refreshDefaultServerUrl()
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -212,7 +219,7 @@ export function StatusPopover() {
|
|||||||
<For each={sortedServers()}>
|
<For each={sortedServers()}>
|
||||||
{(url) => {
|
{(url) => {
|
||||||
const isActive = () => url === server.url
|
const isActive = () => url === server.url
|
||||||
const isDefault = () => url === defaultServerUrl()
|
const isDefault = () => url === store.defaultServerUrl
|
||||||
const status = () => store.status[url]
|
const status = () => store.status[url]
|
||||||
const isBlocked = () => status()?.healthy === false
|
const isBlocked = () => status()?.healthy === false
|
||||||
const [truncated, setTruncated] = createSignal(false)
|
const [truncated, setTruncated] = createSignal(false)
|
||||||
@@ -294,7 +301,7 @@ export function StatusPopover() {
|
|||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
class="mt-3 self-start h-8 px-3 py-1.5"
|
class="mt-3 self-start h-8 px-3 py-1.5"
|
||||||
onClick={() => dialog.show(() => <DialogSelectServer />)}
|
onClick={() => dialog.show(() => <DialogSelectServer />, refreshDefaultServerUrl)}
|
||||||
>
|
>
|
||||||
{language.t("status.popover.action.manageServers")}
|
{language.t("status.popover.action.manageServers")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -321,7 +328,7 @@ export function StatusPopover() {
|
|||||||
type="button"
|
type="button"
|
||||||
class="flex items-center gap-2 w-full h-8 pl-3 pr-2 py-1 rounded-md hover:bg-surface-raised-base-hover transition-colors text-left"
|
class="flex items-center gap-2 w-full h-8 pl-3 pr-2 py-1 rounded-md hover:bg-surface-raised-base-hover transition-colors text-left"
|
||||||
onClick={() => toggleMcp(item.name)}
|
onClick={() => toggleMcp(item.name)}
|
||||||
disabled={loading() === item.name}
|
disabled={store.loading === item.name}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
@@ -337,7 +344,7 @@ export function StatusPopover() {
|
|||||||
<div onClick={(event) => event.stopPropagation()}>
|
<div onClick={(event) => event.stopPropagation()}>
|
||||||
<Switch
|
<Switch
|
||||||
checked={enabled()}
|
checked={enabled()}
|
||||||
disabled={loading() === item.name}
|
disabled={store.loading === item.name}
|
||||||
onChange={() => toggleMcp(item.name)}
|
onChange={() => toggleMcp(item.name)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createEffect, createMemo, createSignal, onCleanup, onMount, type Accessor } from "solid-js"
|
import { createEffect, createMemo, onCleanup, onMount, type Accessor } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
@@ -24,6 +24,15 @@ function normalizeKey(key: string) {
|
|||||||
return key.toLowerCase()
|
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 type KeybindConfig = string
|
||||||
|
|
||||||
export interface Keybind {
|
export interface Keybind {
|
||||||
@@ -156,8 +165,10 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const [registrations, setRegistrations] = createSignal<Accessor<CommandOption[]>[]>([])
|
const [store, setStore] = createStore({
|
||||||
const [suspendCount, setSuspendCount] = createSignal(0)
|
registrations: [] as Accessor<CommandOption[]>[],
|
||||||
|
suspendCount: 0,
|
||||||
|
})
|
||||||
|
|
||||||
const [catalog, setCatalog, _, catalogReady] = persisted(
|
const [catalog, setCatalog, _, catalogReady] = persisted(
|
||||||
Persist.global("command.catalog.v1"),
|
Persist.global("command.catalog.v1"),
|
||||||
@@ -175,7 +186,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||||||
const seen = new Set<string>()
|
const seen = new Set<string>()
|
||||||
const all: CommandOption[] = []
|
const all: CommandOption[] = []
|
||||||
|
|
||||||
for (const reg of registrations()) {
|
for (const reg of store.registrations) {
|
||||||
for (const opt of reg()) {
|
for (const opt of reg()) {
|
||||||
if (seen.has(opt.id)) continue
|
if (seen.has(opt.id)) continue
|
||||||
seen.add(opt.id)
|
seen.add(opt.id)
|
||||||
@@ -221,7 +232,31 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
const suspended = () => suspendCount() > 0
|
const suspended = () => store.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") => {
|
const run = (id: string, source?: "palette" | "keybind" | "slash") => {
|
||||||
for (const option of options()) {
|
for (const option of options()) {
|
||||||
@@ -239,24 +274,18 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (suspended() || dialog.active) return
|
if (suspended() || dialog.active) return
|
||||||
|
|
||||||
const paletteKeybinds = parseKeybind(settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND)
|
const sig = signatureFromEvent(event)
|
||||||
if (matchKeybind(paletteKeybinds, event)) {
|
|
||||||
|
if (palette().has(sig)) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
showPalette()
|
showPalette()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const option of options()) {
|
const option = keymap().get(sig)
|
||||||
if (option.disabled) continue
|
if (!option) return
|
||||||
if (!option.keybind) continue
|
event.preventDefault()
|
||||||
|
option.onSelect?.("keybind")
|
||||||
const keybinds = parseKeybind(option.keybind)
|
|
||||||
if (matchKeybind(keybinds, event)) {
|
|
||||||
event.preventDefault()
|
|
||||||
option.onSelect?.("keybind")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -270,9 +299,9 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||||||
return {
|
return {
|
||||||
register(cb: () => CommandOption[]) {
|
register(cb: () => CommandOption[]) {
|
||||||
const results = createMemo(cb)
|
const results = createMemo(cb)
|
||||||
setRegistrations((arr) => [results, ...arr])
|
setStore("registrations", (arr) => [results, ...arr])
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
setRegistrations((arr) => arr.filter((x) => x !== results))
|
setStore("registrations", (arr) => arr.filter((x) => x !== results))
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
trigger(id: string, source?: "palette" | "keybind" | "slash") {
|
trigger(id: string, source?: "palette" | "keybind" | "slash") {
|
||||||
@@ -294,7 +323,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||||||
},
|
},
|
||||||
show: showPalette,
|
show: showPalette,
|
||||||
keybinds(enabled: boolean) {
|
keybinds(enabled: boolean) {
|
||||||
setSuspendCount((count) => count + (enabled ? -1 : 1))
|
setStore("suspendCount", (count) => count + (enabled ? -1 : 1))
|
||||||
},
|
},
|
||||||
suspended,
|
suspended,
|
||||||
get catalog() {
|
get catalog() {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { batch, createMemo, createRoot, createSignal, onCleanup } from "solid-js"
|
import { batch, createMemo, createRoot, onCleanup } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
@@ -37,8 +37,16 @@ function createCommentSession(dir: string, id: string | undefined) {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const [focus, setFocus] = createSignal<CommentFocus | null>(null)
|
const [state, setState] = createStore({
|
||||||
const [active, setActive] = createSignal<CommentFocus | null>(null)
|
focus: null as CommentFocus | null,
|
||||||
|
active: null as CommentFocus | null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const setFocus = (value: CommentFocus | null | ((value: CommentFocus | null) => CommentFocus | null)) =>
|
||||||
|
setState("focus", value)
|
||||||
|
|
||||||
|
const setActive = (value: CommentFocus | null | ((value: CommentFocus | null) => CommentFocus | null)) =>
|
||||||
|
setState("active", value)
|
||||||
|
|
||||||
const list = (file: string) => store.comments[file] ?? []
|
const list = (file: string) => store.comments[file] ?? []
|
||||||
|
|
||||||
@@ -74,10 +82,10 @@ function createCommentSession(dir: string, id: string | undefined) {
|
|||||||
all,
|
all,
|
||||||
add,
|
add,
|
||||||
remove,
|
remove,
|
||||||
focus: createMemo(() => focus()),
|
focus: createMemo(() => state.focus),
|
||||||
setFocus,
|
setFocus,
|
||||||
clearFocus: () => setFocus(null),
|
clearFocus: () => setFocus(null),
|
||||||
active: createMemo(() => active()),
|
active: createMemo(() => state.active),
|
||||||
setActive,
|
setActive,
|
||||||
clearActive: () => setActive(null),
|
clearActive: () => setActive(null),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createEffect, createMemo, createRoot, onCleanup } from "solid-js"
|
import { createEffect, createMemo, createRoot, onCleanup } from "solid-js"
|
||||||
import { createStore, produce } from "solid-js/store"
|
import { createStore, produce } from "solid-js/store"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import type { FileContent } from "@opencode-ai/sdk/v2"
|
import type { FileContent, FileNode } from "@opencode-ai/sdk/v2"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { getFilename } from "@opencode-ai/util/path"
|
import { getFilename } from "@opencode-ai/util/path"
|
||||||
@@ -39,6 +39,14 @@ export type FileState = {
|
|||||||
content?: FileContent
|
content?: FileContent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DirectoryState = {
|
||||||
|
expanded: boolean
|
||||||
|
loaded?: boolean
|
||||||
|
loading?: boolean
|
||||||
|
error?: string
|
||||||
|
children?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
function stripFileProtocol(input: string) {
|
function stripFileProtocol(input: string) {
|
||||||
if (!input.startsWith("file://")) return input
|
if (!input.startsWith("file://")) return input
|
||||||
return input.slice("file://".length)
|
return input.slice("file://".length)
|
||||||
@@ -57,6 +65,62 @@ function stripQueryAndHash(input: string) {
|
|||||||
return input
|
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 {
|
export function selectionFromLines(range: SelectedLineRange): FileSelection {
|
||||||
const startLine = Math.min(range.start, range.end)
|
const startLine = Math.min(range.start, range.end)
|
||||||
const endLine = Math.max(range.start, range.end)
|
const endLine = Math.max(range.start, range.end)
|
||||||
@@ -197,7 +261,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
|||||||
const root = directory()
|
const root = directory()
|
||||||
const prefix = root.endsWith("/") ? root : root + "/"
|
const prefix = root.endsWith("/") ? root : root + "/"
|
||||||
|
|
||||||
let path = stripQueryAndHash(stripFileProtocol(input))
|
let path = unquoteGitPath(stripQueryAndHash(stripFileProtocol(input)))
|
||||||
|
|
||||||
if (path.startsWith(prefix)) {
|
if (path.startsWith(prefix)) {
|
||||||
path = path.slice(prefix.length)
|
path = path.slice(prefix.length)
|
||||||
@@ -229,6 +293,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const inflight = new Map<string, Promise<void>>()
|
const inflight = new Map<string, Promise<void>>()
|
||||||
|
const treeInflight = new Map<string, Promise<void>>()
|
||||||
|
|
||||||
const [store, setStore] = createStore<{
|
const [store, setStore] = createStore<{
|
||||||
file: Record<string, FileState>
|
file: Record<string, FileState>
|
||||||
@@ -236,10 +301,21 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
|||||||
file: {},
|
file: {},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const [tree, setTree] = createStore<{
|
||||||
|
node: Record<string, FileNode>
|
||||||
|
dir: Record<string, DirectoryState>
|
||||||
|
}>({
|
||||||
|
node: {},
|
||||||
|
dir: { "": { expanded: true } },
|
||||||
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
scope()
|
scope()
|
||||||
inflight.clear()
|
inflight.clear()
|
||||||
|
treeInflight.clear()
|
||||||
setStore("file", {})
|
setStore("file", {})
|
||||||
|
setTree("node", {})
|
||||||
|
setTree("dir", { "": { expanded: true } })
|
||||||
})
|
})
|
||||||
|
|
||||||
const viewCache = new Map<string, ViewCacheEntry>()
|
const viewCache = new Map<string, ViewCacheEntry>()
|
||||||
@@ -351,14 +427,168 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
|||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeDir(input: string) {
|
||||||
|
return normalize(input).replace(/\/+$/, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureDir(path: string) {
|
||||||
|
if (tree.dir[path]) return
|
||||||
|
setTree("dir", path, { expanded: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
function listDir(input: string, options?: { force?: boolean }) {
|
||||||
|
const dir = normalizeDir(input)
|
||||||
|
ensureDir(dir)
|
||||||
|
|
||||||
|
const current = tree.dir[dir]
|
||||||
|
if (!options?.force && current?.loaded) return Promise.resolve()
|
||||||
|
|
||||||
|
const pending = treeInflight.get(dir)
|
||||||
|
if (pending) return pending
|
||||||
|
|
||||||
|
setTree(
|
||||||
|
"dir",
|
||||||
|
dir,
|
||||||
|
produce((draft) => {
|
||||||
|
draft.loading = true
|
||||||
|
draft.error = undefined
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const directory = scope()
|
||||||
|
|
||||||
|
const promise = sdk.client.file
|
||||||
|
.list({ path: dir })
|
||||||
|
.then((x) => {
|
||||||
|
if (scope() !== directory) return
|
||||||
|
const nodes = x.data ?? []
|
||||||
|
const prevChildren = tree.dir[dir]?.children ?? []
|
||||||
|
const nextChildren = nodes.map((node) => node.path)
|
||||||
|
const nextSet = new Set(nextChildren)
|
||||||
|
|
||||||
|
setTree(
|
||||||
|
"node",
|
||||||
|
produce((draft) => {
|
||||||
|
const removedDirs: string[] = []
|
||||||
|
|
||||||
|
for (const child of prevChildren) {
|
||||||
|
if (nextSet.has(child)) continue
|
||||||
|
const existing = draft[child]
|
||||||
|
if (existing?.type === "directory") removedDirs.push(child)
|
||||||
|
delete draft[child]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removedDirs.length > 0) {
|
||||||
|
const keys = Object.keys(draft)
|
||||||
|
for (const key of keys) {
|
||||||
|
for (const removed of removedDirs) {
|
||||||
|
if (!key.startsWith(removed + "/")) continue
|
||||||
|
delete draft[key]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
|
draft[node.path] = node
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
setTree(
|
||||||
|
"dir",
|
||||||
|
dir,
|
||||||
|
produce((draft) => {
|
||||||
|
draft.loaded = true
|
||||||
|
draft.loading = false
|
||||||
|
draft.children = nextChildren
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
if (scope() !== directory) return
|
||||||
|
setTree(
|
||||||
|
"dir",
|
||||||
|
dir,
|
||||||
|
produce((draft) => {
|
||||||
|
draft.loading = false
|
||||||
|
draft.error = e.message
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: language.t("toast.file.listFailed.title"),
|
||||||
|
description: e.message,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
treeInflight.delete(dir)
|
||||||
|
})
|
||||||
|
|
||||||
|
treeInflight.set(dir, promise)
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandDir(input: string) {
|
||||||
|
const dir = normalizeDir(input)
|
||||||
|
ensureDir(dir)
|
||||||
|
setTree("dir", dir, "expanded", true)
|
||||||
|
void listDir(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
function collapseDir(input: string) {
|
||||||
|
const dir = normalizeDir(input)
|
||||||
|
ensureDir(dir)
|
||||||
|
setTree("dir", dir, "expanded", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function dirState(input: string) {
|
||||||
|
const dir = normalizeDir(input)
|
||||||
|
return tree.dir[dir]
|
||||||
|
}
|
||||||
|
|
||||||
|
function children(input: string) {
|
||||||
|
const dir = normalizeDir(input)
|
||||||
|
const ids = tree.dir[dir]?.children
|
||||||
|
if (!ids) return []
|
||||||
|
const out: FileNode[] = []
|
||||||
|
for (const id of ids) {
|
||||||
|
const node = tree.node[id]
|
||||||
|
if (node) out.push(node)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
const stop = sdk.event.listen((e) => {
|
const stop = sdk.event.listen((e) => {
|
||||||
const event = e.details
|
const event = e.details
|
||||||
if (event.type !== "file.watcher.updated") return
|
if (event.type !== "file.watcher.updated") return
|
||||||
const path = normalize(event.properties.file)
|
const path = normalize(event.properties.file)
|
||||||
if (!path) return
|
if (!path) return
|
||||||
if (path.startsWith(".git/")) return
|
if (path.startsWith(".git/")) return
|
||||||
if (!store.file[path]) return
|
|
||||||
load(path, { force: true })
|
if (store.file[path]) {
|
||||||
|
load(path, { force: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const kind = event.properties.event
|
||||||
|
if (kind === "change") {
|
||||||
|
const dir = (() => {
|
||||||
|
if (path === "") return ""
|
||||||
|
const node = tree.node[path]
|
||||||
|
if (node?.type !== "directory") return
|
||||||
|
return path
|
||||||
|
})()
|
||||||
|
if (dir === undefined) return
|
||||||
|
if (!tree.dir[dir]?.loaded) return
|
||||||
|
listDir(dir, { force: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (kind !== "add" && kind !== "unlink") return
|
||||||
|
|
||||||
|
const parent = path.split("/").slice(0, -1).join("/")
|
||||||
|
if (!tree.dir[parent]?.loaded) return
|
||||||
|
|
||||||
|
listDir(parent, { force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
const get = (input: string) => store.file[normalize(input)]
|
const get = (input: string) => store.file[normalize(input)]
|
||||||
@@ -392,6 +622,21 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
|||||||
normalize,
|
normalize,
|
||||||
tab,
|
tab,
|
||||||
pathFromTab,
|
pathFromTab,
|
||||||
|
tree: {
|
||||||
|
list: listDir,
|
||||||
|
refresh: (input: string) => listDir(input, { force: true }),
|
||||||
|
state: dirState,
|
||||||
|
children,
|
||||||
|
expand: expandDir,
|
||||||
|
collapse: collapseDir,
|
||||||
|
toggle(input: string) {
|
||||||
|
if (dirState(input)?.expanded) {
|
||||||
|
collapseDir(input)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expandDir(input)
|
||||||
|
},
|
||||||
|
},
|
||||||
get,
|
get,
|
||||||
load,
|
load,
|
||||||
scrollTop,
|
scrollTop,
|
||||||
|
|||||||
@@ -225,6 +225,65 @@ function createGlobalSync() {
|
|||||||
const sessionLoads = new Map<string, Promise<void>>()
|
const sessionLoads = new Map<string, Promise<void>>()
|
||||||
const sessionMeta = new Map<string, { limit: number }>()
|
const sessionMeta = new Map<string, { limit: number }>()
|
||||||
|
|
||||||
|
const sessionRecentWindow = 4 * 60 * 60 * 1000
|
||||||
|
const sessionRecentLimit = 50
|
||||||
|
|
||||||
|
function sessionUpdatedAt(session: Session) {
|
||||||
|
return session.time.updated ?? session.time.created
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareSessionRecent(a: Session, b: Session) {
|
||||||
|
const aUpdated = sessionUpdatedAt(a)
|
||||||
|
const bUpdated = sessionUpdatedAt(b)
|
||||||
|
if (aUpdated !== bUpdated) return bUpdated - aUpdated
|
||||||
|
return a.id.localeCompare(b.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeRecentSessions(sessions: Session[], limit: number, cutoff: number) {
|
||||||
|
if (limit <= 0) return [] as Session[]
|
||||||
|
const selected: Session[] = []
|
||||||
|
const seen = new Set<string>()
|
||||||
|
for (const session of sessions) {
|
||||||
|
if (!session?.id) continue
|
||||||
|
if (seen.has(session.id)) continue
|
||||||
|
seen.add(session.id)
|
||||||
|
|
||||||
|
if (sessionUpdatedAt(session) <= cutoff) continue
|
||||||
|
|
||||||
|
const index = selected.findIndex((x) => compareSessionRecent(session, x) < 0)
|
||||||
|
if (index === -1) selected.push(session)
|
||||||
|
if (index !== -1) selected.splice(index, 0, session)
|
||||||
|
if (selected.length > limit) selected.pop()
|
||||||
|
}
|
||||||
|
return selected
|
||||||
|
}
|
||||||
|
|
||||||
|
function trimSessions(input: Session[], options: { limit: number; permission: Record<string, PermissionRequest[]> }) {
|
||||||
|
const limit = Math.max(0, options.limit)
|
||||||
|
const cutoff = Date.now() - sessionRecentWindow
|
||||||
|
const all = input
|
||||||
|
.filter((s) => !!s?.id)
|
||||||
|
.filter((s) => !s.time?.archived)
|
||||||
|
.sort((a, b) => a.id.localeCompare(b.id))
|
||||||
|
|
||||||
|
const roots = all.filter((s) => !s.parentID)
|
||||||
|
const children = all.filter((s) => !!s.parentID)
|
||||||
|
|
||||||
|
const base = roots.slice(0, limit)
|
||||||
|
const recent = takeRecentSessions(roots.slice(limit), sessionRecentLimit, cutoff)
|
||||||
|
const keepRoots = [...base, ...recent]
|
||||||
|
|
||||||
|
const keepRootIds = new Set(keepRoots.map((s) => s.id))
|
||||||
|
const keepChildren = children.filter((s) => {
|
||||||
|
if (s.parentID && keepRootIds.has(s.parentID)) return true
|
||||||
|
const perms = options.permission[s.id] ?? []
|
||||||
|
if (perms.length > 0) return true
|
||||||
|
return sessionUpdatedAt(s) > cutoff
|
||||||
|
})
|
||||||
|
|
||||||
|
return [...keepRoots, ...keepChildren].sort((a, b) => a.id.localeCompare(b.id))
|
||||||
|
}
|
||||||
|
|
||||||
function ensureChild(directory: string) {
|
function ensureChild(directory: string) {
|
||||||
if (!directory) console.error("No directory provided")
|
if (!directory) console.error("No directory provided")
|
||||||
if (!children[directory]) {
|
if (!children[directory]) {
|
||||||
@@ -323,7 +382,13 @@ function createGlobalSync() {
|
|||||||
|
|
||||||
const [store, setStore] = child(directory, { bootstrap: false })
|
const [store, setStore] = child(directory, { bootstrap: false })
|
||||||
const meta = sessionMeta.get(directory)
|
const meta = sessionMeta.get(directory)
|
||||||
if (meta && meta.limit >= store.limit) return
|
if (meta && meta.limit >= store.limit) {
|
||||||
|
const next = trimSessions(store.session, { limit: store.limit, permission: store.permission })
|
||||||
|
if (next.length !== store.session.length) {
|
||||||
|
setStore("session", reconcile(next, { key: "id" }))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const promise = globalSDK.client.session
|
const promise = globalSDK.client.session
|
||||||
.list({ directory, roots: true })
|
.list({ directory, roots: true })
|
||||||
@@ -337,21 +402,9 @@ function createGlobalSync() {
|
|||||||
// a request is in-flight still get the expanded result.
|
// a request is in-flight still get the expanded result.
|
||||||
const limit = store.limit
|
const limit = store.limit
|
||||||
|
|
||||||
const sandboxWorkspace = globalStore.project.some((p) => (p.sandboxes ?? []).includes(directory))
|
const children = store.session.filter((s) => !!s.parentID)
|
||||||
if (sandboxWorkspace) {
|
const sessions = trimSessions([...nonArchived, ...children], { limit, permission: store.permission })
|
||||||
setStore("sessionTotal", nonArchived.length)
|
|
||||||
setStore("session", reconcile(nonArchived, { key: "id" }))
|
|
||||||
sessionMeta.set(directory, { limit })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000
|
|
||||||
// Include up to the limit, plus any updated in the last 4 hours
|
|
||||||
const sessions = nonArchived.filter((s, i) => {
|
|
||||||
if (i < limit) return true
|
|
||||||
const updated = new Date(s.time?.updated ?? s.time?.created).getTime()
|
|
||||||
return updated > fourHoursAgo
|
|
||||||
})
|
|
||||||
// Store total session count (used for "load more" pagination)
|
// Store total session count (used for "load more" pagination)
|
||||||
setStore("sessionTotal", nonArchived.length)
|
setStore("sessionTotal", nonArchived.length)
|
||||||
setStore("session", reconcile(sessions, { key: "id" }))
|
setStore("session", reconcile(sessions, { key: "id" }))
|
||||||
@@ -536,25 +589,25 @@ function createGlobalSync() {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "session.created": {
|
case "session.created": {
|
||||||
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
|
const info = event.properties.info
|
||||||
|
const result = Binary.search(store.session, info.id, (s) => s.id)
|
||||||
if (result.found) {
|
if (result.found) {
|
||||||
setStore("session", result.index, reconcile(event.properties.info))
|
setStore("session", result.index, reconcile(info))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
setStore(
|
const next = store.session.slice()
|
||||||
"session",
|
next.splice(result.index, 0, info)
|
||||||
produce((draft) => {
|
const trimmed = trimSessions(next, { limit: store.limit, permission: store.permission })
|
||||||
draft.splice(result.index, 0, event.properties.info)
|
setStore("session", reconcile(trimmed, { key: "id" }))
|
||||||
}),
|
if (!info.parentID) {
|
||||||
)
|
setStore("sessionTotal", (value) => value + 1)
|
||||||
if (!event.properties.info.parentID) {
|
|
||||||
setStore("sessionTotal", store.sessionTotal + 1)
|
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "session.updated": {
|
case "session.updated": {
|
||||||
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
|
const info = event.properties.info
|
||||||
if (event.properties.info.time.archived) {
|
const result = Binary.search(store.session, info.id, (s) => s.id)
|
||||||
|
if (info.time.archived) {
|
||||||
if (result.found) {
|
if (result.found) {
|
||||||
setStore(
|
setStore(
|
||||||
"session",
|
"session",
|
||||||
@@ -563,20 +616,18 @@ function createGlobalSync() {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (event.properties.info.parentID) break
|
if (info.parentID) break
|
||||||
setStore("sessionTotal", (value) => Math.max(0, value - 1))
|
setStore("sessionTotal", (value) => Math.max(0, value - 1))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if (result.found) {
|
if (result.found) {
|
||||||
setStore("session", result.index, reconcile(event.properties.info))
|
setStore("session", result.index, reconcile(info))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
setStore(
|
const next = store.session.slice()
|
||||||
"session",
|
next.splice(result.index, 0, info)
|
||||||
produce((draft) => {
|
const trimmed = trimSessions(next, { limit: store.limit, permission: store.permission })
|
||||||
draft.splice(result.index, 0, event.properties.info)
|
setStore("session", reconcile(trimmed, { key: "id" }))
|
||||||
}),
|
|
||||||
)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "session.deleted": {
|
case "session.deleted": {
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
import { createEffect, createSignal, onCleanup } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
|
import { usePlatform } from "@/context/platform"
|
||||||
|
import { useSettings } from "@/context/settings"
|
||||||
|
import { persisted } from "@/utils/persist"
|
||||||
|
import { DialogReleaseNotes, type Highlight } from "@/components/dialog-release-notes"
|
||||||
|
|
||||||
|
const CHANGELOG_URL = "https://opencode.ai/changelog.json"
|
||||||
|
|
||||||
|
type Store = {
|
||||||
|
version?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParsedRelease = {
|
||||||
|
tag?: string
|
||||||
|
highlights: Highlight[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getText(value: unknown): string | undefined {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const text = value.trim()
|
||||||
|
return text.length > 0 ? text : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "number") return String(value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeVersion(value: string | undefined) {
|
||||||
|
const text = value?.trim()
|
||||||
|
if (!text) return
|
||||||
|
return text.startsWith("v") || text.startsWith("V") ? text.slice(1) : text
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMedia(value: unknown, alt: string): Highlight["media"] | undefined {
|
||||||
|
if (!isRecord(value)) return
|
||||||
|
const type = getText(value.type)?.toLowerCase()
|
||||||
|
const src = getText(value.src) ?? getText(value.url)
|
||||||
|
if (!src) return
|
||||||
|
if (type !== "image" && type !== "video") return
|
||||||
|
|
||||||
|
return { type, src, alt }
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseHighlight(value: unknown): Highlight | undefined {
|
||||||
|
if (!isRecord(value)) return
|
||||||
|
|
||||||
|
const title = getText(value.title)
|
||||||
|
if (!title) return
|
||||||
|
|
||||||
|
const description = getText(value.description) ?? getText(value.shortDescription)
|
||||||
|
if (!description) return
|
||||||
|
|
||||||
|
const media = parseMedia(value.media, title)
|
||||||
|
return { title, description, media }
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRelease(value: unknown): ParsedRelease | undefined {
|
||||||
|
if (!isRecord(value)) return
|
||||||
|
const tag = getText(value.tag) ?? getText(value.tag_name) ?? getText(value.name)
|
||||||
|
|
||||||
|
if (!Array.isArray(value.highlights)) {
|
||||||
|
return { tag, highlights: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
const highlights = value.highlights.flatMap((group) => {
|
||||||
|
if (!isRecord(group)) return []
|
||||||
|
|
||||||
|
const source = getText(group.source)
|
||||||
|
if (!source) return []
|
||||||
|
if (!source.toLowerCase().includes("desktop")) return []
|
||||||
|
|
||||||
|
if (Array.isArray(group.items)) {
|
||||||
|
return group.items.map((item) => parseHighlight(item)).filter((item): item is Highlight => item !== undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = parseHighlight(group)
|
||||||
|
if (!item) return []
|
||||||
|
return [item]
|
||||||
|
})
|
||||||
|
|
||||||
|
return { tag, highlights }
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseChangelog(value: unknown): ParsedRelease[] | undefined {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map(parseRelease).filter((release): release is ParsedRelease => release !== undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isRecord(value)) return
|
||||||
|
if (!Array.isArray(value.releases)) return
|
||||||
|
|
||||||
|
return value.releases.map(parseRelease).filter((release): release is ParsedRelease => release !== undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sliceHighlights(input: { releases: ParsedRelease[]; current?: string; previous?: string }) {
|
||||||
|
const current = normalizeVersion(input.current)
|
||||||
|
const previous = normalizeVersion(input.previous)
|
||||||
|
const releases = input.releases
|
||||||
|
|
||||||
|
const start = (() => {
|
||||||
|
if (!current) return 0
|
||||||
|
const index = releases.findIndex((release) => normalizeVersion(release.tag) === current)
|
||||||
|
return index === -1 ? 0 : index
|
||||||
|
})()
|
||||||
|
|
||||||
|
const end = (() => {
|
||||||
|
if (!previous) return releases.length
|
||||||
|
const index = releases.findIndex((release, i) => i >= start && normalizeVersion(release.tag) === previous)
|
||||||
|
return index === -1 ? releases.length : index
|
||||||
|
})()
|
||||||
|
|
||||||
|
const highlights = releases.slice(start, end).flatMap((release) => release.highlights)
|
||||||
|
const seen = new Set<string>()
|
||||||
|
const unique = highlights.filter((highlight) => {
|
||||||
|
const key = [highlight.title, highlight.description, highlight.media?.type ?? "", highlight.media?.src ?? ""].join(
|
||||||
|
"\n",
|
||||||
|
)
|
||||||
|
if (seen.has(key)) return false
|
||||||
|
seen.add(key)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return unique.slice(0, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const { use: useHighlights, provider: HighlightsProvider } = createSimpleContext({
|
||||||
|
name: "Highlights",
|
||||||
|
gate: false,
|
||||||
|
init: () => {
|
||||||
|
const platform = usePlatform()
|
||||||
|
const dialog = useDialog()
|
||||||
|
const settings = useSettings()
|
||||||
|
const [store, setStore, _, ready] = persisted("highlights.v1", createStore<Store>({ version: undefined }))
|
||||||
|
|
||||||
|
const [from, setFrom] = createSignal<string | undefined>(undefined)
|
||||||
|
const [to, setTo] = createSignal<string | undefined>(undefined)
|
||||||
|
const [timer, setTimer] = createSignal<ReturnType<typeof setTimeout> | undefined>(undefined)
|
||||||
|
const state = { started: false }
|
||||||
|
|
||||||
|
const markSeen = () => {
|
||||||
|
if (!platform.version) return
|
||||||
|
setStore("version", platform.version)
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (state.started) return
|
||||||
|
if (!ready()) return
|
||||||
|
if (!settings.ready()) return
|
||||||
|
if (!platform.version) return
|
||||||
|
state.started = true
|
||||||
|
|
||||||
|
const previous = store.version
|
||||||
|
if (!previous) {
|
||||||
|
setStore("version", platform.version)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previous === platform.version) return
|
||||||
|
|
||||||
|
setFrom(previous)
|
||||||
|
setTo(platform.version)
|
||||||
|
|
||||||
|
if (!settings.general.releaseNotes()) {
|
||||||
|
markSeen()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetcher = platform.fetch ?? fetch
|
||||||
|
const controller = new AbortController()
|
||||||
|
onCleanup(() => {
|
||||||
|
controller.abort()
|
||||||
|
const id = timer()
|
||||||
|
if (id === undefined) return
|
||||||
|
clearTimeout(id)
|
||||||
|
})
|
||||||
|
|
||||||
|
fetcher(CHANGELOG_URL, {
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: { Accept: "application/json" },
|
||||||
|
})
|
||||||
|
.then((response) => (response.ok ? (response.json() as Promise<unknown>) : undefined))
|
||||||
|
.then((json) => {
|
||||||
|
if (!json) return
|
||||||
|
const releases = parseChangelog(json)
|
||||||
|
if (!releases) return
|
||||||
|
if (releases.length === 0) return
|
||||||
|
const highlights = sliceHighlights({
|
||||||
|
releases,
|
||||||
|
current: platform.version,
|
||||||
|
previous,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (controller.signal.aborted) return
|
||||||
|
|
||||||
|
if (highlights.length === 0) {
|
||||||
|
markSeen()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
markSeen()
|
||||||
|
dialog.show(() => <DialogReleaseNotes highlights={highlights} />)
|
||||||
|
}, 500)
|
||||||
|
setTimer(timer)
|
||||||
|
})
|
||||||
|
.catch(() => undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
ready,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
get last() {
|
||||||
|
return store.version
|
||||||
|
},
|
||||||
|
markSeen,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -82,6 +82,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
diffStyle: "split" as ReviewDiffStyle,
|
diffStyle: "split" as ReviewDiffStyle,
|
||||||
panelOpened: true,
|
panelOpened: true,
|
||||||
},
|
},
|
||||||
|
fileTree: {
|
||||||
|
opened: false,
|
||||||
|
width: 260,
|
||||||
|
},
|
||||||
session: {
|
session: {
|
||||||
width: 600,
|
width: 600,
|
||||||
},
|
},
|
||||||
@@ -218,7 +222,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function enrich(project: { worktree: string; expanded: boolean }) {
|
function enrich(project: { worktree: string; expanded: boolean }) {
|
||||||
const [childStore] = globalSync.child(project.worktree)
|
const [childStore] = globalSync.child(project.worktree, { bootstrap: false })
|
||||||
const projectID = childStore.project
|
const projectID = childStore.project
|
||||||
const metadata = projectID
|
const metadata = projectID
|
||||||
? globalSync.data.project.find((x) => x.id === projectID)
|
? globalSync.data.project.find((x) => x.id === projectID)
|
||||||
@@ -449,6 +453,38 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
setStore("review", "diffStyle", diffStyle)
|
setStore("review", "diffStyle", diffStyle)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
fileTree: {
|
||||||
|
opened: createMemo(() => store.fileTree?.opened ?? false),
|
||||||
|
width: createMemo(() => store.fileTree?.width ?? 260),
|
||||||
|
open() {
|
||||||
|
if (!store.fileTree) {
|
||||||
|
setStore("fileTree", { opened: true, width: 260 })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setStore("fileTree", "opened", true)
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
if (!store.fileTree) {
|
||||||
|
setStore("fileTree", { opened: false, width: 260 })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setStore("fileTree", "opened", false)
|
||||||
|
},
|
||||||
|
toggle() {
|
||||||
|
if (!store.fileTree) {
|
||||||
|
setStore("fileTree", { opened: true, width: 260 })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setStore("fileTree", "opened", (x) => !x)
|
||||||
|
},
|
||||||
|
resize(width: number) {
|
||||||
|
if (!store.fileTree) {
|
||||||
|
setStore("fileTree", { opened: true, width })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setStore("fileTree", "width", width)
|
||||||
|
},
|
||||||
|
},
|
||||||
session: {
|
session: {
|
||||||
width: createMemo(() => store.session?.width ?? 600),
|
width: createMemo(() => store.session?.width ?? 600),
|
||||||
resize(width: number) {
|
resize(width: number) {
|
||||||
|
|||||||
@@ -1,49 +1,20 @@
|
|||||||
import { createStore, produce, reconcile } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { batch, createEffect, createMemo, onCleanup } from "solid-js"
|
import { batch, createMemo } 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 { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { useSDK } from "./sdk"
|
import { useSDK } from "./sdk"
|
||||||
import { useSync } from "./sync"
|
import { useSync } from "./sync"
|
||||||
import { base64Encode } from "@opencode-ai/util/encode"
|
import { base64Encode } from "@opencode-ai/util/encode"
|
||||||
import { useProviders } from "@/hooks/use-providers"
|
import { useProviders } from "@/hooks/use-providers"
|
||||||
import { DateTime } from "luxon"
|
import { useModels } from "@/context/models"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
|
||||||
import { useLanguage } from "@/context/language"
|
|
||||||
|
|
||||||
export type LocalFile = FileNode &
|
|
||||||
Partial<{
|
|
||||||
loaded: boolean
|
|
||||||
pinned: boolean
|
|
||||||
expanded: boolean
|
|
||||||
content: FileContent
|
|
||||||
selection: { startLine: number; startChar: number; endLine: number; endChar: number }
|
|
||||||
scrollTop: number
|
|
||||||
view: "raw" | "diff-unified" | "diff-split"
|
|
||||||
folded: string[]
|
|
||||||
selectedChange: number
|
|
||||||
status: FileStatus
|
|
||||||
}>
|
|
||||||
export type TextSelection = LocalFile["selection"]
|
|
||||||
export type View = LocalFile["view"]
|
|
||||||
|
|
||||||
export type LocalModel = Omit<Model, "provider"> & {
|
|
||||||
provider: Provider
|
|
||||||
latest?: boolean
|
|
||||||
}
|
|
||||||
export type ModelKey = { providerID: string; modelID: string }
|
export type ModelKey = { providerID: string; modelID: string }
|
||||||
|
|
||||||
export type FileContext = { type: "file"; path: string; selection?: TextSelection }
|
|
||||||
export type ContextItem = FileContext
|
|
||||||
|
|
||||||
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||||
name: "Local",
|
name: "Local",
|
||||||
init: () => {
|
init: () => {
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const providers = useProviders()
|
const providers = useProviders()
|
||||||
const language = useLanguage()
|
|
||||||
|
|
||||||
function isModelValid(model: ModelKey) {
|
function isModelValid(model: ModelKey) {
|
||||||
const provider = providers.all().find((x) => x.id === model.providerID)
|
const provider = providers.all().find((x) => x.id === model.providerID)
|
||||||
@@ -112,18 +83,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
})()
|
})()
|
||||||
|
|
||||||
const model = (() => {
|
const model = (() => {
|
||||||
const [store, setStore, _, modelReady] = persisted(
|
const models = useModels()
|
||||||
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<{
|
const [ephemeral, setEphemeral] = createStore<{
|
||||||
model: Record<string, ModelKey | undefined>
|
model: Record<string, ModelKey | undefined>
|
||||||
@@ -131,57 +91,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
model: {},
|
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>(() => {
|
const fallbackModel = createMemo<ModelKey | undefined>(() => {
|
||||||
if (sync.data.config.model) {
|
if (sync.data.config.model) {
|
||||||
const [providerID, modelID] = sync.data.config.model.split("/")
|
const [providerID, modelID] = sync.data.config.model.split("/")
|
||||||
@@ -193,7 +102,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const item of store.recent) {
|
for (const item of models.recent.list()) {
|
||||||
if (isModelValid(item)) {
|
if (isModelValid(item)) {
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
@@ -225,10 +134,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
fallbackModel,
|
fallbackModel,
|
||||||
)
|
)
|
||||||
if (!key) return undefined
|
if (!key) return undefined
|
||||||
return find(key)
|
return models.find(key)
|
||||||
})
|
})
|
||||||
|
|
||||||
const recent = createMemo(() => store.recent.map(find).filter(Boolean))
|
const recent = createMemo(() => models.recent.list().map(models.find).filter(Boolean))
|
||||||
|
|
||||||
const cycle = (direction: 1 | -1) => {
|
const cycle = (direction: 1 | -1) => {
|
||||||
const recentList = recent()
|
const recentList = recent()
|
||||||
@@ -253,54 +162,32 @@ 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 {
|
return {
|
||||||
ready: modelReady,
|
ready: models.ready,
|
||||||
current,
|
current,
|
||||||
recent,
|
recent,
|
||||||
list,
|
list: models.list,
|
||||||
cycle,
|
cycle,
|
||||||
set(model: ModelKey | undefined, options?: { recent?: boolean }) {
|
set(model: ModelKey | undefined, options?: { recent?: boolean }) {
|
||||||
batch(() => {
|
batch(() => {
|
||||||
const currentAgent = agent.current()
|
const currentAgent = agent.current()
|
||||||
const next = model ?? fallbackModel()
|
const next = model ?? fallbackModel()
|
||||||
if (currentAgent) setEphemeral("model", currentAgent.name, next)
|
if (currentAgent) setEphemeral("model", currentAgent.name, next)
|
||||||
if (model) updateVisibility(model, "show")
|
if (model) models.setVisibility(model, true)
|
||||||
if (options?.recent && model) {
|
if (options?.recent && model) models.recent.push(model)
|
||||||
const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID)
|
|
||||||
if (uniq.length > 5) uniq.pop()
|
|
||||||
setStore("recent", uniq)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
visible(model: ModelKey) {
|
visible(model: ModelKey) {
|
||||||
const key = `${model.providerID}:${model.modelID}`
|
return models.visible(model)
|
||||||
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) {
|
setVisibility(model: ModelKey, visible: boolean) {
|
||||||
updateVisibility(model, visible ? "show" : "hide")
|
models.setVisibility(model, visible)
|
||||||
},
|
},
|
||||||
variant: {
|
variant: {
|
||||||
current() {
|
current() {
|
||||||
const m = current()
|
const m = current()
|
||||||
if (!m) return undefined
|
if (!m) return undefined
|
||||||
const key = `${m.provider.id}/${m.id}`
|
return models.variant.get({ providerID: m.provider.id, modelID: m.id })
|
||||||
return store.variant?.[key]
|
|
||||||
},
|
},
|
||||||
list() {
|
list() {
|
||||||
const m = current()
|
const m = current()
|
||||||
@@ -311,12 +198,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
set(value: string | undefined) {
|
set(value: string | undefined) {
|
||||||
const m = current()
|
const m = current()
|
||||||
if (!m) return
|
if (!m) return
|
||||||
const key = `${m.provider.id}/${m.id}`
|
models.variant.set({ providerID: m.provider.id, modelID: m.id }, value)
|
||||||
if (!store.variant) {
|
|
||||||
setStore("variant", { [key]: value })
|
|
||||||
} else {
|
|
||||||
setStore("variant", key, value)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
cycle() {
|
cycle() {
|
||||||
const variants = this.list()
|
const variants = this.list()
|
||||||
@@ -337,247 +219,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
const file = (() => {
|
|
||||||
const [store, setStore] = createStore<{
|
|
||||||
node: Record<string, LocalFile>
|
|
||||||
}>({
|
|
||||||
node: {}, // Object.fromEntries(sync.data.node.map((x) => [x.path, x])),
|
|
||||||
})
|
|
||||||
|
|
||||||
const scope = createMemo(() => sdk.directory)
|
|
||||||
createEffect(() => {
|
|
||||||
scope()
|
|
||||||
setStore("node", {})
|
|
||||||
})
|
|
||||||
|
|
||||||
// const changeset = createMemo(() => new Set(sync.data.changes.map((f) => f.path)))
|
|
||||||
// const changes = createMemo(() => Array.from(changeset()).sort((a, b) => a.localeCompare(b)))
|
|
||||||
|
|
||||||
// createEffect((prev: FileStatus[]) => {
|
|
||||||
// const removed = prev.filter((p) => !sync.data.changes.find((c) => c.path === p.path))
|
|
||||||
// for (const p of removed) {
|
|
||||||
// setStore(
|
|
||||||
// "node",
|
|
||||||
// p.path,
|
|
||||||
// produce((draft) => {
|
|
||||||
// draft.status = undefined
|
|
||||||
// draft.view = "raw"
|
|
||||||
// }),
|
|
||||||
// )
|
|
||||||
// load(p.path)
|
|
||||||
// }
|
|
||||||
// for (const p of sync.data.changes) {
|
|
||||||
// if (store.node[p.path] === undefined) {
|
|
||||||
// fetch(p.path).then(() => {
|
|
||||||
// if (store.node[p.path] === undefined) return
|
|
||||||
// setStore("node", p.path, "status", p)
|
|
||||||
// })
|
|
||||||
// } else {
|
|
||||||
// setStore("node", p.path, "status", p)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return sync.data.changes
|
|
||||||
// }, sync.data.changes)
|
|
||||||
|
|
||||||
// const changed = (path: string) => {
|
|
||||||
// const node = store.node[path]
|
|
||||||
// if (node?.status) return true
|
|
||||||
// const set = changeset()
|
|
||||||
// if (set.has(path)) return true
|
|
||||||
// for (const p of set) {
|
|
||||||
// if (p.startsWith(path ? path + "/" : "")) return true
|
|
||||||
// }
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const resetNode = (path: string) => {
|
|
||||||
// setStore("node", path, {
|
|
||||||
// loaded: undefined,
|
|
||||||
// pinned: undefined,
|
|
||||||
// content: undefined,
|
|
||||||
// selection: undefined,
|
|
||||||
// scrollTop: undefined,
|
|
||||||
// folded: undefined,
|
|
||||||
// view: undefined,
|
|
||||||
// selectedChange: undefined,
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
const relative = (path: string) => path.replace(sync.data.path.directory + "/", "")
|
|
||||||
|
|
||||||
const load = async (path: string) => {
|
|
||||||
const directory = scope()
|
|
||||||
const client = sdk.client
|
|
||||||
const relativePath = relative(path)
|
|
||||||
await client.file
|
|
||||||
.read({ path: relativePath })
|
|
||||||
.then((x) => {
|
|
||||||
if (scope() !== directory) return
|
|
||||||
if (!store.node[relativePath]) return
|
|
||||||
setStore(
|
|
||||||
"node",
|
|
||||||
relativePath,
|
|
||||||
produce((draft) => {
|
|
||||||
draft.loaded = true
|
|
||||||
draft.content = x.data
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
if (scope() !== directory) return
|
|
||||||
showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: language.t("toast.file.loadFailed.title"),
|
|
||||||
description: e.message,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetch = async (path: string) => {
|
|
||||||
const relativePath = relative(path)
|
|
||||||
const parent = relativePath.split("/").slice(0, -1).join("/")
|
|
||||||
if (parent) {
|
|
||||||
await list(parent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const init = async (path: string) => {
|
|
||||||
const relativePath = relative(path)
|
|
||||||
if (!store.node[relativePath]) await fetch(path)
|
|
||||||
if (store.node[relativePath]?.loaded) return
|
|
||||||
return load(relativePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
const open = async (path: string, options?: { pinned?: boolean; view?: LocalFile["view"] }) => {
|
|
||||||
const relativePath = relative(path)
|
|
||||||
if (!store.node[relativePath]) await fetch(path)
|
|
||||||
// setStore("opened", (x) => {
|
|
||||||
// if (x.includes(relativePath)) return x
|
|
||||||
// return [
|
|
||||||
// ...opened()
|
|
||||||
// .filter((x) => x.pinned)
|
|
||||||
// .map((x) => x.path),
|
|
||||||
// relativePath,
|
|
||||||
// ]
|
|
||||||
// })
|
|
||||||
// setStore("active", relativePath)
|
|
||||||
// context.addActive()
|
|
||||||
if (options?.pinned) setStore("node", path, "pinned", true)
|
|
||||||
if (options?.view && store.node[relativePath].view === undefined) setStore("node", path, "view", options.view)
|
|
||||||
if (store.node[relativePath]?.loaded) return
|
|
||||||
return load(relativePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
const list = async (path: string) => {
|
|
||||||
const directory = scope()
|
|
||||||
const client = sdk.client
|
|
||||||
return client.file
|
|
||||||
.list({ path: path + "/" })
|
|
||||||
.then((x) => {
|
|
||||||
if (scope() !== directory) return
|
|
||||||
setStore(
|
|
||||||
"node",
|
|
||||||
produce((draft) => {
|
|
||||||
x.data!.forEach((node) => {
|
|
||||||
if (node.path in draft) return
|
|
||||||
draft[node.path] = node
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.catch(() => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchFiles = (query: string) => sdk.client.find.files({ query, dirs: "false" }).then((x) => x.data!)
|
|
||||||
const searchFilesAndDirectories = (query: string) =>
|
|
||||||
sdk.client.find.files({ query, dirs: "true" }).then((x) => x.data!)
|
|
||||||
|
|
||||||
const unsub = sdk.event.listen((e) => {
|
|
||||||
const event = e.details
|
|
||||||
switch (event.type) {
|
|
||||||
case "file.watcher.updated":
|
|
||||||
const relativePath = relative(event.properties.file)
|
|
||||||
if (relativePath.startsWith(".git/")) return
|
|
||||||
if (store.node[relativePath]) load(relativePath)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
})
|
|
||||||
onCleanup(unsub)
|
|
||||||
|
|
||||||
return {
|
|
||||||
node: async (path: string) => {
|
|
||||||
if (!store.node[path] || !store.node[path].loaded) {
|
|
||||||
await init(path)
|
|
||||||
}
|
|
||||||
return store.node[path]
|
|
||||||
},
|
|
||||||
update: (path: string, node: LocalFile) => setStore("node", path, reconcile(node)),
|
|
||||||
open,
|
|
||||||
load,
|
|
||||||
init,
|
|
||||||
expand(path: string) {
|
|
||||||
setStore("node", path, "expanded", true)
|
|
||||||
if (store.node[path]?.loaded) return
|
|
||||||
setStore("node", path, "loaded", true)
|
|
||||||
list(path)
|
|
||||||
},
|
|
||||||
collapse(path: string) {
|
|
||||||
setStore("node", path, "expanded", false)
|
|
||||||
},
|
|
||||||
select(path: string, selection: TextSelection | undefined) {
|
|
||||||
setStore("node", path, "selection", selection)
|
|
||||||
},
|
|
||||||
scroll(path: string, scrollTop: number) {
|
|
||||||
setStore("node", path, "scrollTop", scrollTop)
|
|
||||||
},
|
|
||||||
view(path: string): View {
|
|
||||||
const n = store.node[path]
|
|
||||||
return n && n.view ? n.view : "raw"
|
|
||||||
},
|
|
||||||
setView(path: string, view: View) {
|
|
||||||
setStore("node", path, "view", view)
|
|
||||||
},
|
|
||||||
unfold(path: string, key: string) {
|
|
||||||
setStore("node", path, "folded", (xs) => {
|
|
||||||
const a = xs ?? []
|
|
||||||
if (a.includes(key)) return a
|
|
||||||
return [...a, key]
|
|
||||||
})
|
|
||||||
},
|
|
||||||
fold(path: string, key: string) {
|
|
||||||
setStore("node", path, "folded", (xs) => (xs ?? []).filter((k) => k !== key))
|
|
||||||
},
|
|
||||||
folded(path: string) {
|
|
||||||
const n = store.node[path]
|
|
||||||
return n && n.folded ? n.folded : []
|
|
||||||
},
|
|
||||||
changeIndex(path: string) {
|
|
||||||
return store.node[path]?.selectedChange
|
|
||||||
},
|
|
||||||
setChangeIndex(path: string, index: number | undefined) {
|
|
||||||
setStore("node", path, "selectedChange", index)
|
|
||||||
},
|
|
||||||
// changes,
|
|
||||||
// changed,
|
|
||||||
children(path: string) {
|
|
||||||
return Object.values(store.node).filter(
|
|
||||||
(x) =>
|
|
||||||
x.path.startsWith(path) &&
|
|
||||||
x.path !== path &&
|
|
||||||
!x.path.replace(new RegExp(`^${path + "/"}`), "").includes("/"),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
searchFiles,
|
|
||||||
searchFilesAndDirectories,
|
|
||||||
relative,
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
slug: createMemo(() => base64Encode(sdk.directory)),
|
slug: createMemo(() => base64Encode(sdk.directory)),
|
||||||
model,
|
model,
|
||||||
agent,
|
agent,
|
||||||
file,
|
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
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 { createStore } from "solid-js/store"
|
||||||
import { createEffect, onCleanup } from "solid-js"
|
import { createEffect, createMemo, onCleanup } from "solid-js"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { useGlobalSDK } from "./global-sdk"
|
import { useGlobalSDK } from "./global-sdk"
|
||||||
@@ -52,6 +52,15 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
|||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
const language = useLanguage()
|
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(
|
const [store, setStore, _, ready] = persisted(
|
||||||
Persist.global("notification", ["notification.v1"]),
|
Persist.global("notification", ["notification.v1"]),
|
||||||
createStore({
|
createStore({
|
||||||
@@ -72,13 +81,59 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
|||||||
setStore("list", (list) => pruneNotifications([...list, notification]))
|
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 unsub = globalSDK.event.listen((e) => {
|
||||||
const directory = e.name
|
|
||||||
const event = e.details
|
const event = e.details
|
||||||
|
if (event.type !== "session.idle" && event.type !== "session.error") return
|
||||||
|
|
||||||
|
const directory = e.name
|
||||||
const time = Date.now()
|
const time = Date.now()
|
||||||
const activeDirectory = params.dir ? base64Decode(params.dir) : undefined
|
|
||||||
const activeSession = params.id
|
|
||||||
const viewed = (sessionID?: string) => {
|
const viewed = (sessionID?: string) => {
|
||||||
|
const activeDirectory = currentDirectory()
|
||||||
|
const activeSession = currentSession()
|
||||||
if (!activeDirectory) return false
|
if (!activeDirectory) return false
|
||||||
if (!activeSession) return false
|
if (!activeSession) return false
|
||||||
if (!sessionID) return false
|
if (!sessionID) return false
|
||||||
@@ -88,7 +143,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
|||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case "session.idle": {
|
case "session.idle": {
|
||||||
const sessionID = event.properties.sessionID
|
const sessionID = event.properties.sessionID
|
||||||
const [syncStore] = globalSync.child(directory)
|
const [syncStore] = globalSync.child(directory, { bootstrap: false })
|
||||||
const match = Binary.search(syncStore.session, sessionID, (s) => s.id)
|
const match = Binary.search(syncStore.session, sessionID, (s) => s.id)
|
||||||
const session = match.found ? syncStore.session[match.index] : undefined
|
const session = match.found ? syncStore.session[match.index] : undefined
|
||||||
if (session?.parentID) break
|
if (session?.parentID) break
|
||||||
@@ -115,7 +170,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
|||||||
}
|
}
|
||||||
case "session.error": {
|
case "session.error": {
|
||||||
const sessionID = event.properties.sessionID
|
const sessionID = event.properties.sessionID
|
||||||
const [syncStore] = globalSync.child(directory)
|
const [syncStore] = globalSync.child(directory, { bootstrap: false })
|
||||||
const match = sessionID ? Binary.search(syncStore.session, sessionID, (s) => s.id) : undefined
|
const match = sessionID ? Binary.search(syncStore.session, sessionID, (s) => s.id) : undefined
|
||||||
const session = sessionID && match?.found ? syncStore.session[match.index] : undefined
|
const session = sessionID && match?.found ? syncStore.session[match.index] : undefined
|
||||||
if (session?.parentID) break
|
if (session?.parentID) break
|
||||||
@@ -148,10 +203,10 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
|||||||
ready,
|
ready,
|
||||||
session: {
|
session: {
|
||||||
all(session: string) {
|
all(session: string) {
|
||||||
return store.list.filter((n) => n.session === session)
|
return index().session.all.get(session) ?? empty
|
||||||
},
|
},
|
||||||
unseen(session: string) {
|
unseen(session: string) {
|
||||||
return store.list.filter((n) => n.session === session && !n.viewed)
|
return index().session.unseen.get(session) ?? empty
|
||||||
},
|
},
|
||||||
markViewed(session: string) {
|
markViewed(session: string) {
|
||||||
setStore("list", (n) => n.session === session, "viewed", true)
|
setStore("list", (n) => n.session === session, "viewed", true)
|
||||||
@@ -159,10 +214,10 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
|||||||
},
|
},
|
||||||
project: {
|
project: {
|
||||||
all(directory: string) {
|
all(directory: string) {
|
||||||
return store.list.filter((n) => n.directory === directory)
|
return index().project.all.get(directory) ?? empty
|
||||||
},
|
},
|
||||||
unseen(directory: string) {
|
unseen(directory: string) {
|
||||||
return store.list.filter((n) => n.directory === directory && !n.viewed)
|
return index().project.unseen.get(directory) ?? empty
|
||||||
},
|
},
|
||||||
markViewed(directory: string) {
|
markViewed(directory: string) {
|
||||||
setStore("list", (n) => n.directory === directory, "viewed", true)
|
setStore("list", (n) => n.directory === directory, "viewed", true)
|
||||||
|
|||||||
@@ -41,11 +41,11 @@ export type Platform = {
|
|||||||
/** Fetch override */
|
/** Fetch override */
|
||||||
fetch?: typeof fetch
|
fetch?: typeof fetch
|
||||||
|
|
||||||
/** Get the configured default server URL (desktop only) */
|
/** Get the configured default server URL (platform-specific) */
|
||||||
getDefaultServerUrl?(): Promise<string | null>
|
getDefaultServerUrl?(): Promise<string | null> | string | null
|
||||||
|
|
||||||
/** Set the default server URL to use on app startup (desktop only) */
|
/** Set the default server URL to use on app startup (platform-specific) */
|
||||||
setDefaultServerUrl?(url: string | null): Promise<void>
|
setDefaultServerUrl?(url: string | null): Promise<void> | void
|
||||||
|
|
||||||
/** Parse markdown to HTML using native parser (desktop only, returns unprocessed code blocks) */
|
/** Parse markdown to HTML using native parser (desktop only, returns unprocessed code blocks) */
|
||||||
parseMarkdown?(markdown: string): Promise<string>
|
parseMarkdown?(markdown: string): Promise<string>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { batch, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
|
import { batch, createEffect, createMemo, onCleanup } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
@@ -40,12 +40,17 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const [active, setActiveRaw] = createSignal("")
|
const [state, setState] = createStore({
|
||||||
|
active: "",
|
||||||
|
healthy: undefined as boolean | undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const healthy = () => state.healthy
|
||||||
|
|
||||||
function setActive(input: string) {
|
function setActive(input: string) {
|
||||||
const url = normalizeServerUrl(input)
|
const url = normalizeServerUrl(input)
|
||||||
if (!url) return
|
if (!url) return
|
||||||
setActiveRaw(url)
|
setState("active", url)
|
||||||
}
|
}
|
||||||
|
|
||||||
function add(input: string) {
|
function add(input: string) {
|
||||||
@@ -54,7 +59,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
|
|
||||||
const fallback = normalizeServerUrl(props.defaultUrl)
|
const fallback = normalizeServerUrl(props.defaultUrl)
|
||||||
if (fallback && url === fallback) {
|
if (fallback && url === fallback) {
|
||||||
setActiveRaw(url)
|
setState("active", url)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +67,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
if (!store.list.includes(url)) {
|
if (!store.list.includes(url)) {
|
||||||
setStore("list", store.list.length, url)
|
setStore("list", store.list.length, url)
|
||||||
}
|
}
|
||||||
setActiveRaw(url)
|
setState("active", url)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,25 +76,23 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
if (!url) return
|
if (!url) return
|
||||||
|
|
||||||
const list = store.list.filter((x) => x !== url)
|
const list = store.list.filter((x) => x !== url)
|
||||||
const next = active() === url ? (list[0] ?? normalizeServerUrl(props.defaultUrl) ?? "") : active()
|
const next = state.active === url ? (list[0] ?? normalizeServerUrl(props.defaultUrl) ?? "") : state.active
|
||||||
|
|
||||||
batch(() => {
|
batch(() => {
|
||||||
setStore("list", list)
|
setStore("list", list)
|
||||||
setActiveRaw(next)
|
setState("active", next)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!ready()) return
|
if (!ready()) return
|
||||||
if (active()) return
|
if (state.active) return
|
||||||
const url = normalizeServerUrl(props.defaultUrl)
|
const url = normalizeServerUrl(props.defaultUrl)
|
||||||
if (!url) return
|
if (!url) return
|
||||||
setActiveRaw(url)
|
setState("active", url)
|
||||||
})
|
})
|
||||||
|
|
||||||
const isReady = createMemo(() => ready() && !!active())
|
const isReady = createMemo(() => ready() && !!state.active)
|
||||||
|
|
||||||
const [healthy, setHealthy] = createSignal<boolean | undefined>(undefined)
|
|
||||||
|
|
||||||
const check = (url: string) => {
|
const check = (url: string) => {
|
||||||
const sdk = createOpencodeClient({
|
const sdk = createOpencodeClient({
|
||||||
@@ -104,10 +107,10 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const url = active()
|
const url = state.active
|
||||||
if (!url) return
|
if (!url) return
|
||||||
|
|
||||||
setHealthy(undefined)
|
setState("healthy", undefined)
|
||||||
|
|
||||||
let alive = true
|
let alive = true
|
||||||
let busy = false
|
let busy = false
|
||||||
@@ -118,7 +121,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
void check(url)
|
void check(url)
|
||||||
.then((next) => {
|
.then((next) => {
|
||||||
if (!alive) return
|
if (!alive) return
|
||||||
setHealthy(next)
|
setState("healthy", next)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
busy = false
|
busy = false
|
||||||
@@ -134,7 +137,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const origin = createMemo(() => projectsKey(active()))
|
const origin = createMemo(() => projectsKey(state.active))
|
||||||
const projectsList = createMemo(() => store.projects[origin()] ?? [])
|
const projectsList = createMemo(() => store.projects[origin()] ?? [])
|
||||||
const isLocal = createMemo(() => origin() === "local")
|
const isLocal = createMemo(() => origin() === "local")
|
||||||
|
|
||||||
@@ -143,10 +146,10 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
healthy,
|
healthy,
|
||||||
isLocal,
|
isLocal,
|
||||||
get url() {
|
get url() {
|
||||||
return active()
|
return state.active
|
||||||
},
|
},
|
||||||
get name() {
|
get name() {
|
||||||
return serverDisplayName(active())
|
return serverDisplayName(state.active)
|
||||||
},
|
},
|
||||||
get list() {
|
get list() {
|
||||||
return store.list
|
return store.list
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface SoundSettings {
|
|||||||
export interface Settings {
|
export interface Settings {
|
||||||
general: {
|
general: {
|
||||||
autoSave: boolean
|
autoSave: boolean
|
||||||
|
releaseNotes: boolean
|
||||||
}
|
}
|
||||||
appearance: {
|
appearance: {
|
||||||
fontSize: number
|
fontSize: number
|
||||||
@@ -34,6 +35,7 @@ export interface Settings {
|
|||||||
const defaultSettings: Settings = {
|
const defaultSettings: Settings = {
|
||||||
general: {
|
general: {
|
||||||
autoSave: true,
|
autoSave: true,
|
||||||
|
releaseNotes: true,
|
||||||
},
|
},
|
||||||
appearance: {
|
appearance: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
@@ -97,6 +99,10 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
|||||||
setAutoSave(value: boolean) {
|
setAutoSave(value: boolean) {
|
||||||
setStore("general", "autoSave", value)
|
setStore("general", "autoSave", value)
|
||||||
},
|
},
|
||||||
|
releaseNotes: createMemo(() => store.general?.releaseNotes ?? defaultSettings.general.releaseNotes),
|
||||||
|
setReleaseNotes(value: boolean) {
|
||||||
|
setStore("general", "releaseNotes", value)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
appearance: {
|
appearance: {
|
||||||
fontSize: createMemo(() => store.appearance?.fontSize ?? defaultSettings.appearance.fontSize),
|
fontSize: createMemo(() => store.appearance?.fontSize ?? defaultSettings.appearance.fontSize),
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
|
|
||||||
type Child = ReturnType<(typeof globalSync)["child"]>
|
type Child = ReturnType<(typeof globalSync)["child"]>
|
||||||
type Store = Child[0]
|
|
||||||
type Setter = Child[1]
|
type Setter = Child[1]
|
||||||
|
|
||||||
const current = createMemo(() => globalSync.child(sdk.directory))
|
const current = createMemo(() => globalSync.child(sdk.directory))
|
||||||
@@ -43,18 +42,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
return Math.ceil(count / chunk) * chunk
|
return Math.ceil(count / chunk) * chunk
|
||||||
}
|
}
|
||||||
|
|
||||||
const hydrateMessages = (directory: string, store: Store, sessionID: string) => {
|
|
||||||
const key = keyFor(directory, sessionID)
|
|
||||||
if (meta.limit[key] !== undefined) return
|
|
||||||
|
|
||||||
const messages = store.message[sessionID]
|
|
||||||
if (!messages) return
|
|
||||||
|
|
||||||
const limit = limitFor(messages.length)
|
|
||||||
setMeta("limit", key, limit)
|
|
||||||
setMeta("complete", key, messages.length < limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadMessages = async (input: {
|
const loadMessages = async (input: {
|
||||||
directory: string
|
directory: string
|
||||||
client: typeof sdk.client
|
client: typeof sdk.client
|
||||||
@@ -150,21 +137,20 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
const directory = sdk.directory
|
const directory = sdk.directory
|
||||||
const client = sdk.client
|
const client = sdk.client
|
||||||
const [store, setStore] = globalSync.child(directory)
|
const [store, setStore] = globalSync.child(directory)
|
||||||
|
const key = keyFor(directory, sessionID)
|
||||||
const hasSession = (() => {
|
const hasSession = (() => {
|
||||||
const match = Binary.search(store.session, sessionID, (s) => s.id)
|
const match = Binary.search(store.session, sessionID, (s) => s.id)
|
||||||
return match.found
|
return match.found
|
||||||
})()
|
})()
|
||||||
|
|
||||||
hydrateMessages(directory, store, sessionID)
|
|
||||||
|
|
||||||
const hasMessages = store.message[sessionID] !== undefined
|
const hasMessages = store.message[sessionID] !== undefined
|
||||||
if (hasSession && hasMessages) return
|
const hydrated = meta.limit[key] !== undefined
|
||||||
|
if (hasSession && hasMessages && hydrated) return
|
||||||
const key = keyFor(directory, sessionID)
|
|
||||||
const pending = inflight.get(key)
|
const pending = inflight.get(key)
|
||||||
if (pending) return pending
|
if (pending) return pending
|
||||||
|
|
||||||
const limit = meta.limit[key] ?? chunk
|
const count = store.message[sessionID]?.length ?? 0
|
||||||
|
const limit = hydrated ? (meta.limit[key] ?? chunk) : limitFor(count)
|
||||||
|
|
||||||
const sessionReq = hasSession
|
const sessionReq = hasSession
|
||||||
? Promise.resolve()
|
? Promise.resolve()
|
||||||
@@ -184,15 +170,16 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const messagesReq = hasMessages
|
const messagesReq =
|
||||||
? Promise.resolve()
|
hasMessages && hydrated
|
||||||
: loadMessages({
|
? Promise.resolve()
|
||||||
directory,
|
: loadMessages({
|
||||||
client,
|
directory,
|
||||||
setStore,
|
client,
|
||||||
sessionID,
|
setStore,
|
||||||
limit,
|
sessionID,
|
||||||
})
|
limit,
|
||||||
|
})
|
||||||
|
|
||||||
const promise = Promise.all([sessionReq, messagesReq])
|
const promise = Promise.all([sessionReq, messagesReq])
|
||||||
.then(() => {})
|
.then(() => {})
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { dict as en } from "@/i18n/en"
|
|||||||
import { dict as zh } from "@/i18n/zh"
|
import { dict as zh } from "@/i18n/zh"
|
||||||
import pkg from "../package.json"
|
import pkg from "../package.json"
|
||||||
|
|
||||||
|
const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl"
|
||||||
|
|
||||||
const root = document.getElementById("root")
|
const root = document.getElementById("root")
|
||||||
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
||||||
const locale = (() => {
|
const locale = (() => {
|
||||||
@@ -62,6 +64,26 @@ const platform: Platform = {
|
|||||||
})
|
})
|
||||||
.catch(() => undefined)
|
.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(
|
render(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const dict = {
|
|||||||
"command.category.theme": "سمة",
|
"command.category.theme": "سمة",
|
||||||
"command.category.language": "لغة",
|
"command.category.language": "لغة",
|
||||||
"command.category.file": "ملف",
|
"command.category.file": "ملف",
|
||||||
|
"command.category.context": "سياق",
|
||||||
"command.category.terminal": "محطة طرفية",
|
"command.category.terminal": "محطة طرفية",
|
||||||
"command.category.model": "نموذج",
|
"command.category.model": "نموذج",
|
||||||
"command.category.mcp": "MCP",
|
"command.category.mcp": "MCP",
|
||||||
@@ -42,7 +43,10 @@ export const dict = {
|
|||||||
"command.session.new": "جلسة جديدة",
|
"command.session.new": "جلسة جديدة",
|
||||||
"command.file.open": "فتح ملف",
|
"command.file.open": "فتح ملف",
|
||||||
"command.file.open.description": "البحث في الملفات والأوامر",
|
"command.file.open.description": "البحث في الملفات والأوامر",
|
||||||
|
"command.context.addSelection": "إضافة التحديد إلى السياق",
|
||||||
|
"command.context.addSelection.description": "إضافة الأسطر المحددة من الملف الحالي",
|
||||||
"command.terminal.toggle": "تبديل المحطة الطرفية",
|
"command.terminal.toggle": "تبديل المحطة الطرفية",
|
||||||
|
"command.fileTree.toggle": "تبديل شجرة الملفات",
|
||||||
"command.review.toggle": "تبديل المراجعة",
|
"command.review.toggle": "تبديل المراجعة",
|
||||||
"command.terminal.new": "محطة طرفية جديدة",
|
"command.terminal.new": "محطة طرفية جديدة",
|
||||||
"command.terminal.new.description": "إنشاء علامة تبويب جديدة للمحطة الطرفية",
|
"command.terminal.new.description": "إنشاء علامة تبويب جديدة للمحطة الطرفية",
|
||||||
@@ -137,6 +141,8 @@ export const dict = {
|
|||||||
"provider.connect.toast.connected.title": "تم توصيل {{provider}}",
|
"provider.connect.toast.connected.title": "تم توصيل {{provider}}",
|
||||||
"provider.connect.toast.connected.description": "نماذج {{provider}} متاحة الآن للاستخدام.",
|
"provider.connect.toast.connected.description": "نماذج {{provider}} متاحة الآن للاستخدام.",
|
||||||
|
|
||||||
|
"provider.disconnect.toast.disconnected.title": "تم فصل {{provider}}",
|
||||||
|
"provider.disconnect.toast.disconnected.description": "لم تعد نماذج {{provider}} متاحة.",
|
||||||
"model.tag.free": "مجاني",
|
"model.tag.free": "مجاني",
|
||||||
"model.tag.latest": "الأحدث",
|
"model.tag.latest": "الأحدث",
|
||||||
"model.provider.anthropic": "Anthropic",
|
"model.provider.anthropic": "Anthropic",
|
||||||
@@ -159,6 +165,8 @@ export const dict = {
|
|||||||
"common.loading": "جارٍ التحميل",
|
"common.loading": "جارٍ التحميل",
|
||||||
"common.loading.ellipsis": "...",
|
"common.loading.ellipsis": "...",
|
||||||
"common.cancel": "إلغاء",
|
"common.cancel": "إلغاء",
|
||||||
|
"common.connect": "اتصال",
|
||||||
|
"common.disconnect": "قطع الاتصال",
|
||||||
"common.submit": "إرسال",
|
"common.submit": "إرسال",
|
||||||
"common.save": "حفظ",
|
"common.save": "حفظ",
|
||||||
"common.saving": "جارٍ الحفظ...",
|
"common.saving": "جارٍ الحفظ...",
|
||||||
@@ -167,6 +175,8 @@ export const dict = {
|
|||||||
|
|
||||||
"prompt.placeholder.shell": "أدخل أمر shell...",
|
"prompt.placeholder.shell": "أدخل أمر shell...",
|
||||||
"prompt.placeholder.normal": 'اسأل أي شيء... "{{example}}"',
|
"prompt.placeholder.normal": 'اسأل أي شيء... "{{example}}"',
|
||||||
|
"prompt.placeholder.summarizeComments": "لخّص التعليقات…",
|
||||||
|
"prompt.placeholder.summarizeComment": "لخّص التعليق…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
"prompt.mode.shell.exit": "esc للخروج",
|
"prompt.mode.shell.exit": "esc للخروج",
|
||||||
|
|
||||||
@@ -270,6 +280,9 @@ export const dict = {
|
|||||||
"dialog.project.edit.color": "لون",
|
"dialog.project.edit.color": "لون",
|
||||||
"dialog.project.edit.color.select": "اختر لون {{color}}",
|
"dialog.project.edit.color.select": "اختر لون {{color}}",
|
||||||
|
|
||||||
|
"dialog.project.edit.worktree.startup": "سكريبت بدء تشغيل مساحة العمل",
|
||||||
|
"dialog.project.edit.worktree.startup.description": "يتم تشغيله بعد إنشاء مساحة عمل جديدة (شجرة عمل).",
|
||||||
|
"dialog.project.edit.worktree.startup.placeholder": "مثال: bun install",
|
||||||
"context.breakdown.title": "تفصيل السياق",
|
"context.breakdown.title": "تفصيل السياق",
|
||||||
"context.breakdown.note": 'تفصيل تقريبي لرموز الإدخال. يشمل "أخرى" تعريفات الأدوات والنفقات العامة.',
|
"context.breakdown.note": 'تفصيل تقريبي لرموز الإدخال. يشمل "أخرى" تعريفات الأدوات والنفقات العامة.',
|
||||||
"context.breakdown.system": "النظام",
|
"context.breakdown.system": "النظام",
|
||||||
@@ -335,6 +348,9 @@ export const dict = {
|
|||||||
|
|
||||||
"toast.file.loadFailed.title": "فشل تحميل الملف",
|
"toast.file.loadFailed.title": "فشل تحميل الملف",
|
||||||
|
|
||||||
|
"toast.file.listFailed.title": "فشل سرد الملفات",
|
||||||
|
"toast.context.noLineSelection.title": "لا يوجد تحديد للأسطر",
|
||||||
|
"toast.context.noLineSelection.description": "حدد نطاق أسطر في تبويب ملف أولاً.",
|
||||||
"toast.session.share.copyFailed.title": "فشل نسخ عنوان URL إلى الحافظة",
|
"toast.session.share.copyFailed.title": "فشل نسخ عنوان URL إلى الحافظة",
|
||||||
"toast.session.share.success.title": "تمت مشاركة الجلسة",
|
"toast.session.share.success.title": "تمت مشاركة الجلسة",
|
||||||
"toast.session.share.success.description": "تم نسخ عنوان URL للمشاركة إلى الحافظة!",
|
"toast.session.share.success.description": "تم نسخ عنوان URL للمشاركة إلى الحافظة!",
|
||||||
@@ -408,8 +424,13 @@ export const dict = {
|
|||||||
"session.tab.context": "سياق",
|
"session.tab.context": "سياق",
|
||||||
"session.panel.reviewAndFiles": "المراجعة والملفات",
|
"session.panel.reviewAndFiles": "المراجعة والملفات",
|
||||||
"session.review.filesChanged": "تم تغيير {{count}} ملفات",
|
"session.review.filesChanged": "تم تغيير {{count}} ملفات",
|
||||||
|
"session.review.change.one": "تغيير",
|
||||||
|
"session.review.change.other": "تغييرات",
|
||||||
"session.review.loadingChanges": "جارٍ تحميل التغييرات...",
|
"session.review.loadingChanges": "جارٍ تحميل التغييرات...",
|
||||||
"session.review.empty": "لا توجد تغييرات في هذه الجلسة بعد",
|
"session.review.empty": "لا توجد تغييرات في هذه الجلسة بعد",
|
||||||
|
"session.review.noChanges": "لا توجد تغييرات",
|
||||||
|
"session.files.selectToOpen": "اختر ملفًا لفتحه",
|
||||||
|
"session.files.all": "كل الملفات",
|
||||||
"session.messages.renderEarlier": "عرض الرسائل السابقة",
|
"session.messages.renderEarlier": "عرض الرسائل السابقة",
|
||||||
"session.messages.loadingEarlier": "جارٍ تحميل الرسائل السابقة...",
|
"session.messages.loadingEarlier": "جارٍ تحميل الرسائل السابقة...",
|
||||||
"session.messages.loadEarlier": "تحميل الرسائل السابقة",
|
"session.messages.loadEarlier": "تحميل الرسائل السابقة",
|
||||||
@@ -483,7 +504,9 @@ export const dict = {
|
|||||||
"sidebar.project.recentSessions": "الجلسات الحديثة",
|
"sidebar.project.recentSessions": "الجلسات الحديثة",
|
||||||
"sidebar.project.viewAllSessions": "عرض جميع الجلسات",
|
"sidebar.project.viewAllSessions": "عرض جميع الجلسات",
|
||||||
|
|
||||||
|
"app.name.desktop": "OpenCode Desktop",
|
||||||
"settings.section.desktop": "سطح المكتب",
|
"settings.section.desktop": "سطح المكتب",
|
||||||
|
"settings.section.server": "الخادم",
|
||||||
"settings.tab.general": "عام",
|
"settings.tab.general": "عام",
|
||||||
"settings.tab.shortcuts": "اختصارات",
|
"settings.tab.shortcuts": "اختصارات",
|
||||||
|
|
||||||
@@ -505,6 +528,7 @@ export const dict = {
|
|||||||
"font.option.hack": "Hack",
|
"font.option.hack": "Hack",
|
||||||
"font.option.inconsolata": "Inconsolata",
|
"font.option.inconsolata": "Inconsolata",
|
||||||
"font.option.intelOneMono": "Intel One Mono",
|
"font.option.intelOneMono": "Intel One Mono",
|
||||||
|
"font.option.iosevka": "Iosevka",
|
||||||
"font.option.jetbrainsMono": "JetBrains Mono",
|
"font.option.jetbrainsMono": "JetBrains Mono",
|
||||||
"font.option.mesloLgs": "Meslo LGS",
|
"font.option.mesloLgs": "Meslo LGS",
|
||||||
"font.option.robotoMono": "Roboto Mono",
|
"font.option.robotoMono": "Roboto Mono",
|
||||||
@@ -590,6 +614,13 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.providers.title": "الموفرون",
|
"settings.providers.title": "الموفرون",
|
||||||
"settings.providers.description": "ستكون إعدادات الموفر قابلة للتكوين هنا.",
|
"settings.providers.description": "ستكون إعدادات الموفر قابلة للتكوين هنا.",
|
||||||
|
"settings.providers.section.connected": "الموفرون المتصلون",
|
||||||
|
"settings.providers.connected.empty": "لا يوجد موفرون متصلون",
|
||||||
|
"settings.providers.section.popular": "الموفرون الشائعون",
|
||||||
|
"settings.providers.tag.environment": "البيئة",
|
||||||
|
"settings.providers.tag.config": "التكوين",
|
||||||
|
"settings.providers.tag.custom": "مخصص",
|
||||||
|
"settings.providers.tag.other": "أخرى",
|
||||||
"settings.models.title": "النماذج",
|
"settings.models.title": "النماذج",
|
||||||
"settings.models.description": "ستكون إعدادات النموذج قابلة للتكوين هنا.",
|
"settings.models.description": "ستكون إعدادات النموذج قابلة للتكوين هنا.",
|
||||||
"settings.agents.title": "الوكلاء",
|
"settings.agents.title": "الوكلاء",
|
||||||
@@ -657,6 +688,7 @@ export const dict = {
|
|||||||
"workspace.reset.failed.title": "فشل إعادة تعيين مساحة العمل",
|
"workspace.reset.failed.title": "فشل إعادة تعيين مساحة العمل",
|
||||||
"workspace.reset.success.title": "تمت إعادة تعيين مساحة العمل",
|
"workspace.reset.success.title": "تمت إعادة تعيين مساحة العمل",
|
||||||
"workspace.reset.success.description": "مساحة العمل تطابق الآن الفرع الافتراضي.",
|
"workspace.reset.success.description": "مساحة العمل تطابق الآن الفرع الافتراضي.",
|
||||||
|
"workspace.error.stillPreparing": "مساحة العمل لا تزال قيد الإعداد",
|
||||||
"workspace.status.checking": "التحقق من التغييرات غير المدمجة...",
|
"workspace.status.checking": "التحقق من التغييرات غير المدمجة...",
|
||||||
"workspace.status.error": "تعذر التحقق من حالة git.",
|
"workspace.status.error": "تعذر التحقق من حالة git.",
|
||||||
"workspace.status.clean": "لم يتم اكتشاف تغييرات غير مدمجة.",
|
"workspace.status.clean": "لم يتم اكتشاف تغييرات غير مدمجة.",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const dict = {
|
|||||||
"command.category.theme": "Tema",
|
"command.category.theme": "Tema",
|
||||||
"command.category.language": "Idioma",
|
"command.category.language": "Idioma",
|
||||||
"command.category.file": "Arquivo",
|
"command.category.file": "Arquivo",
|
||||||
|
"command.category.context": "Contexto",
|
||||||
"command.category.terminal": "Terminal",
|
"command.category.terminal": "Terminal",
|
||||||
"command.category.model": "Modelo",
|
"command.category.model": "Modelo",
|
||||||
"command.category.mcp": "MCP",
|
"command.category.mcp": "MCP",
|
||||||
@@ -42,7 +43,10 @@ export const dict = {
|
|||||||
"command.session.new": "Nova sessão",
|
"command.session.new": "Nova sessão",
|
||||||
"command.file.open": "Abrir arquivo",
|
"command.file.open": "Abrir arquivo",
|
||||||
"command.file.open.description": "Buscar arquivos e comandos",
|
"command.file.open.description": "Buscar arquivos e comandos",
|
||||||
|
"command.context.addSelection": "Adicionar seleção ao contexto",
|
||||||
|
"command.context.addSelection.description": "Adicionar as linhas selecionadas do arquivo atual",
|
||||||
"command.terminal.toggle": "Alternar terminal",
|
"command.terminal.toggle": "Alternar terminal",
|
||||||
|
"command.fileTree.toggle": "Alternar árvore de arquivos",
|
||||||
"command.review.toggle": "Alternar revisão",
|
"command.review.toggle": "Alternar revisão",
|
||||||
"command.terminal.new": "Novo terminal",
|
"command.terminal.new": "Novo terminal",
|
||||||
"command.terminal.new.description": "Criar uma nova aba de terminal",
|
"command.terminal.new.description": "Criar uma nova aba de terminal",
|
||||||
@@ -137,6 +141,8 @@ export const dict = {
|
|||||||
"provider.connect.toast.connected.title": "{{provider}} conectado",
|
"provider.connect.toast.connected.title": "{{provider}} conectado",
|
||||||
"provider.connect.toast.connected.description": "Modelos do {{provider}} agora estão disponíveis para uso.",
|
"provider.connect.toast.connected.description": "Modelos do {{provider}} agora estão disponíveis para uso.",
|
||||||
|
|
||||||
|
"provider.disconnect.toast.disconnected.title": "{{provider}} desconectado",
|
||||||
|
"provider.disconnect.toast.disconnected.description": "Os modelos de {{provider}} não estão mais disponíveis.",
|
||||||
"model.tag.free": "Grátis",
|
"model.tag.free": "Grátis",
|
||||||
"model.tag.latest": "Mais recente",
|
"model.tag.latest": "Mais recente",
|
||||||
"model.provider.anthropic": "Anthropic",
|
"model.provider.anthropic": "Anthropic",
|
||||||
@@ -159,6 +165,8 @@ export const dict = {
|
|||||||
"common.loading": "Carregando",
|
"common.loading": "Carregando",
|
||||||
"common.loading.ellipsis": "...",
|
"common.loading.ellipsis": "...",
|
||||||
"common.cancel": "Cancelar",
|
"common.cancel": "Cancelar",
|
||||||
|
"common.connect": "Conectar",
|
||||||
|
"common.disconnect": "Desconectar",
|
||||||
"common.submit": "Enviar",
|
"common.submit": "Enviar",
|
||||||
"common.save": "Salvar",
|
"common.save": "Salvar",
|
||||||
"common.saving": "Salvando...",
|
"common.saving": "Salvando...",
|
||||||
@@ -167,6 +175,8 @@ export const dict = {
|
|||||||
|
|
||||||
"prompt.placeholder.shell": "Digite comando do shell...",
|
"prompt.placeholder.shell": "Digite comando do shell...",
|
||||||
"prompt.placeholder.normal": 'Pergunte qualquer coisa... "{{example}}"',
|
"prompt.placeholder.normal": 'Pergunte qualquer coisa... "{{example}}"',
|
||||||
|
"prompt.placeholder.summarizeComments": "Resumir comentários…",
|
||||||
|
"prompt.placeholder.summarizeComment": "Resumir comentário…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
"prompt.mode.shell.exit": "esc para sair",
|
"prompt.mode.shell.exit": "esc para sair",
|
||||||
|
|
||||||
@@ -223,6 +233,8 @@ export const dict = {
|
|||||||
"dialog.mcp.description": "{{enabled}} de {{total}} habilitados",
|
"dialog.mcp.description": "{{enabled}} de {{total}} habilitados",
|
||||||
"dialog.mcp.empty": "Nenhum MCP configurado",
|
"dialog.mcp.empty": "Nenhum MCP configurado",
|
||||||
|
|
||||||
|
"dialog.lsp.empty": "LSPs detectados automaticamente pelos tipos de arquivo",
|
||||||
|
"dialog.plugins.empty": "Plugins configurados em opencode.json",
|
||||||
"mcp.status.connected": "conectado",
|
"mcp.status.connected": "conectado",
|
||||||
"mcp.status.failed": "falhou",
|
"mcp.status.failed": "falhou",
|
||||||
"mcp.status.needs_auth": "precisa de autenticação",
|
"mcp.status.needs_auth": "precisa de autenticação",
|
||||||
@@ -251,6 +263,12 @@ export const dict = {
|
|||||||
"dialog.server.default.clear": "Limpar",
|
"dialog.server.default.clear": "Limpar",
|
||||||
"dialog.server.action.remove": "Remover servidor",
|
"dialog.server.action.remove": "Remover servidor",
|
||||||
|
|
||||||
|
"dialog.server.menu.edit": "Editar",
|
||||||
|
"dialog.server.menu.default": "Definir como padrão",
|
||||||
|
"dialog.server.menu.defaultRemove": "Remover padrão",
|
||||||
|
"dialog.server.menu.delete": "Excluir",
|
||||||
|
"dialog.server.current": "Servidor atual",
|
||||||
|
"dialog.server.status.default": "Padrão",
|
||||||
"dialog.project.edit.title": "Editar projeto",
|
"dialog.project.edit.title": "Editar projeto",
|
||||||
"dialog.project.edit.name": "Nome",
|
"dialog.project.edit.name": "Nome",
|
||||||
"dialog.project.edit.icon": "Ícone",
|
"dialog.project.edit.icon": "Ícone",
|
||||||
@@ -329,6 +347,9 @@ export const dict = {
|
|||||||
|
|
||||||
"toast.file.loadFailed.title": "Falha ao carregar arquivo",
|
"toast.file.loadFailed.title": "Falha ao carregar arquivo",
|
||||||
|
|
||||||
|
"toast.file.listFailed.title": "Falha ao listar arquivos",
|
||||||
|
"toast.context.noLineSelection.title": "Nenhuma seleção de linhas",
|
||||||
|
"toast.context.noLineSelection.description": "Selecione primeiro um intervalo de linhas em uma aba de arquivo.",
|
||||||
"toast.session.share.copyFailed.title": "Falha ao copiar URL para a área de transferência",
|
"toast.session.share.copyFailed.title": "Falha ao copiar URL para a área de transferência",
|
||||||
"toast.session.share.success.title": "Sessão compartilhada",
|
"toast.session.share.success.title": "Sessão compartilhada",
|
||||||
"toast.session.share.success.description": "URL compartilhada copiada para a área de transferência!",
|
"toast.session.share.success.description": "URL compartilhada copiada para a área de transferência!",
|
||||||
@@ -404,8 +425,13 @@ export const dict = {
|
|||||||
"session.tab.context": "Contexto",
|
"session.tab.context": "Contexto",
|
||||||
"session.panel.reviewAndFiles": "Revisão e arquivos",
|
"session.panel.reviewAndFiles": "Revisão e arquivos",
|
||||||
"session.review.filesChanged": "{{count}} Arquivos Alterados",
|
"session.review.filesChanged": "{{count}} Arquivos Alterados",
|
||||||
|
"session.review.change.one": "Alteração",
|
||||||
|
"session.review.change.other": "Alterações",
|
||||||
"session.review.loadingChanges": "Carregando alterações...",
|
"session.review.loadingChanges": "Carregando alterações...",
|
||||||
"session.review.empty": "Nenhuma alteração nesta sessão ainda",
|
"session.review.empty": "Nenhuma alteração nesta sessão ainda",
|
||||||
|
"session.review.noChanges": "Sem alterações",
|
||||||
|
"session.files.selectToOpen": "Selecione um arquivo para abrir",
|
||||||
|
"session.files.all": "Todos os arquivos",
|
||||||
"session.messages.renderEarlier": "Renderizar mensagens anteriores",
|
"session.messages.renderEarlier": "Renderizar mensagens anteriores",
|
||||||
"session.messages.loadingEarlier": "Carregando mensagens anteriores...",
|
"session.messages.loadingEarlier": "Carregando mensagens anteriores...",
|
||||||
"session.messages.loadEarlier": "Carregar mensagens anteriores",
|
"session.messages.loadEarlier": "Carregar mensagens anteriores",
|
||||||
@@ -482,7 +508,9 @@ export const dict = {
|
|||||||
"sidebar.project.recentSessions": "Sessões recentes",
|
"sidebar.project.recentSessions": "Sessões recentes",
|
||||||
"sidebar.project.viewAllSessions": "Ver todas as sessões",
|
"sidebar.project.viewAllSessions": "Ver todas as sessões",
|
||||||
|
|
||||||
|
"app.name.desktop": "OpenCode Desktop",
|
||||||
"settings.section.desktop": "Desktop",
|
"settings.section.desktop": "Desktop",
|
||||||
|
"settings.section.server": "Servidor",
|
||||||
"settings.tab.general": "Geral",
|
"settings.tab.general": "Geral",
|
||||||
"settings.tab.shortcuts": "Atalhos",
|
"settings.tab.shortcuts": "Atalhos",
|
||||||
|
|
||||||
@@ -504,6 +532,7 @@ export const dict = {
|
|||||||
"font.option.hack": "Hack",
|
"font.option.hack": "Hack",
|
||||||
"font.option.inconsolata": "Inconsolata",
|
"font.option.inconsolata": "Inconsolata",
|
||||||
"font.option.intelOneMono": "Intel One Mono",
|
"font.option.intelOneMono": "Intel One Mono",
|
||||||
|
"font.option.iosevka": "Iosevka",
|
||||||
"font.option.jetbrainsMono": "JetBrains Mono",
|
"font.option.jetbrainsMono": "JetBrains Mono",
|
||||||
"font.option.mesloLgs": "Meslo LGS",
|
"font.option.mesloLgs": "Meslo LGS",
|
||||||
"font.option.robotoMono": "Roboto Mono",
|
"font.option.robotoMono": "Roboto Mono",
|
||||||
@@ -591,6 +620,13 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.providers.title": "Provedores",
|
"settings.providers.title": "Provedores",
|
||||||
"settings.providers.description": "Configurações de provedores estarão disponíveis aqui.",
|
"settings.providers.description": "Configurações de provedores estarão disponíveis aqui.",
|
||||||
|
"settings.providers.section.connected": "Provedores conectados",
|
||||||
|
"settings.providers.connected.empty": "Nenhum provedor conectado",
|
||||||
|
"settings.providers.section.popular": "Provedores populares",
|
||||||
|
"settings.providers.tag.environment": "Ambiente",
|
||||||
|
"settings.providers.tag.config": "Configuração",
|
||||||
|
"settings.providers.tag.custom": "Personalizado",
|
||||||
|
"settings.providers.tag.other": "Outro",
|
||||||
"settings.models.title": "Modelos",
|
"settings.models.title": "Modelos",
|
||||||
"settings.models.description": "Configurações de modelos estarão disponíveis aqui.",
|
"settings.models.description": "Configurações de modelos estarão disponíveis aqui.",
|
||||||
"settings.agents.title": "Agentes",
|
"settings.agents.title": "Agentes",
|
||||||
@@ -658,6 +694,7 @@ export const dict = {
|
|||||||
"workspace.reset.failed.title": "Falha ao redefinir espaço de trabalho",
|
"workspace.reset.failed.title": "Falha ao redefinir espaço de trabalho",
|
||||||
"workspace.reset.success.title": "Espaço de trabalho redefinido",
|
"workspace.reset.success.title": "Espaço de trabalho redefinido",
|
||||||
"workspace.reset.success.description": "Espaço de trabalho agora corresponde ao branch padrão.",
|
"workspace.reset.success.description": "Espaço de trabalho agora corresponde ao branch padrão.",
|
||||||
|
"workspace.error.stillPreparing": "O espaço de trabalho ainda está sendo preparado",
|
||||||
"workspace.status.checking": "Verificando alterações não mescladas...",
|
"workspace.status.checking": "Verificando alterações não mescladas...",
|
||||||
"workspace.status.error": "Não foi possível verificar o status do git.",
|
"workspace.status.error": "Não foi possível verificar o status do git.",
|
||||||
"workspace.status.clean": "Nenhuma alteração não mesclada detectada.",
|
"workspace.status.clean": "Nenhuma alteração não mesclada detectada.",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const dict = {
|
|||||||
"command.category.theme": "Tema",
|
"command.category.theme": "Tema",
|
||||||
"command.category.language": "Sprog",
|
"command.category.language": "Sprog",
|
||||||
"command.category.file": "Fil",
|
"command.category.file": "Fil",
|
||||||
|
"command.category.context": "Kontekst",
|
||||||
"command.category.terminal": "Terminal",
|
"command.category.terminal": "Terminal",
|
||||||
"command.category.model": "Model",
|
"command.category.model": "Model",
|
||||||
"command.category.mcp": "MCP",
|
"command.category.mcp": "MCP",
|
||||||
@@ -15,6 +16,7 @@ export const dict = {
|
|||||||
"command.category.permissions": "Tilladelser",
|
"command.category.permissions": "Tilladelser",
|
||||||
"command.category.workspace": "Arbejdsområde",
|
"command.category.workspace": "Arbejdsområde",
|
||||||
|
|
||||||
|
"command.category.settings": "Indstillinger",
|
||||||
"theme.scheme.system": "System",
|
"theme.scheme.system": "System",
|
||||||
"theme.scheme.light": "Lys",
|
"theme.scheme.light": "Lys",
|
||||||
"theme.scheme.dark": "Mørk",
|
"theme.scheme.dark": "Mørk",
|
||||||
@@ -23,6 +25,7 @@ export const dict = {
|
|||||||
"command.project.open": "Åbn projekt",
|
"command.project.open": "Åbn projekt",
|
||||||
"command.provider.connect": "Tilslut udbyder",
|
"command.provider.connect": "Tilslut udbyder",
|
||||||
"command.server.switch": "Skift server",
|
"command.server.switch": "Skift server",
|
||||||
|
"command.settings.open": "Åbn indstillinger",
|
||||||
"command.session.previous": "Forrige session",
|
"command.session.previous": "Forrige session",
|
||||||
"command.session.next": "Næste session",
|
"command.session.next": "Næste session",
|
||||||
"command.session.archive": "Arkivér session",
|
"command.session.archive": "Arkivér session",
|
||||||
@@ -40,7 +43,10 @@ export const dict = {
|
|||||||
"command.session.new": "Ny session",
|
"command.session.new": "Ny session",
|
||||||
"command.file.open": "Åbn fil",
|
"command.file.open": "Åbn fil",
|
||||||
"command.file.open.description": "Søg i filer og kommandoer",
|
"command.file.open.description": "Søg i filer og kommandoer",
|
||||||
|
"command.context.addSelection": "Tilføj markering til kontekst",
|
||||||
|
"command.context.addSelection.description": "Tilføj markerede linjer fra den aktuelle fil",
|
||||||
"command.terminal.toggle": "Skift terminal",
|
"command.terminal.toggle": "Skift terminal",
|
||||||
|
"command.fileTree.toggle": "Skift filtræ",
|
||||||
"command.review.toggle": "Skift gennemgang",
|
"command.review.toggle": "Skift gennemgang",
|
||||||
"command.terminal.new": "Ny terminal",
|
"command.terminal.new": "Ny terminal",
|
||||||
"command.terminal.new.description": "Opret en ny terminalfane",
|
"command.terminal.new.description": "Opret en ny terminalfane",
|
||||||
@@ -117,6 +123,7 @@ export const dict = {
|
|||||||
"provider.connect.opencodeZen.line2":
|
"provider.connect.opencodeZen.line2":
|
||||||
"Med en enkelt API-nøgle får du adgang til modeller som Claude, GPT, Gemini, GLM og flere.",
|
"Med en enkelt API-nøgle får du adgang til modeller som Claude, GPT, Gemini, GLM og flere.",
|
||||||
"provider.connect.opencodeZen.visit.prefix": "Besøg ",
|
"provider.connect.opencodeZen.visit.prefix": "Besøg ",
|
||||||
|
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
|
||||||
"provider.connect.opencodeZen.visit.suffix": " for at hente din API-nøgle.",
|
"provider.connect.opencodeZen.visit.suffix": " for at hente din API-nøgle.",
|
||||||
"provider.connect.oauth.code.visit.prefix": "Besøg ",
|
"provider.connect.oauth.code.visit.prefix": "Besøg ",
|
||||||
"provider.connect.oauth.code.visit.link": "dette link",
|
"provider.connect.oauth.code.visit.link": "dette link",
|
||||||
@@ -134,13 +141,32 @@ export const dict = {
|
|||||||
"provider.connect.toast.connected.title": "{{provider}} forbundet",
|
"provider.connect.toast.connected.title": "{{provider}} forbundet",
|
||||||
"provider.connect.toast.connected.description": "{{provider}} modeller er nu tilgængelige.",
|
"provider.connect.toast.connected.description": "{{provider}} modeller er nu tilgængelige.",
|
||||||
|
|
||||||
|
"provider.disconnect.toast.disconnected.title": "{{provider}} frakoblet",
|
||||||
|
"provider.disconnect.toast.disconnected.description": "Modeller fra {{provider}} er ikke længere tilgængelige.",
|
||||||
"model.tag.free": "Gratis",
|
"model.tag.free": "Gratis",
|
||||||
"model.tag.latest": "Nyeste",
|
"model.tag.latest": "Nyeste",
|
||||||
|
|
||||||
|
"model.provider.anthropic": "Anthropic",
|
||||||
|
"model.provider.openai": "OpenAI",
|
||||||
|
"model.provider.google": "Google",
|
||||||
|
"model.provider.xai": "xAI",
|
||||||
|
"model.provider.meta": "Meta",
|
||||||
|
"model.input.text": "tekst",
|
||||||
|
"model.input.image": "billede",
|
||||||
|
"model.input.audio": "lyd",
|
||||||
|
"model.input.video": "video",
|
||||||
|
"model.input.pdf": "pdf",
|
||||||
|
"model.tooltip.allows": "Tillader: {{inputs}}",
|
||||||
|
"model.tooltip.reasoning.allowed": "Tillader tænkning",
|
||||||
|
"model.tooltip.reasoning.none": "Ingen tænkning",
|
||||||
|
"model.tooltip.context": "Kontekstgrænse {{limit}}",
|
||||||
"common.search.placeholder": "Søg",
|
"common.search.placeholder": "Søg",
|
||||||
"common.goBack": "Gå tilbage",
|
"common.goBack": "Gå tilbage",
|
||||||
"common.loading": "Indlæser",
|
"common.loading": "Indlæser",
|
||||||
|
"common.loading.ellipsis": "...",
|
||||||
"common.cancel": "Annuller",
|
"common.cancel": "Annuller",
|
||||||
|
"common.connect": "Forbind",
|
||||||
|
"common.disconnect": "Frakobl",
|
||||||
"common.submit": "Indsend",
|
"common.submit": "Indsend",
|
||||||
"common.save": "Gem",
|
"common.save": "Gem",
|
||||||
"common.saving": "Gemmer...",
|
"common.saving": "Gemmer...",
|
||||||
@@ -149,6 +175,8 @@ export const dict = {
|
|||||||
|
|
||||||
"prompt.placeholder.shell": "Indtast shell-kommando...",
|
"prompt.placeholder.shell": "Indtast shell-kommando...",
|
||||||
"prompt.placeholder.normal": 'Spørg om hvad som helst... "{{example}}"',
|
"prompt.placeholder.normal": 'Spørg om hvad som helst... "{{example}}"',
|
||||||
|
"prompt.placeholder.summarizeComments": "Opsummér kommentarer…",
|
||||||
|
"prompt.placeholder.summarizeComment": "Opsummér kommentar…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
"prompt.mode.shell.exit": "esc for at afslutte",
|
"prompt.mode.shell.exit": "esc for at afslutte",
|
||||||
|
|
||||||
@@ -252,6 +280,9 @@ export const dict = {
|
|||||||
"dialog.project.edit.color": "Farve",
|
"dialog.project.edit.color": "Farve",
|
||||||
"dialog.project.edit.color.select": "Vælg farven {{color}}",
|
"dialog.project.edit.color.select": "Vælg farven {{color}}",
|
||||||
|
|
||||||
|
"dialog.project.edit.worktree.startup": "Opstartsscript for arbejdsområde",
|
||||||
|
"dialog.project.edit.worktree.startup.description": "Køres efter oprettelse af et nyt arbejdsområde (worktree).",
|
||||||
|
"dialog.project.edit.worktree.startup.placeholder": "f.eks. bun install",
|
||||||
"context.breakdown.title": "Kontekstfordeling",
|
"context.breakdown.title": "Kontekstfordeling",
|
||||||
"context.breakdown.note":
|
"context.breakdown.note":
|
||||||
'Omtrentlig fordeling af input-tokens. "Andre" inkluderer værktøjsdefinitioner og overhead.',
|
'Omtrentlig fordeling af input-tokens. "Andre" inkluderer værktøjsdefinitioner og overhead.',
|
||||||
@@ -318,6 +349,9 @@ export const dict = {
|
|||||||
|
|
||||||
"toast.file.loadFailed.title": "Kunne ikke indlæse fil",
|
"toast.file.loadFailed.title": "Kunne ikke indlæse fil",
|
||||||
|
|
||||||
|
"toast.file.listFailed.title": "Kunne ikke liste filer",
|
||||||
|
"toast.context.noLineSelection.title": "Ingen linjevalg",
|
||||||
|
"toast.context.noLineSelection.description": "Vælg først et linjeinterval i en filfane.",
|
||||||
"toast.session.share.copyFailed.title": "Kunne ikke kopiere URL til udklipsholder",
|
"toast.session.share.copyFailed.title": "Kunne ikke kopiere URL til udklipsholder",
|
||||||
"toast.session.share.success.title": "Session delt",
|
"toast.session.share.success.title": "Session delt",
|
||||||
"toast.session.share.success.description": "Delings-URL kopieret til udklipsholder!",
|
"toast.session.share.success.description": "Delings-URL kopieret til udklipsholder!",
|
||||||
@@ -392,13 +426,19 @@ export const dict = {
|
|||||||
"session.tab.context": "Kontekst",
|
"session.tab.context": "Kontekst",
|
||||||
"session.panel.reviewAndFiles": "Gennemgang og filer",
|
"session.panel.reviewAndFiles": "Gennemgang og filer",
|
||||||
"session.review.filesChanged": "{{count}} Filer ændret",
|
"session.review.filesChanged": "{{count}} Filer ændret",
|
||||||
|
"session.review.change.one": "Ændring",
|
||||||
|
"session.review.change.other": "Ændringer",
|
||||||
"session.review.loadingChanges": "Indlæser ændringer...",
|
"session.review.loadingChanges": "Indlæser ændringer...",
|
||||||
"session.review.empty": "Ingen ændringer i denne session endnu",
|
"session.review.empty": "Ingen ændringer i denne session endnu",
|
||||||
|
"session.review.noChanges": "Ingen ændringer",
|
||||||
|
"session.files.selectToOpen": "Vælg en fil at åbne",
|
||||||
|
"session.files.all": "Alle filer",
|
||||||
"session.messages.renderEarlier": "Vis tidligere beskeder",
|
"session.messages.renderEarlier": "Vis tidligere beskeder",
|
||||||
"session.messages.loadingEarlier": "Indlæser tidligere beskeder...",
|
"session.messages.loadingEarlier": "Indlæser tidligere beskeder...",
|
||||||
"session.messages.loadEarlier": "Indlæs tidligere beskeder",
|
"session.messages.loadEarlier": "Indlæs tidligere beskeder",
|
||||||
"session.messages.loading": "Indlæser beskeder...",
|
"session.messages.loading": "Indlæser beskeder...",
|
||||||
|
|
||||||
|
"session.messages.jumpToLatest": "Gå til seneste",
|
||||||
"session.context.addToContext": "Tilføj {{selection}} til kontekst",
|
"session.context.addToContext": "Tilføj {{selection}} til kontekst",
|
||||||
|
|
||||||
"session.new.worktree.main": "Hovedgren",
|
"session.new.worktree.main": "Hovedgren",
|
||||||
@@ -440,6 +480,8 @@ export const dict = {
|
|||||||
"terminal.title.numbered": "Terminal {{number}}",
|
"terminal.title.numbered": "Terminal {{number}}",
|
||||||
"terminal.close": "Luk terminal",
|
"terminal.close": "Luk terminal",
|
||||||
|
|
||||||
|
"terminal.connectionLost.title": "Forbindelse mistet",
|
||||||
|
"terminal.connectionLost.description": "Terminalforbindelsen blev afbrudt. Dette kan ske, når serveren genstarter.",
|
||||||
"common.closeTab": "Luk fane",
|
"common.closeTab": "Luk fane",
|
||||||
"common.dismiss": "Afvis",
|
"common.dismiss": "Afvis",
|
||||||
"common.requestFailed": "Forespørgsel mislykkedes",
|
"common.requestFailed": "Forespørgsel mislykkedes",
|
||||||
@@ -453,6 +495,8 @@ export const dict = {
|
|||||||
"common.edit": "Rediger",
|
"common.edit": "Rediger",
|
||||||
"common.loadMore": "Indlæs flere",
|
"common.loadMore": "Indlæs flere",
|
||||||
|
|
||||||
|
"common.key.esc": "ESC",
|
||||||
|
"sidebar.menu.toggle": "Skift menu",
|
||||||
"sidebar.nav.projectsAndSessions": "Projekter og sessioner",
|
"sidebar.nav.projectsAndSessions": "Projekter og sessioner",
|
||||||
"sidebar.settings": "Indstillinger",
|
"sidebar.settings": "Indstillinger",
|
||||||
"sidebar.help": "Hjælp",
|
"sidebar.help": "Hjælp",
|
||||||
@@ -464,7 +508,9 @@ export const dict = {
|
|||||||
"sidebar.project.recentSessions": "Seneste sessioner",
|
"sidebar.project.recentSessions": "Seneste sessioner",
|
||||||
"sidebar.project.viewAllSessions": "Vis alle sessioner",
|
"sidebar.project.viewAllSessions": "Vis alle sessioner",
|
||||||
|
|
||||||
|
"app.name.desktop": "OpenCode Desktop",
|
||||||
"settings.section.desktop": "Desktop",
|
"settings.section.desktop": "Desktop",
|
||||||
|
"settings.section.server": "Server",
|
||||||
"settings.tab.general": "Generelt",
|
"settings.tab.general": "Generelt",
|
||||||
"settings.tab.shortcuts": "Genveje",
|
"settings.tab.shortcuts": "Genveje",
|
||||||
|
|
||||||
@@ -481,6 +527,63 @@ export const dict = {
|
|||||||
"settings.general.row.font.title": "Skrifttype",
|
"settings.general.row.font.title": "Skrifttype",
|
||||||
"settings.general.row.font.description": "Tilpas mono-skrifttypen brugt i kodeblokke",
|
"settings.general.row.font.description": "Tilpas mono-skrifttypen brugt i kodeblokke",
|
||||||
|
|
||||||
|
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||||
|
"font.option.cascadiaCode": "Cascadia Code",
|
||||||
|
"font.option.firaCode": "Fira Code",
|
||||||
|
"font.option.hack": "Hack",
|
||||||
|
"font.option.inconsolata": "Inconsolata",
|
||||||
|
"font.option.intelOneMono": "Intel One Mono",
|
||||||
|
"font.option.iosevka": "Iosevka",
|
||||||
|
"font.option.jetbrainsMono": "JetBrains Mono",
|
||||||
|
"font.option.mesloLgs": "Meslo LGS",
|
||||||
|
"font.option.robotoMono": "Roboto Mono",
|
||||||
|
"font.option.sourceCodePro": "Source Code Pro",
|
||||||
|
"font.option.ubuntuMono": "Ubuntu Mono",
|
||||||
|
"sound.option.alert01": "Alarm 01",
|
||||||
|
"sound.option.alert02": "Alarm 02",
|
||||||
|
"sound.option.alert03": "Alarm 03",
|
||||||
|
"sound.option.alert04": "Alarm 04",
|
||||||
|
"sound.option.alert05": "Alarm 05",
|
||||||
|
"sound.option.alert06": "Alarm 06",
|
||||||
|
"sound.option.alert07": "Alarm 07",
|
||||||
|
"sound.option.alert08": "Alarm 08",
|
||||||
|
"sound.option.alert09": "Alarm 09",
|
||||||
|
"sound.option.alert10": "Alarm 10",
|
||||||
|
"sound.option.bipbop01": "Bip-bop 01",
|
||||||
|
"sound.option.bipbop02": "Bip-bop 02",
|
||||||
|
"sound.option.bipbop03": "Bip-bop 03",
|
||||||
|
"sound.option.bipbop04": "Bip-bop 04",
|
||||||
|
"sound.option.bipbop05": "Bip-bop 05",
|
||||||
|
"sound.option.bipbop06": "Bip-bop 06",
|
||||||
|
"sound.option.bipbop07": "Bip-bop 07",
|
||||||
|
"sound.option.bipbop08": "Bip-bop 08",
|
||||||
|
"sound.option.bipbop09": "Bip-bop 09",
|
||||||
|
"sound.option.bipbop10": "Bip-bop 10",
|
||||||
|
"sound.option.staplebops01": "Staplebops 01",
|
||||||
|
"sound.option.staplebops02": "Staplebops 02",
|
||||||
|
"sound.option.staplebops03": "Staplebops 03",
|
||||||
|
"sound.option.staplebops04": "Staplebops 04",
|
||||||
|
"sound.option.staplebops05": "Staplebops 05",
|
||||||
|
"sound.option.staplebops06": "Staplebops 06",
|
||||||
|
"sound.option.staplebops07": "Staplebops 07",
|
||||||
|
"sound.option.nope01": "Nej 01",
|
||||||
|
"sound.option.nope02": "Nej 02",
|
||||||
|
"sound.option.nope03": "Nej 03",
|
||||||
|
"sound.option.nope04": "Nej 04",
|
||||||
|
"sound.option.nope05": "Nej 05",
|
||||||
|
"sound.option.nope06": "Nej 06",
|
||||||
|
"sound.option.nope07": "Nej 07",
|
||||||
|
"sound.option.nope08": "Nej 08",
|
||||||
|
"sound.option.nope09": "Nej 09",
|
||||||
|
"sound.option.nope10": "Nej 10",
|
||||||
|
"sound.option.nope11": "Nej 11",
|
||||||
|
"sound.option.nope12": "Nej 12",
|
||||||
|
"sound.option.yup01": "Ja 01",
|
||||||
|
"sound.option.yup02": "Ja 02",
|
||||||
|
"sound.option.yup03": "Ja 03",
|
||||||
|
"sound.option.yup04": "Ja 04",
|
||||||
|
"sound.option.yup05": "Ja 05",
|
||||||
|
"sound.option.yup06": "Ja 06",
|
||||||
"settings.general.notifications.agent.title": "Agent",
|
"settings.general.notifications.agent.title": "Agent",
|
||||||
"settings.general.notifications.agent.description":
|
"settings.general.notifications.agent.description":
|
||||||
"Vis systemmeddelelse når agenten er færdig eller kræver opmærksomhed",
|
"Vis systemmeddelelse når agenten er færdig eller kræver opmærksomhed",
|
||||||
@@ -516,6 +619,13 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.providers.title": "Udbydere",
|
"settings.providers.title": "Udbydere",
|
||||||
"settings.providers.description": "Udbyderindstillinger vil kunne konfigureres her.",
|
"settings.providers.description": "Udbyderindstillinger vil kunne konfigureres her.",
|
||||||
|
"settings.providers.section.connected": "Forbundne udbydere",
|
||||||
|
"settings.providers.connected.empty": "Ingen forbundne udbydere",
|
||||||
|
"settings.providers.section.popular": "Populære udbydere",
|
||||||
|
"settings.providers.tag.environment": "Miljø",
|
||||||
|
"settings.providers.tag.config": "Konfiguration",
|
||||||
|
"settings.providers.tag.custom": "Brugerdefineret",
|
||||||
|
"settings.providers.tag.other": "Andet",
|
||||||
"settings.models.title": "Modeller",
|
"settings.models.title": "Modeller",
|
||||||
"settings.models.description": "Modelindstillinger vil kunne konfigureres her.",
|
"settings.models.description": "Modelindstillinger vil kunne konfigureres her.",
|
||||||
"settings.agents.title": "Agenter",
|
"settings.agents.title": "Agenter",
|
||||||
@@ -583,6 +693,7 @@ export const dict = {
|
|||||||
"workspace.reset.failed.title": "Kunne ikke nulstille arbejdsområde",
|
"workspace.reset.failed.title": "Kunne ikke nulstille arbejdsområde",
|
||||||
"workspace.reset.success.title": "Arbejdsområde nulstillet",
|
"workspace.reset.success.title": "Arbejdsområde nulstillet",
|
||||||
"workspace.reset.success.description": "Arbejdsområdet matcher nu hovedgrenen.",
|
"workspace.reset.success.description": "Arbejdsområdet matcher nu hovedgrenen.",
|
||||||
|
"workspace.error.stillPreparing": "Arbejdsområdet er stadig ved at blive klargjort",
|
||||||
"workspace.status.checking": "Tjekker for uflettede ændringer...",
|
"workspace.status.checking": "Tjekker for uflettede ændringer...",
|
||||||
"workspace.status.error": "Kunne ikke bekræfte git-status.",
|
"workspace.status.error": "Kunne ikke bekræfte git-status.",
|
||||||
"workspace.status.clean": "Ingen uflettede ændringer fundet.",
|
"workspace.status.clean": "Ingen uflettede ændringer fundet.",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const dict = {
|
|||||||
"command.category.theme": "Thema",
|
"command.category.theme": "Thema",
|
||||||
"command.category.language": "Sprache",
|
"command.category.language": "Sprache",
|
||||||
"command.category.file": "Datei",
|
"command.category.file": "Datei",
|
||||||
|
"command.category.context": "Kontext",
|
||||||
"command.category.terminal": "Terminal",
|
"command.category.terminal": "Terminal",
|
||||||
"command.category.model": "Modell",
|
"command.category.model": "Modell",
|
||||||
"command.category.mcp": "MCP",
|
"command.category.mcp": "MCP",
|
||||||
@@ -19,6 +20,7 @@ export const dict = {
|
|||||||
"command.category.permissions": "Berechtigungen",
|
"command.category.permissions": "Berechtigungen",
|
||||||
"command.category.workspace": "Arbeitsbereich",
|
"command.category.workspace": "Arbeitsbereich",
|
||||||
|
|
||||||
|
"command.category.settings": "Einstellungen",
|
||||||
"theme.scheme.system": "System",
|
"theme.scheme.system": "System",
|
||||||
"theme.scheme.light": "Hell",
|
"theme.scheme.light": "Hell",
|
||||||
"theme.scheme.dark": "Dunkel",
|
"theme.scheme.dark": "Dunkel",
|
||||||
@@ -27,6 +29,7 @@ export const dict = {
|
|||||||
"command.project.open": "Projekt öffnen",
|
"command.project.open": "Projekt öffnen",
|
||||||
"command.provider.connect": "Anbieter verbinden",
|
"command.provider.connect": "Anbieter verbinden",
|
||||||
"command.server.switch": "Server wechseln",
|
"command.server.switch": "Server wechseln",
|
||||||
|
"command.settings.open": "Einstellungen öffnen",
|
||||||
"command.session.previous": "Vorherige Sitzung",
|
"command.session.previous": "Vorherige Sitzung",
|
||||||
"command.session.next": "Nächste Sitzung",
|
"command.session.next": "Nächste Sitzung",
|
||||||
"command.session.archive": "Sitzung archivieren",
|
"command.session.archive": "Sitzung archivieren",
|
||||||
@@ -44,7 +47,10 @@ export const dict = {
|
|||||||
"command.session.new": "Neue Sitzung",
|
"command.session.new": "Neue Sitzung",
|
||||||
"command.file.open": "Datei öffnen",
|
"command.file.open": "Datei öffnen",
|
||||||
"command.file.open.description": "Dateien und Befehle durchsuchen",
|
"command.file.open.description": "Dateien und Befehle durchsuchen",
|
||||||
|
"command.context.addSelection": "Auswahl zum Kontext hinzufügen",
|
||||||
|
"command.context.addSelection.description": "Ausgewählte Zeilen aus der aktuellen Datei hinzufügen",
|
||||||
"command.terminal.toggle": "Terminal umschalten",
|
"command.terminal.toggle": "Terminal umschalten",
|
||||||
|
"command.fileTree.toggle": "Dateibaum umschalten",
|
||||||
"command.review.toggle": "Überprüfung umschalten",
|
"command.review.toggle": "Überprüfung umschalten",
|
||||||
"command.terminal.new": "Neues Terminal",
|
"command.terminal.new": "Neues Terminal",
|
||||||
"command.terminal.new.description": "Neuen Terminal-Tab erstellen",
|
"command.terminal.new.description": "Neuen Terminal-Tab erstellen",
|
||||||
@@ -121,6 +127,7 @@ export const dict = {
|
|||||||
"provider.connect.opencodeZen.line2":
|
"provider.connect.opencodeZen.line2":
|
||||||
"Mit einem einzigen API-Schlüssel erhalten Sie Zugriff auf Modelle wie Claude, GPT, Gemini, GLM und mehr.",
|
"Mit einem einzigen API-Schlüssel erhalten Sie Zugriff auf Modelle wie Claude, GPT, Gemini, GLM und mehr.",
|
||||||
"provider.connect.opencodeZen.visit.prefix": "Besuchen Sie ",
|
"provider.connect.opencodeZen.visit.prefix": "Besuchen Sie ",
|
||||||
|
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
|
||||||
"provider.connect.opencodeZen.visit.suffix": ", um Ihren API-Schlüssel zu erhalten.",
|
"provider.connect.opencodeZen.visit.suffix": ", um Ihren API-Schlüssel zu erhalten.",
|
||||||
"provider.connect.oauth.code.visit.prefix": "Besuchen Sie ",
|
"provider.connect.oauth.code.visit.prefix": "Besuchen Sie ",
|
||||||
"provider.connect.oauth.code.visit.link": "diesen Link",
|
"provider.connect.oauth.code.visit.link": "diesen Link",
|
||||||
@@ -138,13 +145,32 @@ export const dict = {
|
|||||||
"provider.connect.toast.connected.title": "{{provider}} verbunden",
|
"provider.connect.toast.connected.title": "{{provider}} verbunden",
|
||||||
"provider.connect.toast.connected.description": "{{provider}} Modelle sind jetzt verfügbar.",
|
"provider.connect.toast.connected.description": "{{provider}} Modelle sind jetzt verfügbar.",
|
||||||
|
|
||||||
|
"provider.disconnect.toast.disconnected.title": "{{provider}} getrennt",
|
||||||
|
"provider.disconnect.toast.disconnected.description": "Die {{provider}}-Modelle sind nicht mehr verfügbar.",
|
||||||
"model.tag.free": "Kostenlos",
|
"model.tag.free": "Kostenlos",
|
||||||
"model.tag.latest": "Neueste",
|
"model.tag.latest": "Neueste",
|
||||||
|
|
||||||
|
"model.provider.anthropic": "Anthropic",
|
||||||
|
"model.provider.openai": "OpenAI",
|
||||||
|
"model.provider.google": "Google",
|
||||||
|
"model.provider.xai": "xAI",
|
||||||
|
"model.provider.meta": "Meta",
|
||||||
|
"model.input.text": "Text",
|
||||||
|
"model.input.image": "Bild",
|
||||||
|
"model.input.audio": "Audio",
|
||||||
|
"model.input.video": "Video",
|
||||||
|
"model.input.pdf": "pdf",
|
||||||
|
"model.tooltip.allows": "Erlaubt: {{inputs}}",
|
||||||
|
"model.tooltip.reasoning.allowed": "Erlaubt Reasoning",
|
||||||
|
"model.tooltip.reasoning.none": "Kein Reasoning",
|
||||||
|
"model.tooltip.context": "Kontextlimit {{limit}}",
|
||||||
"common.search.placeholder": "Suchen",
|
"common.search.placeholder": "Suchen",
|
||||||
"common.goBack": "Zurück",
|
"common.goBack": "Zurück",
|
||||||
"common.loading": "Laden",
|
"common.loading": "Laden",
|
||||||
|
"common.loading.ellipsis": "...",
|
||||||
"common.cancel": "Abbrechen",
|
"common.cancel": "Abbrechen",
|
||||||
|
"common.connect": "Verbinden",
|
||||||
|
"common.disconnect": "Trennen",
|
||||||
"common.submit": "Absenden",
|
"common.submit": "Absenden",
|
||||||
"common.save": "Speichern",
|
"common.save": "Speichern",
|
||||||
"common.saving": "Speichert...",
|
"common.saving": "Speichert...",
|
||||||
@@ -153,6 +179,8 @@ export const dict = {
|
|||||||
|
|
||||||
"prompt.placeholder.shell": "Shell-Befehl eingeben...",
|
"prompt.placeholder.shell": "Shell-Befehl eingeben...",
|
||||||
"prompt.placeholder.normal": 'Fragen Sie alles... "{{example}}"',
|
"prompt.placeholder.normal": 'Fragen Sie alles... "{{example}}"',
|
||||||
|
"prompt.placeholder.summarizeComments": "Kommentare zusammenfassen…",
|
||||||
|
"prompt.placeholder.summarizeComment": "Kommentar zusammenfassen…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
"prompt.mode.shell.exit": "esc zum Verlassen",
|
"prompt.mode.shell.exit": "esc zum Verlassen",
|
||||||
|
|
||||||
@@ -257,6 +285,10 @@ export const dict = {
|
|||||||
"dialog.project.edit.color": "Farbe",
|
"dialog.project.edit.color": "Farbe",
|
||||||
"dialog.project.edit.color.select": "{{color}}-Farbe auswählen",
|
"dialog.project.edit.color.select": "{{color}}-Farbe auswählen",
|
||||||
|
|
||||||
|
"dialog.project.edit.worktree.startup": "Startup-Skript für Arbeitsbereich",
|
||||||
|
"dialog.project.edit.worktree.startup.description":
|
||||||
|
"Wird nach dem Erstellen eines neuen Arbeitsbereichs (Worktree) ausgeführt.",
|
||||||
|
"dialog.project.edit.worktree.startup.placeholder": "z. B. bun install",
|
||||||
"context.breakdown.title": "Kontext-Aufschlüsselung",
|
"context.breakdown.title": "Kontext-Aufschlüsselung",
|
||||||
"context.breakdown.note":
|
"context.breakdown.note":
|
||||||
'Ungefähre Aufschlüsselung der Eingabe-Token. "Andere" beinhaltet Werkzeugdefinitionen und Overhead.',
|
'Ungefähre Aufschlüsselung der Eingabe-Token. "Andere" beinhaltet Werkzeugdefinitionen und Overhead.',
|
||||||
@@ -323,6 +355,9 @@ export const dict = {
|
|||||||
|
|
||||||
"toast.file.loadFailed.title": "Datei konnte nicht geladen werden",
|
"toast.file.loadFailed.title": "Datei konnte nicht geladen werden",
|
||||||
|
|
||||||
|
"toast.file.listFailed.title": "Dateien konnten nicht aufgelistet werden",
|
||||||
|
"toast.context.noLineSelection.title": "Keine Zeilenauswahl",
|
||||||
|
"toast.context.noLineSelection.description": "Wählen Sie zuerst einen Zeilenbereich in einem Datei-Tab aus.",
|
||||||
"toast.session.share.copyFailed.title": "URL konnte nicht in die Zwischenablage kopiert werden",
|
"toast.session.share.copyFailed.title": "URL konnte nicht in die Zwischenablage kopiert werden",
|
||||||
"toast.session.share.success.title": "Sitzung geteilt",
|
"toast.session.share.success.title": "Sitzung geteilt",
|
||||||
"toast.session.share.success.description": "Teilen-URL in die Zwischenablage kopiert!",
|
"toast.session.share.success.description": "Teilen-URL in die Zwischenablage kopiert!",
|
||||||
@@ -399,13 +434,19 @@ export const dict = {
|
|||||||
"session.tab.context": "Kontext",
|
"session.tab.context": "Kontext",
|
||||||
"session.panel.reviewAndFiles": "Überprüfung und Dateien",
|
"session.panel.reviewAndFiles": "Überprüfung und Dateien",
|
||||||
"session.review.filesChanged": "{{count}} Dateien geändert",
|
"session.review.filesChanged": "{{count}} Dateien geändert",
|
||||||
|
"session.review.change.one": "Änderung",
|
||||||
|
"session.review.change.other": "Änderungen",
|
||||||
"session.review.loadingChanges": "Lade Änderungen...",
|
"session.review.loadingChanges": "Lade Änderungen...",
|
||||||
"session.review.empty": "Noch keine Änderungen in dieser Sitzung",
|
"session.review.empty": "Noch keine Änderungen in dieser Sitzung",
|
||||||
|
"session.review.noChanges": "Keine Änderungen",
|
||||||
|
"session.files.selectToOpen": "Datei zum Öffnen auswählen",
|
||||||
|
"session.files.all": "Alle Dateien",
|
||||||
"session.messages.renderEarlier": "Frühere Nachrichten rendern",
|
"session.messages.renderEarlier": "Frühere Nachrichten rendern",
|
||||||
"session.messages.loadingEarlier": "Lade frühere Nachrichten...",
|
"session.messages.loadingEarlier": "Lade frühere Nachrichten...",
|
||||||
"session.messages.loadEarlier": "Frühere Nachrichten laden",
|
"session.messages.loadEarlier": "Frühere Nachrichten laden",
|
||||||
"session.messages.loading": "Lade Nachrichten...",
|
"session.messages.loading": "Lade Nachrichten...",
|
||||||
|
|
||||||
|
"session.messages.jumpToLatest": "Zum neuesten springen",
|
||||||
"session.context.addToContext": "{{selection}} zum Kontext hinzufügen",
|
"session.context.addToContext": "{{selection}} zum Kontext hinzufügen",
|
||||||
|
|
||||||
"session.new.worktree.main": "Haupt-Branch",
|
"session.new.worktree.main": "Haupt-Branch",
|
||||||
@@ -447,6 +488,9 @@ export const dict = {
|
|||||||
"terminal.title.numbered": "Terminal {{number}}",
|
"terminal.title.numbered": "Terminal {{number}}",
|
||||||
"terminal.close": "Terminal schließen",
|
"terminal.close": "Terminal schließen",
|
||||||
|
|
||||||
|
"terminal.connectionLost.title": "Verbindung verloren",
|
||||||
|
"terminal.connectionLost.description":
|
||||||
|
"Die Terminalverbindung wurde unterbrochen. Das kann passieren, wenn der Server neu startet.",
|
||||||
"common.closeTab": "Tab schließen",
|
"common.closeTab": "Tab schließen",
|
||||||
"common.dismiss": "Verwerfen",
|
"common.dismiss": "Verwerfen",
|
||||||
"common.requestFailed": "Anfrage fehlgeschlagen",
|
"common.requestFailed": "Anfrage fehlgeschlagen",
|
||||||
@@ -460,6 +504,8 @@ export const dict = {
|
|||||||
"common.edit": "Bearbeiten",
|
"common.edit": "Bearbeiten",
|
||||||
"common.loadMore": "Mehr laden",
|
"common.loadMore": "Mehr laden",
|
||||||
|
|
||||||
|
"common.key.esc": "ESC",
|
||||||
|
"sidebar.menu.toggle": "Menü umschalten",
|
||||||
"sidebar.nav.projectsAndSessions": "Projekte und Sitzungen",
|
"sidebar.nav.projectsAndSessions": "Projekte und Sitzungen",
|
||||||
"sidebar.settings": "Einstellungen",
|
"sidebar.settings": "Einstellungen",
|
||||||
"sidebar.help": "Hilfe",
|
"sidebar.help": "Hilfe",
|
||||||
@@ -472,7 +518,9 @@ export const dict = {
|
|||||||
"sidebar.project.recentSessions": "Letzte Sitzungen",
|
"sidebar.project.recentSessions": "Letzte Sitzungen",
|
||||||
"sidebar.project.viewAllSessions": "Alle Sitzungen anzeigen",
|
"sidebar.project.viewAllSessions": "Alle Sitzungen anzeigen",
|
||||||
|
|
||||||
|
"app.name.desktop": "OpenCode Desktop",
|
||||||
"settings.section.desktop": "Desktop",
|
"settings.section.desktop": "Desktop",
|
||||||
|
"settings.section.server": "Server",
|
||||||
"settings.tab.general": "Allgemein",
|
"settings.tab.general": "Allgemein",
|
||||||
"settings.tab.shortcuts": "Tastenkombinationen",
|
"settings.tab.shortcuts": "Tastenkombinationen",
|
||||||
|
|
||||||
@@ -489,6 +537,63 @@ export const dict = {
|
|||||||
"settings.general.row.font.title": "Schriftart",
|
"settings.general.row.font.title": "Schriftart",
|
||||||
"settings.general.row.font.description": "Die in Codeblöcken verwendete Monospace-Schriftart anpassen",
|
"settings.general.row.font.description": "Die in Codeblöcken verwendete Monospace-Schriftart anpassen",
|
||||||
|
|
||||||
|
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||||
|
"font.option.cascadiaCode": "Cascadia Code",
|
||||||
|
"font.option.firaCode": "Fira Code",
|
||||||
|
"font.option.hack": "Hack",
|
||||||
|
"font.option.inconsolata": "Inconsolata",
|
||||||
|
"font.option.intelOneMono": "Intel One Mono",
|
||||||
|
"font.option.iosevka": "Iosevka",
|
||||||
|
"font.option.jetbrainsMono": "JetBrains Mono",
|
||||||
|
"font.option.mesloLgs": "Meslo LGS",
|
||||||
|
"font.option.robotoMono": "Roboto Mono",
|
||||||
|
"font.option.sourceCodePro": "Source Code Pro",
|
||||||
|
"font.option.ubuntuMono": "Ubuntu Mono",
|
||||||
|
"sound.option.alert01": "Alarm 01",
|
||||||
|
"sound.option.alert02": "Alarm 02",
|
||||||
|
"sound.option.alert03": "Alarm 03",
|
||||||
|
"sound.option.alert04": "Alarm 04",
|
||||||
|
"sound.option.alert05": "Alarm 05",
|
||||||
|
"sound.option.alert06": "Alarm 06",
|
||||||
|
"sound.option.alert07": "Alarm 07",
|
||||||
|
"sound.option.alert08": "Alarm 08",
|
||||||
|
"sound.option.alert09": "Alarm 09",
|
||||||
|
"sound.option.alert10": "Alarm 10",
|
||||||
|
"sound.option.bipbop01": "Bip-bop 01",
|
||||||
|
"sound.option.bipbop02": "Bip-bop 02",
|
||||||
|
"sound.option.bipbop03": "Bip-bop 03",
|
||||||
|
"sound.option.bipbop04": "Bip-bop 04",
|
||||||
|
"sound.option.bipbop05": "Bip-bop 05",
|
||||||
|
"sound.option.bipbop06": "Bip-bop 06",
|
||||||
|
"sound.option.bipbop07": "Bip-bop 07",
|
||||||
|
"sound.option.bipbop08": "Bip-bop 08",
|
||||||
|
"sound.option.bipbop09": "Bip-bop 09",
|
||||||
|
"sound.option.bipbop10": "Bip-bop 10",
|
||||||
|
"sound.option.staplebops01": "Staplebops 01",
|
||||||
|
"sound.option.staplebops02": "Staplebops 02",
|
||||||
|
"sound.option.staplebops03": "Staplebops 03",
|
||||||
|
"sound.option.staplebops04": "Staplebops 04",
|
||||||
|
"sound.option.staplebops05": "Staplebops 05",
|
||||||
|
"sound.option.staplebops06": "Staplebops 06",
|
||||||
|
"sound.option.staplebops07": "Staplebops 07",
|
||||||
|
"sound.option.nope01": "Nein 01",
|
||||||
|
"sound.option.nope02": "Nein 02",
|
||||||
|
"sound.option.nope03": "Nein 03",
|
||||||
|
"sound.option.nope04": "Nein 04",
|
||||||
|
"sound.option.nope05": "Nein 05",
|
||||||
|
"sound.option.nope06": "Nein 06",
|
||||||
|
"sound.option.nope07": "Nein 07",
|
||||||
|
"sound.option.nope08": "Nein 08",
|
||||||
|
"sound.option.nope09": "Nein 09",
|
||||||
|
"sound.option.nope10": "Nein 10",
|
||||||
|
"sound.option.nope11": "Nein 11",
|
||||||
|
"sound.option.nope12": "Nein 12",
|
||||||
|
"sound.option.yup01": "Ja 01",
|
||||||
|
"sound.option.yup02": "Ja 02",
|
||||||
|
"sound.option.yup03": "Ja 03",
|
||||||
|
"sound.option.yup04": "Ja 04",
|
||||||
|
"sound.option.yup05": "Ja 05",
|
||||||
|
"sound.option.yup06": "Ja 06",
|
||||||
"settings.general.notifications.agent.title": "Agent",
|
"settings.general.notifications.agent.title": "Agent",
|
||||||
"settings.general.notifications.agent.description":
|
"settings.general.notifications.agent.description":
|
||||||
"Systembenachrichtigung anzeigen, wenn der Agent fertig ist oder Aufmerksamkeit benötigt",
|
"Systembenachrichtigung anzeigen, wenn der Agent fertig ist oder Aufmerksamkeit benötigt",
|
||||||
@@ -525,6 +630,13 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.providers.title": "Anbieter",
|
"settings.providers.title": "Anbieter",
|
||||||
"settings.providers.description": "Anbietereinstellungen können hier konfiguriert werden.",
|
"settings.providers.description": "Anbietereinstellungen können hier konfiguriert werden.",
|
||||||
|
"settings.providers.section.connected": "Verbundene Anbieter",
|
||||||
|
"settings.providers.connected.empty": "Keine verbundenen Anbieter",
|
||||||
|
"settings.providers.section.popular": "Beliebte Anbieter",
|
||||||
|
"settings.providers.tag.environment": "Umgebung",
|
||||||
|
"settings.providers.tag.config": "Konfiguration",
|
||||||
|
"settings.providers.tag.custom": "Benutzerdefiniert",
|
||||||
|
"settings.providers.tag.other": "Andere",
|
||||||
"settings.models.title": "Modelle",
|
"settings.models.title": "Modelle",
|
||||||
"settings.models.description": "Modelleinstellungen können hier konfiguriert werden.",
|
"settings.models.description": "Modelleinstellungen können hier konfiguriert werden.",
|
||||||
"settings.agents.title": "Agenten",
|
"settings.agents.title": "Agenten",
|
||||||
@@ -592,6 +704,7 @@ export const dict = {
|
|||||||
"workspace.reset.failed.title": "Arbeitsbereich konnte nicht zurückgesetzt werden",
|
"workspace.reset.failed.title": "Arbeitsbereich konnte nicht zurückgesetzt werden",
|
||||||
"workspace.reset.success.title": "Arbeitsbereich zurückgesetzt",
|
"workspace.reset.success.title": "Arbeitsbereich zurückgesetzt",
|
||||||
"workspace.reset.success.description": "Der Arbeitsbereich entspricht jetzt dem Standard-Branch.",
|
"workspace.reset.success.description": "Der Arbeitsbereich entspricht jetzt dem Standard-Branch.",
|
||||||
|
"workspace.error.stillPreparing": "Arbeitsbereich wird noch vorbereitet",
|
||||||
"workspace.status.checking": "Suche nach nicht zusammengeführten Änderungen...",
|
"workspace.status.checking": "Suche nach nicht zusammengeführten Änderungen...",
|
||||||
"workspace.status.error": "Git-Status konnte nicht überprüft werden.",
|
"workspace.status.error": "Git-Status konnte nicht überprüft werden.",
|
||||||
"workspace.status.clean": "Keine nicht zusammengeführten Änderungen erkannt.",
|
"workspace.status.clean": "Keine nicht zusammengeführten Änderungen erkannt.",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const dict = {
|
|||||||
"command.category.theme": "Theme",
|
"command.category.theme": "Theme",
|
||||||
"command.category.language": "Language",
|
"command.category.language": "Language",
|
||||||
"command.category.file": "File",
|
"command.category.file": "File",
|
||||||
|
"command.category.context": "Context",
|
||||||
"command.category.terminal": "Terminal",
|
"command.category.terminal": "Terminal",
|
||||||
"command.category.model": "Model",
|
"command.category.model": "Model",
|
||||||
"command.category.mcp": "MCP",
|
"command.category.mcp": "MCP",
|
||||||
@@ -42,7 +43,10 @@ export const dict = {
|
|||||||
"command.session.new": "New session",
|
"command.session.new": "New session",
|
||||||
"command.file.open": "Open file",
|
"command.file.open": "Open file",
|
||||||
"command.file.open.description": "Search files and commands",
|
"command.file.open.description": "Search files and commands",
|
||||||
|
"command.context.addSelection": "Add selection to context",
|
||||||
|
"command.context.addSelection.description": "Add selected lines from the current file",
|
||||||
"command.terminal.toggle": "Toggle terminal",
|
"command.terminal.toggle": "Toggle terminal",
|
||||||
|
"command.fileTree.toggle": "Toggle file tree",
|
||||||
"command.review.toggle": "Toggle review",
|
"command.review.toggle": "Toggle review",
|
||||||
"command.terminal.new": "New terminal",
|
"command.terminal.new": "New terminal",
|
||||||
"command.terminal.new.description": "Create a new terminal tab",
|
"command.terminal.new.description": "Create a new terminal tab",
|
||||||
@@ -87,9 +91,13 @@ export const dict = {
|
|||||||
"dialog.provider.group.popular": "Popular",
|
"dialog.provider.group.popular": "Popular",
|
||||||
"dialog.provider.group.other": "Other",
|
"dialog.provider.group.other": "Other",
|
||||||
"dialog.provider.tag.recommended": "Recommended",
|
"dialog.provider.tag.recommended": "Recommended",
|
||||||
"dialog.provider.anthropic.note": "Connect with Claude Pro/Max or API key",
|
"dialog.provider.opencode.note": "Curated models including Claude, GPT, Gemini and more",
|
||||||
"dialog.provider.openai.note": "Connect with ChatGPT Pro/Plus or API key",
|
"dialog.provider.anthropic.note": "Direct access to Claude models, including Pro and Max",
|
||||||
"dialog.provider.copilot.note": "Connect with Copilot or API key",
|
"dialog.provider.copilot.note": "Claude models for coding assistance",
|
||||||
|
"dialog.provider.openai.note": "GPT models for fast, capable general AI tasks",
|
||||||
|
"dialog.provider.google.note": "Gemini models for fast, structured responses",
|
||||||
|
"dialog.provider.openrouter.note": "Access all supported models from one provider",
|
||||||
|
"dialog.provider.vercel.note": "Unified access to AI models with smart routing",
|
||||||
|
|
||||||
"dialog.model.select.title": "Select model",
|
"dialog.model.select.title": "Select model",
|
||||||
"dialog.model.search.placeholder": "Search models",
|
"dialog.model.search.placeholder": "Search models",
|
||||||
@@ -172,6 +180,8 @@ export const dict = {
|
|||||||
|
|
||||||
"prompt.placeholder.shell": "Enter shell command...",
|
"prompt.placeholder.shell": "Enter shell command...",
|
||||||
"prompt.placeholder.normal": 'Ask anything... "{{example}}"',
|
"prompt.placeholder.normal": 'Ask anything... "{{example}}"',
|
||||||
|
"prompt.placeholder.summarizeComments": "Summarize comments…",
|
||||||
|
"prompt.placeholder.summarizeComment": "Summarize comment…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
"prompt.mode.shell.exit": "esc to exit",
|
"prompt.mode.shell.exit": "esc to exit",
|
||||||
|
|
||||||
@@ -342,6 +352,10 @@ export const dict = {
|
|||||||
"toast.model.none.description": "Connect a provider to summarize this session",
|
"toast.model.none.description": "Connect a provider to summarize this session",
|
||||||
|
|
||||||
"toast.file.loadFailed.title": "Failed to load file",
|
"toast.file.loadFailed.title": "Failed to load file",
|
||||||
|
"toast.file.listFailed.title": "Failed to list files",
|
||||||
|
|
||||||
|
"toast.context.noLineSelection.title": "No line selection",
|
||||||
|
"toast.context.noLineSelection.description": "Select a line range in a file tab first.",
|
||||||
|
|
||||||
"toast.session.share.copyFailed.title": "Failed to copy URL to clipboard",
|
"toast.session.share.copyFailed.title": "Failed to copy URL to clipboard",
|
||||||
"toast.session.share.success.title": "Session shared",
|
"toast.session.share.success.title": "Session shared",
|
||||||
@@ -417,8 +431,15 @@ export const dict = {
|
|||||||
"session.tab.context": "Context",
|
"session.tab.context": "Context",
|
||||||
"session.panel.reviewAndFiles": "Review and files",
|
"session.panel.reviewAndFiles": "Review and files",
|
||||||
"session.review.filesChanged": "{{count}} Files Changed",
|
"session.review.filesChanged": "{{count}} Files Changed",
|
||||||
|
"session.review.change.one": "Change",
|
||||||
|
"session.review.change.other": "Changes",
|
||||||
"session.review.loadingChanges": "Loading changes...",
|
"session.review.loadingChanges": "Loading changes...",
|
||||||
"session.review.empty": "No changes in this session yet",
|
"session.review.empty": "No changes in this session yet",
|
||||||
|
"session.review.noChanges": "No changes",
|
||||||
|
|
||||||
|
"session.files.selectToOpen": "Select a file to open",
|
||||||
|
"session.files.all": "All files",
|
||||||
|
|
||||||
"session.messages.renderEarlier": "Render earlier messages",
|
"session.messages.renderEarlier": "Render earlier messages",
|
||||||
"session.messages.loadingEarlier": "Loading earlier messages...",
|
"session.messages.loadingEarlier": "Loading earlier messages...",
|
||||||
"session.messages.loadEarlier": "Load earlier messages",
|
"session.messages.loadEarlier": "Load earlier messages",
|
||||||
@@ -495,6 +516,8 @@ export const dict = {
|
|||||||
"sidebar.project.recentSessions": "Recent sessions",
|
"sidebar.project.recentSessions": "Recent sessions",
|
||||||
"sidebar.project.viewAllSessions": "View all sessions",
|
"sidebar.project.viewAllSessions": "View all sessions",
|
||||||
|
|
||||||
|
"app.name.desktop": "OpenCode Desktop",
|
||||||
|
|
||||||
"settings.section.desktop": "Desktop",
|
"settings.section.desktop": "Desktop",
|
||||||
"settings.section.server": "Server",
|
"settings.section.server": "Server",
|
||||||
"settings.tab.general": "General",
|
"settings.tab.general": "General",
|
||||||
@@ -502,6 +525,7 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.general.section.appearance": "Appearance",
|
"settings.general.section.appearance": "Appearance",
|
||||||
"settings.general.section.notifications": "System notifications",
|
"settings.general.section.notifications": "System notifications",
|
||||||
|
"settings.general.section.updates": "Updates",
|
||||||
"settings.general.section.sounds": "Sound effects",
|
"settings.general.section.sounds": "Sound effects",
|
||||||
|
|
||||||
"settings.general.row.language.title": "Language",
|
"settings.general.row.language.title": "Language",
|
||||||
@@ -512,6 +536,9 @@ export const dict = {
|
|||||||
"settings.general.row.theme.description": "Customise how OpenCode is themed.",
|
"settings.general.row.theme.description": "Customise how OpenCode is themed.",
|
||||||
"settings.general.row.font.title": "Font",
|
"settings.general.row.font.title": "Font",
|
||||||
"settings.general.row.font.description": "Customise the mono font used in code blocks",
|
"settings.general.row.font.description": "Customise the mono font used in code blocks",
|
||||||
|
|
||||||
|
"settings.general.row.releaseNotes.title": "Release notes",
|
||||||
|
"settings.general.row.releaseNotes.description": "Show What's New popups after updates",
|
||||||
"font.option.ibmPlexMono": "IBM Plex Mono",
|
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||||
"font.option.cascadiaCode": "Cascadia Code",
|
"font.option.cascadiaCode": "Cascadia Code",
|
||||||
"font.option.firaCode": "Fira Code",
|
"font.option.firaCode": "Fira Code",
|
||||||
@@ -678,6 +705,7 @@ export const dict = {
|
|||||||
"workspace.reset.failed.title": "Failed to reset workspace",
|
"workspace.reset.failed.title": "Failed to reset workspace",
|
||||||
"workspace.reset.success.title": "Workspace reset",
|
"workspace.reset.success.title": "Workspace reset",
|
||||||
"workspace.reset.success.description": "Workspace now matches the default branch.",
|
"workspace.reset.success.description": "Workspace now matches the default branch.",
|
||||||
|
"workspace.error.stillPreparing": "Workspace is still preparing",
|
||||||
"workspace.status.checking": "Checking for unmerged changes...",
|
"workspace.status.checking": "Checking for unmerged changes...",
|
||||||
"workspace.status.error": "Unable to verify git status.",
|
"workspace.status.error": "Unable to verify git status.",
|
||||||
"workspace.status.clean": "No unmerged changes detected.",
|
"workspace.status.clean": "No unmerged changes detected.",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const dict = {
|
|||||||
"command.category.theme": "Tema",
|
"command.category.theme": "Tema",
|
||||||
"command.category.language": "Idioma",
|
"command.category.language": "Idioma",
|
||||||
"command.category.file": "Archivo",
|
"command.category.file": "Archivo",
|
||||||
|
"command.category.context": "Contexto",
|
||||||
"command.category.terminal": "Terminal",
|
"command.category.terminal": "Terminal",
|
||||||
"command.category.model": "Modelo",
|
"command.category.model": "Modelo",
|
||||||
"command.category.mcp": "MCP",
|
"command.category.mcp": "MCP",
|
||||||
@@ -15,6 +16,7 @@ export const dict = {
|
|||||||
"command.category.permissions": "Permisos",
|
"command.category.permissions": "Permisos",
|
||||||
"command.category.workspace": "Espacio de trabajo",
|
"command.category.workspace": "Espacio de trabajo",
|
||||||
|
|
||||||
|
"command.category.settings": "Ajustes",
|
||||||
"theme.scheme.system": "Sistema",
|
"theme.scheme.system": "Sistema",
|
||||||
"theme.scheme.light": "Claro",
|
"theme.scheme.light": "Claro",
|
||||||
"theme.scheme.dark": "Oscuro",
|
"theme.scheme.dark": "Oscuro",
|
||||||
@@ -23,6 +25,7 @@ export const dict = {
|
|||||||
"command.project.open": "Abrir proyecto",
|
"command.project.open": "Abrir proyecto",
|
||||||
"command.provider.connect": "Conectar proveedor",
|
"command.provider.connect": "Conectar proveedor",
|
||||||
"command.server.switch": "Cambiar servidor",
|
"command.server.switch": "Cambiar servidor",
|
||||||
|
"command.settings.open": "Abrir ajustes",
|
||||||
"command.session.previous": "Sesión anterior",
|
"command.session.previous": "Sesión anterior",
|
||||||
"command.session.next": "Siguiente sesión",
|
"command.session.next": "Siguiente sesión",
|
||||||
"command.session.archive": "Archivar sesión",
|
"command.session.archive": "Archivar sesión",
|
||||||
@@ -40,7 +43,10 @@ export const dict = {
|
|||||||
"command.session.new": "Nueva sesión",
|
"command.session.new": "Nueva sesión",
|
||||||
"command.file.open": "Abrir archivo",
|
"command.file.open": "Abrir archivo",
|
||||||
"command.file.open.description": "Buscar archivos y comandos",
|
"command.file.open.description": "Buscar archivos y comandos",
|
||||||
|
"command.context.addSelection": "Añadir selección al contexto",
|
||||||
|
"command.context.addSelection.description": "Añadir las líneas seleccionadas del archivo actual",
|
||||||
"command.terminal.toggle": "Alternar terminal",
|
"command.terminal.toggle": "Alternar terminal",
|
||||||
|
"command.fileTree.toggle": "Alternar árbol de archivos",
|
||||||
"command.review.toggle": "Alternar revisión",
|
"command.review.toggle": "Alternar revisión",
|
||||||
"command.terminal.new": "Nueva terminal",
|
"command.terminal.new": "Nueva terminal",
|
||||||
"command.terminal.new.description": "Crear una nueva pestaña de terminal",
|
"command.terminal.new.description": "Crear una nueva pestaña de terminal",
|
||||||
@@ -117,6 +123,7 @@ export const dict = {
|
|||||||
"provider.connect.opencodeZen.line2":
|
"provider.connect.opencodeZen.line2":
|
||||||
"Con una sola clave API obtendrás acceso a modelos como Claude, GPT, Gemini, GLM y más.",
|
"Con una sola clave API obtendrás acceso a modelos como Claude, GPT, Gemini, GLM y más.",
|
||||||
"provider.connect.opencodeZen.visit.prefix": "Visita ",
|
"provider.connect.opencodeZen.visit.prefix": "Visita ",
|
||||||
|
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
|
||||||
"provider.connect.opencodeZen.visit.suffix": " para obtener tu clave API.",
|
"provider.connect.opencodeZen.visit.suffix": " para obtener tu clave API.",
|
||||||
"provider.connect.oauth.code.visit.prefix": "Visita ",
|
"provider.connect.oauth.code.visit.prefix": "Visita ",
|
||||||
"provider.connect.oauth.code.visit.link": "este enlace",
|
"provider.connect.oauth.code.visit.link": "este enlace",
|
||||||
@@ -134,13 +141,32 @@ export const dict = {
|
|||||||
"provider.connect.toast.connected.title": "{{provider}} conectado",
|
"provider.connect.toast.connected.title": "{{provider}} conectado",
|
||||||
"provider.connect.toast.connected.description": "Los modelos de {{provider}} ahora están disponibles para usar.",
|
"provider.connect.toast.connected.description": "Los modelos de {{provider}} ahora están disponibles para usar.",
|
||||||
|
|
||||||
|
"provider.disconnect.toast.disconnected.title": "{{provider}} desconectado",
|
||||||
|
"provider.disconnect.toast.disconnected.description": "Los modelos de {{provider}} ya no están disponibles.",
|
||||||
"model.tag.free": "Gratis",
|
"model.tag.free": "Gratis",
|
||||||
"model.tag.latest": "Último",
|
"model.tag.latest": "Último",
|
||||||
|
|
||||||
|
"model.provider.anthropic": "Anthropic",
|
||||||
|
"model.provider.openai": "OpenAI",
|
||||||
|
"model.provider.google": "Google",
|
||||||
|
"model.provider.xai": "xAI",
|
||||||
|
"model.provider.meta": "Meta",
|
||||||
|
"model.input.text": "texto",
|
||||||
|
"model.input.image": "imagen",
|
||||||
|
"model.input.audio": "audio",
|
||||||
|
"model.input.video": "video",
|
||||||
|
"model.input.pdf": "pdf",
|
||||||
|
"model.tooltip.allows": "Permite: {{inputs}}",
|
||||||
|
"model.tooltip.reasoning.allowed": "Permite razonamiento",
|
||||||
|
"model.tooltip.reasoning.none": "Sin razonamiento",
|
||||||
|
"model.tooltip.context": "Límite de contexto {{limit}}",
|
||||||
"common.search.placeholder": "Buscar",
|
"common.search.placeholder": "Buscar",
|
||||||
"common.goBack": "Volver",
|
"common.goBack": "Volver",
|
||||||
"common.loading": "Cargando",
|
"common.loading": "Cargando",
|
||||||
|
"common.loading.ellipsis": "...",
|
||||||
"common.cancel": "Cancelar",
|
"common.cancel": "Cancelar",
|
||||||
|
"common.connect": "Conectar",
|
||||||
|
"common.disconnect": "Desconectar",
|
||||||
"common.submit": "Enviar",
|
"common.submit": "Enviar",
|
||||||
"common.save": "Guardar",
|
"common.save": "Guardar",
|
||||||
"common.saving": "Guardando...",
|
"common.saving": "Guardando...",
|
||||||
@@ -149,6 +175,8 @@ export const dict = {
|
|||||||
|
|
||||||
"prompt.placeholder.shell": "Introduce comando de shell...",
|
"prompt.placeholder.shell": "Introduce comando de shell...",
|
||||||
"prompt.placeholder.normal": 'Pregunta cualquier cosa... "{{example}}"',
|
"prompt.placeholder.normal": 'Pregunta cualquier cosa... "{{example}}"',
|
||||||
|
"prompt.placeholder.summarizeComments": "Resumir comentarios…",
|
||||||
|
"prompt.placeholder.summarizeComment": "Resumir comentario…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
"prompt.mode.shell.exit": "esc para salir",
|
"prompt.mode.shell.exit": "esc para salir",
|
||||||
|
|
||||||
@@ -252,6 +280,10 @@ export const dict = {
|
|||||||
"dialog.project.edit.color": "Color",
|
"dialog.project.edit.color": "Color",
|
||||||
"dialog.project.edit.color.select": "Seleccionar color {{color}}",
|
"dialog.project.edit.color.select": "Seleccionar color {{color}}",
|
||||||
|
|
||||||
|
"dialog.project.edit.worktree.startup": "Script de inicio del espacio de trabajo",
|
||||||
|
"dialog.project.edit.worktree.startup.description":
|
||||||
|
"Se ejecuta después de crear un nuevo espacio de trabajo (árbol de trabajo).",
|
||||||
|
"dialog.project.edit.worktree.startup.placeholder": "p. ej. bun install",
|
||||||
"context.breakdown.title": "Desglose de Contexto",
|
"context.breakdown.title": "Desglose de Contexto",
|
||||||
"context.breakdown.note":
|
"context.breakdown.note":
|
||||||
'Desglose aproximado de tokens de entrada. "Otro" incluye definiciones de herramientas y sobrecarga.',
|
'Desglose aproximado de tokens de entrada. "Otro" incluye definiciones de herramientas y sobrecarga.',
|
||||||
@@ -318,6 +350,9 @@ export const dict = {
|
|||||||
|
|
||||||
"toast.file.loadFailed.title": "Fallo al cargar archivo",
|
"toast.file.loadFailed.title": "Fallo al cargar archivo",
|
||||||
|
|
||||||
|
"toast.file.listFailed.title": "Fallo al listar archivos",
|
||||||
|
"toast.context.noLineSelection.title": "Sin selección de líneas",
|
||||||
|
"toast.context.noLineSelection.description": "Primero selecciona un rango de líneas en una pestaña de archivo.",
|
||||||
"toast.session.share.copyFailed.title": "Fallo al copiar URL al portapapeles",
|
"toast.session.share.copyFailed.title": "Fallo al copiar URL al portapapeles",
|
||||||
"toast.session.share.success.title": "Sesión compartida",
|
"toast.session.share.success.title": "Sesión compartida",
|
||||||
"toast.session.share.success.description": "¡URL compartida copiada al portapapeles!",
|
"toast.session.share.success.description": "¡URL compartida copiada al portapapeles!",
|
||||||
@@ -393,13 +428,19 @@ export const dict = {
|
|||||||
"session.tab.context": "Contexto",
|
"session.tab.context": "Contexto",
|
||||||
"session.panel.reviewAndFiles": "Revisión y archivos",
|
"session.panel.reviewAndFiles": "Revisión y archivos",
|
||||||
"session.review.filesChanged": "{{count}} Archivos Cambiados",
|
"session.review.filesChanged": "{{count}} Archivos Cambiados",
|
||||||
|
"session.review.change.one": "Cambio",
|
||||||
|
"session.review.change.other": "Cambios",
|
||||||
"session.review.loadingChanges": "Cargando cambios...",
|
"session.review.loadingChanges": "Cargando cambios...",
|
||||||
"session.review.empty": "No hay cambios en esta sesión aún",
|
"session.review.empty": "No hay cambios en esta sesión aún",
|
||||||
|
"session.review.noChanges": "Sin cambios",
|
||||||
|
"session.files.selectToOpen": "Selecciona un archivo para abrir",
|
||||||
|
"session.files.all": "Todos los archivos",
|
||||||
"session.messages.renderEarlier": "Renderizar mensajes anteriores",
|
"session.messages.renderEarlier": "Renderizar mensajes anteriores",
|
||||||
"session.messages.loadingEarlier": "Cargando mensajes anteriores...",
|
"session.messages.loadingEarlier": "Cargando mensajes anteriores...",
|
||||||
"session.messages.loadEarlier": "Cargar mensajes anteriores",
|
"session.messages.loadEarlier": "Cargar mensajes anteriores",
|
||||||
"session.messages.loading": "Cargando mensajes...",
|
"session.messages.loading": "Cargando mensajes...",
|
||||||
|
|
||||||
|
"session.messages.jumpToLatest": "Ir al último",
|
||||||
"session.context.addToContext": "Añadir {{selection}} al contexto",
|
"session.context.addToContext": "Añadir {{selection}} al contexto",
|
||||||
|
|
||||||
"session.new.worktree.main": "Rama principal",
|
"session.new.worktree.main": "Rama principal",
|
||||||
@@ -441,6 +482,9 @@ export const dict = {
|
|||||||
"terminal.title.numbered": "Terminal {{number}}",
|
"terminal.title.numbered": "Terminal {{number}}",
|
||||||
"terminal.close": "Cerrar terminal",
|
"terminal.close": "Cerrar terminal",
|
||||||
|
|
||||||
|
"terminal.connectionLost.title": "Conexión perdida",
|
||||||
|
"terminal.connectionLost.description":
|
||||||
|
"La conexión del terminal se interrumpió. Esto puede ocurrir cuando el servidor se reinicia.",
|
||||||
"common.closeTab": "Cerrar pestaña",
|
"common.closeTab": "Cerrar pestaña",
|
||||||
"common.dismiss": "Descartar",
|
"common.dismiss": "Descartar",
|
||||||
"common.requestFailed": "Solicitud fallida",
|
"common.requestFailed": "Solicitud fallida",
|
||||||
@@ -454,6 +498,8 @@ export const dict = {
|
|||||||
"common.edit": "Editar",
|
"common.edit": "Editar",
|
||||||
"common.loadMore": "Cargar más",
|
"common.loadMore": "Cargar más",
|
||||||
|
|
||||||
|
"common.key.esc": "ESC",
|
||||||
|
"sidebar.menu.toggle": "Alternar menú",
|
||||||
"sidebar.nav.projectsAndSessions": "Proyectos y sesiones",
|
"sidebar.nav.projectsAndSessions": "Proyectos y sesiones",
|
||||||
"sidebar.settings": "Ajustes",
|
"sidebar.settings": "Ajustes",
|
||||||
"sidebar.help": "Ayuda",
|
"sidebar.help": "Ayuda",
|
||||||
@@ -465,7 +511,9 @@ export const dict = {
|
|||||||
"sidebar.project.recentSessions": "Sesiones recientes",
|
"sidebar.project.recentSessions": "Sesiones recientes",
|
||||||
"sidebar.project.viewAllSessions": "Ver todas las sesiones",
|
"sidebar.project.viewAllSessions": "Ver todas las sesiones",
|
||||||
|
|
||||||
|
"app.name.desktop": "OpenCode Desktop",
|
||||||
"settings.section.desktop": "Escritorio",
|
"settings.section.desktop": "Escritorio",
|
||||||
|
"settings.section.server": "Servidor",
|
||||||
"settings.tab.general": "General",
|
"settings.tab.general": "General",
|
||||||
"settings.tab.shortcuts": "Atajos",
|
"settings.tab.shortcuts": "Atajos",
|
||||||
|
|
||||||
@@ -482,6 +530,63 @@ export const dict = {
|
|||||||
"settings.general.row.font.title": "Fuente",
|
"settings.general.row.font.title": "Fuente",
|
||||||
"settings.general.row.font.description": "Personaliza la fuente mono usada en bloques de código",
|
"settings.general.row.font.description": "Personaliza la fuente mono usada en bloques de código",
|
||||||
|
|
||||||
|
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||||
|
"font.option.cascadiaCode": "Cascadia Code",
|
||||||
|
"font.option.firaCode": "Fira Code",
|
||||||
|
"font.option.hack": "Hack",
|
||||||
|
"font.option.inconsolata": "Inconsolata",
|
||||||
|
"font.option.intelOneMono": "Intel One Mono",
|
||||||
|
"font.option.iosevka": "Iosevka",
|
||||||
|
"font.option.jetbrainsMono": "JetBrains Mono",
|
||||||
|
"font.option.mesloLgs": "Meslo LGS",
|
||||||
|
"font.option.robotoMono": "Roboto Mono",
|
||||||
|
"font.option.sourceCodePro": "Source Code Pro",
|
||||||
|
"font.option.ubuntuMono": "Ubuntu Mono",
|
||||||
|
"sound.option.alert01": "Alerta 01",
|
||||||
|
"sound.option.alert02": "Alerta 02",
|
||||||
|
"sound.option.alert03": "Alerta 03",
|
||||||
|
"sound.option.alert04": "Alerta 04",
|
||||||
|
"sound.option.alert05": "Alerta 05",
|
||||||
|
"sound.option.alert06": "Alerta 06",
|
||||||
|
"sound.option.alert07": "Alerta 07",
|
||||||
|
"sound.option.alert08": "Alerta 08",
|
||||||
|
"sound.option.alert09": "Alerta 09",
|
||||||
|
"sound.option.alert10": "Alerta 10",
|
||||||
|
"sound.option.bipbop01": "Bip-bop 01",
|
||||||
|
"sound.option.bipbop02": "Bip-bop 02",
|
||||||
|
"sound.option.bipbop03": "Bip-bop 03",
|
||||||
|
"sound.option.bipbop04": "Bip-bop 04",
|
||||||
|
"sound.option.bipbop05": "Bip-bop 05",
|
||||||
|
"sound.option.bipbop06": "Bip-bop 06",
|
||||||
|
"sound.option.bipbop07": "Bip-bop 07",
|
||||||
|
"sound.option.bipbop08": "Bip-bop 08",
|
||||||
|
"sound.option.bipbop09": "Bip-bop 09",
|
||||||
|
"sound.option.bipbop10": "Bip-bop 10",
|
||||||
|
"sound.option.staplebops01": "Staplebops 01",
|
||||||
|
"sound.option.staplebops02": "Staplebops 02",
|
||||||
|
"sound.option.staplebops03": "Staplebops 03",
|
||||||
|
"sound.option.staplebops04": "Staplebops 04",
|
||||||
|
"sound.option.staplebops05": "Staplebops 05",
|
||||||
|
"sound.option.staplebops06": "Staplebops 06",
|
||||||
|
"sound.option.staplebops07": "Staplebops 07",
|
||||||
|
"sound.option.nope01": "No 01",
|
||||||
|
"sound.option.nope02": "No 02",
|
||||||
|
"sound.option.nope03": "No 03",
|
||||||
|
"sound.option.nope04": "No 04",
|
||||||
|
"sound.option.nope05": "No 05",
|
||||||
|
"sound.option.nope06": "No 06",
|
||||||
|
"sound.option.nope07": "No 07",
|
||||||
|
"sound.option.nope08": "No 08",
|
||||||
|
"sound.option.nope09": "No 09",
|
||||||
|
"sound.option.nope10": "No 10",
|
||||||
|
"sound.option.nope11": "No 11",
|
||||||
|
"sound.option.nope12": "No 12",
|
||||||
|
"sound.option.yup01": "Sí 01",
|
||||||
|
"sound.option.yup02": "Sí 02",
|
||||||
|
"sound.option.yup03": "Sí 03",
|
||||||
|
"sound.option.yup04": "Sí 04",
|
||||||
|
"sound.option.yup05": "Sí 05",
|
||||||
|
"sound.option.yup06": "Sí 06",
|
||||||
"settings.general.notifications.agent.title": "Agente",
|
"settings.general.notifications.agent.title": "Agente",
|
||||||
"settings.general.notifications.agent.description":
|
"settings.general.notifications.agent.description":
|
||||||
"Mostrar notificación del sistema cuando el agente termine o necesite atención",
|
"Mostrar notificación del sistema cuando el agente termine o necesite atención",
|
||||||
@@ -519,6 +624,13 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.providers.title": "Proveedores",
|
"settings.providers.title": "Proveedores",
|
||||||
"settings.providers.description": "La configuración de proveedores estará disponible aquí.",
|
"settings.providers.description": "La configuración de proveedores estará disponible aquí.",
|
||||||
|
"settings.providers.section.connected": "Proveedores conectados",
|
||||||
|
"settings.providers.connected.empty": "No hay proveedores conectados",
|
||||||
|
"settings.providers.section.popular": "Proveedores populares",
|
||||||
|
"settings.providers.tag.environment": "Entorno",
|
||||||
|
"settings.providers.tag.config": "Configuración",
|
||||||
|
"settings.providers.tag.custom": "Personalizado",
|
||||||
|
"settings.providers.tag.other": "Otro",
|
||||||
"settings.models.title": "Modelos",
|
"settings.models.title": "Modelos",
|
||||||
"settings.models.description": "La configuración de modelos estará disponible aquí.",
|
"settings.models.description": "La configuración de modelos estará disponible aquí.",
|
||||||
"settings.agents.title": "Agentes",
|
"settings.agents.title": "Agentes",
|
||||||
@@ -586,6 +698,7 @@ export const dict = {
|
|||||||
"workspace.reset.failed.title": "Fallo al restablecer espacio de trabajo",
|
"workspace.reset.failed.title": "Fallo al restablecer espacio de trabajo",
|
||||||
"workspace.reset.success.title": "Espacio de trabajo restablecido",
|
"workspace.reset.success.title": "Espacio de trabajo restablecido",
|
||||||
"workspace.reset.success.description": "El espacio de trabajo ahora coincide con la rama predeterminada.",
|
"workspace.reset.success.description": "El espacio de trabajo ahora coincide con la rama predeterminada.",
|
||||||
|
"workspace.error.stillPreparing": "El espacio de trabajo aún se está preparando",
|
||||||
"workspace.status.checking": "Comprobando cambios no fusionados...",
|
"workspace.status.checking": "Comprobando cambios no fusionados...",
|
||||||
"workspace.status.error": "No se pudo verificar el estado de git.",
|
"workspace.status.error": "No se pudo verificar el estado de git.",
|
||||||
"workspace.status.clean": "No se detectaron cambios no fusionados.",
|
"workspace.status.clean": "No se detectaron cambios no fusionados.",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const dict = {
|
|||||||
"command.category.theme": "Thème",
|
"command.category.theme": "Thème",
|
||||||
"command.category.language": "Langue",
|
"command.category.language": "Langue",
|
||||||
"command.category.file": "Fichier",
|
"command.category.file": "Fichier",
|
||||||
|
"command.category.context": "Contexte",
|
||||||
"command.category.terminal": "Terminal",
|
"command.category.terminal": "Terminal",
|
||||||
"command.category.model": "Modèle",
|
"command.category.model": "Modèle",
|
||||||
"command.category.mcp": "MCP",
|
"command.category.mcp": "MCP",
|
||||||
@@ -15,6 +16,7 @@ export const dict = {
|
|||||||
"command.category.permissions": "Permissions",
|
"command.category.permissions": "Permissions",
|
||||||
"command.category.workspace": "Espace de travail",
|
"command.category.workspace": "Espace de travail",
|
||||||
|
|
||||||
|
"command.category.settings": "Paramètres",
|
||||||
"theme.scheme.system": "Système",
|
"theme.scheme.system": "Système",
|
||||||
"theme.scheme.light": "Clair",
|
"theme.scheme.light": "Clair",
|
||||||
"theme.scheme.dark": "Sombre",
|
"theme.scheme.dark": "Sombre",
|
||||||
@@ -23,6 +25,7 @@ export const dict = {
|
|||||||
"command.project.open": "Ouvrir un projet",
|
"command.project.open": "Ouvrir un projet",
|
||||||
"command.provider.connect": "Connecter un fournisseur",
|
"command.provider.connect": "Connecter un fournisseur",
|
||||||
"command.server.switch": "Changer de serveur",
|
"command.server.switch": "Changer de serveur",
|
||||||
|
"command.settings.open": "Ouvrir les paramètres",
|
||||||
"command.session.previous": "Session précédente",
|
"command.session.previous": "Session précédente",
|
||||||
"command.session.next": "Session suivante",
|
"command.session.next": "Session suivante",
|
||||||
"command.session.archive": "Archiver la session",
|
"command.session.archive": "Archiver la session",
|
||||||
@@ -40,7 +43,10 @@ export const dict = {
|
|||||||
"command.session.new": "Nouvelle session",
|
"command.session.new": "Nouvelle session",
|
||||||
"command.file.open": "Ouvrir un fichier",
|
"command.file.open": "Ouvrir un fichier",
|
||||||
"command.file.open.description": "Rechercher des fichiers et des commandes",
|
"command.file.open.description": "Rechercher des fichiers et des commandes",
|
||||||
|
"command.context.addSelection": "Ajouter la sélection au contexte",
|
||||||
|
"command.context.addSelection.description": "Ajouter les lignes sélectionnées du fichier actuel",
|
||||||
"command.terminal.toggle": "Basculer le terminal",
|
"command.terminal.toggle": "Basculer le terminal",
|
||||||
|
"command.fileTree.toggle": "Basculer l'arborescence des fichiers",
|
||||||
"command.review.toggle": "Basculer la revue",
|
"command.review.toggle": "Basculer la revue",
|
||||||
"command.terminal.new": "Nouveau terminal",
|
"command.terminal.new": "Nouveau terminal",
|
||||||
"command.terminal.new.description": "Créer un nouvel onglet de terminal",
|
"command.terminal.new.description": "Créer un nouvel onglet de terminal",
|
||||||
@@ -117,6 +123,7 @@ export const dict = {
|
|||||||
"provider.connect.opencodeZen.line2":
|
"provider.connect.opencodeZen.line2":
|
||||||
"Avec une seule clé API, vous aurez accès à des modèles tels que Claude, GPT, Gemini, GLM et plus encore.",
|
"Avec une seule clé API, vous aurez accès à des modèles tels que Claude, GPT, Gemini, GLM et plus encore.",
|
||||||
"provider.connect.opencodeZen.visit.prefix": "Visitez ",
|
"provider.connect.opencodeZen.visit.prefix": "Visitez ",
|
||||||
|
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
|
||||||
"provider.connect.opencodeZen.visit.suffix": " pour récupérer votre clé API.",
|
"provider.connect.opencodeZen.visit.suffix": " pour récupérer votre clé API.",
|
||||||
"provider.connect.oauth.code.visit.prefix": "Visitez ",
|
"provider.connect.oauth.code.visit.prefix": "Visitez ",
|
||||||
"provider.connect.oauth.code.visit.link": "ce lien",
|
"provider.connect.oauth.code.visit.link": "ce lien",
|
||||||
@@ -134,13 +141,32 @@ export const dict = {
|
|||||||
"provider.connect.toast.connected.title": "{{provider}} connecté",
|
"provider.connect.toast.connected.title": "{{provider}} connecté",
|
||||||
"provider.connect.toast.connected.description": "Les modèles {{provider}} sont maintenant disponibles.",
|
"provider.connect.toast.connected.description": "Les modèles {{provider}} sont maintenant disponibles.",
|
||||||
|
|
||||||
|
"provider.disconnect.toast.disconnected.title": "{{provider}} déconnecté",
|
||||||
|
"provider.disconnect.toast.disconnected.description": "Les modèles {{provider}} ne sont plus disponibles.",
|
||||||
"model.tag.free": "Gratuit",
|
"model.tag.free": "Gratuit",
|
||||||
"model.tag.latest": "Dernier",
|
"model.tag.latest": "Dernier",
|
||||||
|
|
||||||
|
"model.provider.anthropic": "Anthropic",
|
||||||
|
"model.provider.openai": "OpenAI",
|
||||||
|
"model.provider.google": "Google",
|
||||||
|
"model.provider.xai": "xAI",
|
||||||
|
"model.provider.meta": "Meta",
|
||||||
|
"model.input.text": "texte",
|
||||||
|
"model.input.image": "image",
|
||||||
|
"model.input.audio": "audio",
|
||||||
|
"model.input.video": "vidéo",
|
||||||
|
"model.input.pdf": "pdf",
|
||||||
|
"model.tooltip.allows": "Autorise : {{inputs}}",
|
||||||
|
"model.tooltip.reasoning.allowed": "Autorise le raisonnement",
|
||||||
|
"model.tooltip.reasoning.none": "Sans raisonnement",
|
||||||
|
"model.tooltip.context": "Limite de contexte {{limit}}",
|
||||||
"common.search.placeholder": "Rechercher",
|
"common.search.placeholder": "Rechercher",
|
||||||
"common.goBack": "Retour",
|
"common.goBack": "Retour",
|
||||||
"common.loading": "Chargement",
|
"common.loading": "Chargement",
|
||||||
|
"common.loading.ellipsis": "...",
|
||||||
"common.cancel": "Annuler",
|
"common.cancel": "Annuler",
|
||||||
|
"common.connect": "Connecter",
|
||||||
|
"common.disconnect": "Déconnecter",
|
||||||
"common.submit": "Soumettre",
|
"common.submit": "Soumettre",
|
||||||
"common.save": "Enregistrer",
|
"common.save": "Enregistrer",
|
||||||
"common.saving": "Enregistrement...",
|
"common.saving": "Enregistrement...",
|
||||||
@@ -149,6 +175,8 @@ export const dict = {
|
|||||||
|
|
||||||
"prompt.placeholder.shell": "Entrez une commande shell...",
|
"prompt.placeholder.shell": "Entrez une commande shell...",
|
||||||
"prompt.placeholder.normal": 'Demandez n\'importe quoi... "{{example}}"',
|
"prompt.placeholder.normal": 'Demandez n\'importe quoi... "{{example}}"',
|
||||||
|
"prompt.placeholder.summarizeComments": "Résumer les commentaires…",
|
||||||
|
"prompt.placeholder.summarizeComment": "Résumer le commentaire…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
"prompt.mode.shell.exit": "esc pour quitter",
|
"prompt.mode.shell.exit": "esc pour quitter",
|
||||||
|
|
||||||
@@ -252,6 +280,10 @@ export const dict = {
|
|||||||
"dialog.project.edit.color": "Couleur",
|
"dialog.project.edit.color": "Couleur",
|
||||||
"dialog.project.edit.color.select": "Sélectionner la couleur {{color}}",
|
"dialog.project.edit.color.select": "Sélectionner la couleur {{color}}",
|
||||||
|
|
||||||
|
"dialog.project.edit.worktree.startup": "Script de démarrage de l'espace de travail",
|
||||||
|
"dialog.project.edit.worktree.startup.description":
|
||||||
|
"S'exécute après la création d'un nouvel espace de travail (arbre de travail).",
|
||||||
|
"dialog.project.edit.worktree.startup.placeholder": "p. ex. bun install",
|
||||||
"context.breakdown.title": "Répartition du contexte",
|
"context.breakdown.title": "Répartition du contexte",
|
||||||
"context.breakdown.note":
|
"context.breakdown.note":
|
||||||
"Répartition approximative des jetons d'entrée. \"Autre\" inclut les définitions d'outils et les frais généraux.",
|
"Répartition approximative des jetons d'entrée. \"Autre\" inclut les définitions d'outils et les frais généraux.",
|
||||||
@@ -320,6 +352,9 @@ export const dict = {
|
|||||||
|
|
||||||
"toast.file.loadFailed.title": "Échec du chargement du fichier",
|
"toast.file.loadFailed.title": "Échec du chargement du fichier",
|
||||||
|
|
||||||
|
"toast.file.listFailed.title": "Échec de la liste des fichiers",
|
||||||
|
"toast.context.noLineSelection.title": "Aucune sélection de lignes",
|
||||||
|
"toast.context.noLineSelection.description": "Sélectionnez d'abord une plage de lignes dans un onglet de fichier.",
|
||||||
"toast.session.share.copyFailed.title": "Échec de la copie de l'URL dans le presse-papiers",
|
"toast.session.share.copyFailed.title": "Échec de la copie de l'URL dans le presse-papiers",
|
||||||
"toast.session.share.success.title": "Session partagée",
|
"toast.session.share.success.title": "Session partagée",
|
||||||
"toast.session.share.success.description": "URL de partage copiée dans le presse-papiers !",
|
"toast.session.share.success.description": "URL de partage copiée dans le presse-papiers !",
|
||||||
@@ -398,13 +433,19 @@ export const dict = {
|
|||||||
"session.tab.context": "Contexte",
|
"session.tab.context": "Contexte",
|
||||||
"session.panel.reviewAndFiles": "Revue et fichiers",
|
"session.panel.reviewAndFiles": "Revue et fichiers",
|
||||||
"session.review.filesChanged": "{{count}} fichiers modifiés",
|
"session.review.filesChanged": "{{count}} fichiers modifiés",
|
||||||
|
"session.review.change.one": "Modification",
|
||||||
|
"session.review.change.other": "Modifications",
|
||||||
"session.review.loadingChanges": "Chargement des modifications...",
|
"session.review.loadingChanges": "Chargement des modifications...",
|
||||||
"session.review.empty": "Aucune modification dans cette session pour l'instant",
|
"session.review.empty": "Aucune modification dans cette session pour l'instant",
|
||||||
|
"session.review.noChanges": "Aucune modification",
|
||||||
|
"session.files.selectToOpen": "Sélectionnez un fichier à ouvrir",
|
||||||
|
"session.files.all": "Tous les fichiers",
|
||||||
"session.messages.renderEarlier": "Afficher les messages précédents",
|
"session.messages.renderEarlier": "Afficher les messages précédents",
|
||||||
"session.messages.loadingEarlier": "Chargement des messages précédents...",
|
"session.messages.loadingEarlier": "Chargement des messages précédents...",
|
||||||
"session.messages.loadEarlier": "Charger les messages précédents",
|
"session.messages.loadEarlier": "Charger les messages précédents",
|
||||||
"session.messages.loading": "Chargement des messages...",
|
"session.messages.loading": "Chargement des messages...",
|
||||||
|
|
||||||
|
"session.messages.jumpToLatest": "Aller au dernier",
|
||||||
"session.context.addToContext": "Ajouter {{selection}} au contexte",
|
"session.context.addToContext": "Ajouter {{selection}} au contexte",
|
||||||
|
|
||||||
"session.new.worktree.main": "Branche principale",
|
"session.new.worktree.main": "Branche principale",
|
||||||
@@ -446,6 +487,9 @@ export const dict = {
|
|||||||
"terminal.title.numbered": "Terminal {{number}}",
|
"terminal.title.numbered": "Terminal {{number}}",
|
||||||
"terminal.close": "Fermer le terminal",
|
"terminal.close": "Fermer le terminal",
|
||||||
|
|
||||||
|
"terminal.connectionLost.title": "Connexion perdue",
|
||||||
|
"terminal.connectionLost.description":
|
||||||
|
"La connexion au terminal a été interrompue. Cela peut arriver lorsque le serveur redémarre.",
|
||||||
"common.closeTab": "Fermer l'onglet",
|
"common.closeTab": "Fermer l'onglet",
|
||||||
"common.dismiss": "Ignorer",
|
"common.dismiss": "Ignorer",
|
||||||
"common.requestFailed": "La demande a échoué",
|
"common.requestFailed": "La demande a échoué",
|
||||||
@@ -459,6 +503,8 @@ export const dict = {
|
|||||||
"common.edit": "Modifier",
|
"common.edit": "Modifier",
|
||||||
"common.loadMore": "Charger plus",
|
"common.loadMore": "Charger plus",
|
||||||
|
|
||||||
|
"common.key.esc": "ESC",
|
||||||
|
"sidebar.menu.toggle": "Basculer le menu",
|
||||||
"sidebar.nav.projectsAndSessions": "Projets et sessions",
|
"sidebar.nav.projectsAndSessions": "Projets et sessions",
|
||||||
"sidebar.settings": "Paramètres",
|
"sidebar.settings": "Paramètres",
|
||||||
"sidebar.help": "Aide",
|
"sidebar.help": "Aide",
|
||||||
@@ -472,7 +518,9 @@ export const dict = {
|
|||||||
"sidebar.project.recentSessions": "Sessions récentes",
|
"sidebar.project.recentSessions": "Sessions récentes",
|
||||||
"sidebar.project.viewAllSessions": "Voir toutes les sessions",
|
"sidebar.project.viewAllSessions": "Voir toutes les sessions",
|
||||||
|
|
||||||
|
"app.name.desktop": "OpenCode Desktop",
|
||||||
"settings.section.desktop": "Bureau",
|
"settings.section.desktop": "Bureau",
|
||||||
|
"settings.section.server": "Serveur",
|
||||||
"settings.tab.general": "Général",
|
"settings.tab.general": "Général",
|
||||||
"settings.tab.shortcuts": "Raccourcis",
|
"settings.tab.shortcuts": "Raccourcis",
|
||||||
|
|
||||||
@@ -489,6 +537,63 @@ export const dict = {
|
|||||||
"settings.general.row.font.title": "Police",
|
"settings.general.row.font.title": "Police",
|
||||||
"settings.general.row.font.description": "Personnaliser la police mono utilisée dans les blocs de code",
|
"settings.general.row.font.description": "Personnaliser la police mono utilisée dans les blocs de code",
|
||||||
|
|
||||||
|
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||||
|
"font.option.cascadiaCode": "Cascadia Code",
|
||||||
|
"font.option.firaCode": "Fira Code",
|
||||||
|
"font.option.hack": "Hack",
|
||||||
|
"font.option.inconsolata": "Inconsolata",
|
||||||
|
"font.option.intelOneMono": "Intel One Mono",
|
||||||
|
"font.option.iosevka": "Iosevka",
|
||||||
|
"font.option.jetbrainsMono": "JetBrains Mono",
|
||||||
|
"font.option.mesloLgs": "Meslo LGS",
|
||||||
|
"font.option.robotoMono": "Roboto Mono",
|
||||||
|
"font.option.sourceCodePro": "Source Code Pro",
|
||||||
|
"font.option.ubuntuMono": "Ubuntu Mono",
|
||||||
|
"sound.option.alert01": "Alerte 01",
|
||||||
|
"sound.option.alert02": "Alerte 02",
|
||||||
|
"sound.option.alert03": "Alerte 03",
|
||||||
|
"sound.option.alert04": "Alerte 04",
|
||||||
|
"sound.option.alert05": "Alerte 05",
|
||||||
|
"sound.option.alert06": "Alerte 06",
|
||||||
|
"sound.option.alert07": "Alerte 07",
|
||||||
|
"sound.option.alert08": "Alerte 08",
|
||||||
|
"sound.option.alert09": "Alerte 09",
|
||||||
|
"sound.option.alert10": "Alerte 10",
|
||||||
|
"sound.option.bipbop01": "Bip-bop 01",
|
||||||
|
"sound.option.bipbop02": "Bip-bop 02",
|
||||||
|
"sound.option.bipbop03": "Bip-bop 03",
|
||||||
|
"sound.option.bipbop04": "Bip-bop 04",
|
||||||
|
"sound.option.bipbop05": "Bip-bop 05",
|
||||||
|
"sound.option.bipbop06": "Bip-bop 06",
|
||||||
|
"sound.option.bipbop07": "Bip-bop 07",
|
||||||
|
"sound.option.bipbop08": "Bip-bop 08",
|
||||||
|
"sound.option.bipbop09": "Bip-bop 09",
|
||||||
|
"sound.option.bipbop10": "Bip-bop 10",
|
||||||
|
"sound.option.staplebops01": "Staplebops 01",
|
||||||
|
"sound.option.staplebops02": "Staplebops 02",
|
||||||
|
"sound.option.staplebops03": "Staplebops 03",
|
||||||
|
"sound.option.staplebops04": "Staplebops 04",
|
||||||
|
"sound.option.staplebops05": "Staplebops 05",
|
||||||
|
"sound.option.staplebops06": "Staplebops 06",
|
||||||
|
"sound.option.staplebops07": "Staplebops 07",
|
||||||
|
"sound.option.nope01": "Non 01",
|
||||||
|
"sound.option.nope02": "Non 02",
|
||||||
|
"sound.option.nope03": "Non 03",
|
||||||
|
"sound.option.nope04": "Non 04",
|
||||||
|
"sound.option.nope05": "Non 05",
|
||||||
|
"sound.option.nope06": "Non 06",
|
||||||
|
"sound.option.nope07": "Non 07",
|
||||||
|
"sound.option.nope08": "Non 08",
|
||||||
|
"sound.option.nope09": "Non 09",
|
||||||
|
"sound.option.nope10": "Non 10",
|
||||||
|
"sound.option.nope11": "Non 11",
|
||||||
|
"sound.option.nope12": "Non 12",
|
||||||
|
"sound.option.yup01": "Oui 01",
|
||||||
|
"sound.option.yup02": "Oui 02",
|
||||||
|
"sound.option.yup03": "Oui 03",
|
||||||
|
"sound.option.yup04": "Oui 04",
|
||||||
|
"sound.option.yup05": "Oui 05",
|
||||||
|
"sound.option.yup06": "Oui 06",
|
||||||
"settings.general.notifications.agent.title": "Agent",
|
"settings.general.notifications.agent.title": "Agent",
|
||||||
"settings.general.notifications.agent.description":
|
"settings.general.notifications.agent.description":
|
||||||
"Afficher une notification système lorsque l'agent a terminé ou nécessite une attention",
|
"Afficher une notification système lorsque l'agent a terminé ou nécessite une attention",
|
||||||
@@ -525,6 +630,13 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.providers.title": "Fournisseurs",
|
"settings.providers.title": "Fournisseurs",
|
||||||
"settings.providers.description": "Les paramètres des fournisseurs seront configurables ici.",
|
"settings.providers.description": "Les paramètres des fournisseurs seront configurables ici.",
|
||||||
|
"settings.providers.section.connected": "Fournisseurs connectés",
|
||||||
|
"settings.providers.connected.empty": "Aucun fournisseur connecté",
|
||||||
|
"settings.providers.section.popular": "Fournisseurs populaires",
|
||||||
|
"settings.providers.tag.environment": "Environnement",
|
||||||
|
"settings.providers.tag.config": "Configuration",
|
||||||
|
"settings.providers.tag.custom": "Personnalisé",
|
||||||
|
"settings.providers.tag.other": "Autre",
|
||||||
"settings.models.title": "Modèles",
|
"settings.models.title": "Modèles",
|
||||||
"settings.models.description": "Les paramètres des modèles seront configurables ici.",
|
"settings.models.description": "Les paramètres des modèles seront configurables ici.",
|
||||||
"settings.agents.title": "Agents",
|
"settings.agents.title": "Agents",
|
||||||
@@ -593,6 +705,7 @@ export const dict = {
|
|||||||
"workspace.reset.failed.title": "Échec de la réinitialisation de l'espace de travail",
|
"workspace.reset.failed.title": "Échec de la réinitialisation de l'espace de travail",
|
||||||
"workspace.reset.success.title": "Espace de travail réinitialisé",
|
"workspace.reset.success.title": "Espace de travail réinitialisé",
|
||||||
"workspace.reset.success.description": "L'espace de travail correspond maintenant à la branche par défaut.",
|
"workspace.reset.success.description": "L'espace de travail correspond maintenant à la branche par défaut.",
|
||||||
|
"workspace.error.stillPreparing": "L'espace de travail est encore en cours de préparation",
|
||||||
"workspace.status.checking": "Vérification des modifications non fusionnées...",
|
"workspace.status.checking": "Vérification des modifications non fusionnées...",
|
||||||
"workspace.status.error": "Impossible de vérifier le statut git.",
|
"workspace.status.error": "Impossible de vérifier le statut git.",
|
||||||
"workspace.status.clean": "Aucune modification non fusionnée détectée.",
|
"workspace.status.clean": "Aucune modification non fusionnée détectée.",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const dict = {
|
|||||||
"command.category.theme": "テーマ",
|
"command.category.theme": "テーマ",
|
||||||
"command.category.language": "言語",
|
"command.category.language": "言語",
|
||||||
"command.category.file": "ファイル",
|
"command.category.file": "ファイル",
|
||||||
|
"command.category.context": "コンテキスト",
|
||||||
"command.category.terminal": "ターミナル",
|
"command.category.terminal": "ターミナル",
|
||||||
"command.category.model": "モデル",
|
"command.category.model": "モデル",
|
||||||
"command.category.mcp": "MCP",
|
"command.category.mcp": "MCP",
|
||||||
@@ -15,6 +16,7 @@ export const dict = {
|
|||||||
"command.category.permissions": "権限",
|
"command.category.permissions": "権限",
|
||||||
"command.category.workspace": "ワークスペース",
|
"command.category.workspace": "ワークスペース",
|
||||||
|
|
||||||
|
"command.category.settings": "設定",
|
||||||
"theme.scheme.system": "システム",
|
"theme.scheme.system": "システム",
|
||||||
"theme.scheme.light": "ライト",
|
"theme.scheme.light": "ライト",
|
||||||
"theme.scheme.dark": "ダーク",
|
"theme.scheme.dark": "ダーク",
|
||||||
@@ -23,6 +25,7 @@ export const dict = {
|
|||||||
"command.project.open": "プロジェクトを開く",
|
"command.project.open": "プロジェクトを開く",
|
||||||
"command.provider.connect": "プロバイダーに接続",
|
"command.provider.connect": "プロバイダーに接続",
|
||||||
"command.server.switch": "サーバーの切り替え",
|
"command.server.switch": "サーバーの切り替え",
|
||||||
|
"command.settings.open": "設定を開く",
|
||||||
"command.session.previous": "前のセッション",
|
"command.session.previous": "前のセッション",
|
||||||
"command.session.next": "次のセッション",
|
"command.session.next": "次のセッション",
|
||||||
"command.session.archive": "セッションをアーカイブ",
|
"command.session.archive": "セッションをアーカイブ",
|
||||||
@@ -40,7 +43,10 @@ export const dict = {
|
|||||||
"command.session.new": "新しいセッション",
|
"command.session.new": "新しいセッション",
|
||||||
"command.file.open": "ファイルを開く",
|
"command.file.open": "ファイルを開く",
|
||||||
"command.file.open.description": "ファイルとコマンドを検索",
|
"command.file.open.description": "ファイルとコマンドを検索",
|
||||||
|
"command.context.addSelection": "選択範囲をコンテキストに追加",
|
||||||
|
"command.context.addSelection.description": "現在のファイルから選択した行を追加",
|
||||||
"command.terminal.toggle": "ターミナルの切り替え",
|
"command.terminal.toggle": "ターミナルの切り替え",
|
||||||
|
"command.fileTree.toggle": "ファイルツリーを切り替え",
|
||||||
"command.review.toggle": "レビューの切り替え",
|
"command.review.toggle": "レビューの切り替え",
|
||||||
"command.terminal.new": "新しいターミナル",
|
"command.terminal.new": "新しいターミナル",
|
||||||
"command.terminal.new.description": "新しいターミナルタブを作成",
|
"command.terminal.new.description": "新しいターミナルタブを作成",
|
||||||
@@ -116,6 +122,7 @@ export const dict = {
|
|||||||
"OpenCode Zenは、コーディングエージェント向けに最適化された信頼性の高いモデルへのアクセスを提供します。",
|
"OpenCode Zenは、コーディングエージェント向けに最適化された信頼性の高いモデルへのアクセスを提供します。",
|
||||||
"provider.connect.opencodeZen.line2": "1つのAPIキーで、Claude、GPT、Gemini、GLMなどのモデルにアクセスできます。",
|
"provider.connect.opencodeZen.line2": "1つのAPIキーで、Claude、GPT、Gemini、GLMなどのモデルにアクセスできます。",
|
||||||
"provider.connect.opencodeZen.visit.prefix": " ",
|
"provider.connect.opencodeZen.visit.prefix": " ",
|
||||||
|
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
|
||||||
"provider.connect.opencodeZen.visit.suffix": " にアクセスしてAPIキーを取得してください。",
|
"provider.connect.opencodeZen.visit.suffix": " にアクセスしてAPIキーを取得してください。",
|
||||||
"provider.connect.oauth.code.visit.prefix": " ",
|
"provider.connect.oauth.code.visit.prefix": " ",
|
||||||
"provider.connect.oauth.code.visit.link": "このリンク",
|
"provider.connect.oauth.code.visit.link": "このリンク",
|
||||||
@@ -133,13 +140,32 @@ export const dict = {
|
|||||||
"provider.connect.toast.connected.title": "{{provider}}が接続されました",
|
"provider.connect.toast.connected.title": "{{provider}}が接続されました",
|
||||||
"provider.connect.toast.connected.description": "{{provider}}モデルが使用可能になりました。",
|
"provider.connect.toast.connected.description": "{{provider}}モデルが使用可能になりました。",
|
||||||
|
|
||||||
|
"provider.disconnect.toast.disconnected.title": "{{provider}}が切断されました",
|
||||||
|
"provider.disconnect.toast.disconnected.description": "{{provider}}のモデルは利用できなくなりました。",
|
||||||
"model.tag.free": "無料",
|
"model.tag.free": "無料",
|
||||||
"model.tag.latest": "最新",
|
"model.tag.latest": "最新",
|
||||||
|
|
||||||
|
"model.provider.anthropic": "Anthropic",
|
||||||
|
"model.provider.openai": "OpenAI",
|
||||||
|
"model.provider.google": "Google",
|
||||||
|
"model.provider.xai": "xAI",
|
||||||
|
"model.provider.meta": "Meta",
|
||||||
|
"model.input.text": "テキスト",
|
||||||
|
"model.input.image": "画像",
|
||||||
|
"model.input.audio": "音声",
|
||||||
|
"model.input.video": "動画",
|
||||||
|
"model.input.pdf": "pdf",
|
||||||
|
"model.tooltip.allows": "対応: {{inputs}}",
|
||||||
|
"model.tooltip.reasoning.allowed": "推論を許可",
|
||||||
|
"model.tooltip.reasoning.none": "推論なし",
|
||||||
|
"model.tooltip.context": "コンテキスト上限 {{limit}}",
|
||||||
"common.search.placeholder": "検索",
|
"common.search.placeholder": "検索",
|
||||||
"common.goBack": "戻る",
|
"common.goBack": "戻る",
|
||||||
"common.loading": "読み込み中",
|
"common.loading": "読み込み中",
|
||||||
|
"common.loading.ellipsis": "...",
|
||||||
"common.cancel": "キャンセル",
|
"common.cancel": "キャンセル",
|
||||||
|
"common.connect": "接続",
|
||||||
|
"common.disconnect": "切断",
|
||||||
"common.submit": "送信",
|
"common.submit": "送信",
|
||||||
"common.save": "保存",
|
"common.save": "保存",
|
||||||
"common.saving": "保存中...",
|
"common.saving": "保存中...",
|
||||||
@@ -148,6 +174,8 @@ export const dict = {
|
|||||||
|
|
||||||
"prompt.placeholder.shell": "シェルコマンドを入力...",
|
"prompt.placeholder.shell": "シェルコマンドを入力...",
|
||||||
"prompt.placeholder.normal": '何でも聞いてください... "{{example}}"',
|
"prompt.placeholder.normal": '何でも聞いてください... "{{example}}"',
|
||||||
|
"prompt.placeholder.summarizeComments": "コメントを要約…",
|
||||||
|
"prompt.placeholder.summarizeComment": "コメントを要約…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
"prompt.mode.shell.exit": "escで終了",
|
"prompt.mode.shell.exit": "escで終了",
|
||||||
|
|
||||||
@@ -251,6 +279,10 @@ export const dict = {
|
|||||||
"dialog.project.edit.color": "色",
|
"dialog.project.edit.color": "色",
|
||||||
"dialog.project.edit.color.select": "{{color}}の色を選択",
|
"dialog.project.edit.color.select": "{{color}}の色を選択",
|
||||||
|
|
||||||
|
"dialog.project.edit.worktree.startup": "ワークスペース起動スクリプト",
|
||||||
|
"dialog.project.edit.worktree.startup.description":
|
||||||
|
"新しいワークスペース (ワークツリー) を作成した後に実行されます。",
|
||||||
|
"dialog.project.edit.worktree.startup.placeholder": "例: bun install",
|
||||||
"context.breakdown.title": "コンテキストの内訳",
|
"context.breakdown.title": "コンテキストの内訳",
|
||||||
"context.breakdown.note": '入力トークンのおおよその内訳です。"その他"にはツールの定義やオーバーヘッドが含まれます。',
|
"context.breakdown.note": '入力トークンのおおよその内訳です。"その他"にはツールの定義やオーバーヘッドが含まれます。',
|
||||||
"context.breakdown.system": "システム",
|
"context.breakdown.system": "システム",
|
||||||
@@ -316,6 +348,9 @@ export const dict = {
|
|||||||
|
|
||||||
"toast.file.loadFailed.title": "ファイルの読み込みに失敗しました",
|
"toast.file.loadFailed.title": "ファイルの読み込みに失敗しました",
|
||||||
|
|
||||||
|
"toast.file.listFailed.title": "ファイル一覧の取得に失敗しました",
|
||||||
|
"toast.context.noLineSelection.title": "行が選択されていません",
|
||||||
|
"toast.context.noLineSelection.description": "まずファイルタブで行範囲を選択してください。",
|
||||||
"toast.session.share.copyFailed.title": "URLのコピーに失敗しました",
|
"toast.session.share.copyFailed.title": "URLのコピーに失敗しました",
|
||||||
"toast.session.share.success.title": "セッションを共有しました",
|
"toast.session.share.success.title": "セッションを共有しました",
|
||||||
"toast.session.share.success.description": "共有URLをクリップボードにコピーしました!",
|
"toast.session.share.success.description": "共有URLをクリップボードにコピーしました!",
|
||||||
@@ -390,13 +425,19 @@ export const dict = {
|
|||||||
"session.tab.context": "コンテキスト",
|
"session.tab.context": "コンテキスト",
|
||||||
"session.panel.reviewAndFiles": "レビューとファイル",
|
"session.panel.reviewAndFiles": "レビューとファイル",
|
||||||
"session.review.filesChanged": "{{count}} ファイル変更",
|
"session.review.filesChanged": "{{count}} ファイル変更",
|
||||||
|
"session.review.change.one": "変更",
|
||||||
|
"session.review.change.other": "変更",
|
||||||
"session.review.loadingChanges": "変更を読み込み中...",
|
"session.review.loadingChanges": "変更を読み込み中...",
|
||||||
"session.review.empty": "このセッションでの変更はまだありません",
|
"session.review.empty": "このセッションでの変更はまだありません",
|
||||||
|
"session.review.noChanges": "変更なし",
|
||||||
|
"session.files.selectToOpen": "開くファイルを選択",
|
||||||
|
"session.files.all": "すべてのファイル",
|
||||||
"session.messages.renderEarlier": "以前のメッセージを表示",
|
"session.messages.renderEarlier": "以前のメッセージを表示",
|
||||||
"session.messages.loadingEarlier": "以前のメッセージを読み込み中...",
|
"session.messages.loadingEarlier": "以前のメッセージを読み込み中...",
|
||||||
"session.messages.loadEarlier": "以前のメッセージを読み込む",
|
"session.messages.loadEarlier": "以前のメッセージを読み込む",
|
||||||
"session.messages.loading": "メッセージを読み込み中...",
|
"session.messages.loading": "メッセージを読み込み中...",
|
||||||
|
|
||||||
|
"session.messages.jumpToLatest": "最新へジャンプ",
|
||||||
"session.context.addToContext": "{{selection}}をコンテキストに追加",
|
"session.context.addToContext": "{{selection}}をコンテキストに追加",
|
||||||
|
|
||||||
"session.new.worktree.main": "メインブランチ",
|
"session.new.worktree.main": "メインブランチ",
|
||||||
@@ -438,6 +479,9 @@ export const dict = {
|
|||||||
"terminal.title.numbered": "ターミナル {{number}}",
|
"terminal.title.numbered": "ターミナル {{number}}",
|
||||||
"terminal.close": "ターミナルを閉じる",
|
"terminal.close": "ターミナルを閉じる",
|
||||||
|
|
||||||
|
"terminal.connectionLost.title": "接続が失われました",
|
||||||
|
"terminal.connectionLost.description":
|
||||||
|
"ターミナルの接続が中断されました。これはサーバーが再起動したときに発生することがあります。",
|
||||||
"common.closeTab": "タブを閉じる",
|
"common.closeTab": "タブを閉じる",
|
||||||
"common.dismiss": "閉じる",
|
"common.dismiss": "閉じる",
|
||||||
"common.requestFailed": "リクエスト失敗",
|
"common.requestFailed": "リクエスト失敗",
|
||||||
@@ -451,6 +495,8 @@ export const dict = {
|
|||||||
"common.edit": "編集",
|
"common.edit": "編集",
|
||||||
"common.loadMore": "さらに読み込む",
|
"common.loadMore": "さらに読み込む",
|
||||||
|
|
||||||
|
"common.key.esc": "ESC",
|
||||||
|
"sidebar.menu.toggle": "メニューを切り替え",
|
||||||
"sidebar.nav.projectsAndSessions": "プロジェクトとセッション",
|
"sidebar.nav.projectsAndSessions": "プロジェクトとセッション",
|
||||||
"sidebar.settings": "設定",
|
"sidebar.settings": "設定",
|
||||||
"sidebar.help": "ヘルプ",
|
"sidebar.help": "ヘルプ",
|
||||||
@@ -462,7 +508,9 @@ export const dict = {
|
|||||||
"sidebar.project.recentSessions": "最近のセッション",
|
"sidebar.project.recentSessions": "最近のセッション",
|
||||||
"sidebar.project.viewAllSessions": "すべてのセッションを表示",
|
"sidebar.project.viewAllSessions": "すべてのセッションを表示",
|
||||||
|
|
||||||
|
"app.name.desktop": "OpenCode Desktop",
|
||||||
"settings.section.desktop": "デスクトップ",
|
"settings.section.desktop": "デスクトップ",
|
||||||
|
"settings.section.server": "サーバー",
|
||||||
"settings.tab.general": "一般",
|
"settings.tab.general": "一般",
|
||||||
"settings.tab.shortcuts": "ショートカット",
|
"settings.tab.shortcuts": "ショートカット",
|
||||||
|
|
||||||
@@ -479,6 +527,63 @@ export const dict = {
|
|||||||
"settings.general.row.font.title": "フォント",
|
"settings.general.row.font.title": "フォント",
|
||||||
"settings.general.row.font.description": "コードブロックで使用する等幅フォントをカスタマイズします",
|
"settings.general.row.font.description": "コードブロックで使用する等幅フォントをカスタマイズします",
|
||||||
|
|
||||||
|
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||||
|
"font.option.cascadiaCode": "Cascadia Code",
|
||||||
|
"font.option.firaCode": "Fira Code",
|
||||||
|
"font.option.hack": "Hack",
|
||||||
|
"font.option.inconsolata": "Inconsolata",
|
||||||
|
"font.option.intelOneMono": "Intel One Mono",
|
||||||
|
"font.option.iosevka": "Iosevka",
|
||||||
|
"font.option.jetbrainsMono": "JetBrains Mono",
|
||||||
|
"font.option.mesloLgs": "Meslo LGS",
|
||||||
|
"font.option.robotoMono": "Roboto Mono",
|
||||||
|
"font.option.sourceCodePro": "Source Code Pro",
|
||||||
|
"font.option.ubuntuMono": "Ubuntu Mono",
|
||||||
|
"sound.option.alert01": "アラート 01",
|
||||||
|
"sound.option.alert02": "アラート 02",
|
||||||
|
"sound.option.alert03": "アラート 03",
|
||||||
|
"sound.option.alert04": "アラート 04",
|
||||||
|
"sound.option.alert05": "アラート 05",
|
||||||
|
"sound.option.alert06": "アラート 06",
|
||||||
|
"sound.option.alert07": "アラート 07",
|
||||||
|
"sound.option.alert08": "アラート 08",
|
||||||
|
"sound.option.alert09": "アラート 09",
|
||||||
|
"sound.option.alert10": "アラート 10",
|
||||||
|
"sound.option.bipbop01": "ビップボップ 01",
|
||||||
|
"sound.option.bipbop02": "ビップボップ 02",
|
||||||
|
"sound.option.bipbop03": "ビップボップ 03",
|
||||||
|
"sound.option.bipbop04": "ビップボップ 04",
|
||||||
|
"sound.option.bipbop05": "ビップボップ 05",
|
||||||
|
"sound.option.bipbop06": "ビップボップ 06",
|
||||||
|
"sound.option.bipbop07": "ビップボップ 07",
|
||||||
|
"sound.option.bipbop08": "ビップボップ 08",
|
||||||
|
"sound.option.bipbop09": "ビップボップ 09",
|
||||||
|
"sound.option.bipbop10": "ビップボップ 10",
|
||||||
|
"sound.option.staplebops01": "ステープルボップス 01",
|
||||||
|
"sound.option.staplebops02": "ステープルボップス 02",
|
||||||
|
"sound.option.staplebops03": "ステープルボップス 03",
|
||||||
|
"sound.option.staplebops04": "ステープルボップス 04",
|
||||||
|
"sound.option.staplebops05": "ステープルボップス 05",
|
||||||
|
"sound.option.staplebops06": "ステープルボップス 06",
|
||||||
|
"sound.option.staplebops07": "ステープルボップス 07",
|
||||||
|
"sound.option.nope01": "いいえ 01",
|
||||||
|
"sound.option.nope02": "いいえ 02",
|
||||||
|
"sound.option.nope03": "いいえ 03",
|
||||||
|
"sound.option.nope04": "いいえ 04",
|
||||||
|
"sound.option.nope05": "いいえ 05",
|
||||||
|
"sound.option.nope06": "いいえ 06",
|
||||||
|
"sound.option.nope07": "いいえ 07",
|
||||||
|
"sound.option.nope08": "いいえ 08",
|
||||||
|
"sound.option.nope09": "いいえ 09",
|
||||||
|
"sound.option.nope10": "いいえ 10",
|
||||||
|
"sound.option.nope11": "いいえ 11",
|
||||||
|
"sound.option.nope12": "いいえ 12",
|
||||||
|
"sound.option.yup01": "はい 01",
|
||||||
|
"sound.option.yup02": "はい 02",
|
||||||
|
"sound.option.yup03": "はい 03",
|
||||||
|
"sound.option.yup04": "はい 04",
|
||||||
|
"sound.option.yup05": "はい 05",
|
||||||
|
"sound.option.yup06": "はい 06",
|
||||||
"settings.general.notifications.agent.title": "エージェント",
|
"settings.general.notifications.agent.title": "エージェント",
|
||||||
"settings.general.notifications.agent.description":
|
"settings.general.notifications.agent.description":
|
||||||
"エージェントが完了したか、注意が必要な場合にシステム通知を表示します",
|
"エージェントが完了したか、注意が必要な場合にシステム通知を表示します",
|
||||||
@@ -514,6 +619,13 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.providers.title": "プロバイダー",
|
"settings.providers.title": "プロバイダー",
|
||||||
"settings.providers.description": "プロバイダー設定はここで構成できます。",
|
"settings.providers.description": "プロバイダー設定はここで構成できます。",
|
||||||
|
"settings.providers.section.connected": "接続済みプロバイダー",
|
||||||
|
"settings.providers.connected.empty": "接続済みプロバイダーはありません",
|
||||||
|
"settings.providers.section.popular": "人気のプロバイダー",
|
||||||
|
"settings.providers.tag.environment": "環境",
|
||||||
|
"settings.providers.tag.config": "設定",
|
||||||
|
"settings.providers.tag.custom": "カスタム",
|
||||||
|
"settings.providers.tag.other": "その他",
|
||||||
"settings.models.title": "モデル",
|
"settings.models.title": "モデル",
|
||||||
"settings.models.description": "モデル設定はここで構成できます。",
|
"settings.models.description": "モデル設定はここで構成できます。",
|
||||||
"settings.agents.title": "エージェント",
|
"settings.agents.title": "エージェント",
|
||||||
@@ -580,6 +692,7 @@ export const dict = {
|
|||||||
"workspace.reset.failed.title": "ワークスペースのリセットに失敗しました",
|
"workspace.reset.failed.title": "ワークスペースのリセットに失敗しました",
|
||||||
"workspace.reset.success.title": "ワークスペースをリセットしました",
|
"workspace.reset.success.title": "ワークスペースをリセットしました",
|
||||||
"workspace.reset.success.description": "ワークスペースはデフォルトブランチと一致しています。",
|
"workspace.reset.success.description": "ワークスペースはデフォルトブランチと一致しています。",
|
||||||
|
"workspace.error.stillPreparing": "ワークスペースはまだ準備中です",
|
||||||
"workspace.status.checking": "未マージの変更を確認中...",
|
"workspace.status.checking": "未マージの変更を確認中...",
|
||||||
"workspace.status.error": "gitステータスを確認できません。",
|
"workspace.status.error": "gitステータスを確認できません。",
|
||||||
"workspace.status.clean": "未マージの変更は検出されませんでした。",
|
"workspace.status.clean": "未マージの変更は検出されませんでした。",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const dict = {
|
|||||||
"command.category.theme": "테마",
|
"command.category.theme": "테마",
|
||||||
"command.category.language": "언어",
|
"command.category.language": "언어",
|
||||||
"command.category.file": "파일",
|
"command.category.file": "파일",
|
||||||
|
"command.category.context": "컨텍스트",
|
||||||
"command.category.terminal": "터미널",
|
"command.category.terminal": "터미널",
|
||||||
"command.category.model": "모델",
|
"command.category.model": "모델",
|
||||||
"command.category.mcp": "MCP",
|
"command.category.mcp": "MCP",
|
||||||
@@ -19,6 +20,7 @@ export const dict = {
|
|||||||
"command.category.permissions": "권한",
|
"command.category.permissions": "권한",
|
||||||
"command.category.workspace": "작업 공간",
|
"command.category.workspace": "작업 공간",
|
||||||
|
|
||||||
|
"command.category.settings": "설정",
|
||||||
"theme.scheme.system": "시스템",
|
"theme.scheme.system": "시스템",
|
||||||
"theme.scheme.light": "라이트",
|
"theme.scheme.light": "라이트",
|
||||||
"theme.scheme.dark": "다크",
|
"theme.scheme.dark": "다크",
|
||||||
@@ -27,6 +29,7 @@ export const dict = {
|
|||||||
"command.project.open": "프로젝트 열기",
|
"command.project.open": "프로젝트 열기",
|
||||||
"command.provider.connect": "공급자 연결",
|
"command.provider.connect": "공급자 연결",
|
||||||
"command.server.switch": "서버 전환",
|
"command.server.switch": "서버 전환",
|
||||||
|
"command.settings.open": "설정 열기",
|
||||||
"command.session.previous": "이전 세션",
|
"command.session.previous": "이전 세션",
|
||||||
"command.session.next": "다음 세션",
|
"command.session.next": "다음 세션",
|
||||||
"command.session.archive": "세션 보관",
|
"command.session.archive": "세션 보관",
|
||||||
@@ -44,7 +47,10 @@ export const dict = {
|
|||||||
"command.session.new": "새 세션",
|
"command.session.new": "새 세션",
|
||||||
"command.file.open": "파일 열기",
|
"command.file.open": "파일 열기",
|
||||||
"command.file.open.description": "파일 및 명령어 검색",
|
"command.file.open.description": "파일 및 명령어 검색",
|
||||||
|
"command.context.addSelection": "선택 영역을 컨텍스트에 추가",
|
||||||
|
"command.context.addSelection.description": "현재 파일에서 선택한 줄을 추가",
|
||||||
"command.terminal.toggle": "터미널 토글",
|
"command.terminal.toggle": "터미널 토글",
|
||||||
|
"command.fileTree.toggle": "파일 트리 토글",
|
||||||
"command.review.toggle": "검토 토글",
|
"command.review.toggle": "검토 토글",
|
||||||
"command.terminal.new": "새 터미널",
|
"command.terminal.new": "새 터미널",
|
||||||
"command.terminal.new.description": "새 터미널 탭 생성",
|
"command.terminal.new.description": "새 터미널 탭 생성",
|
||||||
@@ -120,6 +126,7 @@ export const dict = {
|
|||||||
"OpenCode Zen은 코딩 에이전트를 위해 최적화된 신뢰할 수 있는 엄선된 모델에 대한 액세스를 제공합니다.",
|
"OpenCode Zen은 코딩 에이전트를 위해 최적화된 신뢰할 수 있는 엄선된 모델에 대한 액세스를 제공합니다.",
|
||||||
"provider.connect.opencodeZen.line2": "단일 API 키로 Claude, GPT, Gemini, GLM 등 다양한 모델에 액세스할 수 있습니다.",
|
"provider.connect.opencodeZen.line2": "단일 API 키로 Claude, GPT, Gemini, GLM 등 다양한 모델에 액세스할 수 있습니다.",
|
||||||
"provider.connect.opencodeZen.visit.prefix": "",
|
"provider.connect.opencodeZen.visit.prefix": "",
|
||||||
|
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
|
||||||
"provider.connect.opencodeZen.visit.suffix": "를 방문하여 API 키를 받으세요.",
|
"provider.connect.opencodeZen.visit.suffix": "를 방문하여 API 키를 받으세요.",
|
||||||
"provider.connect.oauth.code.visit.prefix": "",
|
"provider.connect.oauth.code.visit.prefix": "",
|
||||||
"provider.connect.oauth.code.visit.link": "이 링크",
|
"provider.connect.oauth.code.visit.link": "이 링크",
|
||||||
@@ -137,13 +144,32 @@ export const dict = {
|
|||||||
"provider.connect.toast.connected.title": "{{provider}} 연결됨",
|
"provider.connect.toast.connected.title": "{{provider}} 연결됨",
|
||||||
"provider.connect.toast.connected.description": "이제 {{provider}} 모델을 사용할 수 있습니다.",
|
"provider.connect.toast.connected.description": "이제 {{provider}} 모델을 사용할 수 있습니다.",
|
||||||
|
|
||||||
|
"provider.disconnect.toast.disconnected.title": "{{provider}} 연결 해제됨",
|
||||||
|
"provider.disconnect.toast.disconnected.description": "{{provider}} 모델을 더 이상 사용할 수 없습니다.",
|
||||||
"model.tag.free": "무료",
|
"model.tag.free": "무료",
|
||||||
"model.tag.latest": "최신",
|
"model.tag.latest": "최신",
|
||||||
|
|
||||||
|
"model.provider.anthropic": "Anthropic",
|
||||||
|
"model.provider.openai": "OpenAI",
|
||||||
|
"model.provider.google": "Google",
|
||||||
|
"model.provider.xai": "xAI",
|
||||||
|
"model.provider.meta": "Meta",
|
||||||
|
"model.input.text": "텍스트",
|
||||||
|
"model.input.image": "이미지",
|
||||||
|
"model.input.audio": "오디오",
|
||||||
|
"model.input.video": "비디오",
|
||||||
|
"model.input.pdf": "pdf",
|
||||||
|
"model.tooltip.allows": "지원: {{inputs}}",
|
||||||
|
"model.tooltip.reasoning.allowed": "추론 허용",
|
||||||
|
"model.tooltip.reasoning.none": "추론 없음",
|
||||||
|
"model.tooltip.context": "컨텍스트 제한 {{limit}}",
|
||||||
"common.search.placeholder": "검색",
|
"common.search.placeholder": "검색",
|
||||||
"common.goBack": "뒤로 가기",
|
"common.goBack": "뒤로 가기",
|
||||||
"common.loading": "로딩 중",
|
"common.loading": "로딩 중",
|
||||||
|
"common.loading.ellipsis": "...",
|
||||||
"common.cancel": "취소",
|
"common.cancel": "취소",
|
||||||
|
"common.connect": "연결",
|
||||||
|
"common.disconnect": "연결 해제",
|
||||||
"common.submit": "제출",
|
"common.submit": "제출",
|
||||||
"common.save": "저장",
|
"common.save": "저장",
|
||||||
"common.saving": "저장 중...",
|
"common.saving": "저장 중...",
|
||||||
@@ -152,6 +178,8 @@ export const dict = {
|
|||||||
|
|
||||||
"prompt.placeholder.shell": "셸 명령어 입력...",
|
"prompt.placeholder.shell": "셸 명령어 입력...",
|
||||||
"prompt.placeholder.normal": '무엇이든 물어보세요... "{{example}}"',
|
"prompt.placeholder.normal": '무엇이든 물어보세요... "{{example}}"',
|
||||||
|
"prompt.placeholder.summarizeComments": "댓글 요약…",
|
||||||
|
"prompt.placeholder.summarizeComment": "댓글 요약…",
|
||||||
"prompt.mode.shell": "셸",
|
"prompt.mode.shell": "셸",
|
||||||
"prompt.mode.shell.exit": "종료하려면 esc",
|
"prompt.mode.shell.exit": "종료하려면 esc",
|
||||||
|
|
||||||
@@ -255,6 +283,9 @@ export const dict = {
|
|||||||
"dialog.project.edit.color": "색상",
|
"dialog.project.edit.color": "색상",
|
||||||
"dialog.project.edit.color.select": "{{color}} 색상 선택",
|
"dialog.project.edit.color.select": "{{color}} 색상 선택",
|
||||||
|
|
||||||
|
"dialog.project.edit.worktree.startup": "작업 공간 시작 스크립트",
|
||||||
|
"dialog.project.edit.worktree.startup.description": "새 작업 공간(작업 트리)을 만든 뒤 실행됩니다.",
|
||||||
|
"dialog.project.edit.worktree.startup.placeholder": "예: bun install",
|
||||||
"context.breakdown.title": "컨텍스트 분석",
|
"context.breakdown.title": "컨텍스트 분석",
|
||||||
"context.breakdown.note": '입력 토큰의 대략적인 분석입니다. "기타"에는 도구 정의 및 오버헤드가 포함됩니다.',
|
"context.breakdown.note": '입력 토큰의 대략적인 분석입니다. "기타"에는 도구 정의 및 오버헤드가 포함됩니다.',
|
||||||
"context.breakdown.system": "시스템",
|
"context.breakdown.system": "시스템",
|
||||||
@@ -320,6 +351,9 @@ export const dict = {
|
|||||||
|
|
||||||
"toast.file.loadFailed.title": "파일 로드 실패",
|
"toast.file.loadFailed.title": "파일 로드 실패",
|
||||||
|
|
||||||
|
"toast.file.listFailed.title": "파일 목록을 불러오지 못했습니다",
|
||||||
|
"toast.context.noLineSelection.title": "줄 선택 없음",
|
||||||
|
"toast.context.noLineSelection.description": "먼저 파일 탭에서 줄 범위를 선택하세요.",
|
||||||
"toast.session.share.copyFailed.title": "URL 클립보드 복사 실패",
|
"toast.session.share.copyFailed.title": "URL 클립보드 복사 실패",
|
||||||
"toast.session.share.success.title": "세션 공유됨",
|
"toast.session.share.success.title": "세션 공유됨",
|
||||||
"toast.session.share.success.description": "공유 URL이 클립보드에 복사되었습니다!",
|
"toast.session.share.success.description": "공유 URL이 클립보드에 복사되었습니다!",
|
||||||
@@ -393,13 +427,19 @@ export const dict = {
|
|||||||
"session.tab.context": "컨텍스트",
|
"session.tab.context": "컨텍스트",
|
||||||
"session.panel.reviewAndFiles": "검토 및 파일",
|
"session.panel.reviewAndFiles": "검토 및 파일",
|
||||||
"session.review.filesChanged": "{{count}}개 파일 변경됨",
|
"session.review.filesChanged": "{{count}}개 파일 변경됨",
|
||||||
|
"session.review.change.one": "변경",
|
||||||
|
"session.review.change.other": "변경",
|
||||||
"session.review.loadingChanges": "변경 사항 로드 중...",
|
"session.review.loadingChanges": "변경 사항 로드 중...",
|
||||||
"session.review.empty": "이 세션에 변경 사항이 아직 없습니다",
|
"session.review.empty": "이 세션에 변경 사항이 아직 없습니다",
|
||||||
|
"session.review.noChanges": "변경 없음",
|
||||||
|
"session.files.selectToOpen": "열 파일을 선택하세요",
|
||||||
|
"session.files.all": "모든 파일",
|
||||||
"session.messages.renderEarlier": "이전 메시지 렌더링",
|
"session.messages.renderEarlier": "이전 메시지 렌더링",
|
||||||
"session.messages.loadingEarlier": "이전 메시지 로드 중...",
|
"session.messages.loadingEarlier": "이전 메시지 로드 중...",
|
||||||
"session.messages.loadEarlier": "이전 메시지 로드",
|
"session.messages.loadEarlier": "이전 메시지 로드",
|
||||||
"session.messages.loading": "메시지 로드 중...",
|
"session.messages.loading": "메시지 로드 중...",
|
||||||
|
|
||||||
|
"session.messages.jumpToLatest": "최신으로 이동",
|
||||||
"session.context.addToContext": "컨텍스트에 {{selection}} 추가",
|
"session.context.addToContext": "컨텍스트에 {{selection}} 추가",
|
||||||
|
|
||||||
"session.new.worktree.main": "메인 브랜치",
|
"session.new.worktree.main": "메인 브랜치",
|
||||||
@@ -440,6 +480,9 @@ export const dict = {
|
|||||||
"terminal.title.numbered": "터미널 {{number}}",
|
"terminal.title.numbered": "터미널 {{number}}",
|
||||||
"terminal.close": "터미널 닫기",
|
"terminal.close": "터미널 닫기",
|
||||||
|
|
||||||
|
"terminal.connectionLost.title": "연결 끊김",
|
||||||
|
"terminal.connectionLost.description":
|
||||||
|
"터미널 연결이 중단되었습니다. 서버가 재시작하면 이런 일이 발생할 수 있습니다.",
|
||||||
"common.closeTab": "탭 닫기",
|
"common.closeTab": "탭 닫기",
|
||||||
"common.dismiss": "닫기",
|
"common.dismiss": "닫기",
|
||||||
"common.requestFailed": "요청 실패",
|
"common.requestFailed": "요청 실패",
|
||||||
@@ -453,6 +496,8 @@ export const dict = {
|
|||||||
"common.edit": "편집",
|
"common.edit": "편집",
|
||||||
"common.loadMore": "더 불러오기",
|
"common.loadMore": "더 불러오기",
|
||||||
|
|
||||||
|
"common.key.esc": "ESC",
|
||||||
|
"sidebar.menu.toggle": "메뉴 토글",
|
||||||
"sidebar.nav.projectsAndSessions": "프로젝트 및 세션",
|
"sidebar.nav.projectsAndSessions": "프로젝트 및 세션",
|
||||||
"sidebar.settings": "설정",
|
"sidebar.settings": "설정",
|
||||||
"sidebar.help": "도움말",
|
"sidebar.help": "도움말",
|
||||||
@@ -464,7 +509,9 @@ export const dict = {
|
|||||||
"sidebar.project.recentSessions": "최근 세션",
|
"sidebar.project.recentSessions": "최근 세션",
|
||||||
"sidebar.project.viewAllSessions": "모든 세션 보기",
|
"sidebar.project.viewAllSessions": "모든 세션 보기",
|
||||||
|
|
||||||
|
"app.name.desktop": "OpenCode Desktop",
|
||||||
"settings.section.desktop": "데스크톱",
|
"settings.section.desktop": "데스크톱",
|
||||||
|
"settings.section.server": "서버",
|
||||||
"settings.tab.general": "일반",
|
"settings.tab.general": "일반",
|
||||||
"settings.tab.shortcuts": "단축키",
|
"settings.tab.shortcuts": "단축키",
|
||||||
|
|
||||||
@@ -481,6 +528,63 @@ export const dict = {
|
|||||||
"settings.general.row.font.title": "글꼴",
|
"settings.general.row.font.title": "글꼴",
|
||||||
"settings.general.row.font.description": "코드 블록에 사용되는 고정폭 글꼴 사용자 지정",
|
"settings.general.row.font.description": "코드 블록에 사용되는 고정폭 글꼴 사용자 지정",
|
||||||
|
|
||||||
|
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||||
|
"font.option.cascadiaCode": "Cascadia Code",
|
||||||
|
"font.option.firaCode": "Fira Code",
|
||||||
|
"font.option.hack": "Hack",
|
||||||
|
"font.option.inconsolata": "Inconsolata",
|
||||||
|
"font.option.intelOneMono": "Intel One Mono",
|
||||||
|
"font.option.iosevka": "Iosevka",
|
||||||
|
"font.option.jetbrainsMono": "JetBrains Mono",
|
||||||
|
"font.option.mesloLgs": "Meslo LGS",
|
||||||
|
"font.option.robotoMono": "Roboto Mono",
|
||||||
|
"font.option.sourceCodePro": "Source Code Pro",
|
||||||
|
"font.option.ubuntuMono": "Ubuntu Mono",
|
||||||
|
"sound.option.alert01": "알림 01",
|
||||||
|
"sound.option.alert02": "알림 02",
|
||||||
|
"sound.option.alert03": "알림 03",
|
||||||
|
"sound.option.alert04": "알림 04",
|
||||||
|
"sound.option.alert05": "알림 05",
|
||||||
|
"sound.option.alert06": "알림 06",
|
||||||
|
"sound.option.alert07": "알림 07",
|
||||||
|
"sound.option.alert08": "알림 08",
|
||||||
|
"sound.option.alert09": "알림 09",
|
||||||
|
"sound.option.alert10": "알림 10",
|
||||||
|
"sound.option.bipbop01": "빕-밥 01",
|
||||||
|
"sound.option.bipbop02": "빕-밥 02",
|
||||||
|
"sound.option.bipbop03": "빕-밥 03",
|
||||||
|
"sound.option.bipbop04": "빕-밥 04",
|
||||||
|
"sound.option.bipbop05": "빕-밥 05",
|
||||||
|
"sound.option.bipbop06": "빕-밥 06",
|
||||||
|
"sound.option.bipbop07": "빕-밥 07",
|
||||||
|
"sound.option.bipbop08": "빕-밥 08",
|
||||||
|
"sound.option.bipbop09": "빕-밥 09",
|
||||||
|
"sound.option.bipbop10": "빕-밥 10",
|
||||||
|
"sound.option.staplebops01": "스테이플밥스 01",
|
||||||
|
"sound.option.staplebops02": "스테이플밥스 02",
|
||||||
|
"sound.option.staplebops03": "스테이플밥스 03",
|
||||||
|
"sound.option.staplebops04": "스테이플밥스 04",
|
||||||
|
"sound.option.staplebops05": "스테이플밥스 05",
|
||||||
|
"sound.option.staplebops06": "스테이플밥스 06",
|
||||||
|
"sound.option.staplebops07": "스테이플밥스 07",
|
||||||
|
"sound.option.nope01": "아니오 01",
|
||||||
|
"sound.option.nope02": "아니오 02",
|
||||||
|
"sound.option.nope03": "아니오 03",
|
||||||
|
"sound.option.nope04": "아니오 04",
|
||||||
|
"sound.option.nope05": "아니오 05",
|
||||||
|
"sound.option.nope06": "아니오 06",
|
||||||
|
"sound.option.nope07": "아니오 07",
|
||||||
|
"sound.option.nope08": "아니오 08",
|
||||||
|
"sound.option.nope09": "아니오 09",
|
||||||
|
"sound.option.nope10": "아니오 10",
|
||||||
|
"sound.option.nope11": "아니오 11",
|
||||||
|
"sound.option.nope12": "아니오 12",
|
||||||
|
"sound.option.yup01": "네 01",
|
||||||
|
"sound.option.yup02": "네 02",
|
||||||
|
"sound.option.yup03": "네 03",
|
||||||
|
"sound.option.yup04": "네 04",
|
||||||
|
"sound.option.yup05": "네 05",
|
||||||
|
"sound.option.yup06": "네 06",
|
||||||
"settings.general.notifications.agent.title": "에이전트",
|
"settings.general.notifications.agent.title": "에이전트",
|
||||||
"settings.general.notifications.agent.description": "에이전트가 완료되거나 주의가 필요할 때 시스템 알림 표시",
|
"settings.general.notifications.agent.description": "에이전트가 완료되거나 주의가 필요할 때 시스템 알림 표시",
|
||||||
"settings.general.notifications.permissions.title": "권한",
|
"settings.general.notifications.permissions.title": "권한",
|
||||||
@@ -515,6 +619,13 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.providers.title": "공급자",
|
"settings.providers.title": "공급자",
|
||||||
"settings.providers.description": "공급자 설정은 여기서 구성할 수 있습니다.",
|
"settings.providers.description": "공급자 설정은 여기서 구성할 수 있습니다.",
|
||||||
|
"settings.providers.section.connected": "연결된 공급자",
|
||||||
|
"settings.providers.connected.empty": "연결된 공급자 없음",
|
||||||
|
"settings.providers.section.popular": "인기 공급자",
|
||||||
|
"settings.providers.tag.environment": "환경",
|
||||||
|
"settings.providers.tag.config": "구성",
|
||||||
|
"settings.providers.tag.custom": "사용자 지정",
|
||||||
|
"settings.providers.tag.other": "기타",
|
||||||
"settings.models.title": "모델",
|
"settings.models.title": "모델",
|
||||||
"settings.models.description": "모델 설정은 여기서 구성할 수 있습니다.",
|
"settings.models.description": "모델 설정은 여기서 구성할 수 있습니다.",
|
||||||
"settings.agents.title": "에이전트",
|
"settings.agents.title": "에이전트",
|
||||||
@@ -581,6 +692,7 @@ export const dict = {
|
|||||||
"workspace.reset.failed.title": "작업 공간 재설정 실패",
|
"workspace.reset.failed.title": "작업 공간 재설정 실패",
|
||||||
"workspace.reset.success.title": "작업 공간 재설정됨",
|
"workspace.reset.success.title": "작업 공간 재설정됨",
|
||||||
"workspace.reset.success.description": "작업 공간이 이제 기본 브랜치와 일치합니다.",
|
"workspace.reset.success.description": "작업 공간이 이제 기본 브랜치와 일치합니다.",
|
||||||
|
"workspace.error.stillPreparing": "작업 공간이 아직 준비 중입니다",
|
||||||
"workspace.status.checking": "병합되지 않은 변경 사항 확인 중...",
|
"workspace.status.checking": "병합되지 않은 변경 사항 확인 중...",
|
||||||
"workspace.status.error": "Git 상태를 확인할 수 없습니다.",
|
"workspace.status.error": "Git 상태를 확인할 수 없습니다.",
|
||||||
"workspace.status.clean": "병합되지 않은 변경 사항이 감지되지 않았습니다.",
|
"workspace.status.clean": "병합되지 않은 변경 사항이 감지되지 않았습니다.",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const dict = {
|
|||||||
"command.category.theme": "Tema",
|
"command.category.theme": "Tema",
|
||||||
"command.category.language": "Språk",
|
"command.category.language": "Språk",
|
||||||
"command.category.file": "Fil",
|
"command.category.file": "Fil",
|
||||||
|
"command.category.context": "Kontekst",
|
||||||
"command.category.terminal": "Terminal",
|
"command.category.terminal": "Terminal",
|
||||||
"command.category.model": "Modell",
|
"command.category.model": "Modell",
|
||||||
"command.category.mcp": "MCP",
|
"command.category.mcp": "MCP",
|
||||||
@@ -45,7 +46,10 @@ export const dict = {
|
|||||||
"command.session.new": "Ny sesjon",
|
"command.session.new": "Ny sesjon",
|
||||||
"command.file.open": "Åpne fil",
|
"command.file.open": "Åpne fil",
|
||||||
"command.file.open.description": "Søk i filer og kommandoer",
|
"command.file.open.description": "Søk i filer og kommandoer",
|
||||||
|
"command.context.addSelection": "Legg til markering i kontekst",
|
||||||
|
"command.context.addSelection.description": "Legg til valgte linjer fra gjeldende fil",
|
||||||
"command.terminal.toggle": "Veksle terminal",
|
"command.terminal.toggle": "Veksle terminal",
|
||||||
|
"command.fileTree.toggle": "Veksle filtre",
|
||||||
"command.review.toggle": "Veksle gjennomgang",
|
"command.review.toggle": "Veksle gjennomgang",
|
||||||
"command.terminal.new": "Ny terminal",
|
"command.terminal.new": "Ny terminal",
|
||||||
"command.terminal.new.description": "Opprett en ny terminalfane",
|
"command.terminal.new.description": "Opprett en ny terminalfane",
|
||||||
@@ -140,6 +144,8 @@ export const dict = {
|
|||||||
"provider.connect.toast.connected.title": "{{provider}} tilkoblet",
|
"provider.connect.toast.connected.title": "{{provider}} tilkoblet",
|
||||||
"provider.connect.toast.connected.description": "{{provider}}-modeller er nå tilgjengelige.",
|
"provider.connect.toast.connected.description": "{{provider}}-modeller er nå tilgjengelige.",
|
||||||
|
|
||||||
|
"provider.disconnect.toast.disconnected.title": "{{provider}} frakoblet",
|
||||||
|
"provider.disconnect.toast.disconnected.description": "Modeller fra {{provider}} er ikke lenger tilgjengelige.",
|
||||||
"model.tag.free": "Gratis",
|
"model.tag.free": "Gratis",
|
||||||
"model.tag.latest": "Nyeste",
|
"model.tag.latest": "Nyeste",
|
||||||
"model.provider.anthropic": "Anthropic",
|
"model.provider.anthropic": "Anthropic",
|
||||||
@@ -162,6 +168,8 @@ export const dict = {
|
|||||||
"common.loading": "Laster",
|
"common.loading": "Laster",
|
||||||
"common.loading.ellipsis": "...",
|
"common.loading.ellipsis": "...",
|
||||||
"common.cancel": "Avbryt",
|
"common.cancel": "Avbryt",
|
||||||
|
"common.connect": "Koble til",
|
||||||
|
"common.disconnect": "Koble fra",
|
||||||
"common.submit": "Send inn",
|
"common.submit": "Send inn",
|
||||||
"common.save": "Lagre",
|
"common.save": "Lagre",
|
||||||
"common.saving": "Lagrer...",
|
"common.saving": "Lagrer...",
|
||||||
@@ -170,6 +178,8 @@ export const dict = {
|
|||||||
|
|
||||||
"prompt.placeholder.shell": "Skriv inn shell-kommando...",
|
"prompt.placeholder.shell": "Skriv inn shell-kommando...",
|
||||||
"prompt.placeholder.normal": 'Spør om hva som helst... "{{example}}"',
|
"prompt.placeholder.normal": 'Spør om hva som helst... "{{example}}"',
|
||||||
|
"prompt.placeholder.summarizeComments": "Oppsummer kommentarer…",
|
||||||
|
"prompt.placeholder.summarizeComment": "Oppsummer kommentar…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
"prompt.mode.shell.exit": "ESC for å avslutte",
|
"prompt.mode.shell.exit": "ESC for å avslutte",
|
||||||
|
|
||||||
@@ -273,6 +283,9 @@ export const dict = {
|
|||||||
"dialog.project.edit.color": "Farge",
|
"dialog.project.edit.color": "Farge",
|
||||||
"dialog.project.edit.color.select": "Velg fargen {{color}}",
|
"dialog.project.edit.color.select": "Velg fargen {{color}}",
|
||||||
|
|
||||||
|
"dialog.project.edit.worktree.startup": "Oppstartsskript for arbeidsområde",
|
||||||
|
"dialog.project.edit.worktree.startup.description": "Kjører etter at et nytt arbeidsområde (worktree) er opprettet.",
|
||||||
|
"dialog.project.edit.worktree.startup.placeholder": "f.eks. bun install",
|
||||||
"context.breakdown.title": "Kontekstfordeling",
|
"context.breakdown.title": "Kontekstfordeling",
|
||||||
"context.breakdown.note": 'Omtrentlig fordeling av input-tokens. "Annet" inkluderer verktøydefinisjoner og overhead.',
|
"context.breakdown.note": 'Omtrentlig fordeling av input-tokens. "Annet" inkluderer verktøydefinisjoner og overhead.',
|
||||||
"context.breakdown.system": "System",
|
"context.breakdown.system": "System",
|
||||||
@@ -338,6 +351,9 @@ export const dict = {
|
|||||||
|
|
||||||
"toast.file.loadFailed.title": "Kunne ikke laste fil",
|
"toast.file.loadFailed.title": "Kunne ikke laste fil",
|
||||||
|
|
||||||
|
"toast.file.listFailed.title": "Kunne ikke liste filer",
|
||||||
|
"toast.context.noLineSelection.title": "Ingen linjevalg",
|
||||||
|
"toast.context.noLineSelection.description": "Velg først et linjeområde i en filfane.",
|
||||||
"toast.session.share.copyFailed.title": "Kunne ikke kopiere URL til utklippstavlen",
|
"toast.session.share.copyFailed.title": "Kunne ikke kopiere URL til utklippstavlen",
|
||||||
"toast.session.share.success.title": "Sesjon delt",
|
"toast.session.share.success.title": "Sesjon delt",
|
||||||
"toast.session.share.success.description": "Delings-URL kopiert til utklippstavlen!",
|
"toast.session.share.success.description": "Delings-URL kopiert til utklippstavlen!",
|
||||||
@@ -412,8 +428,13 @@ export const dict = {
|
|||||||
"session.tab.context": "Kontekst",
|
"session.tab.context": "Kontekst",
|
||||||
"session.panel.reviewAndFiles": "Gjennomgang og filer",
|
"session.panel.reviewAndFiles": "Gjennomgang og filer",
|
||||||
"session.review.filesChanged": "{{count}} filer endret",
|
"session.review.filesChanged": "{{count}} filer endret",
|
||||||
|
"session.review.change.one": "Endring",
|
||||||
|
"session.review.change.other": "Endringer",
|
||||||
"session.review.loadingChanges": "Laster endringer...",
|
"session.review.loadingChanges": "Laster endringer...",
|
||||||
"session.review.empty": "Ingen endringer i denne sesjonen ennå",
|
"session.review.empty": "Ingen endringer i denne sesjonen ennå",
|
||||||
|
"session.review.noChanges": "Ingen endringer",
|
||||||
|
"session.files.selectToOpen": "Velg en fil å åpne",
|
||||||
|
"session.files.all": "Alle filer",
|
||||||
"session.messages.renderEarlier": "Vis tidligere meldinger",
|
"session.messages.renderEarlier": "Vis tidligere meldinger",
|
||||||
"session.messages.loadingEarlier": "Laster inn tidligere meldinger...",
|
"session.messages.loadingEarlier": "Laster inn tidligere meldinger...",
|
||||||
"session.messages.loadEarlier": "Last inn tidligere meldinger",
|
"session.messages.loadEarlier": "Last inn tidligere meldinger",
|
||||||
@@ -471,6 +492,7 @@ export const dict = {
|
|||||||
"common.learnMore": "Lær mer",
|
"common.learnMore": "Lær mer",
|
||||||
"common.rename": "Gi nytt navn",
|
"common.rename": "Gi nytt navn",
|
||||||
"common.reset": "Tilbakestill",
|
"common.reset": "Tilbakestill",
|
||||||
|
"common.archive": "Arkiver",
|
||||||
"common.delete": "Slett",
|
"common.delete": "Slett",
|
||||||
"common.close": "Lukk",
|
"common.close": "Lukk",
|
||||||
"common.edit": "Rediger",
|
"common.edit": "Rediger",
|
||||||
@@ -489,7 +511,9 @@ export const dict = {
|
|||||||
"sidebar.project.recentSessions": "Nylige sesjoner",
|
"sidebar.project.recentSessions": "Nylige sesjoner",
|
||||||
"sidebar.project.viewAllSessions": "Vis alle sesjoner",
|
"sidebar.project.viewAllSessions": "Vis alle sesjoner",
|
||||||
|
|
||||||
|
"app.name.desktop": "OpenCode Desktop",
|
||||||
"settings.section.desktop": "Skrivebord",
|
"settings.section.desktop": "Skrivebord",
|
||||||
|
"settings.section.server": "Server",
|
||||||
"settings.tab.general": "Generelt",
|
"settings.tab.general": "Generelt",
|
||||||
"settings.tab.shortcuts": "Snarveier",
|
"settings.tab.shortcuts": "Snarveier",
|
||||||
|
|
||||||
@@ -506,6 +530,63 @@ export const dict = {
|
|||||||
"settings.general.row.font.title": "Skrift",
|
"settings.general.row.font.title": "Skrift",
|
||||||
"settings.general.row.font.description": "Tilpass mono-skriften som brukes i kodeblokker",
|
"settings.general.row.font.description": "Tilpass mono-skriften som brukes i kodeblokker",
|
||||||
|
|
||||||
|
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||||
|
"font.option.cascadiaCode": "Cascadia Code",
|
||||||
|
"font.option.firaCode": "Fira Code",
|
||||||
|
"font.option.hack": "Hack",
|
||||||
|
"font.option.inconsolata": "Inconsolata",
|
||||||
|
"font.option.intelOneMono": "Intel One Mono",
|
||||||
|
"font.option.iosevka": "Iosevka",
|
||||||
|
"font.option.jetbrainsMono": "JetBrains Mono",
|
||||||
|
"font.option.mesloLgs": "Meslo LGS",
|
||||||
|
"font.option.robotoMono": "Roboto Mono",
|
||||||
|
"font.option.sourceCodePro": "Source Code Pro",
|
||||||
|
"font.option.ubuntuMono": "Ubuntu Mono",
|
||||||
|
"sound.option.alert01": "Varsel 01",
|
||||||
|
"sound.option.alert02": "Varsel 02",
|
||||||
|
"sound.option.alert03": "Varsel 03",
|
||||||
|
"sound.option.alert04": "Varsel 04",
|
||||||
|
"sound.option.alert05": "Varsel 05",
|
||||||
|
"sound.option.alert06": "Varsel 06",
|
||||||
|
"sound.option.alert07": "Varsel 07",
|
||||||
|
"sound.option.alert08": "Varsel 08",
|
||||||
|
"sound.option.alert09": "Varsel 09",
|
||||||
|
"sound.option.alert10": "Varsel 10",
|
||||||
|
"sound.option.bipbop01": "Bip-bop 01",
|
||||||
|
"sound.option.bipbop02": "Bip-bop 02",
|
||||||
|
"sound.option.bipbop03": "Bip-bop 03",
|
||||||
|
"sound.option.bipbop04": "Bip-bop 04",
|
||||||
|
"sound.option.bipbop05": "Bip-bop 05",
|
||||||
|
"sound.option.bipbop06": "Bip-bop 06",
|
||||||
|
"sound.option.bipbop07": "Bip-bop 07",
|
||||||
|
"sound.option.bipbop08": "Bip-bop 08",
|
||||||
|
"sound.option.bipbop09": "Bip-bop 09",
|
||||||
|
"sound.option.bipbop10": "Bip-bop 10",
|
||||||
|
"sound.option.staplebops01": "Staplebops 01",
|
||||||
|
"sound.option.staplebops02": "Staplebops 02",
|
||||||
|
"sound.option.staplebops03": "Staplebops 03",
|
||||||
|
"sound.option.staplebops04": "Staplebops 04",
|
||||||
|
"sound.option.staplebops05": "Staplebops 05",
|
||||||
|
"sound.option.staplebops06": "Staplebops 06",
|
||||||
|
"sound.option.staplebops07": "Staplebops 07",
|
||||||
|
"sound.option.nope01": "Nei 01",
|
||||||
|
"sound.option.nope02": "Nei 02",
|
||||||
|
"sound.option.nope03": "Nei 03",
|
||||||
|
"sound.option.nope04": "Nei 04",
|
||||||
|
"sound.option.nope05": "Nei 05",
|
||||||
|
"sound.option.nope06": "Nei 06",
|
||||||
|
"sound.option.nope07": "Nei 07",
|
||||||
|
"sound.option.nope08": "Nei 08",
|
||||||
|
"sound.option.nope09": "Nei 09",
|
||||||
|
"sound.option.nope10": "Nei 10",
|
||||||
|
"sound.option.nope11": "Nei 11",
|
||||||
|
"sound.option.nope12": "Nei 12",
|
||||||
|
"sound.option.yup01": "Ja 01",
|
||||||
|
"sound.option.yup02": "Ja 02",
|
||||||
|
"sound.option.yup03": "Ja 03",
|
||||||
|
"sound.option.yup04": "Ja 04",
|
||||||
|
"sound.option.yup05": "Ja 05",
|
||||||
|
"sound.option.yup06": "Ja 06",
|
||||||
"settings.general.notifications.agent.title": "Agent",
|
"settings.general.notifications.agent.title": "Agent",
|
||||||
"settings.general.notifications.agent.description":
|
"settings.general.notifications.agent.description":
|
||||||
"Vis systemvarsel når agenten er ferdig eller trenger oppmerksomhet",
|
"Vis systemvarsel når agenten er ferdig eller trenger oppmerksomhet",
|
||||||
@@ -541,6 +622,13 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.providers.title": "Leverandører",
|
"settings.providers.title": "Leverandører",
|
||||||
"settings.providers.description": "Leverandørinnstillinger vil kunne konfigureres her.",
|
"settings.providers.description": "Leverandørinnstillinger vil kunne konfigureres her.",
|
||||||
|
"settings.providers.section.connected": "Tilkoblede leverandører",
|
||||||
|
"settings.providers.connected.empty": "Ingen tilkoblede leverandører",
|
||||||
|
"settings.providers.section.popular": "Populære leverandører",
|
||||||
|
"settings.providers.tag.environment": "Miljø",
|
||||||
|
"settings.providers.tag.config": "Konfigurasjon",
|
||||||
|
"settings.providers.tag.custom": "Tilpasset",
|
||||||
|
"settings.providers.tag.other": "Annet",
|
||||||
"settings.models.title": "Modeller",
|
"settings.models.title": "Modeller",
|
||||||
"settings.models.description": "Modellinnstillinger vil kunne konfigureres her.",
|
"settings.models.description": "Modellinnstillinger vil kunne konfigureres her.",
|
||||||
"settings.agents.title": "Agenter",
|
"settings.agents.title": "Agenter",
|
||||||
@@ -593,6 +681,10 @@ export const dict = {
|
|||||||
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
||||||
"settings.permissions.tool.doom_loop.description": "Oppdager gjentatte verktøykall med identisk input",
|
"settings.permissions.tool.doom_loop.description": "Oppdager gjentatte verktøykall med identisk input",
|
||||||
|
|
||||||
|
"session.delete.failed.title": "Kunne ikke slette sesjon",
|
||||||
|
"session.delete.title": "Slett sesjon",
|
||||||
|
"session.delete.confirm": 'Slette sesjonen "{{name}}"?',
|
||||||
|
"session.delete.button": "Slett sesjon",
|
||||||
"workspace.new": "Nytt arbeidsområde",
|
"workspace.new": "Nytt arbeidsområde",
|
||||||
"workspace.type.local": "lokal",
|
"workspace.type.local": "lokal",
|
||||||
"workspace.type.sandbox": "sandkasse",
|
"workspace.type.sandbox": "sandkasse",
|
||||||
@@ -603,6 +695,7 @@ export const dict = {
|
|||||||
"workspace.reset.failed.title": "Kunne ikke tilbakestille arbeidsområde",
|
"workspace.reset.failed.title": "Kunne ikke tilbakestille arbeidsområde",
|
||||||
"workspace.reset.success.title": "Arbeidsområde tilbakestilt",
|
"workspace.reset.success.title": "Arbeidsområde tilbakestilt",
|
||||||
"workspace.reset.success.description": "Arbeidsområdet samsvarer nå med standardgrenen.",
|
"workspace.reset.success.description": "Arbeidsområdet samsvarer nå med standardgrenen.",
|
||||||
|
"workspace.error.stillPreparing": "Arbeidsområdet klargjøres fortsatt",
|
||||||
"workspace.status.checking": "Sjekker for ikke-sammenslåtte endringer...",
|
"workspace.status.checking": "Sjekker for ikke-sammenslåtte endringer...",
|
||||||
"workspace.status.error": "Kunne ikke bekrefte git-status.",
|
"workspace.status.error": "Kunne ikke bekrefte git-status.",
|
||||||
"workspace.status.clean": "Ingen ikke-sammenslåtte endringer oppdaget.",
|
"workspace.status.clean": "Ingen ikke-sammenslåtte endringer oppdaget.",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const dict = {
|
|||||||
"command.category.theme": "Motyw",
|
"command.category.theme": "Motyw",
|
||||||
"command.category.language": "Język",
|
"command.category.language": "Język",
|
||||||
"command.category.file": "Plik",
|
"command.category.file": "Plik",
|
||||||
|
"command.category.context": "Kontekst",
|
||||||
"command.category.terminal": "Terminal",
|
"command.category.terminal": "Terminal",
|
||||||
"command.category.model": "Model",
|
"command.category.model": "Model",
|
||||||
"command.category.mcp": "MCP",
|
"command.category.mcp": "MCP",
|
||||||
@@ -42,7 +43,10 @@ export const dict = {
|
|||||||
"command.session.new": "Nowa sesja",
|
"command.session.new": "Nowa sesja",
|
||||||
"command.file.open": "Otwórz plik",
|
"command.file.open": "Otwórz plik",
|
||||||
"command.file.open.description": "Szukaj plików i poleceń",
|
"command.file.open.description": "Szukaj plików i poleceń",
|
||||||
|
"command.context.addSelection": "Dodaj zaznaczenie do kontekstu",
|
||||||
|
"command.context.addSelection.description": "Dodaj zaznaczone linie z bieżącego pliku",
|
||||||
"command.terminal.toggle": "Przełącz terminal",
|
"command.terminal.toggle": "Przełącz terminal",
|
||||||
|
"command.fileTree.toggle": "Przełącz drzewo plików",
|
||||||
"command.review.toggle": "Przełącz przegląd",
|
"command.review.toggle": "Przełącz przegląd",
|
||||||
"command.terminal.new": "Nowy terminal",
|
"command.terminal.new": "Nowy terminal",
|
||||||
"command.terminal.new.description": "Utwórz nową kartę terminala",
|
"command.terminal.new.description": "Utwórz nową kartę terminala",
|
||||||
@@ -137,6 +141,8 @@ export const dict = {
|
|||||||
"provider.connect.toast.connected.title": "Połączono {{provider}}",
|
"provider.connect.toast.connected.title": "Połączono {{provider}}",
|
||||||
"provider.connect.toast.connected.description": "Modele {{provider}} są teraz dostępne do użycia.",
|
"provider.connect.toast.connected.description": "Modele {{provider}} są teraz dostępne do użycia.",
|
||||||
|
|
||||||
|
"provider.disconnect.toast.disconnected.title": "Rozłączono {{provider}}",
|
||||||
|
"provider.disconnect.toast.disconnected.description": "Modele {{provider}} nie są już dostępne.",
|
||||||
"model.tag.free": "Darmowy",
|
"model.tag.free": "Darmowy",
|
||||||
"model.tag.latest": "Najnowszy",
|
"model.tag.latest": "Najnowszy",
|
||||||
"model.provider.anthropic": "Anthropic",
|
"model.provider.anthropic": "Anthropic",
|
||||||
@@ -159,6 +165,8 @@ export const dict = {
|
|||||||
"common.loading": "Ładowanie",
|
"common.loading": "Ładowanie",
|
||||||
"common.loading.ellipsis": "...",
|
"common.loading.ellipsis": "...",
|
||||||
"common.cancel": "Anuluj",
|
"common.cancel": "Anuluj",
|
||||||
|
"common.connect": "Połącz",
|
||||||
|
"common.disconnect": "Rozłącz",
|
||||||
"common.submit": "Prześlij",
|
"common.submit": "Prześlij",
|
||||||
"common.save": "Zapisz",
|
"common.save": "Zapisz",
|
||||||
"common.saving": "Zapisywanie...",
|
"common.saving": "Zapisywanie...",
|
||||||
@@ -167,6 +175,8 @@ export const dict = {
|
|||||||
|
|
||||||
"prompt.placeholder.shell": "Wpisz polecenie terminala...",
|
"prompt.placeholder.shell": "Wpisz polecenie terminala...",
|
||||||
"prompt.placeholder.normal": 'Zapytaj o cokolwiek... "{{example}}"',
|
"prompt.placeholder.normal": 'Zapytaj o cokolwiek... "{{example}}"',
|
||||||
|
"prompt.placeholder.summarizeComments": "Podsumuj komentarze…",
|
||||||
|
"prompt.placeholder.summarizeComment": "Podsumuj komentarz…",
|
||||||
"prompt.mode.shell": "Terminal",
|
"prompt.mode.shell": "Terminal",
|
||||||
"prompt.mode.shell.exit": "esc aby wyjść",
|
"prompt.mode.shell.exit": "esc aby wyjść",
|
||||||
|
|
||||||
@@ -270,6 +280,10 @@ export const dict = {
|
|||||||
"dialog.project.edit.color": "Kolor",
|
"dialog.project.edit.color": "Kolor",
|
||||||
"dialog.project.edit.color.select": "Wybierz kolor {{color}}",
|
"dialog.project.edit.color.select": "Wybierz kolor {{color}}",
|
||||||
|
|
||||||
|
"dialog.project.edit.worktree.startup": "Skrypt uruchamiania przestrzeni roboczej",
|
||||||
|
"dialog.project.edit.worktree.startup.description":
|
||||||
|
"Uruchamiany po utworzeniu nowej przestrzeni roboczej (drzewa roboczego).",
|
||||||
|
"dialog.project.edit.worktree.startup.placeholder": "np. bun install",
|
||||||
"context.breakdown.title": "Podział kontekstu",
|
"context.breakdown.title": "Podział kontekstu",
|
||||||
"context.breakdown.note": 'Przybliżony podział tokenów wejściowych. "Inne" obejmuje definicje narzędzi i narzut.',
|
"context.breakdown.note": 'Przybliżony podział tokenów wejściowych. "Inne" obejmuje definicje narzędzi i narzut.',
|
||||||
"context.breakdown.system": "System",
|
"context.breakdown.system": "System",
|
||||||
@@ -335,6 +349,9 @@ export const dict = {
|
|||||||
|
|
||||||
"toast.file.loadFailed.title": "Nie udało się załadować pliku",
|
"toast.file.loadFailed.title": "Nie udało się załadować pliku",
|
||||||
|
|
||||||
|
"toast.file.listFailed.title": "Nie udało się wyświetlić listy plików",
|
||||||
|
"toast.context.noLineSelection.title": "Brak zaznaczenia linii",
|
||||||
|
"toast.context.noLineSelection.description": "Najpierw wybierz zakres linii w zakładce pliku.",
|
||||||
"toast.session.share.copyFailed.title": "Nie udało się skopiować URL do schowka",
|
"toast.session.share.copyFailed.title": "Nie udało się skopiować URL do schowka",
|
||||||
"toast.session.share.success.title": "Sesja udostępniona",
|
"toast.session.share.success.title": "Sesja udostępniona",
|
||||||
"toast.session.share.success.description": "Link udostępniania skopiowany do schowka!",
|
"toast.session.share.success.description": "Link udostępniania skopiowany do schowka!",
|
||||||
@@ -410,8 +427,13 @@ export const dict = {
|
|||||||
"session.tab.context": "Kontekst",
|
"session.tab.context": "Kontekst",
|
||||||
"session.panel.reviewAndFiles": "Przegląd i pliki",
|
"session.panel.reviewAndFiles": "Przegląd i pliki",
|
||||||
"session.review.filesChanged": "Zmieniono {{count}} plików",
|
"session.review.filesChanged": "Zmieniono {{count}} plików",
|
||||||
|
"session.review.change.one": "Zmiana",
|
||||||
|
"session.review.change.other": "Zmiany",
|
||||||
"session.review.loadingChanges": "Ładowanie zmian...",
|
"session.review.loadingChanges": "Ładowanie zmian...",
|
||||||
"session.review.empty": "Brak zmian w tej sesji",
|
"session.review.empty": "Brak zmian w tej sesji",
|
||||||
|
"session.review.noChanges": "Brak zmian",
|
||||||
|
"session.files.selectToOpen": "Wybierz plik do otwarcia",
|
||||||
|
"session.files.all": "Wszystkie pliki",
|
||||||
"session.messages.renderEarlier": "Renderuj wcześniejsze wiadomości",
|
"session.messages.renderEarlier": "Renderuj wcześniejsze wiadomości",
|
||||||
"session.messages.loadingEarlier": "Ładowanie wcześniejszych wiadomości...",
|
"session.messages.loadingEarlier": "Ładowanie wcześniejszych wiadomości...",
|
||||||
"session.messages.loadEarlier": "Załaduj wcześniejsze wiadomości",
|
"session.messages.loadEarlier": "Załaduj wcześniejsze wiadomości",
|
||||||
@@ -488,7 +510,9 @@ export const dict = {
|
|||||||
"sidebar.project.recentSessions": "Ostatnie sesje",
|
"sidebar.project.recentSessions": "Ostatnie sesje",
|
||||||
"sidebar.project.viewAllSessions": "Zobacz wszystkie sesje",
|
"sidebar.project.viewAllSessions": "Zobacz wszystkie sesje",
|
||||||
|
|
||||||
|
"app.name.desktop": "OpenCode Desktop",
|
||||||
"settings.section.desktop": "Pulpit",
|
"settings.section.desktop": "Pulpit",
|
||||||
|
"settings.section.server": "Serwer",
|
||||||
"settings.tab.general": "Ogólne",
|
"settings.tab.general": "Ogólne",
|
||||||
"settings.tab.shortcuts": "Skróty",
|
"settings.tab.shortcuts": "Skróty",
|
||||||
|
|
||||||
@@ -510,6 +534,7 @@ export const dict = {
|
|||||||
"font.option.hack": "Hack",
|
"font.option.hack": "Hack",
|
||||||
"font.option.inconsolata": "Inconsolata",
|
"font.option.inconsolata": "Inconsolata",
|
||||||
"font.option.intelOneMono": "Intel One Mono",
|
"font.option.intelOneMono": "Intel One Mono",
|
||||||
|
"font.option.iosevka": "Iosevka",
|
||||||
"font.option.jetbrainsMono": "JetBrains Mono",
|
"font.option.jetbrainsMono": "JetBrains Mono",
|
||||||
"font.option.mesloLgs": "Meslo LGS",
|
"font.option.mesloLgs": "Meslo LGS",
|
||||||
"font.option.robotoMono": "Roboto Mono",
|
"font.option.robotoMono": "Roboto Mono",
|
||||||
@@ -597,6 +622,13 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.providers.title": "Dostawcy",
|
"settings.providers.title": "Dostawcy",
|
||||||
"settings.providers.description": "Ustawienia dostawców będą tutaj konfigurowalne.",
|
"settings.providers.description": "Ustawienia dostawców będą tutaj konfigurowalne.",
|
||||||
|
"settings.providers.section.connected": "Połączeni dostawcy",
|
||||||
|
"settings.providers.connected.empty": "Brak połączonych dostawców",
|
||||||
|
"settings.providers.section.popular": "Popularni dostawcy",
|
||||||
|
"settings.providers.tag.environment": "Środowisko",
|
||||||
|
"settings.providers.tag.config": "Konfiguracja",
|
||||||
|
"settings.providers.tag.custom": "Niestandardowe",
|
||||||
|
"settings.providers.tag.other": "Inne",
|
||||||
"settings.models.title": "Modele",
|
"settings.models.title": "Modele",
|
||||||
"settings.models.description": "Ustawienia modeli będą tutaj konfigurowalne.",
|
"settings.models.description": "Ustawienia modeli będą tutaj konfigurowalne.",
|
||||||
"settings.agents.title": "Agenci",
|
"settings.agents.title": "Agenci",
|
||||||
@@ -663,6 +695,7 @@ export const dict = {
|
|||||||
"workspace.reset.failed.title": "Nie udało się zresetować przestrzeni roboczej",
|
"workspace.reset.failed.title": "Nie udało się zresetować przestrzeni roboczej",
|
||||||
"workspace.reset.success.title": "Przestrzeń robocza zresetowana",
|
"workspace.reset.success.title": "Przestrzeń robocza zresetowana",
|
||||||
"workspace.reset.success.description": "Przestrzeń robocza odpowiada teraz domyślnej gałęzi.",
|
"workspace.reset.success.description": "Przestrzeń robocza odpowiada teraz domyślnej gałęzi.",
|
||||||
|
"workspace.error.stillPreparing": "Przestrzeń robocza jest wciąż przygotowywana",
|
||||||
"workspace.status.checking": "Sprawdzanie niezscalonych zmian...",
|
"workspace.status.checking": "Sprawdzanie niezscalonych zmian...",
|
||||||
"workspace.status.error": "Nie można zweryfikować statusu git.",
|
"workspace.status.error": "Nie można zweryfikować statusu git.",
|
||||||
"workspace.status.clean": "Nie wykryto niezscalonych zmian.",
|
"workspace.status.clean": "Nie wykryto niezscalonych zmian.",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const dict = {
|
|||||||
"command.category.theme": "Тема",
|
"command.category.theme": "Тема",
|
||||||
"command.category.language": "Язык",
|
"command.category.language": "Язык",
|
||||||
"command.category.file": "Файл",
|
"command.category.file": "Файл",
|
||||||
|
"command.category.context": "Контекст",
|
||||||
"command.category.terminal": "Терминал",
|
"command.category.terminal": "Терминал",
|
||||||
"command.category.model": "Модель",
|
"command.category.model": "Модель",
|
||||||
"command.category.mcp": "MCP",
|
"command.category.mcp": "MCP",
|
||||||
@@ -42,7 +43,10 @@ export const dict = {
|
|||||||
"command.session.new": "Новая сессия",
|
"command.session.new": "Новая сессия",
|
||||||
"command.file.open": "Открыть файл",
|
"command.file.open": "Открыть файл",
|
||||||
"command.file.open.description": "Поиск файлов и команд",
|
"command.file.open.description": "Поиск файлов и команд",
|
||||||
|
"command.context.addSelection": "Добавить выделение в контекст",
|
||||||
|
"command.context.addSelection.description": "Добавить выбранные строки из текущего файла",
|
||||||
"command.terminal.toggle": "Переключить терминал",
|
"command.terminal.toggle": "Переключить терминал",
|
||||||
|
"command.fileTree.toggle": "Переключить дерево файлов",
|
||||||
"command.review.toggle": "Переключить обзор",
|
"command.review.toggle": "Переключить обзор",
|
||||||
"command.terminal.new": "Новый терминал",
|
"command.terminal.new": "Новый терминал",
|
||||||
"command.terminal.new.description": "Создать новую вкладку терминала",
|
"command.terminal.new.description": "Создать новую вкладку терминала",
|
||||||
@@ -137,6 +141,8 @@ export const dict = {
|
|||||||
"provider.connect.toast.connected.title": "{{provider}} подключён",
|
"provider.connect.toast.connected.title": "{{provider}} подключён",
|
||||||
"provider.connect.toast.connected.description": "Модели {{provider}} теперь доступны.",
|
"provider.connect.toast.connected.description": "Модели {{provider}} теперь доступны.",
|
||||||
|
|
||||||
|
"provider.disconnect.toast.disconnected.title": "{{provider}} отключён",
|
||||||
|
"provider.disconnect.toast.disconnected.description": "Модели {{provider}} больше недоступны.",
|
||||||
"model.tag.free": "Бесплатно",
|
"model.tag.free": "Бесплатно",
|
||||||
"model.tag.latest": "Последняя",
|
"model.tag.latest": "Последняя",
|
||||||
"model.provider.anthropic": "Anthropic",
|
"model.provider.anthropic": "Anthropic",
|
||||||
@@ -159,6 +165,8 @@ export const dict = {
|
|||||||
"common.loading": "Загрузка",
|
"common.loading": "Загрузка",
|
||||||
"common.loading.ellipsis": "...",
|
"common.loading.ellipsis": "...",
|
||||||
"common.cancel": "Отмена",
|
"common.cancel": "Отмена",
|
||||||
|
"common.connect": "Подключить",
|
||||||
|
"common.disconnect": "Отключить",
|
||||||
"common.submit": "Отправить",
|
"common.submit": "Отправить",
|
||||||
"common.save": "Сохранить",
|
"common.save": "Сохранить",
|
||||||
"common.saving": "Сохранение...",
|
"common.saving": "Сохранение...",
|
||||||
@@ -167,6 +175,8 @@ export const dict = {
|
|||||||
|
|
||||||
"prompt.placeholder.shell": "Введите команду оболочки...",
|
"prompt.placeholder.shell": "Введите команду оболочки...",
|
||||||
"prompt.placeholder.normal": 'Спросите что угодно... "{{example}}"',
|
"prompt.placeholder.normal": 'Спросите что угодно... "{{example}}"',
|
||||||
|
"prompt.placeholder.summarizeComments": "Суммировать комментарии…",
|
||||||
|
"prompt.placeholder.summarizeComment": "Суммировать комментарий…",
|
||||||
"prompt.mode.shell": "Оболочка",
|
"prompt.mode.shell": "Оболочка",
|
||||||
"prompt.mode.shell.exit": "esc для выхода",
|
"prompt.mode.shell.exit": "esc для выхода",
|
||||||
|
|
||||||
@@ -270,6 +280,10 @@ export const dict = {
|
|||||||
"dialog.project.edit.color": "Цвет",
|
"dialog.project.edit.color": "Цвет",
|
||||||
"dialog.project.edit.color.select": "Выбрать цвет {{color}}",
|
"dialog.project.edit.color.select": "Выбрать цвет {{color}}",
|
||||||
|
|
||||||
|
"dialog.project.edit.worktree.startup": "Скрипт запуска рабочего пространства",
|
||||||
|
"dialog.project.edit.worktree.startup.description":
|
||||||
|
"Запускается после создания нового рабочего пространства (worktree).",
|
||||||
|
"dialog.project.edit.worktree.startup.placeholder": "например, bun install",
|
||||||
"context.breakdown.title": "Разбивка контекста",
|
"context.breakdown.title": "Разбивка контекста",
|
||||||
"context.breakdown.note":
|
"context.breakdown.note":
|
||||||
'Приблизительная разбивка входных токенов. "Другое" включает определения инструментов и накладные расходы.',
|
'Приблизительная разбивка входных токенов. "Другое" включает определения инструментов и накладные расходы.',
|
||||||
@@ -336,6 +350,9 @@ export const dict = {
|
|||||||
|
|
||||||
"toast.file.loadFailed.title": "Не удалось загрузить файл",
|
"toast.file.loadFailed.title": "Не удалось загрузить файл",
|
||||||
|
|
||||||
|
"toast.file.listFailed.title": "Не удалось получить список файлов",
|
||||||
|
"toast.context.noLineSelection.title": "Нет выделения строк",
|
||||||
|
"toast.context.noLineSelection.description": "Сначала выберите диапазон строк во вкладке файла.",
|
||||||
"toast.session.share.copyFailed.title": "Не удалось скопировать URL в буфер обмена",
|
"toast.session.share.copyFailed.title": "Не удалось скопировать URL в буфер обмена",
|
||||||
"toast.session.share.success.title": "Сессия опубликована",
|
"toast.session.share.success.title": "Сессия опубликована",
|
||||||
"toast.session.share.success.description": "URL скопирован в буфер обмена!",
|
"toast.session.share.success.description": "URL скопирован в буфер обмена!",
|
||||||
@@ -412,8 +429,13 @@ export const dict = {
|
|||||||
"session.tab.context": "Контекст",
|
"session.tab.context": "Контекст",
|
||||||
"session.panel.reviewAndFiles": "Обзор и файлы",
|
"session.panel.reviewAndFiles": "Обзор и файлы",
|
||||||
"session.review.filesChanged": "{{count}} файлов изменено",
|
"session.review.filesChanged": "{{count}} файлов изменено",
|
||||||
|
"session.review.change.one": "Изменение",
|
||||||
|
"session.review.change.other": "Изменения",
|
||||||
"session.review.loadingChanges": "Загрузка изменений...",
|
"session.review.loadingChanges": "Загрузка изменений...",
|
||||||
"session.review.empty": "Изменений в этой сессии пока нет",
|
"session.review.empty": "Изменений в этой сессии пока нет",
|
||||||
|
"session.review.noChanges": "Нет изменений",
|
||||||
|
"session.files.selectToOpen": "Выберите файл, чтобы открыть",
|
||||||
|
"session.files.all": "Все файлы",
|
||||||
"session.messages.renderEarlier": "Показать предыдущие сообщения",
|
"session.messages.renderEarlier": "Показать предыдущие сообщения",
|
||||||
"session.messages.loadingEarlier": "Загрузка предыдущих сообщений...",
|
"session.messages.loadingEarlier": "Загрузка предыдущих сообщений...",
|
||||||
"session.messages.loadEarlier": "Загрузить предыдущие сообщения",
|
"session.messages.loadEarlier": "Загрузить предыдущие сообщения",
|
||||||
@@ -491,7 +513,9 @@ export const dict = {
|
|||||||
"sidebar.project.recentSessions": "Недавние сессии",
|
"sidebar.project.recentSessions": "Недавние сессии",
|
||||||
"sidebar.project.viewAllSessions": "Посмотреть все сессии",
|
"sidebar.project.viewAllSessions": "Посмотреть все сессии",
|
||||||
|
|
||||||
|
"app.name.desktop": "OpenCode Desktop",
|
||||||
"settings.section.desktop": "Приложение",
|
"settings.section.desktop": "Приложение",
|
||||||
|
"settings.section.server": "Сервер",
|
||||||
"settings.tab.general": "Основные",
|
"settings.tab.general": "Основные",
|
||||||
"settings.tab.shortcuts": "Горячие клавиши",
|
"settings.tab.shortcuts": "Горячие клавиши",
|
||||||
|
|
||||||
@@ -513,6 +537,7 @@ export const dict = {
|
|||||||
"font.option.hack": "Hack",
|
"font.option.hack": "Hack",
|
||||||
"font.option.inconsolata": "Inconsolata",
|
"font.option.inconsolata": "Inconsolata",
|
||||||
"font.option.intelOneMono": "Intel One Mono",
|
"font.option.intelOneMono": "Intel One Mono",
|
||||||
|
"font.option.iosevka": "Iosevka",
|
||||||
"font.option.jetbrainsMono": "JetBrains Mono",
|
"font.option.jetbrainsMono": "JetBrains Mono",
|
||||||
"font.option.mesloLgs": "Meslo LGS",
|
"font.option.mesloLgs": "Meslo LGS",
|
||||||
"font.option.robotoMono": "Roboto Mono",
|
"font.option.robotoMono": "Roboto Mono",
|
||||||
@@ -600,6 +625,13 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.providers.title": "Провайдеры",
|
"settings.providers.title": "Провайдеры",
|
||||||
"settings.providers.description": "Настройки провайдеров будут доступны здесь.",
|
"settings.providers.description": "Настройки провайдеров будут доступны здесь.",
|
||||||
|
"settings.providers.section.connected": "Подключённые провайдеры",
|
||||||
|
"settings.providers.connected.empty": "Нет подключённых провайдеров",
|
||||||
|
"settings.providers.section.popular": "Популярные провайдеры",
|
||||||
|
"settings.providers.tag.environment": "Среда",
|
||||||
|
"settings.providers.tag.config": "Конфигурация",
|
||||||
|
"settings.providers.tag.custom": "Пользовательский",
|
||||||
|
"settings.providers.tag.other": "Другое",
|
||||||
"settings.models.title": "Модели",
|
"settings.models.title": "Модели",
|
||||||
"settings.models.description": "Настройки моделей будут доступны здесь.",
|
"settings.models.description": "Настройки моделей будут доступны здесь.",
|
||||||
"settings.agents.title": "Агенты",
|
"settings.agents.title": "Агенты",
|
||||||
@@ -667,6 +699,7 @@ export const dict = {
|
|||||||
"workspace.reset.failed.title": "Не удалось сбросить рабочее пространство",
|
"workspace.reset.failed.title": "Не удалось сбросить рабочее пространство",
|
||||||
"workspace.reset.success.title": "Рабочее пространство сброшено",
|
"workspace.reset.success.title": "Рабочее пространство сброшено",
|
||||||
"workspace.reset.success.description": "Рабочее пространство теперь соответствует ветке по умолчанию.",
|
"workspace.reset.success.description": "Рабочее пространство теперь соответствует ветке по умолчанию.",
|
||||||
|
"workspace.error.stillPreparing": "Рабочее пространство всё ещё готовится",
|
||||||
"workspace.status.checking": "Проверка наличия неслитых изменений...",
|
"workspace.status.checking": "Проверка наличия неслитых изменений...",
|
||||||
"workspace.status.error": "Не удалось проверить статус git.",
|
"workspace.status.error": "Не удалось проверить статус git.",
|
||||||
"workspace.status.clean": "Неслитые изменения не обнаружены.",
|
"workspace.status.clean": "Неслитые изменения не обнаружены.",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const dict = {
|
|||||||
"command.category.theme": "主题",
|
"command.category.theme": "主题",
|
||||||
"command.category.language": "语言",
|
"command.category.language": "语言",
|
||||||
"command.category.file": "文件",
|
"command.category.file": "文件",
|
||||||
|
"command.category.context": "上下文",
|
||||||
"command.category.terminal": "终端",
|
"command.category.terminal": "终端",
|
||||||
"command.category.model": "模型",
|
"command.category.model": "模型",
|
||||||
"command.category.mcp": "MCP",
|
"command.category.mcp": "MCP",
|
||||||
@@ -19,6 +20,7 @@ export const dict = {
|
|||||||
"command.category.permissions": "权限",
|
"command.category.permissions": "权限",
|
||||||
"command.category.workspace": "工作区",
|
"command.category.workspace": "工作区",
|
||||||
|
|
||||||
|
"command.category.settings": "设置",
|
||||||
"theme.scheme.system": "系统",
|
"theme.scheme.system": "系统",
|
||||||
"theme.scheme.light": "浅色",
|
"theme.scheme.light": "浅色",
|
||||||
"theme.scheme.dark": "深色",
|
"theme.scheme.dark": "深色",
|
||||||
@@ -27,6 +29,7 @@ export const dict = {
|
|||||||
"command.project.open": "打开项目",
|
"command.project.open": "打开项目",
|
||||||
"command.provider.connect": "连接提供商",
|
"command.provider.connect": "连接提供商",
|
||||||
"command.server.switch": "切换服务器",
|
"command.server.switch": "切换服务器",
|
||||||
|
"command.settings.open": "打开设置",
|
||||||
"command.session.previous": "上一个会话",
|
"command.session.previous": "上一个会话",
|
||||||
"command.session.next": "下一个会话",
|
"command.session.next": "下一个会话",
|
||||||
"command.session.archive": "归档会话",
|
"command.session.archive": "归档会话",
|
||||||
@@ -44,7 +47,10 @@ export const dict = {
|
|||||||
"command.session.new": "新建会话",
|
"command.session.new": "新建会话",
|
||||||
"command.file.open": "打开文件",
|
"command.file.open": "打开文件",
|
||||||
"command.file.open.description": "搜索文件和命令",
|
"command.file.open.description": "搜索文件和命令",
|
||||||
|
"command.context.addSelection": "将所选内容添加到上下文",
|
||||||
|
"command.context.addSelection.description": "添加当前文件中选中的行",
|
||||||
"command.terminal.toggle": "切换终端",
|
"command.terminal.toggle": "切换终端",
|
||||||
|
"command.fileTree.toggle": "切换文件树",
|
||||||
"command.review.toggle": "切换审查",
|
"command.review.toggle": "切换审查",
|
||||||
"command.terminal.new": "新建终端",
|
"command.terminal.new": "新建终端",
|
||||||
"command.terminal.new.description": "创建新的终端标签页",
|
"command.terminal.new.description": "创建新的终端标签页",
|
||||||
@@ -119,6 +125,7 @@ export const dict = {
|
|||||||
"provider.connect.opencodeZen.line1": "OpenCode Zen 为你提供一组精选的可靠优化模型,用于代码智能体。",
|
"provider.connect.opencodeZen.line1": "OpenCode Zen 为你提供一组精选的可靠优化模型,用于代码智能体。",
|
||||||
"provider.connect.opencodeZen.line2": "只需一个 API 密钥,你就能使用 Claude、GPT、Gemini、GLM 等模型。",
|
"provider.connect.opencodeZen.line2": "只需一个 API 密钥,你就能使用 Claude、GPT、Gemini、GLM 等模型。",
|
||||||
"provider.connect.opencodeZen.visit.prefix": "访问 ",
|
"provider.connect.opencodeZen.visit.prefix": "访问 ",
|
||||||
|
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
|
||||||
"provider.connect.opencodeZen.visit.suffix": " 获取你的 API 密钥。",
|
"provider.connect.opencodeZen.visit.suffix": " 获取你的 API 密钥。",
|
||||||
"provider.connect.oauth.code.visit.prefix": "访问 ",
|
"provider.connect.oauth.code.visit.prefix": "访问 ",
|
||||||
"provider.connect.oauth.code.visit.link": "此链接",
|
"provider.connect.oauth.code.visit.link": "此链接",
|
||||||
@@ -134,13 +141,32 @@ export const dict = {
|
|||||||
"provider.connect.toast.connected.title": "{{provider}} 已连接",
|
"provider.connect.toast.connected.title": "{{provider}} 已连接",
|
||||||
"provider.connect.toast.connected.description": "现在可以使用 {{provider}} 模型了。",
|
"provider.connect.toast.connected.description": "现在可以使用 {{provider}} 模型了。",
|
||||||
|
|
||||||
|
"provider.disconnect.toast.disconnected.title": "{{provider}} 已断开连接",
|
||||||
|
"provider.disconnect.toast.disconnected.description": "{{provider}} 模型已不再可用。",
|
||||||
"model.tag.free": "免费",
|
"model.tag.free": "免费",
|
||||||
"model.tag.latest": "最新",
|
"model.tag.latest": "最新",
|
||||||
|
|
||||||
|
"model.provider.anthropic": "Anthropic",
|
||||||
|
"model.provider.openai": "OpenAI",
|
||||||
|
"model.provider.google": "Google",
|
||||||
|
"model.provider.xai": "xAI",
|
||||||
|
"model.provider.meta": "Meta",
|
||||||
|
"model.input.text": "文本",
|
||||||
|
"model.input.image": "图像",
|
||||||
|
"model.input.audio": "音频",
|
||||||
|
"model.input.video": "视频",
|
||||||
|
"model.input.pdf": "pdf",
|
||||||
|
"model.tooltip.allows": "支持: {{inputs}}",
|
||||||
|
"model.tooltip.reasoning.allowed": "支持推理",
|
||||||
|
"model.tooltip.reasoning.none": "不支持推理",
|
||||||
|
"model.tooltip.context": "上下文上限 {{limit}}",
|
||||||
"common.search.placeholder": "搜索",
|
"common.search.placeholder": "搜索",
|
||||||
"common.goBack": "返回",
|
"common.goBack": "返回",
|
||||||
"common.loading": "加载中",
|
"common.loading": "加载中",
|
||||||
|
"common.loading.ellipsis": "...",
|
||||||
"common.cancel": "取消",
|
"common.cancel": "取消",
|
||||||
|
"common.connect": "连接",
|
||||||
|
"common.disconnect": "断开连接",
|
||||||
"common.submit": "提交",
|
"common.submit": "提交",
|
||||||
"common.save": "保存",
|
"common.save": "保存",
|
||||||
"common.saving": "保存中...",
|
"common.saving": "保存中...",
|
||||||
@@ -149,6 +175,8 @@ export const dict = {
|
|||||||
|
|
||||||
"prompt.placeholder.shell": "输入 shell 命令...",
|
"prompt.placeholder.shell": "输入 shell 命令...",
|
||||||
"prompt.placeholder.normal": '随便问点什么... "{{example}}"',
|
"prompt.placeholder.normal": '随便问点什么... "{{example}}"',
|
||||||
|
"prompt.placeholder.summarizeComments": "总结评论…",
|
||||||
|
"prompt.placeholder.summarizeComment": "总结该评论…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
"prompt.mode.shell.exit": "按 esc 退出",
|
"prompt.mode.shell.exit": "按 esc 退出",
|
||||||
|
|
||||||
@@ -251,6 +279,9 @@ export const dict = {
|
|||||||
"dialog.project.edit.color": "颜色",
|
"dialog.project.edit.color": "颜色",
|
||||||
"dialog.project.edit.color.select": "选择{{color}}颜色",
|
"dialog.project.edit.color.select": "选择{{color}}颜色",
|
||||||
|
|
||||||
|
"dialog.project.edit.worktree.startup": "工作区启动脚本",
|
||||||
|
"dialog.project.edit.worktree.startup.description": "在创建新的工作区 (worktree) 后运行。",
|
||||||
|
"dialog.project.edit.worktree.startup.placeholder": "例如 bun install",
|
||||||
"context.breakdown.title": "上下文拆分",
|
"context.breakdown.title": "上下文拆分",
|
||||||
"context.breakdown.note": "输入 token 的大致拆分。“其他”包含工具定义和开销。",
|
"context.breakdown.note": "输入 token 的大致拆分。“其他”包含工具定义和开销。",
|
||||||
"context.breakdown.system": "系统",
|
"context.breakdown.system": "系统",
|
||||||
@@ -316,6 +347,9 @@ export const dict = {
|
|||||||
|
|
||||||
"toast.file.loadFailed.title": "加载文件失败",
|
"toast.file.loadFailed.title": "加载文件失败",
|
||||||
|
|
||||||
|
"toast.file.listFailed.title": "列出文件失败",
|
||||||
|
"toast.context.noLineSelection.title": "未选择行",
|
||||||
|
"toast.context.noLineSelection.description": "请先在文件标签中选择行范围。",
|
||||||
"toast.session.share.copyFailed.title": "无法复制链接到剪贴板",
|
"toast.session.share.copyFailed.title": "无法复制链接到剪贴板",
|
||||||
"toast.session.share.success.title": "会话已分享",
|
"toast.session.share.success.title": "会话已分享",
|
||||||
"toast.session.share.success.description": "分享链接已复制到剪贴板",
|
"toast.session.share.success.description": "分享链接已复制到剪贴板",
|
||||||
@@ -388,13 +422,19 @@ export const dict = {
|
|||||||
"session.tab.context": "上下文",
|
"session.tab.context": "上下文",
|
||||||
"session.panel.reviewAndFiles": "审查和文件",
|
"session.panel.reviewAndFiles": "审查和文件",
|
||||||
"session.review.filesChanged": "{{count}} 个文件变更",
|
"session.review.filesChanged": "{{count}} 个文件变更",
|
||||||
|
"session.review.change.one": "更改",
|
||||||
|
"session.review.change.other": "更改",
|
||||||
"session.review.loadingChanges": "正在加载更改...",
|
"session.review.loadingChanges": "正在加载更改...",
|
||||||
"session.review.empty": "此会话暂无更改",
|
"session.review.empty": "此会话暂无更改",
|
||||||
|
"session.review.noChanges": "无更改",
|
||||||
|
"session.files.selectToOpen": "选择要打开的文件",
|
||||||
|
"session.files.all": "所有文件",
|
||||||
"session.messages.renderEarlier": "显示更早的消息",
|
"session.messages.renderEarlier": "显示更早的消息",
|
||||||
"session.messages.loadingEarlier": "正在加载更早的消息...",
|
"session.messages.loadingEarlier": "正在加载更早的消息...",
|
||||||
"session.messages.loadEarlier": "加载更早的消息",
|
"session.messages.loadEarlier": "加载更早的消息",
|
||||||
"session.messages.loading": "正在加载消息...",
|
"session.messages.loading": "正在加载消息...",
|
||||||
|
|
||||||
|
"session.messages.jumpToLatest": "跳转到最新",
|
||||||
"session.context.addToContext": "将 {{selection}} 添加到上下文",
|
"session.context.addToContext": "将 {{selection}} 添加到上下文",
|
||||||
|
|
||||||
"session.new.worktree.main": "主分支",
|
"session.new.worktree.main": "主分支",
|
||||||
@@ -434,6 +474,8 @@ export const dict = {
|
|||||||
"terminal.title.numbered": "终端 {{number}}",
|
"terminal.title.numbered": "终端 {{number}}",
|
||||||
"terminal.close": "关闭终端",
|
"terminal.close": "关闭终端",
|
||||||
|
|
||||||
|
"terminal.connectionLost.title": "连接已丢失",
|
||||||
|
"terminal.connectionLost.description": "终端连接已中断。这可能发生在服务器重启时。",
|
||||||
"common.closeTab": "关闭标签页",
|
"common.closeTab": "关闭标签页",
|
||||||
"common.dismiss": "忽略",
|
"common.dismiss": "忽略",
|
||||||
"common.requestFailed": "请求失败",
|
"common.requestFailed": "请求失败",
|
||||||
@@ -447,6 +489,8 @@ export const dict = {
|
|||||||
"common.edit": "编辑",
|
"common.edit": "编辑",
|
||||||
"common.loadMore": "加载更多",
|
"common.loadMore": "加载更多",
|
||||||
|
|
||||||
|
"common.key.esc": "ESC",
|
||||||
|
"sidebar.menu.toggle": "切换菜单",
|
||||||
"sidebar.nav.projectsAndSessions": "项目和会话",
|
"sidebar.nav.projectsAndSessions": "项目和会话",
|
||||||
"sidebar.settings": "设置",
|
"sidebar.settings": "设置",
|
||||||
"sidebar.help": "帮助",
|
"sidebar.help": "帮助",
|
||||||
@@ -458,7 +502,9 @@ export const dict = {
|
|||||||
"sidebar.project.recentSessions": "最近会话",
|
"sidebar.project.recentSessions": "最近会话",
|
||||||
"sidebar.project.viewAllSessions": "查看全部会话",
|
"sidebar.project.viewAllSessions": "查看全部会话",
|
||||||
|
|
||||||
|
"app.name.desktop": "OpenCode Desktop",
|
||||||
"settings.section.desktop": "桌面",
|
"settings.section.desktop": "桌面",
|
||||||
|
"settings.section.server": "服务器",
|
||||||
"settings.tab.general": "通用",
|
"settings.tab.general": "通用",
|
||||||
"settings.tab.shortcuts": "快捷键",
|
"settings.tab.shortcuts": "快捷键",
|
||||||
|
|
||||||
@@ -475,6 +521,63 @@ export const dict = {
|
|||||||
"settings.general.row.font.title": "字体",
|
"settings.general.row.font.title": "字体",
|
||||||
"settings.general.row.font.description": "自定义代码块使用的等宽字体",
|
"settings.general.row.font.description": "自定义代码块使用的等宽字体",
|
||||||
|
|
||||||
|
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||||
|
"font.option.cascadiaCode": "Cascadia Code",
|
||||||
|
"font.option.firaCode": "Fira Code",
|
||||||
|
"font.option.hack": "Hack",
|
||||||
|
"font.option.inconsolata": "Inconsolata",
|
||||||
|
"font.option.intelOneMono": "Intel One Mono",
|
||||||
|
"font.option.iosevka": "Iosevka",
|
||||||
|
"font.option.jetbrainsMono": "JetBrains Mono",
|
||||||
|
"font.option.mesloLgs": "Meslo LGS",
|
||||||
|
"font.option.robotoMono": "Roboto Mono",
|
||||||
|
"font.option.sourceCodePro": "Source Code Pro",
|
||||||
|
"font.option.ubuntuMono": "Ubuntu Mono",
|
||||||
|
"sound.option.alert01": "警报 01",
|
||||||
|
"sound.option.alert02": "警报 02",
|
||||||
|
"sound.option.alert03": "警报 03",
|
||||||
|
"sound.option.alert04": "警报 04",
|
||||||
|
"sound.option.alert05": "警报 05",
|
||||||
|
"sound.option.alert06": "警报 06",
|
||||||
|
"sound.option.alert07": "警报 07",
|
||||||
|
"sound.option.alert08": "警报 08",
|
||||||
|
"sound.option.alert09": "警报 09",
|
||||||
|
"sound.option.alert10": "警报 10",
|
||||||
|
"sound.option.bipbop01": "哔啵 01",
|
||||||
|
"sound.option.bipbop02": "哔啵 02",
|
||||||
|
"sound.option.bipbop03": "哔啵 03",
|
||||||
|
"sound.option.bipbop04": "哔啵 04",
|
||||||
|
"sound.option.bipbop05": "哔啵 05",
|
||||||
|
"sound.option.bipbop06": "哔啵 06",
|
||||||
|
"sound.option.bipbop07": "哔啵 07",
|
||||||
|
"sound.option.bipbop08": "哔啵 08",
|
||||||
|
"sound.option.bipbop09": "哔啵 09",
|
||||||
|
"sound.option.bipbop10": "哔啵 10",
|
||||||
|
"sound.option.staplebops01": "斯泰普博普斯 01",
|
||||||
|
"sound.option.staplebops02": "斯泰普博普斯 02",
|
||||||
|
"sound.option.staplebops03": "斯泰普博普斯 03",
|
||||||
|
"sound.option.staplebops04": "斯泰普博普斯 04",
|
||||||
|
"sound.option.staplebops05": "斯泰普博普斯 05",
|
||||||
|
"sound.option.staplebops06": "斯泰普博普斯 06",
|
||||||
|
"sound.option.staplebops07": "斯泰普博普斯 07",
|
||||||
|
"sound.option.nope01": "否 01",
|
||||||
|
"sound.option.nope02": "否 02",
|
||||||
|
"sound.option.nope03": "否 03",
|
||||||
|
"sound.option.nope04": "否 04",
|
||||||
|
"sound.option.nope05": "否 05",
|
||||||
|
"sound.option.nope06": "否 06",
|
||||||
|
"sound.option.nope07": "否 07",
|
||||||
|
"sound.option.nope08": "否 08",
|
||||||
|
"sound.option.nope09": "否 09",
|
||||||
|
"sound.option.nope10": "否 10",
|
||||||
|
"sound.option.nope11": "否 11",
|
||||||
|
"sound.option.nope12": "否 12",
|
||||||
|
"sound.option.yup01": "是 01",
|
||||||
|
"sound.option.yup02": "是 02",
|
||||||
|
"sound.option.yup03": "是 03",
|
||||||
|
"sound.option.yup04": "是 04",
|
||||||
|
"sound.option.yup05": "是 05",
|
||||||
|
"sound.option.yup06": "是 06",
|
||||||
"settings.general.notifications.agent.title": "智能体",
|
"settings.general.notifications.agent.title": "智能体",
|
||||||
"settings.general.notifications.agent.description": "当智能体完成或需要注意时显示系统通知",
|
"settings.general.notifications.agent.description": "当智能体完成或需要注意时显示系统通知",
|
||||||
"settings.general.notifications.permissions.title": "权限",
|
"settings.general.notifications.permissions.title": "权限",
|
||||||
@@ -509,6 +612,13 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.providers.title": "提供商",
|
"settings.providers.title": "提供商",
|
||||||
"settings.providers.description": "提供商设置将在此处可配置。",
|
"settings.providers.description": "提供商设置将在此处可配置。",
|
||||||
|
"settings.providers.section.connected": "已连接的提供商",
|
||||||
|
"settings.providers.connected.empty": "没有已连接的提供商",
|
||||||
|
"settings.providers.section.popular": "热门提供商",
|
||||||
|
"settings.providers.tag.environment": "环境",
|
||||||
|
"settings.providers.tag.config": "配置",
|
||||||
|
"settings.providers.tag.custom": "自定义",
|
||||||
|
"settings.providers.tag.other": "其他",
|
||||||
"settings.models.title": "模型",
|
"settings.models.title": "模型",
|
||||||
"settings.models.description": "模型设置将在此处可配置。",
|
"settings.models.description": "模型设置将在此处可配置。",
|
||||||
"settings.agents.title": "智能体",
|
"settings.agents.title": "智能体",
|
||||||
@@ -575,6 +685,7 @@ export const dict = {
|
|||||||
"workspace.reset.failed.title": "重置工作区失败",
|
"workspace.reset.failed.title": "重置工作区失败",
|
||||||
"workspace.reset.success.title": "工作区已重置",
|
"workspace.reset.success.title": "工作区已重置",
|
||||||
"workspace.reset.success.description": "工作区已与默认分支保持一致。",
|
"workspace.reset.success.description": "工作区已与默认分支保持一致。",
|
||||||
|
"workspace.error.stillPreparing": "工作区仍在准备中",
|
||||||
"workspace.status.checking": "正在检查未合并的更改...",
|
"workspace.status.checking": "正在检查未合并的更改...",
|
||||||
"workspace.status.error": "无法验证 git 状态。",
|
"workspace.status.error": "无法验证 git 状态。",
|
||||||
"workspace.status.clean": "未检测到未合并的更改。",
|
"workspace.status.clean": "未检测到未合并的更改。",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const dict = {
|
|||||||
"command.category.theme": "主題",
|
"command.category.theme": "主題",
|
||||||
"command.category.language": "語言",
|
"command.category.language": "語言",
|
||||||
"command.category.file": "檔案",
|
"command.category.file": "檔案",
|
||||||
|
"command.category.context": "上下文",
|
||||||
"command.category.terminal": "終端機",
|
"command.category.terminal": "終端機",
|
||||||
"command.category.model": "模型",
|
"command.category.model": "模型",
|
||||||
"command.category.mcp": "MCP",
|
"command.category.mcp": "MCP",
|
||||||
@@ -19,6 +20,7 @@ export const dict = {
|
|||||||
"command.category.permissions": "權限",
|
"command.category.permissions": "權限",
|
||||||
"command.category.workspace": "工作區",
|
"command.category.workspace": "工作區",
|
||||||
|
|
||||||
|
"command.category.settings": "設定",
|
||||||
"theme.scheme.system": "系統",
|
"theme.scheme.system": "系統",
|
||||||
"theme.scheme.light": "淺色",
|
"theme.scheme.light": "淺色",
|
||||||
"theme.scheme.dark": "深色",
|
"theme.scheme.dark": "深色",
|
||||||
@@ -27,6 +29,7 @@ export const dict = {
|
|||||||
"command.project.open": "開啟專案",
|
"command.project.open": "開啟專案",
|
||||||
"command.provider.connect": "連接提供者",
|
"command.provider.connect": "連接提供者",
|
||||||
"command.server.switch": "切換伺服器",
|
"command.server.switch": "切換伺服器",
|
||||||
|
"command.settings.open": "開啟設定",
|
||||||
"command.session.previous": "上一個工作階段",
|
"command.session.previous": "上一個工作階段",
|
||||||
"command.session.next": "下一個工作階段",
|
"command.session.next": "下一個工作階段",
|
||||||
"command.session.archive": "封存工作階段",
|
"command.session.archive": "封存工作階段",
|
||||||
@@ -44,7 +47,10 @@ export const dict = {
|
|||||||
"command.session.new": "新增工作階段",
|
"command.session.new": "新增工作階段",
|
||||||
"command.file.open": "開啟檔案",
|
"command.file.open": "開啟檔案",
|
||||||
"command.file.open.description": "搜尋檔案和命令",
|
"command.file.open.description": "搜尋檔案和命令",
|
||||||
|
"command.context.addSelection": "將選取內容加入上下文",
|
||||||
|
"command.context.addSelection.description": "加入目前檔案中選取的行",
|
||||||
"command.terminal.toggle": "切換終端機",
|
"command.terminal.toggle": "切換終端機",
|
||||||
|
"command.fileTree.toggle": "切換檔案樹",
|
||||||
"command.review.toggle": "切換審查",
|
"command.review.toggle": "切換審查",
|
||||||
"command.terminal.new": "新增終端機",
|
"command.terminal.new": "新增終端機",
|
||||||
"command.terminal.new.description": "建立新的終端機標籤頁",
|
"command.terminal.new.description": "建立新的終端機標籤頁",
|
||||||
@@ -136,13 +142,32 @@ export const dict = {
|
|||||||
"provider.connect.toast.connected.title": "{{provider}} 已連線",
|
"provider.connect.toast.connected.title": "{{provider}} 已連線",
|
||||||
"provider.connect.toast.connected.description": "現在可以使用 {{provider}} 模型了。",
|
"provider.connect.toast.connected.description": "現在可以使用 {{provider}} 模型了。",
|
||||||
|
|
||||||
|
"provider.disconnect.toast.disconnected.title": "{{provider}} 已中斷連線",
|
||||||
|
"provider.disconnect.toast.disconnected.description": "{{provider}} 模型已不再可用。",
|
||||||
"model.tag.free": "免費",
|
"model.tag.free": "免費",
|
||||||
"model.tag.latest": "最新",
|
"model.tag.latest": "最新",
|
||||||
|
|
||||||
|
"model.provider.anthropic": "Anthropic",
|
||||||
|
"model.provider.openai": "OpenAI",
|
||||||
|
"model.provider.google": "Google",
|
||||||
|
"model.provider.xai": "xAI",
|
||||||
|
"model.provider.meta": "Meta",
|
||||||
|
"model.input.text": "文字",
|
||||||
|
"model.input.image": "圖片",
|
||||||
|
"model.input.audio": "音訊",
|
||||||
|
"model.input.video": "影片",
|
||||||
|
"model.input.pdf": "pdf",
|
||||||
|
"model.tooltip.allows": "支援: {{inputs}}",
|
||||||
|
"model.tooltip.reasoning.allowed": "支援推理",
|
||||||
|
"model.tooltip.reasoning.none": "不支援推理",
|
||||||
|
"model.tooltip.context": "上下文上限 {{limit}}",
|
||||||
"common.search.placeholder": "搜尋",
|
"common.search.placeholder": "搜尋",
|
||||||
"common.goBack": "返回",
|
"common.goBack": "返回",
|
||||||
"common.loading": "載入中",
|
"common.loading": "載入中",
|
||||||
|
"common.loading.ellipsis": "...",
|
||||||
"common.cancel": "取消",
|
"common.cancel": "取消",
|
||||||
|
"common.connect": "連線",
|
||||||
|
"common.disconnect": "中斷連線",
|
||||||
"common.submit": "提交",
|
"common.submit": "提交",
|
||||||
"common.save": "儲存",
|
"common.save": "儲存",
|
||||||
"common.saving": "儲存中...",
|
"common.saving": "儲存中...",
|
||||||
@@ -151,6 +176,8 @@ export const dict = {
|
|||||||
|
|
||||||
"prompt.placeholder.shell": "輸入 shell 命令...",
|
"prompt.placeholder.shell": "輸入 shell 命令...",
|
||||||
"prompt.placeholder.normal": '隨便問點什麼... "{{example}}"',
|
"prompt.placeholder.normal": '隨便問點什麼... "{{example}}"',
|
||||||
|
"prompt.placeholder.summarizeComments": "摘要評論…",
|
||||||
|
"prompt.placeholder.summarizeComment": "摘要這則評論…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
"prompt.mode.shell.exit": "按 esc 退出",
|
"prompt.mode.shell.exit": "按 esc 退出",
|
||||||
|
|
||||||
@@ -253,6 +280,9 @@ export const dict = {
|
|||||||
"dialog.project.edit.color": "顏色",
|
"dialog.project.edit.color": "顏色",
|
||||||
"dialog.project.edit.color.select": "選擇{{color}}顏色",
|
"dialog.project.edit.color.select": "選擇{{color}}顏色",
|
||||||
|
|
||||||
|
"dialog.project.edit.worktree.startup": "工作區啟動腳本",
|
||||||
|
"dialog.project.edit.worktree.startup.description": "在建立新的工作區 (worktree) 後執行。",
|
||||||
|
"dialog.project.edit.worktree.startup.placeholder": "例如 bun install",
|
||||||
"context.breakdown.title": "上下文拆分",
|
"context.breakdown.title": "上下文拆分",
|
||||||
"context.breakdown.note": "輸入 token 的大致拆分。「其他」包含工具定義和額外開銷。",
|
"context.breakdown.note": "輸入 token 的大致拆分。「其他」包含工具定義和額外開銷。",
|
||||||
"context.breakdown.system": "系統",
|
"context.breakdown.system": "系統",
|
||||||
@@ -318,6 +348,9 @@ export const dict = {
|
|||||||
|
|
||||||
"toast.file.loadFailed.title": "載入檔案失敗",
|
"toast.file.loadFailed.title": "載入檔案失敗",
|
||||||
|
|
||||||
|
"toast.file.listFailed.title": "列出檔案失敗",
|
||||||
|
"toast.context.noLineSelection.title": "未選取行",
|
||||||
|
"toast.context.noLineSelection.description": "請先在檔案分頁中選取行範圍。",
|
||||||
"toast.session.share.copyFailed.title": "無法複製連結到剪貼簿",
|
"toast.session.share.copyFailed.title": "無法複製連結到剪貼簿",
|
||||||
"toast.session.share.success.title": "工作階段已分享",
|
"toast.session.share.success.title": "工作階段已分享",
|
||||||
"toast.session.share.success.description": "分享連結已複製到剪貼簿",
|
"toast.session.share.success.description": "分享連結已複製到剪貼簿",
|
||||||
@@ -390,13 +423,19 @@ export const dict = {
|
|||||||
"session.tab.context": "上下文",
|
"session.tab.context": "上下文",
|
||||||
"session.panel.reviewAndFiles": "審查與檔案",
|
"session.panel.reviewAndFiles": "審查與檔案",
|
||||||
"session.review.filesChanged": "{{count}} 個檔案變更",
|
"session.review.filesChanged": "{{count}} 個檔案變更",
|
||||||
|
"session.review.change.one": "變更",
|
||||||
|
"session.review.change.other": "變更",
|
||||||
"session.review.loadingChanges": "正在載入變更...",
|
"session.review.loadingChanges": "正在載入變更...",
|
||||||
"session.review.empty": "此工作階段暫無變更",
|
"session.review.empty": "此工作階段暫無變更",
|
||||||
|
"session.review.noChanges": "沒有變更",
|
||||||
|
"session.files.selectToOpen": "選取要開啟的檔案",
|
||||||
|
"session.files.all": "所有檔案",
|
||||||
"session.messages.renderEarlier": "顯示更早的訊息",
|
"session.messages.renderEarlier": "顯示更早的訊息",
|
||||||
"session.messages.loadingEarlier": "正在載入更早的訊息...",
|
"session.messages.loadingEarlier": "正在載入更早的訊息...",
|
||||||
"session.messages.loadEarlier": "載入更早的訊息",
|
"session.messages.loadEarlier": "載入更早的訊息",
|
||||||
"session.messages.loading": "正在載入訊息...",
|
"session.messages.loading": "正在載入訊息...",
|
||||||
|
|
||||||
|
"session.messages.jumpToLatest": "跳到最新",
|
||||||
"session.context.addToContext": "將 {{selection}} 新增到上下文",
|
"session.context.addToContext": "將 {{selection}} 新增到上下文",
|
||||||
|
|
||||||
"session.new.worktree.main": "主分支",
|
"session.new.worktree.main": "主分支",
|
||||||
@@ -436,6 +475,8 @@ export const dict = {
|
|||||||
"terminal.title.numbered": "終端機 {{number}}",
|
"terminal.title.numbered": "終端機 {{number}}",
|
||||||
"terminal.close": "關閉終端機",
|
"terminal.close": "關閉終端機",
|
||||||
|
|
||||||
|
"terminal.connectionLost.title": "連線中斷",
|
||||||
|
"terminal.connectionLost.description": "終端機連線已中斷。這可能會在伺服器重新啟動時發生。",
|
||||||
"common.closeTab": "關閉標籤頁",
|
"common.closeTab": "關閉標籤頁",
|
||||||
"common.dismiss": "忽略",
|
"common.dismiss": "忽略",
|
||||||
"common.requestFailed": "要求失敗",
|
"common.requestFailed": "要求失敗",
|
||||||
@@ -449,6 +490,8 @@ export const dict = {
|
|||||||
"common.edit": "編輯",
|
"common.edit": "編輯",
|
||||||
"common.loadMore": "載入更多",
|
"common.loadMore": "載入更多",
|
||||||
|
|
||||||
|
"common.key.esc": "ESC",
|
||||||
|
"sidebar.menu.toggle": "切換選單",
|
||||||
"sidebar.nav.projectsAndSessions": "專案與工作階段",
|
"sidebar.nav.projectsAndSessions": "專案與工作階段",
|
||||||
"sidebar.settings": "設定",
|
"sidebar.settings": "設定",
|
||||||
"sidebar.help": "說明",
|
"sidebar.help": "說明",
|
||||||
@@ -460,7 +503,9 @@ export const dict = {
|
|||||||
"sidebar.project.recentSessions": "最近工作階段",
|
"sidebar.project.recentSessions": "最近工作階段",
|
||||||
"sidebar.project.viewAllSessions": "查看全部工作階段",
|
"sidebar.project.viewAllSessions": "查看全部工作階段",
|
||||||
|
|
||||||
|
"app.name.desktop": "OpenCode Desktop",
|
||||||
"settings.section.desktop": "桌面",
|
"settings.section.desktop": "桌面",
|
||||||
|
"settings.section.server": "伺服器",
|
||||||
"settings.tab.general": "一般",
|
"settings.tab.general": "一般",
|
||||||
"settings.tab.shortcuts": "快速鍵",
|
"settings.tab.shortcuts": "快速鍵",
|
||||||
|
|
||||||
@@ -477,6 +522,63 @@ export const dict = {
|
|||||||
"settings.general.row.font.title": "字型",
|
"settings.general.row.font.title": "字型",
|
||||||
"settings.general.row.font.description": "自訂程式碼區塊使用的等寬字型",
|
"settings.general.row.font.description": "自訂程式碼區塊使用的等寬字型",
|
||||||
|
|
||||||
|
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||||
|
"font.option.cascadiaCode": "Cascadia Code",
|
||||||
|
"font.option.firaCode": "Fira Code",
|
||||||
|
"font.option.hack": "Hack",
|
||||||
|
"font.option.inconsolata": "Inconsolata",
|
||||||
|
"font.option.intelOneMono": "Intel One Mono",
|
||||||
|
"font.option.iosevka": "Iosevka",
|
||||||
|
"font.option.jetbrainsMono": "JetBrains Mono",
|
||||||
|
"font.option.mesloLgs": "Meslo LGS",
|
||||||
|
"font.option.robotoMono": "Roboto Mono",
|
||||||
|
"font.option.sourceCodePro": "Source Code Pro",
|
||||||
|
"font.option.ubuntuMono": "Ubuntu Mono",
|
||||||
|
"sound.option.alert01": "警報 01",
|
||||||
|
"sound.option.alert02": "警報 02",
|
||||||
|
"sound.option.alert03": "警報 03",
|
||||||
|
"sound.option.alert04": "警報 04",
|
||||||
|
"sound.option.alert05": "警報 05",
|
||||||
|
"sound.option.alert06": "警報 06",
|
||||||
|
"sound.option.alert07": "警報 07",
|
||||||
|
"sound.option.alert08": "警報 08",
|
||||||
|
"sound.option.alert09": "警報 09",
|
||||||
|
"sound.option.alert10": "警報 10",
|
||||||
|
"sound.option.bipbop01": "嗶啵 01",
|
||||||
|
"sound.option.bipbop02": "嗶啵 02",
|
||||||
|
"sound.option.bipbop03": "嗶啵 03",
|
||||||
|
"sound.option.bipbop04": "嗶啵 04",
|
||||||
|
"sound.option.bipbop05": "嗶啵 05",
|
||||||
|
"sound.option.bipbop06": "嗶啵 06",
|
||||||
|
"sound.option.bipbop07": "嗶啵 07",
|
||||||
|
"sound.option.bipbop08": "嗶啵 08",
|
||||||
|
"sound.option.bipbop09": "嗶啵 09",
|
||||||
|
"sound.option.bipbop10": "嗶啵 10",
|
||||||
|
"sound.option.staplebops01": "斯泰普博普斯 01",
|
||||||
|
"sound.option.staplebops02": "斯泰普博普斯 02",
|
||||||
|
"sound.option.staplebops03": "斯泰普博普斯 03",
|
||||||
|
"sound.option.staplebops04": "斯泰普博普斯 04",
|
||||||
|
"sound.option.staplebops05": "斯泰普博普斯 05",
|
||||||
|
"sound.option.staplebops06": "斯泰普博普斯 06",
|
||||||
|
"sound.option.staplebops07": "斯泰普博普斯 07",
|
||||||
|
"sound.option.nope01": "否 01",
|
||||||
|
"sound.option.nope02": "否 02",
|
||||||
|
"sound.option.nope03": "否 03",
|
||||||
|
"sound.option.nope04": "否 04",
|
||||||
|
"sound.option.nope05": "否 05",
|
||||||
|
"sound.option.nope06": "否 06",
|
||||||
|
"sound.option.nope07": "否 07",
|
||||||
|
"sound.option.nope08": "否 08",
|
||||||
|
"sound.option.nope09": "否 09",
|
||||||
|
"sound.option.nope10": "否 10",
|
||||||
|
"sound.option.nope11": "否 11",
|
||||||
|
"sound.option.nope12": "否 12",
|
||||||
|
"sound.option.yup01": "是 01",
|
||||||
|
"sound.option.yup02": "是 02",
|
||||||
|
"sound.option.yup03": "是 03",
|
||||||
|
"sound.option.yup04": "是 04",
|
||||||
|
"sound.option.yup05": "是 05",
|
||||||
|
"sound.option.yup06": "是 06",
|
||||||
"settings.general.notifications.agent.title": "代理程式",
|
"settings.general.notifications.agent.title": "代理程式",
|
||||||
"settings.general.notifications.agent.description": "當代理程式完成或需要注意時顯示系統通知",
|
"settings.general.notifications.agent.description": "當代理程式完成或需要注意時顯示系統通知",
|
||||||
"settings.general.notifications.permissions.title": "權限",
|
"settings.general.notifications.permissions.title": "權限",
|
||||||
@@ -511,6 +613,13 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.providers.title": "提供者",
|
"settings.providers.title": "提供者",
|
||||||
"settings.providers.description": "提供者設定將在此處可設定。",
|
"settings.providers.description": "提供者設定將在此處可設定。",
|
||||||
|
"settings.providers.section.connected": "已連線的提供商",
|
||||||
|
"settings.providers.connected.empty": "沒有已連線的提供商",
|
||||||
|
"settings.providers.section.popular": "熱門提供商",
|
||||||
|
"settings.providers.tag.environment": "環境",
|
||||||
|
"settings.providers.tag.config": "配置",
|
||||||
|
"settings.providers.tag.custom": "自訂",
|
||||||
|
"settings.providers.tag.other": "其他",
|
||||||
"settings.models.title": "模型",
|
"settings.models.title": "模型",
|
||||||
"settings.models.description": "模型設定將在此處可設定。",
|
"settings.models.description": "模型設定將在此處可設定。",
|
||||||
"settings.agents.title": "代理程式",
|
"settings.agents.title": "代理程式",
|
||||||
@@ -577,6 +686,7 @@ export const dict = {
|
|||||||
"workspace.reset.failed.title": "重設工作區失敗",
|
"workspace.reset.failed.title": "重設工作區失敗",
|
||||||
"workspace.reset.success.title": "工作區已重設",
|
"workspace.reset.success.title": "工作區已重設",
|
||||||
"workspace.reset.success.description": "工作區已與預設分支保持一致。",
|
"workspace.reset.success.description": "工作區已與預設分支保持一致。",
|
||||||
|
"workspace.error.stillPreparing": "工作區仍在準備中",
|
||||||
"workspace.status.checking": "正在檢查未合併的變更...",
|
"workspace.status.checking": "正在檢查未合併的變更...",
|
||||||
"workspace.status.error": "無法驗證 git 狀態。",
|
"workspace.status.error": "無法驗證 git 狀態。",
|
||||||
"workspace.status.clean": "未偵測到未合併的變更。",
|
"workspace.status.clean": "未偵測到未合併的變更。",
|
||||||
|
|||||||
@@ -55,3 +55,30 @@
|
|||||||
scrollbar-width: thin !important;
|
scrollbar-width: thin !important;
|
||||||
scrollbar-color: var(--border-weak-base) transparent !important;
|
scrollbar-color: var(--border-weak-base) transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Wider dialog variant for release notes modal */
|
||||||
|
[data-component="dialog"]:has(.dialog-release-notes) {
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
[data-slot="dialog-container"] {
|
||||||
|
width: min(100%, 720px);
|
||||||
|
height: min(100%, 400px);
|
||||||
|
margin-top: -80px;
|
||||||
|
|
||||||
|
[data-slot="dialog-content"] {
|
||||||
|
min-height: auto;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
box-shadow: var(--shadow-lg-border-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="dialog-body"] {
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ export default function Home() {
|
|||||||
const server = useServer()
|
const server = useServer()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const homedir = createMemo(() => sync.data.path.home)
|
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) {
|
function openProject(directory: string) {
|
||||||
layout.projects.open(directory)
|
layout.projects.open(directory)
|
||||||
@@ -84,11 +89,7 @@ export default function Home() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<ul class="flex flex-col gap-2">
|
<ul class="flex flex-col gap-2">
|
||||||
<For
|
<For each={recent()}>
|
||||||
each={sync.data.project
|
|
||||||
.toSorted((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
|
|
||||||
.slice(0, 5)}
|
|
||||||
>
|
|
||||||
{(project) => (
|
{(project) => (
|
||||||
<Button
|
<Button
|
||||||
size="large"
|
size="large"
|
||||||
|
|||||||
+604
-431
File diff suppressed because it is too large
Load Diff
+1044
-719
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
|||||||
import { createSignal, onCleanup } from "solid-js"
|
import { onCleanup } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
|
||||||
// Minimal types to avoid relying on non-standard DOM typings
|
// Minimal types to avoid relying on non-standard DOM typings
|
||||||
type RecognitionResult = {
|
type RecognitionResult = {
|
||||||
@@ -59,9 +60,15 @@ export function createSpeechRecognition(opts?: {
|
|||||||
typeof window !== "undefined" &&
|
typeof window !== "undefined" &&
|
||||||
Boolean((window as any).webkitSpeechRecognition || (window as any).SpeechRecognition)
|
Boolean((window as any).webkitSpeechRecognition || (window as any).SpeechRecognition)
|
||||||
|
|
||||||
const [isRecording, setIsRecording] = createSignal(false)
|
const [store, setStore] = createStore({
|
||||||
const [committed, setCommitted] = createSignal("")
|
isRecording: false,
|
||||||
const [interim, setInterim] = createSignal("")
|
committed: "",
|
||||||
|
interim: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
const isRecording = () => store.isRecording
|
||||||
|
const committed = () => store.committed
|
||||||
|
const interim = () => store.interim
|
||||||
|
|
||||||
let recognition: Recognition | undefined
|
let recognition: Recognition | undefined
|
||||||
let shouldContinue = false
|
let shouldContinue = false
|
||||||
@@ -82,7 +89,7 @@ export function createSpeechRecognition(opts?: {
|
|||||||
const nextCommitted = appendSegment(committedText, segment)
|
const nextCommitted = appendSegment(committedText, segment)
|
||||||
if (nextCommitted === committedText) return
|
if (nextCommitted === committedText) return
|
||||||
committedText = nextCommitted
|
committedText = nextCommitted
|
||||||
setCommitted(committedText)
|
setStore("committed", committedText)
|
||||||
if (opts?.onFinal) opts.onFinal(segment.trim())
|
if (opts?.onFinal) opts.onFinal(segment.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +105,7 @@ export function createSpeechRecognition(opts?: {
|
|||||||
pendingHypothesis = ""
|
pendingHypothesis = ""
|
||||||
lastInterimSuffix = ""
|
lastInterimSuffix = ""
|
||||||
shrinkCandidate = undefined
|
shrinkCandidate = undefined
|
||||||
setInterim("")
|
setStore("interim", "")
|
||||||
if (opts?.onInterim) opts.onInterim("")
|
if (opts?.onInterim) opts.onInterim("")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +114,7 @@ export function createSpeechRecognition(opts?: {
|
|||||||
pendingHypothesis = hypothesis
|
pendingHypothesis = hypothesis
|
||||||
lastInterimSuffix = suffix
|
lastInterimSuffix = suffix
|
||||||
shrinkCandidate = undefined
|
shrinkCandidate = undefined
|
||||||
setInterim(suffix)
|
setStore("interim", suffix)
|
||||||
if (opts?.onInterim) {
|
if (opts?.onInterim) {
|
||||||
opts.onInterim(suffix ? appendSegment(committedText, suffix) : "")
|
opts.onInterim(suffix ? appendSegment(committedText, suffix) : "")
|
||||||
}
|
}
|
||||||
@@ -122,7 +129,7 @@ export function createSpeechRecognition(opts?: {
|
|||||||
pendingHypothesis = ""
|
pendingHypothesis = ""
|
||||||
lastInterimSuffix = ""
|
lastInterimSuffix = ""
|
||||||
shrinkCandidate = undefined
|
shrinkCandidate = undefined
|
||||||
setInterim("")
|
setStore("interim", "")
|
||||||
if (opts?.onInterim) opts.onInterim("")
|
if (opts?.onInterim) opts.onInterim("")
|
||||||
}, COMMIT_DELAY)
|
}, COMMIT_DELAY)
|
||||||
}
|
}
|
||||||
@@ -162,7 +169,7 @@ export function createSpeechRecognition(opts?: {
|
|||||||
pendingHypothesis = ""
|
pendingHypothesis = ""
|
||||||
lastInterimSuffix = ""
|
lastInterimSuffix = ""
|
||||||
shrinkCandidate = undefined
|
shrinkCandidate = undefined
|
||||||
setInterim("")
|
setStore("interim", "")
|
||||||
if (opts?.onInterim) opts.onInterim("")
|
if (opts?.onInterim) opts.onInterim("")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -211,7 +218,7 @@ export function createSpeechRecognition(opts?: {
|
|||||||
lastInterimSuffix = ""
|
lastInterimSuffix = ""
|
||||||
shrinkCandidate = undefined
|
shrinkCandidate = undefined
|
||||||
if (e.error === "no-speech" && shouldContinue) {
|
if (e.error === "no-speech" && shouldContinue) {
|
||||||
setInterim("")
|
setStore("interim", "")
|
||||||
if (opts?.onInterim) opts.onInterim("")
|
if (opts?.onInterim) opts.onInterim("")
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
@@ -221,7 +228,7 @@ export function createSpeechRecognition(opts?: {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
shouldContinue = false
|
shouldContinue = false
|
||||||
setIsRecording(false)
|
setStore("isRecording", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
recognition.onstart = () => {
|
recognition.onstart = () => {
|
||||||
@@ -230,16 +237,16 @@ export function createSpeechRecognition(opts?: {
|
|||||||
cancelPendingCommit()
|
cancelPendingCommit()
|
||||||
lastInterimSuffix = ""
|
lastInterimSuffix = ""
|
||||||
shrinkCandidate = undefined
|
shrinkCandidate = undefined
|
||||||
setInterim("")
|
setStore("interim", "")
|
||||||
if (opts?.onInterim) opts.onInterim("")
|
if (opts?.onInterim) opts.onInterim("")
|
||||||
setIsRecording(true)
|
setStore("isRecording", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
recognition.onend = () => {
|
recognition.onend = () => {
|
||||||
cancelPendingCommit()
|
cancelPendingCommit()
|
||||||
lastInterimSuffix = ""
|
lastInterimSuffix = ""
|
||||||
shrinkCandidate = undefined
|
shrinkCandidate = undefined
|
||||||
setIsRecording(false)
|
setStore("isRecording", false)
|
||||||
if (shouldContinue) {
|
if (shouldContinue) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
@@ -258,7 +265,7 @@ export function createSpeechRecognition(opts?: {
|
|||||||
cancelPendingCommit()
|
cancelPendingCommit()
|
||||||
lastInterimSuffix = ""
|
lastInterimSuffix = ""
|
||||||
shrinkCandidate = undefined
|
shrinkCandidate = undefined
|
||||||
setInterim("")
|
setStore("interim", "")
|
||||||
try {
|
try {
|
||||||
recognition.start()
|
recognition.start()
|
||||||
} catch {}
|
} catch {}
|
||||||
@@ -271,7 +278,7 @@ export function createSpeechRecognition(opts?: {
|
|||||||
cancelPendingCommit()
|
cancelPendingCommit()
|
||||||
lastInterimSuffix = ""
|
lastInterimSuffix = ""
|
||||||
shrinkCandidate = undefined
|
shrinkCandidate = undefined
|
||||||
setInterim("")
|
setStore("interim", "")
|
||||||
if (opts?.onInterim) opts.onInterim("")
|
if (opts?.onInterim) opts.onInterim("")
|
||||||
try {
|
try {
|
||||||
recognition.stop()
|
recognition.stop()
|
||||||
@@ -284,7 +291,7 @@ export function createSpeechRecognition(opts?: {
|
|||||||
cancelPendingCommit()
|
cancelPendingCommit()
|
||||||
lastInterimSuffix = ""
|
lastInterimSuffix = ""
|
||||||
shrinkCandidate = undefined
|
shrinkCandidate = undefined
|
||||||
setInterim("")
|
setStore("interim", "")
|
||||||
if (opts?.onInterim) opts.onInterim("")
|
if (opts?.onInterim) opts.onInterim("")
|
||||||
try {
|
try {
|
||||||
recognition?.stop()
|
recognition?.stop()
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import { query } from "@solidjs/router"
|
||||||
|
|
||||||
|
type Release = {
|
||||||
|
tag_name: string
|
||||||
|
name: string
|
||||||
|
body: string
|
||||||
|
published_at: string
|
||||||
|
html_url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HighlightMedia =
|
||||||
|
| { type: "video"; src: string }
|
||||||
|
| { type: "image"; src: string; width: string; height: string }
|
||||||
|
|
||||||
|
export type HighlightItem = {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
shortDescription?: string
|
||||||
|
media: HighlightMedia
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HighlightGroup = {
|
||||||
|
source: string
|
||||||
|
items: HighlightItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChangelogRelease = {
|
||||||
|
tag: string
|
||||||
|
name: string
|
||||||
|
date: string
|
||||||
|
url: string
|
||||||
|
highlights: HighlightGroup[]
|
||||||
|
sections: { title: string; items: string[] }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChangelogData = {
|
||||||
|
ok: boolean
|
||||||
|
releases: ChangelogRelease[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadChangelog(): Promise<ChangelogData> {
|
||||||
|
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 RequestInit).catch(() => undefined)
|
||||||
|
|
||||||
|
if (!response?.ok) return { ok: false, releases: [] }
|
||||||
|
|
||||||
|
const data = await response.json().catch(() => undefined)
|
||||||
|
if (!Array.isArray(data)) return { ok: false, releases: [] }
|
||||||
|
|
||||||
|
const releases = (data as Release[]).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,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return { ok: true, releases }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const changelog = query(async () => {
|
||||||
|
"use server"
|
||||||
|
const result = await loadChangelog()
|
||||||
|
return result.releases
|
||||||
|
}, "changelog")
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
const media = (() => {
|
||||||
|
if (videoMatch) return { type: "video", src: videoMatch[1] } satisfies HighlightMedia
|
||||||
|
if (imgMatch) {
|
||||||
|
return {
|
||||||
|
type: "image",
|
||||||
|
src: imgMatch[3],
|
||||||
|
width: imgMatch[1],
|
||||||
|
height: imgMatch[2],
|
||||||
|
} satisfies HighlightMedia
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
if (!titleMatch || !media) continue
|
||||||
|
|
||||||
|
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)
|
||||||
|
current = { title: line.slice(3).trim(), items: [] }
|
||||||
|
skip = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith("**Thank you")) {
|
||||||
|
skip = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith("- ") && !skip) current?.items.push(line.slice(2).trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current) sections.push(current)
|
||||||
|
return { sections, highlights: parseHighlights(body) }
|
||||||
|
}
|
||||||
@@ -1,114 +1,30 @@
|
|||||||
type Release = {
|
import { loadChangelog } from "~/lib/changelog"
|
||||||
tag_name: string
|
|
||||||
name: string
|
const cors = {
|
||||||
body: string
|
"Access-Control-Allow-Origin": "*",
|
||||||
published_at: string
|
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
||||||
html_url: string
|
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
||||||
}
|
}
|
||||||
|
|
||||||
type HighlightMedia = { type: "video"; src: string } | { type: "image"; src: string; width: string; height: string }
|
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"
|
||||||
type HighlightItem = {
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
shortDescription?: string
|
|
||||||
media: HighlightMedia
|
|
||||||
}
|
|
||||||
|
|
||||||
type HighlightGroup = {
|
|
||||||
source: string
|
|
||||||
items: HighlightItem[]
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
export async function GET() {
|
||||||
const response = await fetch("https://api.github.com/repos/anomalyco/opencode/releases?per_page=20", {
|
const result = await loadChangelog().catch(() => ({ ok: false, releases: [] }))
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({ releases: result.releases }), {
|
||||||
|
status: result.ok ? 200 : 503,
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/vnd.github.v3+json",
|
"Content-Type": "application/json",
|
||||||
"User-Agent": "OpenCode-Console",
|
"Cache-Control": result.ok ? ok : error,
|
||||||
|
...cors,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
export async function OPTIONS() {
|
||||||
return { releases: [] }
|
return new Response(null, {
|
||||||
}
|
status: 200,
|
||||||
|
headers: cors,
|
||||||
const releases = (await response.json()) as Release[]
|
})
|
||||||
|
|
||||||
return {
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -371,7 +371,7 @@
|
|||||||
top: 80px;
|
top: 80px;
|
||||||
align-self: start;
|
align-self: start;
|
||||||
background: var(--color-background);
|
background: var(--color-background);
|
||||||
padding: 8px 0;
|
padding: 44px 0 8px;
|
||||||
|
|
||||||
@media (max-width: 50rem) {
|
@media (max-width: 50rem) {
|
||||||
position: static;
|
position: static;
|
||||||
|
|||||||
@@ -1,36 +1,14 @@
|
|||||||
import "./index.css"
|
import "./index.css"
|
||||||
import { Title, Meta, Link } from "@solidjs/meta"
|
import { Title, Meta, Link } from "@solidjs/meta"
|
||||||
import { createAsync, query } from "@solidjs/router"
|
import { createAsync } from "@solidjs/router"
|
||||||
import { Header } from "~/component/header"
|
import { Header } from "~/component/header"
|
||||||
import { Footer } from "~/component/footer"
|
import { Footer } from "~/component/footer"
|
||||||
import { Legal } from "~/component/legal"
|
import { Legal } from "~/component/legal"
|
||||||
import { config } from "~/config"
|
import { config } from "~/config"
|
||||||
|
import { changelog } from "~/lib/changelog"
|
||||||
|
import type { HighlightGroup } from "~/lib/changelog"
|
||||||
import { For, Show, createSignal } from "solid-js"
|
import { For, Show, createSignal } from "solid-js"
|
||||||
|
|
||||||
type Release = {
|
|
||||||
tag_name: string
|
|
||||||
name: string
|
|
||||||
body: string
|
|
||||||
published_at: string
|
|
||||||
html_url: string
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
function formatDate(dateString: string) {
|
||||||
const date = new Date(dateString)
|
const date = new Date(dateString)
|
||||||
return date.toLocaleDateString("en-US", {
|
return date.toLocaleDateString("en-US", {
|
||||||
@@ -40,84 +18,6 @@ function formatDate(dateString: 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[]
|
|
||||||
}
|
|
||||||
|
|
||||||
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 }
|
|
||||||
}
|
|
||||||
|
|
||||||
function ReleaseItem(props: { item: string }) {
|
function ReleaseItem(props: { item: string }) {
|
||||||
const parts = () => {
|
const parts = () => {
|
||||||
const match = props.item.match(/^(.+?)(\s*\(@([\w-]+)\))?$/)
|
const match = props.item.match(/^(.+?)(\s*\(@([\w-]+)\))?$/)
|
||||||
@@ -197,7 +97,8 @@ function CollapsibleSections(props: { sections: { title: string; items: string[]
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Changelog() {
|
export default function Changelog() {
|
||||||
const releases = createAsync(() => getReleases())
|
const data = createAsync(() => changelog())
|
||||||
|
const releases = () => data() ?? []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main data-page="changelog">
|
<main data-page="changelog">
|
||||||
@@ -215,30 +116,34 @@ export default function Changelog() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section data-component="releases">
|
<section data-component="releases">
|
||||||
|
<Show when={releases().length === 0}>
|
||||||
|
<p>
|
||||||
|
No changelog entries found. <a href="/changelog.json">View JSON</a>
|
||||||
|
</p>
|
||||||
|
</Show>
|
||||||
<For each={releases()}>
|
<For each={releases()}>
|
||||||
{(release) => {
|
{(release) => {
|
||||||
const parsed = () => parseMarkdown(release.body || "")
|
|
||||||
return (
|
return (
|
||||||
<article data-component="release">
|
<article data-component="release">
|
||||||
<header>
|
<header>
|
||||||
<div data-slot="version">
|
<div data-slot="version">
|
||||||
<a href={release.html_url} target="_blank" rel="noopener noreferrer">
|
<a href={release.url} target="_blank" rel="noopener noreferrer">
|
||||||
{release.tag_name}
|
{release.tag}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<time dateTime={release.published_at}>{formatDate(release.published_at)}</time>
|
<time dateTime={release.date}>{formatDate(release.date)}</time>
|
||||||
</header>
|
</header>
|
||||||
<div data-slot="content">
|
<div data-slot="content">
|
||||||
<Show when={parsed().highlights.length > 0}>
|
<Show when={release.highlights.length > 0}>
|
||||||
<div data-component="highlights">
|
<div data-component="highlights">
|
||||||
<For each={parsed().highlights}>{(group) => <HighlightSection group={group} />}</For>
|
<For each={release.highlights}>{(group) => <HighlightSection group={group} />}</For>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={parsed().highlights.length > 0 && parsed().sections.length > 0}>
|
<Show when={release.highlights.length > 0 && release.sections.length > 0}>
|
||||||
<CollapsibleSections sections={parsed().sections} />
|
<CollapsibleSections sections={release.sections} />
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={parsed().highlights.length === 0}>
|
<Show when={release.highlights.length === 0}>
|
||||||
<For each={parsed().sections}>
|
<For each={release.sections}>
|
||||||
{(section) => (
|
{(section) => (
|
||||||
<div data-component="section">
|
<div data-component="section">
|
||||||
<h3>{section.title}</h3>
|
<h3>{section.title}</h3>
|
||||||
@@ -255,9 +160,9 @@ export default function Changelog() {
|
|||||||
}}
|
}}
|
||||||
</For>
|
</For>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Footer />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Legal />
|
<Legal />
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root" class="flex flex-col h-dvh"></div>
|
<div id="root" class="flex flex-col h-dvh p-px"></div>
|
||||||
<div data-tauri-decorum-tb class="w-0 h-0 hidden" />
|
<div data-tauri-decorum-tb class="w-0 h-0 hidden" />
|
||||||
<script src="/src/index.tsx" type="module"></script>
|
<script src="/src/index.tsx" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Generated
+2
@@ -3028,6 +3028,8 @@ dependencies = [
|
|||||||
"futures",
|
"futures",
|
||||||
"gtk",
|
"gtk",
|
||||||
"listeners",
|
"listeners",
|
||||||
|
"objc2 0.6.3",
|
||||||
|
"objc2-web-kit",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ comrak = { version = "0.50", default-features = false }
|
|||||||
gtk = "0.18.2"
|
gtk = "0.18.2"
|
||||||
webkit2gtk = "=2.0.1"
|
webkit2gtk = "=2.0.1"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
objc2 = "0.6"
|
||||||
|
objc2-web-kit = "0.3"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows = { version = "0.61", features = [
|
windows = { version = "0.61", features = [
|
||||||
"Win32_Foundation",
|
"Win32_Foundation",
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ pub fn create_command(app: &tauri::AppHandle, args: &str) -> Command {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.args(args.split_whitespace())
|
.args(args.split_whitespace())
|
||||||
.env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true")
|
.env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true")
|
||||||
|
.env("OPENCODE_EXPERIMENTAL_FILEWATCHER", "true")
|
||||||
.env("OPENCODE_CLIENT", "desktop")
|
.env("OPENCODE_CLIENT", "desktop")
|
||||||
.env("XDG_STATE_HOME", &state_dir);
|
.env("XDG_STATE_HOME", &state_dir);
|
||||||
|
|
||||||
@@ -174,6 +175,7 @@ pub fn create_command(app: &tauri::AppHandle, args: &str) -> Command {
|
|||||||
app.shell()
|
app.shell()
|
||||||
.command(&shell)
|
.command(&shell)
|
||||||
.env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true")
|
.env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true")
|
||||||
|
.env("OPENCODE_EXPERIMENTAL_FILEWATCHER", "true")
|
||||||
.env("OPENCODE_CLIENT", "desktop")
|
.env("OPENCODE_CLIENT", "desktop")
|
||||||
.env("XDG_STATE_HOME", &state_dir)
|
.env("XDG_STATE_HOME", &state_dir)
|
||||||
.args(["-il", "-c", &cmd])
|
.args(["-il", "-c", &cmd])
|
||||||
|
|||||||
@@ -29,6 +29,18 @@ impl<R: Runtime> Plugin<R> for PinchZoomDisablePlugin {
|
|||||||
gobject_ffi::g_signal_handlers_destroy(data.as_ptr().cast());
|
gobject_ffi::g_signal_handlers_destroy(data.as_ptr().cast());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
unsafe {
|
||||||
|
use objc2::rc::Retained;
|
||||||
|
use objc2_web_kit::WKWebView;
|
||||||
|
|
||||||
|
// Get the WKWebView pointer and disable magnification gestures
|
||||||
|
// This prevents Cmd+Ctrl+scroll and pinch-to-zoom from changing the zoom level
|
||||||
|
let wk_webview: Retained<WKWebView> =
|
||||||
|
Retained::retain(_webview.inner().cast()).unwrap();
|
||||||
|
wk_webview.setAllowsMagnification(false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { createStore } from "solid-js/store"
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
import NotFound from "../[...404]"
|
import NotFound from "../[...404]"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
|
import { MessageNav } from "@opencode-ai/ui/message-nav"
|
||||||
import { preloadMultiFileDiff, PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
|
import { preloadMultiFileDiff, PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
|
||||||
import { Diff as SSRDiff } from "@opencode-ai/ui/diff-ssr"
|
import { Diff as SSRDiff } from "@opencode-ai/ui/diff-ssr"
|
||||||
import { clientOnly } from "@solidjs/start"
|
import { clientOnly } from "@solidjs/start"
|
||||||
@@ -362,6 +363,15 @@ export default function () {
|
|||||||
{title()}
|
{title()}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-start justify-start h-full min-h-0">
|
<div class="flex items-start justify-start h-full min-h-0">
|
||||||
|
<Show when={messages().length > 1}>
|
||||||
|
<MessageNav
|
||||||
|
class="sticky top-0 shrink-0 py-2 pl-4"
|
||||||
|
messages={messages()}
|
||||||
|
current={activeMessage()}
|
||||||
|
size="compact"
|
||||||
|
onMessageSelect={setActiveMessage}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
<SessionTurn
|
<SessionTurn
|
||||||
sessionID={data().sessionID}
|
sessionID={data().sessionID}
|
||||||
messageID={store.messageId ?? firstUserMessage()!.id!}
|
messageID={store.messageId ?? firstUserMessage()!.id!}
|
||||||
|
|||||||
@@ -3,5 +3,3 @@ preload = ["@opentui/solid/preload"]
|
|||||||
[test]
|
[test]
|
||||||
preload = ["./test/preload.ts"]
|
preload = ["./test/preload.ts"]
|
||||||
timeout = 10000 # 10 seconds (default is 5000ms)
|
timeout = 10000 # 10 seconds (default is 5000ms)
|
||||||
# Enable code coverage
|
|
||||||
coverage = true
|
|
||||||
|
|||||||
@@ -82,8 +82,8 @@
|
|||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
"@openrouter/ai-sdk-provider": "1.5.2",
|
"@openrouter/ai-sdk-provider": "1.5.2",
|
||||||
"@opentui/core": "0.1.74",
|
"@opentui/core": "0.1.75",
|
||||||
"@opentui/solid": "0.1.74",
|
"@opentui/solid": "0.1.75",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ export namespace Agent {
|
|||||||
const result: Record<string, Info> = {
|
const result: Record<string, Info> = {
|
||||||
build: {
|
build: {
|
||||||
name: "build",
|
name: "build",
|
||||||
|
description: "The default agent. Executes tools based on configured permissions.",
|
||||||
options: {},
|
options: {},
|
||||||
permission: PermissionNext.merge(
|
permission: PermissionNext.merge(
|
||||||
defaults,
|
defaults,
|
||||||
@@ -87,6 +88,7 @@ export namespace Agent {
|
|||||||
},
|
},
|
||||||
plan: {
|
plan: {
|
||||||
name: "plan",
|
name: "plan",
|
||||||
|
description: "Plan mode. Disallows all edit tools.",
|
||||||
options: {},
|
options: {},
|
||||||
permission: PermissionNext.merge(
|
permission: PermissionNext.merge(
|
||||||
defaults,
|
defaults,
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ async function createToolContext(agent: Agent.Info) {
|
|||||||
callID: Identifier.ascending("part"),
|
callID: Identifier.ascending("part"),
|
||||||
agent: agent.name,
|
agent: agent.name,
|
||||||
abort: new AbortController().signal,
|
abort: new AbortController().signal,
|
||||||
|
messages: [],
|
||||||
metadata: () => {},
|
metadata: () => {},
|
||||||
async ask(req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) {
|
async ask(req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) {
|
||||||
for (const pattern of req.patterns) {
|
for (const pattern of req.patterns) {
|
||||||
|
|||||||
@@ -570,6 +570,16 @@ function App() {
|
|||||||
dialog.clear()
|
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(() => {
|
createEffect(() => {
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ export function Session() {
|
|||||||
const [showDetails, setShowDetails] = kv.signal("tool_details_visibility", true)
|
const [showDetails, setShowDetails] = kv.signal("tool_details_visibility", true)
|
||||||
const [showAssistantMetadata, setShowAssistantMetadata] = kv.signal("assistant_metadata_visibility", true)
|
const [showAssistantMetadata, setShowAssistantMetadata] = kv.signal("assistant_metadata_visibility", true)
|
||||||
const [showScrollbar, setShowScrollbar] = kv.signal("scrollbar_visible", false)
|
const [showScrollbar, setShowScrollbar] = kv.signal("scrollbar_visible", false)
|
||||||
const [diffWrapMode, setDiffWrapMode] = createSignal<"word" | "none">("word")
|
const [diffWrapMode] = kv.signal<"word" | "none">("diff_wrap_mode", "word")
|
||||||
const [animationsEnabled, setAnimationsEnabled] = kv.signal("animations_enabled", true)
|
const [animationsEnabled, setAnimationsEnabled] = kv.signal("animations_enabled", true)
|
||||||
|
|
||||||
const wide = createMemo(() => dimensions().width > 120)
|
const wide = createMemo(() => dimensions().width > 120)
|
||||||
@@ -503,7 +503,7 @@ export function Session() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Toggle code concealment",
|
title: conceal() ? "Disable code concealment" : "Enable code concealment",
|
||||||
value: "session.toggle.conceal",
|
value: "session.toggle.conceal",
|
||||||
keybind: "messages_toggle_conceal" as any,
|
keybind: "messages_toggle_conceal" as any,
|
||||||
category: "Session",
|
category: "Session",
|
||||||
@@ -538,18 +538,6 @@ export function Session() {
|
|||||||
dialog.clear()
|
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",
|
title: showDetails() ? "Hide tool details" : "Show tool details",
|
||||||
value: "session.toggle.actions",
|
value: "session.toggle.actions",
|
||||||
@@ -1705,10 +1693,29 @@ function Glob(props: ToolProps<typeof GlobTool>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Read(props: ToolProps<typeof ReadTool>) {
|
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 (
|
return (
|
||||||
<InlineTool icon="→" pending="Reading file..." complete={props.input.filePath} part={props.part}>
|
<>
|
||||||
Read {normalizePath(props.input.filePath!)} {input(props.input, ["filePath"])}
|
<InlineTool icon="→" pending="Reading file..." complete={props.input.filePath} part={props.part}>
|
||||||
</InlineTool>
|
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>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -206,7 +206,11 @@ export namespace File {
|
|||||||
const project = Instance.project
|
const project = Instance.project
|
||||||
if (project.vcs !== "git") return []
|
if (project.vcs !== "git") return []
|
||||||
|
|
||||||
const diffOutput = await $`git diff --numstat HEAD`.cwd(Instance.directory).quiet().nothrow().text()
|
const diffOutput = await $`git -c core.quotepath=false diff --numstat HEAD`
|
||||||
|
.cwd(Instance.directory)
|
||||||
|
.quiet()
|
||||||
|
.nothrow()
|
||||||
|
.text()
|
||||||
|
|
||||||
const changedFiles: Info[] = []
|
const changedFiles: Info[] = []
|
||||||
|
|
||||||
@@ -223,7 +227,7 @@ export namespace File {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const untrackedOutput = await $`git ls-files --others --exclude-standard`
|
const untrackedOutput = await $`git -c core.quotepath=false ls-files --others --exclude-standard`
|
||||||
.cwd(Instance.directory)
|
.cwd(Instance.directory)
|
||||||
.quiet()
|
.quiet()
|
||||||
.nothrow()
|
.nothrow()
|
||||||
@@ -248,7 +252,7 @@ export namespace File {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get deleted files
|
// Get deleted files
|
||||||
const deletedOutput = await $`git diff --name-only --diff-filter=D HEAD`
|
const deletedOutput = await $`git -c core.quotepath=false diff --name-only --diff-filter=D HEAD`
|
||||||
.cwd(Instance.directory)
|
.cwd(Instance.directory)
|
||||||
.quiet()
|
.quiet()
|
||||||
.nothrow()
|
.nothrow()
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
|
|||||||
const info = await getAuth()
|
const info = await getAuth()
|
||||||
if (!info || info.type !== "oauth") return {}
|
if (!info || info.type !== "oauth") return {}
|
||||||
|
|
||||||
|
const enterpriseUrl = info.enterpriseUrl
|
||||||
|
const baseURL = enterpriseUrl ? `https://copilot-api.${normalizeDomain(enterpriseUrl)}` : undefined
|
||||||
|
|
||||||
if (provider && provider.models) {
|
if (provider && provider.models) {
|
||||||
for (const model of Object.values(provider.models)) {
|
for (const model of Object.values(provider.models)) {
|
||||||
model.cost = {
|
model.cost = {
|
||||||
@@ -36,16 +39,23 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
|
|||||||
write: 0,
|
write: 0,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: move some of this hacky-ness to models.dev presets once we have better grasp of things here...
|
||||||
|
const base = baseURL ?? model.api.url
|
||||||
|
const claude = model.id.includes("claude")
|
||||||
|
const url = iife(() => {
|
||||||
|
if (!claude) return base
|
||||||
|
if (base.endsWith("/v1")) return base
|
||||||
|
if (base.endsWith("/")) return `${base}v1`
|
||||||
|
return `${base}/v1`
|
||||||
|
})
|
||||||
|
|
||||||
|
model.api.url = url
|
||||||
|
model.api.npm = claude ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const enterpriseUrl = info.enterpriseUrl
|
|
||||||
const baseURL = enterpriseUrl
|
|
||||||
? `https://copilot-api.${normalizeDomain(enterpriseUrl)}`
|
|
||||||
: "https://api.githubcopilot.com"
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
baseURL,
|
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
async fetch(request: RequestInfo | URL, init?: RequestInit) {
|
async fetch(request: RequestInfo | URL, init?: RequestInit) {
|
||||||
const info = await getAuth()
|
const info = await getAuth()
|
||||||
@@ -267,6 +277,11 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
|
|||||||
},
|
},
|
||||||
"chat.headers": async (input, output) => {
|
"chat.headers": async (input, output) => {
|
||||||
if (!input.model.providerID.includes("github-copilot")) return
|
if (!input.model.providerID.includes("github-copilot")) return
|
||||||
|
|
||||||
|
if (input.model.api.npm === "@ai-sdk/anthropic") {
|
||||||
|
output.headers["anthropic-beta"] = "interleaved-thinking-2025-05-14"
|
||||||
|
}
|
||||||
|
|
||||||
const session = await sdk.session
|
const session = await sdk.session
|
||||||
.get({
|
.get({
|
||||||
path: {
|
path: {
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ export namespace Provider {
|
|||||||
return {
|
return {
|
||||||
autoload: false,
|
autoload: false,
|
||||||
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
|
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
|
||||||
|
if (sdk.responses === undefined && sdk.chat === undefined) return sdk.languageModel(modelID)
|
||||||
return shouldUseCopilotResponsesApi(modelID) ? sdk.responses(modelID) : sdk.chat(modelID)
|
return shouldUseCopilotResponsesApi(modelID) ? sdk.responses(modelID) : sdk.chat(modelID)
|
||||||
},
|
},
|
||||||
options: {},
|
options: {},
|
||||||
@@ -141,6 +142,7 @@ export namespace Provider {
|
|||||||
return {
|
return {
|
||||||
autoload: false,
|
autoload: false,
|
||||||
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
|
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
|
||||||
|
if (sdk.responses === undefined && sdk.chat === undefined) return sdk.languageModel(modelID)
|
||||||
return shouldUseCopilotResponsesApi(modelID) ? sdk.responses(modelID) : sdk.chat(modelID)
|
return shouldUseCopilotResponsesApi(modelID) ? sdk.responses(modelID) : sdk.chat(modelID)
|
||||||
},
|
},
|
||||||
options: {},
|
options: {},
|
||||||
@@ -601,10 +603,7 @@ export namespace Provider {
|
|||||||
api: {
|
api: {
|
||||||
id: model.id,
|
id: model.id,
|
||||||
url: provider.api!,
|
url: provider.api!,
|
||||||
npm: iife(() => {
|
npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible",
|
||||||
if (provider.id.startsWith("github-copilot")) return "@ai-sdk/github-copilot"
|
|
||||||
return model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible"
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
status: model.status ?? "active",
|
status: model.status ?? "active",
|
||||||
headers: model.headers ?? {},
|
headers: model.headers ?? {},
|
||||||
@@ -924,6 +923,8 @@ export namespace Provider {
|
|||||||
)
|
)
|
||||||
delete provider.models[modelID]
|
delete provider.models[modelID]
|
||||||
|
|
||||||
|
model.variants = mapValues(ProviderTransform.variants(model), (v) => v)
|
||||||
|
|
||||||
// Filter out disabled variants from config
|
// Filter out disabled variants from config
|
||||||
const configVariants = configProvider?.models?.[modelID]?.variants
|
const configVariants = configProvider?.models?.[modelID]?.variants
|
||||||
if (configVariants && model.variants) {
|
if (configVariants && model.variants) {
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -150,14 +150,20 @@ export namespace LLM {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxOutputTokens = isCodex
|
const maxOutputTokens = isCodex ? undefined : undefined
|
||||||
? undefined
|
log.info("max_output_tokens", {
|
||||||
: ProviderTransform.maxOutputTokens(
|
tokens: ProviderTransform.maxOutputTokens(
|
||||||
input.model.api.npm,
|
input.model.api.npm,
|
||||||
params.options,
|
params.options,
|
||||||
input.model.limit.output,
|
input.model.limit.output,
|
||||||
OUTPUT_TOKEN_MAX,
|
OUTPUT_TOKEN_MAX,
|
||||||
)
|
),
|
||||||
|
modelOptions: params.options,
|
||||||
|
outputLimit: input.model.limit.output,
|
||||||
|
})
|
||||||
|
// tokens = 32000
|
||||||
|
// outputLimit = 64000
|
||||||
|
// modelOptions={"reasoningEffort":"minimal"}
|
||||||
|
|
||||||
const tools = await resolveTools(input)
|
const tools = await resolveTools(input)
|
||||||
|
|
||||||
|
|||||||
@@ -631,7 +631,7 @@ export namespace MessageV2 {
|
|||||||
sessionID: Identifier.schema("session"),
|
sessionID: Identifier.schema("session"),
|
||||||
messageID: Identifier.schema("message"),
|
messageID: Identifier.schema("message"),
|
||||||
}),
|
}),
|
||||||
async (input) => {
|
async (input): Promise<WithParts> => {
|
||||||
return {
|
return {
|
||||||
info: await Storage.read<MessageV2.Info>(["message", input.sessionID, input.messageID]),
|
info: await Storage.read<MessageV2.Info>(["message", input.sessionID, input.messageID]),
|
||||||
parts: await parts(input.messageID),
|
parts: await parts(input.messageID),
|
||||||
@@ -656,6 +656,13 @@ export namespace MessageV2 {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isOpenAiErrorRetryable = (e: APICallError) => {
|
||||||
|
const status = e.statusCode
|
||||||
|
if (!status) return e.isRetryable
|
||||||
|
// openai sometimes returns 404 for models that are actually available
|
||||||
|
return status === 404 || e.isRetryable
|
||||||
|
}
|
||||||
|
|
||||||
export function fromError(e: unknown, ctx: { providerID: string }) {
|
export function fromError(e: unknown, ctx: { providerID: string }) {
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case e instanceof DOMException && e.name === "AbortError":
|
case e instanceof DOMException && e.name === "AbortError":
|
||||||
@@ -724,7 +731,7 @@ export namespace MessageV2 {
|
|||||||
{
|
{
|
||||||
message,
|
message,
|
||||||
statusCode: e.statusCode,
|
statusCode: e.statusCode,
|
||||||
isRetryable: e.isRetryable,
|
isRetryable: ctx.providerID.startsWith("openai") ? isOpenAiErrorRetryable(e) : e.isRetryable,
|
||||||
responseHeaders: e.responseHeaders,
|
responseHeaders: e.responseHeaders,
|
||||||
responseBody: e.responseBody,
|
responseBody: e.responseBody,
|
||||||
metadata,
|
metadata,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { Instance } from "../project/instance"
|
|||||||
import { Bus } from "../bus"
|
import { Bus } from "../bus"
|
||||||
import { ProviderTransform } from "../provider/transform"
|
import { ProviderTransform } from "../provider/transform"
|
||||||
import { SystemPrompt } from "./system"
|
import { SystemPrompt } from "./system"
|
||||||
|
import { InstructionPrompt } from "./instruction"
|
||||||
import { Plugin } from "../plugin"
|
import { Plugin } from "../plugin"
|
||||||
import PROMPT_PLAN from "../session/prompt/plan.txt"
|
import PROMPT_PLAN from "../session/prompt/plan.txt"
|
||||||
import BUILD_SWITCH from "../session/prompt/build-switch.txt"
|
import BUILD_SWITCH from "../session/prompt/build-switch.txt"
|
||||||
@@ -386,6 +387,7 @@ export namespace SessionPrompt {
|
|||||||
abort,
|
abort,
|
||||||
callID: part.callID,
|
callID: part.callID,
|
||||||
extra: { bypassAgentCheck: true },
|
extra: { bypassAgentCheck: true },
|
||||||
|
messages: msgs,
|
||||||
async metadata(input) {
|
async metadata(input) {
|
||||||
await Session.updatePart({
|
await Session.updatePart({
|
||||||
...part,
|
...part,
|
||||||
@@ -561,6 +563,7 @@ export namespace SessionPrompt {
|
|||||||
tools: lastUser.tools,
|
tools: lastUser.tools,
|
||||||
processor,
|
processor,
|
||||||
bypassAgentCheck,
|
bypassAgentCheck,
|
||||||
|
messages: msgs,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (step === 1) {
|
if (step === 1) {
|
||||||
@@ -598,7 +601,7 @@ export namespace SessionPrompt {
|
|||||||
agent,
|
agent,
|
||||||
abort,
|
abort,
|
||||||
sessionID,
|
sessionID,
|
||||||
system: [...(await SystemPrompt.environment(model)), ...(await SystemPrompt.custom())],
|
system: [...(await SystemPrompt.environment(model)), ...(await InstructionPrompt.system())],
|
||||||
messages: [
|
messages: [
|
||||||
...MessageV2.toModelMessages(sessionMessages, model),
|
...MessageV2.toModelMessages(sessionMessages, model),
|
||||||
...(isLastStep
|
...(isLastStep
|
||||||
@@ -650,6 +653,7 @@ export namespace SessionPrompt {
|
|||||||
tools?: Record<string, boolean>
|
tools?: Record<string, boolean>
|
||||||
processor: SessionProcessor.Info
|
processor: SessionProcessor.Info
|
||||||
bypassAgentCheck: boolean
|
bypassAgentCheck: boolean
|
||||||
|
messages: MessageV2.WithParts[]
|
||||||
}) {
|
}) {
|
||||||
using _ = log.time("resolveTools")
|
using _ = log.time("resolveTools")
|
||||||
const tools: Record<string, AITool> = {}
|
const tools: Record<string, AITool> = {}
|
||||||
@@ -661,6 +665,7 @@ export namespace SessionPrompt {
|
|||||||
callID: options.toolCallId,
|
callID: options.toolCallId,
|
||||||
extra: { model: input.model, bypassAgentCheck: input.bypassAgentCheck },
|
extra: { model: input.model, bypassAgentCheck: input.bypassAgentCheck },
|
||||||
agent: input.agent.name,
|
agent: input.agent.name,
|
||||||
|
messages: input.messages,
|
||||||
metadata: async (val: { title?: string; metadata?: any }) => {
|
metadata: async (val: { title?: string; metadata?: any }) => {
|
||||||
const match = input.processor.partFromToolCall(options.toolCallId)
|
const match = input.processor.partFromToolCall(options.toolCallId)
|
||||||
if (match && match.state.status === "running") {
|
if (match && match.state.status === "running") {
|
||||||
@@ -1008,6 +1013,7 @@ export namespace SessionPrompt {
|
|||||||
agent: input.agent!,
|
agent: input.agent!,
|
||||||
messageID: info.id,
|
messageID: info.id,
|
||||||
extra: { bypassCwdCheck: true, model },
|
extra: { bypassCwdCheck: true, model },
|
||||||
|
messages: [],
|
||||||
metadata: async () => {},
|
metadata: async () => {},
|
||||||
ask: async () => {},
|
ask: async () => {},
|
||||||
}
|
}
|
||||||
@@ -1069,6 +1075,7 @@ export namespace SessionPrompt {
|
|||||||
agent: input.agent!,
|
agent: input.agent!,
|
||||||
messageID: info.id,
|
messageID: info.id,
|
||||||
extra: { bypassCwdCheck: true },
|
extra: { bypassCwdCheck: true },
|
||||||
|
messages: [],
|
||||||
metadata: async () => {},
|
metadata: async () => {},
|
||||||
ask: async () => {},
|
ask: async () => {},
|
||||||
}
|
}
|
||||||
@@ -1349,7 +1356,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
|||||||
|
|
||||||
const session = await Session.get(input.sessionID)
|
const session = await Session.get(input.sessionID)
|
||||||
if (session.revert) {
|
if (session.revert) {
|
||||||
SessionRevert.cleanup(session)
|
await SessionRevert.cleanup(session)
|
||||||
}
|
}
|
||||||
const agent = await Agent.get(input.agent)
|
const agent = await Agent.get(input.agent)
|
||||||
const model = input.model ?? agent.model ?? (await lastModel(input.sessionID))
|
const model = input.model ?? agent.model ?? (await lastModel(input.sessionID))
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { NamedError } from "@opencode-ai/util/error"
|
import type { NamedError } from "@opencode-ai/util/error"
|
||||||
import { MessageV2 } from "./message-v2"
|
import { MessageV2 } from "./message-v2"
|
||||||
|
import { iife } from "@/util/iife"
|
||||||
|
|
||||||
export namespace SessionRetry {
|
export namespace SessionRetry {
|
||||||
export const RETRY_INITIAL_DELAY = 2000
|
export const RETRY_INITIAL_DELAY = 2000
|
||||||
@@ -63,28 +64,36 @@ export namespace SessionRetry {
|
|||||||
return error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message
|
return error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof error.data?.message === "string") {
|
const json = iife(() => {
|
||||||
try {
|
try {
|
||||||
const json = JSON.parse(error.data.message)
|
if (typeof error.data?.message === "string") {
|
||||||
if (json.type === "error" && json.error?.type === "too_many_requests") {
|
const parsed = JSON.parse(error.data.message)
|
||||||
return "Too Many Requests"
|
return parsed
|
||||||
}
|
}
|
||||||
if (json.code.includes("exhausted") || json.code.includes("unavailable")) {
|
|
||||||
return "Provider is overloaded"
|
|
||||||
}
|
|
||||||
if (json.type === "error" && json.error?.code?.includes("rate_limit")) {
|
|
||||||
return "Rate Limited"
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
json.error?.message?.includes("no_kv_space") ||
|
|
||||||
(json.type === "error" && json.error?.type === "server_error") ||
|
|
||||||
!!json.error
|
|
||||||
) {
|
|
||||||
return "Provider Server Error"
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined
|
return JSON.parse(error.data.message)
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!json || typeof json !== "object") return undefined
|
||||||
|
const code = typeof json.code === "string" ? json.code : ""
|
||||||
|
|
||||||
|
if (json.type === "error" && json.error?.type === "too_many_requests") {
|
||||||
|
return "Too Many Requests"
|
||||||
|
}
|
||||||
|
if (code.includes("exhausted") || code.includes("unavailable")) {
|
||||||
|
return "Provider is overloaded"
|
||||||
|
}
|
||||||
|
if (json.type === "error" && json.error?.code?.includes("rate_limit")) {
|
||||||
|
return "Rate Limited"
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
json.error?.message?.includes("no_kv_space") ||
|
||||||
|
(json.type === "error" && json.error?.type === "server_error") ||
|
||||||
|
!!json.error
|
||||||
|
) {
|
||||||
|
return "Provider Server Error"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,62 @@ import { Agent } from "@/agent/agent"
|
|||||||
export namespace SessionSummary {
|
export namespace SessionSummary {
|
||||||
const log = Log.create({ service: "session.summary" })
|
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(
|
export const summarize = fn(
|
||||||
z.object({
|
z.object({
|
||||||
sessionID: z.string(),
|
sessionID: z.string(),
|
||||||
@@ -116,7 +172,18 @@ export namespace SessionSummary {
|
|||||||
messageID: Identifier.schema("message").optional(),
|
messageID: Identifier.schema("message").optional(),
|
||||||
}),
|
}),
|
||||||
async (input) => {
|
async (input) => {
|
||||||
return Storage.read<Snapshot.FileDiff[]>(["session_diff", input.sessionID]).catch(() => [])
|
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
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,14 @@
|
|||||||
import { Ripgrep } from "../file/ripgrep"
|
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 { Instance } from "../project/instance"
|
||||||
import path from "path"
|
|
||||||
import os from "os"
|
|
||||||
|
|
||||||
import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
|
import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
|
||||||
import PROMPT_ANTHROPIC_WITHOUT_TODO from "./prompt/qwen.txt"
|
import PROMPT_ANTHROPIC_WITHOUT_TODO from "./prompt/qwen.txt"
|
||||||
import PROMPT_BEAST from "./prompt/beast.txt"
|
import PROMPT_BEAST from "./prompt/beast.txt"
|
||||||
import PROMPT_GEMINI from "./prompt/gemini.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 PROMPT_CODEX from "./prompt/codex_header.txt"
|
||||||
import type { Provider } from "@/provider/provider"
|
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 namespace SystemPrompt {
|
||||||
export function instructions() {
|
export function instructions() {
|
||||||
@@ -72,81 +49,4 @@ export namespace SystemPrompt {
|
|||||||
].join("\n"),
|
].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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user