Compare commits

...
Author SHA1 Message Date
Ryan Vogel 5422f7d6af fix(script): remove highlights template from release notes 2026-01-28 17:32:53 -05:00
adamelmore 213c0e18ab fix(app): only show files in select dialog when clicking + tab 2026-01-26 20:07:50 -06:00
adamelmore a8c18dba82 fix(core): expose Instance.directory to custom tools 2026-01-26 20:07:47 -06:00
Aiden Cline 6cf2c3e3db fix: use Instance.directory instead of process.cwd() in read tool 2026-01-26 20:57:50 -05:00
Ryan Vogel b59aec6f04 feat: add /learn command to extract session learnings to scoped AGENTS.md files (#10717) 2026-01-26 20:50:44 -05:00
Chris Yang d9e8b2b65d fix(desktop): disable magnification gestures on macOS (#10605) 2026-01-26 18:50:51 -06:00
Rahul A Mistry 7655f51e10 fix(app): add connect provier in model selector (#10706) 2026-01-26 18:46:04 -06:00
GitHub Action e7c6267323 chore: generate 2026-01-27 00:45:43 +00:00
Fabio Martino 64a3661a32 docs: add Italian README (#10732) 2026-01-26 18:45:05 -06:00
Ryan Vogel bf463aee04 feat(release): add highlights template to draft releases (#10745) 2026-01-26 19:37:54 -05:00
adamelmore b24fd90fe8 test(app): file tree spec 2026-01-26 17:42:23 -06:00
adamelmore 6897bb7d02 chore: cleanup 2026-01-26 17:02:56 -06:00
adamelmore 8371ba5aec chore: cleanup 2026-01-26 17:01:04 -06:00
adamelmore 36577479c5 fix(app): enable file watcher 2026-01-26 16:57:44 -06:00
adamelmore bb178e9352 chore: cleanup 2026-01-26 16:57:43 -06:00
GitHub Action 4075f9e1ab chore: generate 2026-01-26 22:49:23 +00:00
adamelmore 021d9d105e fix(app): reactive file tree 2026-01-26 16:46:09 -06:00
adamelmore b07d7cdb71 fix(app): file tree performance 2026-01-26 16:41:18 -06:00
adamelmore 77f11dfabe chore: don't flip github draft release automatically 2026-01-26 16:30:19 -06:00
Aiden Cline 45b09c1465 tweak: when using messages api for copilot, attach anthropic beta headers 2026-01-26 17:17:54 -05:00
adamelmore ccc7aa49c3 wip: highlights 2026-01-26 15:36:59 -06:00
adamelmore 53ac394c68 wip: highlights 2026-01-26 15:36:59 -06:00
adamelmore 8b6484ac1a wip: highlights 2026-01-26 15:36:56 -06:00
adamelmore c1e840b9b2 chore: cleanup 2026-01-26 15:35:09 -06:00
David Hill a77df3c174 wip: new release modal
- highlight key updates or new features
- needs some transition love
- all copy including text and video placeholder
2026-01-26 15:35:09 -06:00
David Hill 9d1cf98192 fix: search clear icon 2026-01-26 15:35:09 -06:00
adamelmore 7b3d5f1d68 chore: cleanup 2026-01-26 15:30:01 -06:00
GitHub Action b21f82f5b0 chore: generate 2026-01-26 21:24:09 +00:00
adamelmore d82e94c209 fix(app): zen disconnect not working 2026-01-26 15:23:25 -06:00
adamelmore cbe8f265b9 fix(app): disconnect zen provider 2026-01-26 15:23:24 -06:00
Aiden Cline 8b5dde5536 tweak: retry logic to catch certain provider problems 2026-01-26 16:12:41 -05:00
Aiden Cline 6a62b44593 ci: add dry-run option to stale PR closer workflow
Allows testing stale PR closure without actually closing PRs
2026-01-26 15:42:50 -05:00
Aiden Cline c700b928e4 ci: add stale pr job 2026-01-26 15:38:40 -05:00
adamelmore ae815cca3a test(app): fix e2e test 2026-01-26 14:32:49 -06:00
David Hill 5a16d99b60 fix(app): disable tooltips in filetree tabs 2026-01-26 19:53:05 +00:00
David Hill 3f9b59c798 fix(app): move file tree toggle to right 2026-01-26 19:50:05 +00:00
David Hill c6febd8ddd fix(app): fade filetree guide lines on hover 2026-01-26 19:47:58 +00:00
David Hill 010ed55590 fix(app): dim non-deep filetree guide lines 2026-01-26 19:47:58 +00:00
David Hill 2be4598011 fix(app): reduce filetree folder indent 2026-01-26 19:47:58 +00:00
David Hill 99cd7f3468 fix(app): refine filetree row spacing and indent 2026-01-26 19:47:58 +00:00
David Hill fca0825b71 fix(app): use medium font for filetree items 2026-01-26 19:47:58 +00:00
David Hill 9babdb80cf fix(app): use chevron icons for filetree folders 2026-01-26 19:47:58 +00:00
David Hill f4392e023a fix(app): tighten filetree row spacing 2026-01-26 19:47:58 +00:00
David Hill 0e08c6c9fd fix(app): adjust filetree panel padding 2026-01-26 19:47:58 +00:00
David Hill 0dcb850a7a fix(ui): scope filetree pill tabs styling 2026-01-26 19:47:58 +00:00
David Hill d9a61cd94c feat(app): add Vercel AI Gateway provider description 2026-01-26 19:47:58 +00:00
David Hill 0f4a10f4a3 feat(app): add provider descriptions to settings 2026-01-26 19:47:58 +00:00
David Hill c551f7e47b fix(ui): reduce dialog transition in time to 150ms 2026-01-26 19:47:58 +00:00
David Hill 7962ff38b0 feat(app): add transition to command palette 2026-01-26 19:47:58 +00:00
David Hill 3ac11df66a feat(app): add transition to select provider dialog 2026-01-26 19:47:57 +00:00
David Hill 7caf59b433 fix(ui): prevent double-close and fix dialog replacement 2026-01-26 19:47:57 +00:00
David Hill 92229b44f8 feat(ui): add optional transition animations to dialog 2026-01-26 19:47:57 +00:00
David Hill 0a572afd46 fix(app): style view all button with interactive color and margin 2026-01-26 19:47:57 +00:00
David Hill ecd04a118a feat(ui): add models icon and use in settings 2026-01-26 19:47:57 +00:00
David Hill 6f3d413472 feat(ui): add providers icon and use in settings 2026-01-26 19:47:57 +00:00
David Hill 7c96d704d3 fix(app): use default cursor for env provider text 2026-01-26 19:47:57 +00:00
David Hill 9346c1ae3f fix(app): add hover text for env-connected providers 2026-01-26 19:47:57 +00:00
David Hill e0e97e9d93 fix(app): set provider row height to 56px 2026-01-26 19:47:57 +00:00
David Hill 810bc012b6 fix(ui): update button styles and disconnect button size 2026-01-26 19:47:57 +00:00
GitHub Action 36b832880d chore: generate 2026-01-26 19:37:41 +00:00
adamelmore d4e3acf17e fix(app): session sync issue 2026-01-26 13:36:36 -06:00
adamelmore 04337f6202 chore: cleanup 2026-01-26 13:03:34 -06:00
adamelmore 97aec21cb3 chore(app): missing i18n strings 2026-01-26 13:03:34 -06:00
adamelmore 319ad2a391 fix(app): session load cap 2026-01-26 12:58:51 -06:00
adamelmore de3b654dcd chore: refactor changelog 2026-01-26 12:51:35 -06:00
adamelmore 8b17ac656c test(app): e2e test for sidebar nav 2026-01-26 12:51:35 -06:00
GitHub Action 18bfc740c8 chore: generate 2026-01-26 18:50:58 +00:00
Tim 837037cd04 fix: ensure openai 404 errors are retried (#10590) 2026-01-26 13:50:04 -05:00
adamelmore b0f865eae5 chore: debug changelog 2026-01-26 12:21:27 -06:00
Aiden Cline ac53a372b0 feat: use anthropic compat messages api for anthropic models through copilot 2026-01-26 13:18:08 -05:00
Aiden Cline 7795cae0b5 ignore: tweak ai deps 2026-01-26 13:18:08 -05:00
adamelmore 4c9d879624 Revert "fix(app): restore external link opening in system browser (#10697)"
This reverts commit 984518b1c0.
2026-01-26 12:09:02 -06:00
adamelmore 3fdd08d66e chore: fix changelog page 2026-01-26 11:58:02 -06:00
zerone0xandClaude ec2ab639bb fix(enterprise): add message navigation to share page desktop view (#10071)
Co-authored-by: Claude <noreply@anthropic.com>
2026-01-26 12:32:16 -05:00
adamelmore d05ed5ca83 chore(app): createStore over signals 2026-01-26 11:26:17 -06:00
adamelmore 37f1a1a4ef chore: cleanup 2026-01-26 11:07:52 -06:00
adamelmore b8e8d82323 chore: cleanup 2026-01-26 11:07:52 -06:00
adamelmore 801eb5d2cb wip(app): file tree mode 2026-01-26 11:07:52 -06:00
adamelmore ebeed03115 wip(app): file tree mode 2026-01-26 11:07:51 -06:00
Adam d9eed4c6ca feat(app): file tree 2026-01-26 11:07:51 -06:00
Ryan Vogel 7e34d27b77 fix: add 44px top padding to sticky version header on changelog (#10715) 2026-01-26 12:02:20 -05:00
Rahul A Mistry 783121c06e fix(ui): use focus-visible instead of focus to prevent sticky hover effect on click (#10651) 2026-01-26 10:58:29 -06:00
Alex Yaroshuk 984518b1c0 fix(app): restore external link opening in system browser (#10697) 2026-01-26 10:57:49 -06:00
Sam Huckaby 7fcdbd155b fix(app): Order themes alphabetically (#10698) 2026-01-26 10:55:43 -06:00
Ryan Vogel 5856ea4e75 fix: move changelog footer outside content div to fix padding (#10712) 2026-01-26 11:46:56 -05:00
Github Action 32a0dcedcb chore: update nix node_modules hashes 2026-01-26 16:21:24 +00:00
Sebastian Herrlinger f48784d152 upgrade opentui to v0.1.75 2026-01-26 11:17:26 -05:00
adamelmore 3dce6a6608 chore: gen changelog page off changelog json 2026-01-26 09:53:10 -06:00
Aiden Cline 39a73d4894 feat: dynamically resolve AGENTS.md files from subdirectories as agent explores them (#10678) 2026-01-26 10:49:41 -05:00
Steffen Deusch b1fbfa7e94 feat(opencode): add agent description (#10680) 2026-01-26 10:43:10 -05:00
GitHub Action 805ae19c9a chore: generate 2026-01-26 15:34:31 +00:00
adamelmore fcea7e18a5 chore: stale-while-revalidate headers for changelog 2026-01-26 09:33:43 -06:00
adamelmore 7c34319b19 fix(app): query selector with non-latin chars 2026-01-26 09:33:42 -06:00
adamelmore cd4676171b feat(app): better sidebar hover when collapsed 2026-01-26 09:33:42 -06:00
adamelmore 7016be0739 wip(app): full-height sidebar 2026-01-26 09:33:42 -06:00
adamelmore ff35db0360 wip(app): full-height sidebar 2026-01-26 09:33:42 -06:00
adamelmore af3d8c383e wip(app): sidebar hover full 2026-01-26 09:33:42 -06:00
GitHub Action 7f75f71f6b chore: generate 2026-01-26 14:15:46 +00:00
adamelmore 84b12a8fb7 feat(app): model settings 2026-01-26 08:15:01 -06:00
adamelmore 1934ee13d8 wip(app): model settings 2026-01-26 08:15:01 -06:00
adamelmore 6c1e18f111 fix(app): line selection waits on ready 2026-01-26 08:15:00 -06:00
adamelmore 3296b90372 fix(app): handle non-tool call permissions 2026-01-26 08:15:00 -06:00
adamelmore 0d651eab3b feat(app): default servers on web 2026-01-26 08:15:00 -06:00
Ariane Emory 0edd304f42 fix: Make diff wrapping toggle always available in command_list and correct a type error (resolves #10682) (#10683) 2026-01-26 09:01:23 -05:00
Noam Bressler 6b83b172ae fix: await SessionRevert.cleanup for shell (#10669) 2026-01-26 07:36:17 -05:00
Idris Gadi 444934a4c1 fix(tui): add visual feedback for diff wrap and conceal toggles (#10655) 2026-01-26 07:34:09 -05:00
adamelmore c4f1087e58 chore: details 2026-01-26 06:24:47 -06:00
adamelmore c87232d5df perf(app): performance improvements 2026-01-26 06:08:09 -06:00
adamelmore d03c5f6b3f perf(app): performance improvements 2026-01-26 06:08:09 -06:00
GitHub Action 9a33b1ec88 ignore: update download stats 2026-01-26 2026-01-26 12:06:03 +00:00
157 changed files with 6404 additions and 2450 deletions
+83
View File
@@ -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}`)
}
+1 -1
View File
@@ -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.
+42
View File
@@ -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
+1
View File
@@ -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> |
+1
View File
@@ -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> |
+1
View File
@@ -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> |
+1
View File
@@ -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> |
+1
View File
@@ -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> |
+1
View File
@@ -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
View File
@@ -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">Lagente 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>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](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 lultima 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 dellutente (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 levoluzione dei modelli, le differenze tra di essi si ridurranno e i prezzi scenderanno, quindi essere indipendenti dal provider è importante.
- Supporto LSP pronto alluso
- 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 unapp 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)
+1
View File
@@ -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> |
+1
View File
@@ -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> |
+1
View File
@@ -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> |
+1
View File
@@ -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> |
+1
View File
@@ -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> |
+1
View File
@@ -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> |
+1
View File
@@ -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> |
+1
View File
@@ -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> |
+1
View File
@@ -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) |
+10 -10
View File
@@ -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
View File
@@ -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="
} }
} }
+36
View File
@@ -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()
})
+50 -1
View File
@@ -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)
}
})
+21 -6
View File
@@ -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)
+10 -10
View File
@@ -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> */}
+222 -72
View File
@@ -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>
) )
+5 -5
View File
@@ -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>
+126 -5
View File
@@ -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">&quot;{list.filter()}&quot;</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 />)
}} }}
+21 -14
View File
@@ -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>
+50 -21
View File
@@ -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() {
+13 -5
View File
@@ -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),
} }
+249 -4
View File
@@ -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,
+86 -35
View File
@@ -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": {
+225
View File
@@ -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,
}
},
})
+37 -1
View File
@@ -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) {
+15 -370
View File
@@ -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
}, },
+140
View File
@@ -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,
},
}
},
})
+65 -10
View File
@@ -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)
+4 -4
View File
@@ -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>
+21 -18
View File
@@ -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
+6
View File
@@ -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),
+15 -28
View File
@@ -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(() => {})
+22
View File
@@ -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(
+32
View File
@@ -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": "لم يتم اكتشاف تغييرات غير مدمجة.",
+37
View File
@@ -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.",
+111
View File
@@ -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.",
+113
View File
@@ -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.",
+31 -3
View File
@@ -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.",
+113
View File
@@ -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.",
+113
View File
@@ -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.",
+113
View File
@@ -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": "未マージの変更は検出されませんでした。",
+112
View File
@@ -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": "병합되지 않은 변경 사항이 감지되지 않았습니다.",
+93
View File
@@ -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.",
+33
View File
@@ -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.",
+33
View File
@@ -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": "Неслитые изменения не обнаружены.",
+111
View File
@@ -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": "未检测到未合并的更改。",
+110
View File
@@ -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": "未偵測到未合併的變更。",
+27
View File
@@ -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;
}
}
}
+6 -5
View File
@@ -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"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+24 -17
View File
@@ -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()
+146
View File
@@ -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) }
}
+21 -105
View File
@@ -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 />
+1 -1
View File
@@ -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>
+2
View File
@@ -3028,6 +3028,8 @@ dependencies = [
"futures", "futures",
"gtk", "gtk",
"listeners", "listeners",
"objc2 0.6.3",
"objc2-web-kit",
"reqwest", "reqwest",
"semver", "semver",
"serde", "serde",
+4
View File
@@ -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",
+2
View File
@@ -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!}
-2
View File
@@ -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
+2 -2
View File
@@ -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",
+2
View File
@@ -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) {
+10
View File
@@ -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>
</>
) )
} }
+7 -3
View File
@@ -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()
+21 -6
View File
@@ -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: {
+5 -4
View File
@@ -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
}
}
+14 -8
View File
@@ -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)
+9 -2
View File
@@ -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,
+9 -2
View File
@@ -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))
+29 -20
View File
@@ -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"
}
} }
} }
+68 -1
View File
@@ -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
}, },
) )
-100
View File
@@ -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