Compare commits
75
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b3d027e3b | ||
|
|
ce209e22a2 | ||
|
|
56fa267e09 | ||
|
|
2b73a08916 | ||
|
|
b0190116a7 | ||
|
|
11c0ad24aa | ||
|
|
4cfe8a8bf8 | ||
|
|
754951bbbd | ||
|
|
38d4d03ba8 | ||
|
|
162da4bd97 | ||
|
|
18563363da | ||
|
|
d18891523d | ||
|
|
bda7a488d4 | ||
|
|
bde80e24e5 | ||
|
|
0114a3f317 | ||
|
|
a4d4a3f545 | ||
|
|
e8b4eb8972 | ||
|
|
7c18b95826 | ||
|
|
381afd6e10 | ||
|
|
a8d5b14d1a | ||
|
|
39b4f861f3 | ||
|
|
13f89d5aa5 | ||
|
|
f29cb81c7d | ||
|
|
d034a61635 | ||
|
|
2eca96c80f | ||
|
|
057208e022 | ||
|
|
6db0f9855c | ||
|
|
e0894d7b63 | ||
|
|
cd3a58a4c2 | ||
|
|
a5614f988f | ||
|
|
0f58efe030 | ||
|
|
4abb464345 | ||
|
|
d1d3d420bf | ||
|
|
1d68cd288c | ||
|
|
0b8f8bc196 | ||
|
|
4d30ad1e7c | ||
|
|
c90640e0e1 | ||
|
|
36b51cad33 | ||
|
|
776e61d1ec | ||
|
|
28aebb2772 | ||
|
|
6494f48136 | ||
|
|
15fae6cb60 | ||
|
|
aacf1d20d3 | ||
|
|
bcf7817127 | ||
|
|
abf79ae24c | ||
|
|
922633ea9d | ||
|
|
49b40e3c90 | ||
|
|
df3276fc87 | ||
|
|
f8f986536b | ||
|
|
785635caef | ||
|
|
ec27518eca | ||
|
|
8ee4ada38e | ||
|
|
ab7b1d78bf | ||
|
|
2f44d1900e | ||
|
|
cb535eef9d | ||
|
|
d3ec6f75f4 | ||
|
|
9a8b2ae0b1 | ||
|
|
eadb0e25da | ||
|
|
ddd30ef304 | ||
|
|
2abf1100ee | ||
|
|
bd2e34f3bd | ||
|
|
a45c3a0049 | ||
|
|
52d1ee70a0 | ||
|
|
0a9fcab56f | ||
|
|
62fae6d182 | ||
|
|
3a5be7ad33 | ||
|
|
f1e88d35ba | ||
|
|
b737e87d9a | ||
|
|
bd6e81f30b | ||
|
|
f080147363 | ||
|
|
0051b605ae | ||
|
|
56e0e5ce65 | ||
|
|
d065d5a8ec | ||
|
|
cf79208055 | ||
|
|
f276a8db42 |
@@ -13,4 +13,3 @@ R44VC0RP
|
|||||||
rekram1-node
|
rekram1-node
|
||||||
RhysSullivan
|
RhysSullivan
|
||||||
thdxr
|
thdxr
|
||||||
simonklee
|
|
||||||
|
|||||||
@@ -12,13 +12,9 @@ adamdotdevin
|
|||||||
ariane-emory
|
ariane-emory
|
||||||
-atharvau AI review spamming literally every PR
|
-atharvau AI review spamming literally every PR
|
||||||
-borealbytes
|
-borealbytes
|
||||||
-carycooper777
|
|
||||||
-danieljoshuanazareth
|
-danieljoshuanazareth
|
||||||
-danieljoshuanazareth
|
-danieljoshuanazareth
|
||||||
-davidbernat looks to be a clawdbot that spams team and sends super weird emails, doesnt appear to be a real person
|
|
||||||
dmtrkovalenko
|
|
||||||
edemaine
|
edemaine
|
||||||
fahreddinozcan
|
|
||||||
-florianleibert
|
-florianleibert
|
||||||
fwang
|
fwang
|
||||||
iamdavidhill
|
iamdavidhill
|
||||||
@@ -31,10 +27,8 @@ r44vc0rp
|
|||||||
rekram1-node
|
rekram1-node
|
||||||
-ricardo-m-l
|
-ricardo-m-l
|
||||||
-robinmordasiewicz
|
-robinmordasiewicz
|
||||||
rubdos
|
|
||||||
shantur
|
shantur
|
||||||
simonklee
|
simonklee
|
||||||
-spider-yamet clawdbot/llm psychosis, spam pinging the team
|
-spider-yamet clawdbot/llm psychosis, spam pinging the team
|
||||||
-terisuke
|
|
||||||
thdxr
|
thdxr
|
||||||
-toastythebot
|
-toastythebot
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
name: "Setup Bun"
|
name: "Setup Bun"
|
||||||
description: "Setup Bun with caching and install dependencies"
|
description: "Setup Bun with caching and install dependencies"
|
||||||
inputs:
|
|
||||||
install-flags:
|
|
||||||
description: "Additional flags to pass to 'bun install'"
|
|
||||||
required: false
|
|
||||||
default: ""
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
@@ -51,8 +46,8 @@ runs:
|
|||||||
# e.g. ./patches/ for standard-openapi
|
# e.g. ./patches/ for standard-openapi
|
||||||
# https://github.com/oven-sh/bun/issues/28147
|
# https://github.com/oven-sh/bun/issues/28147
|
||||||
if [ "$RUNNER_OS" = "Windows" ]; then
|
if [ "$RUNNER_OS" = "Windows" ]; then
|
||||||
bun install --linker hoisted ${{ inputs.install-flags }}
|
bun install --linker hoisted
|
||||||
else
|
else
|
||||||
bun install ${{ inputs.install-flags }}
|
bun install
|
||||||
fi
|
fi
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
"on":
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- opencode-remote-voice
|
||||||
|
name: Deploy to apn-relay
|
||||||
|
jobs:
|
||||||
|
porter-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Set Github tag
|
||||||
|
id: vars
|
||||||
|
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||||
|
- name: Setup porter
|
||||||
|
uses: porter-dev/setup-porter@v0.1.0
|
||||||
|
- name: Deploy stack
|
||||||
|
timeout-minutes: 30
|
||||||
|
run: porter apply
|
||||||
|
env:
|
||||||
|
PORTER_APP_NAME: apn-relay
|
||||||
|
PORTER_CLUSTER: "5534"
|
||||||
|
PORTER_DEPLOYMENT_TARGET_ID: d60e67f5-b0a6-4275-8ed6-3cebaf092147
|
||||||
|
PORTER_HOST: https://dashboard.porter.run
|
||||||
|
PORTER_PROJECT: "18525"
|
||||||
|
PORTER_TAG: ${{ steps.vars.outputs.sha_short }}
|
||||||
|
PORTER_TOKEN: ${{ secrets.PORTER_APP_18525_975734319 }}
|
||||||
@@ -88,7 +88,7 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
id: build
|
id: build
|
||||||
run: |
|
run: |
|
||||||
./packages/opencode/script/build.ts ${{ (github.ref_name == 'beta' && '--sourcemaps') || '' }}
|
./packages/opencode/script/build.ts
|
||||||
env:
|
env:
|
||||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||||
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
|
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
|
||||||
@@ -402,14 +402,12 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
settings:
|
settings:
|
||||||
- host: macos-26-intel
|
- host: macos-latest
|
||||||
target: x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
platform_flag: --mac --x64
|
platform_flag: --mac --x64
|
||||||
bun_install_flags: --os=darwin --cpu=x64
|
- host: macos-latest
|
||||||
- host: macos-26
|
|
||||||
target: aarch64-apple-darwin
|
target: aarch64-apple-darwin
|
||||||
platform_flag: --mac --arm64
|
platform_flag: --mac --arm64
|
||||||
bun_install_flags: --os=darwin --cpu=arm64
|
|
||||||
# github-hosted: blacksmith lacks ARM64 MSVC cross-compilation toolchain
|
# github-hosted: blacksmith lacks ARM64 MSVC cross-compilation toolchain
|
||||||
- host: "windows-2025"
|
- host: "windows-2025"
|
||||||
target: aarch64-pc-windows-msvc
|
target: aarch64-pc-windows-msvc
|
||||||
@@ -439,8 +437,6 @@ jobs:
|
|||||||
run: echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8
|
run: echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8
|
||||||
|
|
||||||
- uses: ./.github/actions/setup-bun
|
- uses: ./.github/actions/setup-bun
|
||||||
with:
|
|
||||||
install-flags: ${{ matrix.settings.bun_install_flags }}
|
|
||||||
|
|
||||||
- name: Azure login
|
- name: Azure login
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
|
|||||||
@@ -45,13 +45,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Check PR guidelines compliance
|
- name: Check PR guidelines compliance
|
||||||
env:
|
env:
|
||||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
OPENCODE_PERMISSION: '{ "bash": { "*": "deny", "gh*": "allow", "gh pr review*": "deny" } }'
|
OPENCODE_PERMISSION: '{ "bash": { "*": "deny", "gh*": "allow", "gh pr review*": "deny" } }'
|
||||||
PR_TITLE: ${{ steps.pr-details.outputs.title }}
|
PR_TITLE: ${{ steps.pr-details.outputs.title }}
|
||||||
run: |
|
run: |
|
||||||
PR_BODY=$(jq -r .body pr_data.json)
|
PR_BODY=$(jq -r .body pr_data.json)
|
||||||
opencode run -m opencode/gpt-5.5 --variant medium "A new pull request has been created: '${PR_TITLE}'
|
opencode run -m anthropic/claude-opus-4-5 "A new pull request has been created: '${PR_TITLE}'
|
||||||
|
|
||||||
<pr-number>
|
<pr-number>
|
||||||
${{ steps.pr-number.outputs.number }}
|
${{ steps.pr-number.outputs.number }}
|
||||||
|
|||||||
@@ -0,0 +1,899 @@
|
|||||||
|
---
|
||||||
|
description: Translate content for a specified locale while preserving technical terms
|
||||||
|
mode: subagent
|
||||||
|
model: opencode/gpt-5.4
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a professional translator and localization specialist.
|
||||||
|
|
||||||
|
Translate the user's content into the requested target locale (language + region, e.g. fr-FR, de-DE).
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
|
||||||
|
- Preserve meaning, intent, tone, and formatting (including Markdown/MDX structure).
|
||||||
|
- Preserve all technical terms and artifacts exactly: product/company names, API names, identifiers, code, commands/flags, file paths, URLs, versions, error messages, config keys/values, and anything inside inline code or code blocks.
|
||||||
|
- Also preserve every term listed in the Do-Not-Translate glossary below.
|
||||||
|
- Also apply locale-specific guidance from `.opencode/glossary/<locale>.md` when available (for example, `zh-cn.md`).
|
||||||
|
- Do not modify fenced code blocks.
|
||||||
|
- Output ONLY the translation (no commentary).
|
||||||
|
|
||||||
|
If the target locale is missing, ask the user to provide it.
|
||||||
|
If no locale-specific glossary exists, use the global glossary only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Locale-Specific Glossaries
|
||||||
|
|
||||||
|
When a locale glossary exists, use it to:
|
||||||
|
|
||||||
|
- Apply preferred wording for recurring UI/docs terms in that locale
|
||||||
|
- Preserve locale-specific do-not-translate terms and casing decisions
|
||||||
|
- Prefer natural phrasing over literal translation when the locale file calls it out
|
||||||
|
- If the repo uses a locale alias slug, apply that file too (for example, `pt-BR` maps to `br.md` in this repo)
|
||||||
|
|
||||||
|
Locale guidance does not override code/command preservation rules or the global Do-Not-Translate glossary below.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Do-Not-Translate Terms (OpenCode Docs)
|
||||||
|
|
||||||
|
Generated from: `packages/web/src/content/docs/*.mdx` (default English docs)
|
||||||
|
Generated on: 2026-02-10
|
||||||
|
|
||||||
|
Use this as a translation QA checklist / glossary. Preserve listed terms exactly (spelling, casing, punctuation).
|
||||||
|
|
||||||
|
General rules (verbatim, even if not listed below):
|
||||||
|
|
||||||
|
- Anything inside inline code (single backticks) or fenced code blocks (triple backticks)
|
||||||
|
- MDX/JS code in docs: `import ... from "..."`, component tags, identifiers
|
||||||
|
- CLI commands, flags, config keys/values, file paths, URLs/domains, and env vars
|
||||||
|
|
||||||
|
## Proper nouns and product names
|
||||||
|
|
||||||
|
Additional (not reliably captured via link text):
|
||||||
|
|
||||||
|
```text
|
||||||
|
Astro
|
||||||
|
Bun
|
||||||
|
Chocolatey
|
||||||
|
Cursor
|
||||||
|
Docker
|
||||||
|
Git
|
||||||
|
GitHub Actions
|
||||||
|
GitLab CI
|
||||||
|
GNOME Terminal
|
||||||
|
Homebrew
|
||||||
|
Mise
|
||||||
|
Neovim
|
||||||
|
Node.js
|
||||||
|
npm
|
||||||
|
Obsidian
|
||||||
|
opencode
|
||||||
|
opencode-ai
|
||||||
|
Paru
|
||||||
|
pnpm
|
||||||
|
ripgrep
|
||||||
|
Scoop
|
||||||
|
SST
|
||||||
|
Starlight
|
||||||
|
Visual Studio Code
|
||||||
|
VS Code
|
||||||
|
VSCodium
|
||||||
|
Windsurf
|
||||||
|
Windows Terminal
|
||||||
|
Yarn
|
||||||
|
Zellij
|
||||||
|
Zed
|
||||||
|
anomalyco
|
||||||
|
```
|
||||||
|
|
||||||
|
Extracted from link labels in the English docs (review and prune as desired):
|
||||||
|
|
||||||
|
```text
|
||||||
|
@openspoon/subtask2
|
||||||
|
302.AI console
|
||||||
|
ACP progress report
|
||||||
|
Agent Client Protocol
|
||||||
|
Agent Skills
|
||||||
|
Agentic
|
||||||
|
AGENTS.md
|
||||||
|
AI SDK
|
||||||
|
Alacritty
|
||||||
|
Anthropic
|
||||||
|
Anthropic's Data Policies
|
||||||
|
Atom One
|
||||||
|
Avante.nvim
|
||||||
|
Ayu
|
||||||
|
Azure AI Foundry
|
||||||
|
Azure portal
|
||||||
|
Baseten
|
||||||
|
built-in GITHUB_TOKEN
|
||||||
|
Bun.$
|
||||||
|
Catppuccin
|
||||||
|
Cerebras console
|
||||||
|
ChatGPT Plus or Pro
|
||||||
|
Cloudflare dashboard
|
||||||
|
CodeCompanion.nvim
|
||||||
|
CodeNomad
|
||||||
|
Configuring Adapters: Environment Variables
|
||||||
|
Context7 MCP server
|
||||||
|
Cortecs console
|
||||||
|
Deep Infra dashboard
|
||||||
|
DeepSeek console
|
||||||
|
Duo Agent Platform
|
||||||
|
Everforest
|
||||||
|
Fireworks AI console
|
||||||
|
Firmware dashboard
|
||||||
|
Ghostty
|
||||||
|
GitLab CLI agents docs
|
||||||
|
GitLab docs
|
||||||
|
GitLab User Settings > Access Tokens
|
||||||
|
Granular Rules (Object Syntax)
|
||||||
|
Grep by Vercel
|
||||||
|
Groq console
|
||||||
|
Gruvbox
|
||||||
|
Helicone
|
||||||
|
Helicone documentation
|
||||||
|
Helicone Header Directory
|
||||||
|
Helicone's Model Directory
|
||||||
|
Hugging Face Inference Providers
|
||||||
|
Hugging Face settings
|
||||||
|
install WSL
|
||||||
|
IO.NET console
|
||||||
|
JetBrains IDE
|
||||||
|
Kanagawa
|
||||||
|
Kitty
|
||||||
|
MiniMax API Console
|
||||||
|
Models.dev
|
||||||
|
Moonshot AI console
|
||||||
|
Nebius Token Factory console
|
||||||
|
Nord
|
||||||
|
OAuth
|
||||||
|
Ollama integration docs
|
||||||
|
OpenAI's Data Policies
|
||||||
|
OpenChamber
|
||||||
|
OpenCode
|
||||||
|
OpenCode config
|
||||||
|
OpenCode Config
|
||||||
|
OpenCode TUI with the opencode theme
|
||||||
|
OpenCode Web - Active Session
|
||||||
|
OpenCode Web - New Session
|
||||||
|
OpenCode Web - See Servers
|
||||||
|
OpenCode Zen
|
||||||
|
OpenCode-Obsidian
|
||||||
|
OpenRouter dashboard
|
||||||
|
OpenWork
|
||||||
|
OVHcloud panel
|
||||||
|
Pro+ subscription
|
||||||
|
SAP BTP Cockpit
|
||||||
|
Scaleway Console IAM settings
|
||||||
|
Scaleway Generative APIs
|
||||||
|
SDK documentation
|
||||||
|
Sentry MCP server
|
||||||
|
shell API
|
||||||
|
Together AI console
|
||||||
|
Tokyonight
|
||||||
|
Unified Billing
|
||||||
|
Venice AI console
|
||||||
|
Vercel dashboard
|
||||||
|
WezTerm
|
||||||
|
Windows Subsystem for Linux (WSL)
|
||||||
|
WSL
|
||||||
|
WSL (Windows Subsystem for Linux)
|
||||||
|
WSL extension
|
||||||
|
xAI console
|
||||||
|
Z.AI API console
|
||||||
|
Zed
|
||||||
|
ZenMux dashboard
|
||||||
|
Zod
|
||||||
|
```
|
||||||
|
|
||||||
|
## Acronyms and initialisms
|
||||||
|
|
||||||
|
```text
|
||||||
|
ACP
|
||||||
|
AGENTS
|
||||||
|
AI
|
||||||
|
AI21
|
||||||
|
ANSI
|
||||||
|
API
|
||||||
|
AST
|
||||||
|
AWS
|
||||||
|
BTP
|
||||||
|
CD
|
||||||
|
CDN
|
||||||
|
CI
|
||||||
|
CLI
|
||||||
|
CMD
|
||||||
|
CORS
|
||||||
|
DEBUG
|
||||||
|
EKS
|
||||||
|
ERROR
|
||||||
|
FAQ
|
||||||
|
GLM
|
||||||
|
GNOME
|
||||||
|
GPT
|
||||||
|
HTML
|
||||||
|
HTTP
|
||||||
|
HTTPS
|
||||||
|
IAM
|
||||||
|
ID
|
||||||
|
IDE
|
||||||
|
INFO
|
||||||
|
IO
|
||||||
|
IP
|
||||||
|
IRSA
|
||||||
|
JS
|
||||||
|
JSON
|
||||||
|
JSONC
|
||||||
|
K2
|
||||||
|
LLM
|
||||||
|
LM
|
||||||
|
LSP
|
||||||
|
M2
|
||||||
|
MCP
|
||||||
|
MR
|
||||||
|
NET
|
||||||
|
NPM
|
||||||
|
NTLM
|
||||||
|
OIDC
|
||||||
|
OS
|
||||||
|
PAT
|
||||||
|
PATH
|
||||||
|
PHP
|
||||||
|
PR
|
||||||
|
PTY
|
||||||
|
README
|
||||||
|
RFC
|
||||||
|
RPC
|
||||||
|
SAP
|
||||||
|
SDK
|
||||||
|
SKILL
|
||||||
|
SSE
|
||||||
|
SSO
|
||||||
|
TS
|
||||||
|
TTY
|
||||||
|
TUI
|
||||||
|
UI
|
||||||
|
URL
|
||||||
|
US
|
||||||
|
UX
|
||||||
|
VCS
|
||||||
|
VPC
|
||||||
|
VPN
|
||||||
|
VS
|
||||||
|
WARN
|
||||||
|
WSL
|
||||||
|
X11
|
||||||
|
YAML
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code identifiers used in prose (CamelCase, mixedCase)
|
||||||
|
|
||||||
|
```text
|
||||||
|
apiKey
|
||||||
|
AppleScript
|
||||||
|
AssistantMessage
|
||||||
|
baseURL
|
||||||
|
BurntSushi
|
||||||
|
ChatGPT
|
||||||
|
ClangFormat
|
||||||
|
CodeCompanion
|
||||||
|
CodeNomad
|
||||||
|
DeepSeek
|
||||||
|
DefaultV2
|
||||||
|
FileContent
|
||||||
|
FileDiff
|
||||||
|
FileNode
|
||||||
|
fineGrained
|
||||||
|
FormatterStatus
|
||||||
|
GitHub
|
||||||
|
GitLab
|
||||||
|
iTerm2
|
||||||
|
JavaScript
|
||||||
|
JetBrains
|
||||||
|
macOS
|
||||||
|
mDNS
|
||||||
|
MiniMax
|
||||||
|
NeuralNomadsAI
|
||||||
|
NickvanDyke
|
||||||
|
NoeFabris
|
||||||
|
OpenAI
|
||||||
|
OpenAPI
|
||||||
|
OpenChamber
|
||||||
|
OpenCode
|
||||||
|
OpenRouter
|
||||||
|
OpenTUI
|
||||||
|
OpenWork
|
||||||
|
ownUserPermissions
|
||||||
|
PowerShell
|
||||||
|
ProviderAuthAuthorization
|
||||||
|
ProviderAuthMethod
|
||||||
|
ProviderInitError
|
||||||
|
SessionStatus
|
||||||
|
TabItem
|
||||||
|
tokenType
|
||||||
|
ToolIDs
|
||||||
|
ToolList
|
||||||
|
TypeScript
|
||||||
|
typesUrl
|
||||||
|
UserMessage
|
||||||
|
VcsInfo
|
||||||
|
WebView2
|
||||||
|
WezTerm
|
||||||
|
xAI
|
||||||
|
ZenMux
|
||||||
|
```
|
||||||
|
|
||||||
|
## OpenCode CLI commands (as shown in docs)
|
||||||
|
|
||||||
|
```text
|
||||||
|
opencode
|
||||||
|
opencode [project]
|
||||||
|
opencode /path/to/project
|
||||||
|
opencode acp
|
||||||
|
opencode agent [command]
|
||||||
|
opencode agent create
|
||||||
|
opencode agent list
|
||||||
|
opencode attach [url]
|
||||||
|
opencode attach http://10.20.30.40:4096
|
||||||
|
opencode attach http://localhost:4096
|
||||||
|
opencode auth [command]
|
||||||
|
opencode auth list
|
||||||
|
opencode auth login
|
||||||
|
opencode auth logout
|
||||||
|
opencode auth ls
|
||||||
|
opencode export [sessionID]
|
||||||
|
opencode github [command]
|
||||||
|
opencode github install
|
||||||
|
opencode github run
|
||||||
|
opencode import <file>
|
||||||
|
opencode import https://opncd.ai/s/abc123
|
||||||
|
opencode import session.json
|
||||||
|
opencode mcp [command]
|
||||||
|
opencode mcp add
|
||||||
|
opencode mcp auth [name]
|
||||||
|
opencode mcp auth list
|
||||||
|
opencode mcp auth ls
|
||||||
|
opencode mcp auth my-oauth-server
|
||||||
|
opencode mcp auth sentry
|
||||||
|
opencode mcp debug <name>
|
||||||
|
opencode mcp debug my-oauth-server
|
||||||
|
opencode mcp list
|
||||||
|
opencode mcp logout [name]
|
||||||
|
opencode mcp logout my-oauth-server
|
||||||
|
opencode mcp ls
|
||||||
|
opencode models --refresh
|
||||||
|
opencode models [provider]
|
||||||
|
opencode models anthropic
|
||||||
|
opencode run [message..]
|
||||||
|
opencode run Explain the use of context in Go
|
||||||
|
opencode serve
|
||||||
|
opencode serve --cors http://localhost:5173 --cors https://app.example.com
|
||||||
|
opencode serve --hostname 0.0.0.0 --port 4096
|
||||||
|
opencode serve [--port <number>] [--hostname <string>] [--cors <origin>]
|
||||||
|
opencode session [command]
|
||||||
|
opencode session list
|
||||||
|
opencode session delete <sessionID>
|
||||||
|
opencode stats
|
||||||
|
opencode uninstall
|
||||||
|
opencode upgrade
|
||||||
|
opencode upgrade [target]
|
||||||
|
opencode upgrade v0.1.48
|
||||||
|
opencode web
|
||||||
|
opencode web --cors https://example.com
|
||||||
|
opencode web --hostname 0.0.0.0
|
||||||
|
opencode web --mdns
|
||||||
|
opencode web --mdns --mdns-domain myproject.local
|
||||||
|
opencode web --port 4096
|
||||||
|
opencode web --port 4096 --hostname 0.0.0.0
|
||||||
|
opencode.server.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Slash commands and routes
|
||||||
|
|
||||||
|
```text
|
||||||
|
/agent
|
||||||
|
/auth/:id
|
||||||
|
/clear
|
||||||
|
/command
|
||||||
|
/config
|
||||||
|
/config/providers
|
||||||
|
/connect
|
||||||
|
/continue
|
||||||
|
/doc
|
||||||
|
/editor
|
||||||
|
/event
|
||||||
|
/experimental/tool?provider=<p>&model=<m>
|
||||||
|
/experimental/tool/ids
|
||||||
|
/export
|
||||||
|
/file?path=<path>
|
||||||
|
/file/content?path=<p>
|
||||||
|
/file/status
|
||||||
|
/find?pattern=<pat>
|
||||||
|
/find/file
|
||||||
|
/find/file?query=<q>
|
||||||
|
/find/symbol?query=<q>
|
||||||
|
/formatter
|
||||||
|
/global/event
|
||||||
|
/global/health
|
||||||
|
/help
|
||||||
|
/init
|
||||||
|
/instance/dispose
|
||||||
|
/log
|
||||||
|
/lsp
|
||||||
|
/mcp
|
||||||
|
/mnt/
|
||||||
|
/mnt/c/
|
||||||
|
/mnt/d/
|
||||||
|
/models
|
||||||
|
/oc
|
||||||
|
/opencode
|
||||||
|
/path
|
||||||
|
/project
|
||||||
|
/project/current
|
||||||
|
/provider
|
||||||
|
/provider/{id}/oauth/authorize
|
||||||
|
/provider/{id}/oauth/callback
|
||||||
|
/provider/auth
|
||||||
|
/q
|
||||||
|
/quit
|
||||||
|
/redo
|
||||||
|
/resume
|
||||||
|
/session
|
||||||
|
/session/:id
|
||||||
|
/session/:id/abort
|
||||||
|
/session/:id/children
|
||||||
|
/session/:id/command
|
||||||
|
/session/:id/diff
|
||||||
|
/session/:id/fork
|
||||||
|
/session/:id/init
|
||||||
|
/session/:id/message
|
||||||
|
/session/:id/message/:messageID
|
||||||
|
/session/:id/permissions/:permissionID
|
||||||
|
/session/:id/prompt_async
|
||||||
|
/session/:id/revert
|
||||||
|
/session/:id/share
|
||||||
|
/session/:id/shell
|
||||||
|
/session/:id/summarize
|
||||||
|
/session/:id/todo
|
||||||
|
/session/:id/unrevert
|
||||||
|
/session/status
|
||||||
|
/share
|
||||||
|
/summarize
|
||||||
|
/theme
|
||||||
|
/tui
|
||||||
|
/tui/append-prompt
|
||||||
|
/tui/clear-prompt
|
||||||
|
/tui/control/next
|
||||||
|
/tui/control/response
|
||||||
|
/tui/execute-command
|
||||||
|
/tui/open-help
|
||||||
|
/tui/open-models
|
||||||
|
/tui/open-sessions
|
||||||
|
/tui/open-themes
|
||||||
|
/tui/show-toast
|
||||||
|
/tui/submit-prompt
|
||||||
|
/undo
|
||||||
|
/Users/username
|
||||||
|
/Users/username/projects/*
|
||||||
|
/vcs
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI flags and short options
|
||||||
|
|
||||||
|
```text
|
||||||
|
--agent
|
||||||
|
--attach
|
||||||
|
--command
|
||||||
|
--continue
|
||||||
|
--cors
|
||||||
|
--cwd
|
||||||
|
--days
|
||||||
|
--dir
|
||||||
|
--dry-run
|
||||||
|
--event
|
||||||
|
--file
|
||||||
|
--force
|
||||||
|
--fork
|
||||||
|
--format
|
||||||
|
--help
|
||||||
|
--hostname
|
||||||
|
--hostname 0.0.0.0
|
||||||
|
--keep-config
|
||||||
|
--keep-data
|
||||||
|
--log-level
|
||||||
|
--max-count
|
||||||
|
--mdns
|
||||||
|
--mdns-domain
|
||||||
|
--method
|
||||||
|
--model
|
||||||
|
--models
|
||||||
|
--port
|
||||||
|
--print-logs
|
||||||
|
--project
|
||||||
|
--prompt
|
||||||
|
--refresh
|
||||||
|
--session
|
||||||
|
--share
|
||||||
|
--title
|
||||||
|
--token
|
||||||
|
--tools
|
||||||
|
--verbose
|
||||||
|
--version
|
||||||
|
--wait
|
||||||
|
|
||||||
|
-c
|
||||||
|
-d
|
||||||
|
-f
|
||||||
|
-h
|
||||||
|
-m
|
||||||
|
-n
|
||||||
|
-s
|
||||||
|
-v
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
```text
|
||||||
|
AI_API_URL
|
||||||
|
AI_FLOW_CONTEXT
|
||||||
|
AI_FLOW_EVENT
|
||||||
|
AI_FLOW_INPUT
|
||||||
|
AICORE_DEPLOYMENT_ID
|
||||||
|
AICORE_RESOURCE_GROUP
|
||||||
|
AICORE_SERVICE_KEY
|
||||||
|
ANTHROPIC_API_KEY
|
||||||
|
AWS_ACCESS_KEY_ID
|
||||||
|
AWS_BEARER_TOKEN_BEDROCK
|
||||||
|
AWS_PROFILE
|
||||||
|
AWS_REGION
|
||||||
|
AWS_ROLE_ARN
|
||||||
|
AWS_SECRET_ACCESS_KEY
|
||||||
|
AWS_WEB_IDENTITY_TOKEN_FILE
|
||||||
|
AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
|
||||||
|
AZURE_RESOURCE_NAME
|
||||||
|
CI_PROJECT_DIR
|
||||||
|
CI_SERVER_FQDN
|
||||||
|
CI_WORKLOAD_REF
|
||||||
|
CLOUDFLARE_ACCOUNT_ID
|
||||||
|
CLOUDFLARE_API_TOKEN
|
||||||
|
CLOUDFLARE_GATEWAY_ID
|
||||||
|
CONTEXT7_API_KEY
|
||||||
|
GITHUB_TOKEN
|
||||||
|
GITLAB_AI_GATEWAY_URL
|
||||||
|
GITLAB_HOST
|
||||||
|
GITLAB_INSTANCE_URL
|
||||||
|
GITLAB_OAUTH_CLIENT_ID
|
||||||
|
GITLAB_TOKEN
|
||||||
|
GITLAB_TOKEN_OPENCODE
|
||||||
|
GOOGLE_APPLICATION_CREDENTIALS
|
||||||
|
GOOGLE_CLOUD_PROJECT
|
||||||
|
HTTP_PROXY
|
||||||
|
HTTPS_PROXY
|
||||||
|
K2_
|
||||||
|
MY_API_KEY
|
||||||
|
MY_ENV_VAR
|
||||||
|
MY_MCP_CLIENT_ID
|
||||||
|
MY_MCP_CLIENT_SECRET
|
||||||
|
NO_PROXY
|
||||||
|
NODE_ENV
|
||||||
|
NODE_EXTRA_CA_CERTS
|
||||||
|
NPM_AUTH_TOKEN
|
||||||
|
OC_ALLOW_WAYLAND
|
||||||
|
OPENCODE_API_KEY
|
||||||
|
OPENCODE_AUTH_JSON
|
||||||
|
OPENCODE_AUTO_SHARE
|
||||||
|
OPENCODE_CLIENT
|
||||||
|
OPENCODE_CONFIG
|
||||||
|
OPENCODE_CONFIG_CONTENT
|
||||||
|
OPENCODE_CONFIG_DIR
|
||||||
|
OPENCODE_DISABLE_AUTOCOMPACT
|
||||||
|
OPENCODE_DISABLE_AUTOUPDATE
|
||||||
|
OPENCODE_DISABLE_CLAUDE_CODE
|
||||||
|
OPENCODE_DISABLE_CLAUDE_CODE_PROMPT
|
||||||
|
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS
|
||||||
|
OPENCODE_DISABLE_DEFAULT_PLUGINS
|
||||||
|
OPENCODE_DISABLE_LSP_DOWNLOAD
|
||||||
|
OPENCODE_DISABLE_MODELS_FETCH
|
||||||
|
OPENCODE_DISABLE_PRUNE
|
||||||
|
OPENCODE_DISABLE_TERMINAL_TITLE
|
||||||
|
OPENCODE_ENABLE_EXA
|
||||||
|
OPENCODE_ENABLE_EXPERIMENTAL_MODELS
|
||||||
|
OPENCODE_EXPERIMENTAL
|
||||||
|
OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS
|
||||||
|
OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT
|
||||||
|
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER
|
||||||
|
OPENCODE_EXPERIMENTAL_EXA
|
||||||
|
OPENCODE_EXPERIMENTAL_FILEWATCHER
|
||||||
|
OPENCODE_EXPERIMENTAL_ICON_DISCOVERY
|
||||||
|
OPENCODE_EXPERIMENTAL_LSP_TOOL
|
||||||
|
OPENCODE_EXPERIMENTAL_LSP_TY
|
||||||
|
OPENCODE_EXPERIMENTAL_MARKDOWN
|
||||||
|
OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX
|
||||||
|
OPENCODE_EXPERIMENTAL_OXFMT
|
||||||
|
OPENCODE_EXPERIMENTAL_PLAN_MODE
|
||||||
|
OPENCODE_ENABLE_QUESTION_TOOL
|
||||||
|
OPENCODE_FAKE_VCS
|
||||||
|
OPENCODE_GIT_BASH_PATH
|
||||||
|
OPENCODE_MODEL
|
||||||
|
OPENCODE_MODELS_URL
|
||||||
|
OPENCODE_PERMISSION
|
||||||
|
OPENCODE_PORT
|
||||||
|
OPENCODE_SERVER_PASSWORD
|
||||||
|
OPENCODE_SERVER_USERNAME
|
||||||
|
PROJECT_ROOT
|
||||||
|
RESOURCE_NAME
|
||||||
|
RUST_LOG
|
||||||
|
VARIABLE_NAME
|
||||||
|
VERTEX_LOCATION
|
||||||
|
XDG_CONFIG_HOME
|
||||||
|
```
|
||||||
|
|
||||||
|
## Package/module identifiers
|
||||||
|
|
||||||
|
```text
|
||||||
|
../../../config.mjs
|
||||||
|
@astrojs/starlight/components
|
||||||
|
@opencode-ai/plugin
|
||||||
|
@opencode-ai/sdk
|
||||||
|
path
|
||||||
|
shescape
|
||||||
|
zod
|
||||||
|
|
||||||
|
@
|
||||||
|
@ai-sdk/anthropic
|
||||||
|
@ai-sdk/cerebras
|
||||||
|
@ai-sdk/google
|
||||||
|
@ai-sdk/openai
|
||||||
|
@ai-sdk/openai-compatible
|
||||||
|
@File#L37-42
|
||||||
|
@modelcontextprotocol/server-everything
|
||||||
|
@opencode
|
||||||
|
```
|
||||||
|
|
||||||
|
## GitHub owner/repo slugs referenced in docs
|
||||||
|
|
||||||
|
```text
|
||||||
|
24601/opencode-zellij-namer
|
||||||
|
angristan/opencode-wakatime
|
||||||
|
anomalyco/opencode
|
||||||
|
apps/opencode-agent
|
||||||
|
athal7/opencode-devcontainers
|
||||||
|
awesome-opencode/awesome-opencode
|
||||||
|
backnotprop/plannotator
|
||||||
|
ben-vargas/ai-sdk-provider-opencode-sdk
|
||||||
|
btriapitsyn/openchamber
|
||||||
|
BurntSushi/ripgrep
|
||||||
|
Cluster444/agentic
|
||||||
|
code-yeongyu/oh-my-opencode
|
||||||
|
darrenhinde/opencode-agents
|
||||||
|
different-ai/opencode-scheduler
|
||||||
|
different-ai/openwork
|
||||||
|
features/copilot
|
||||||
|
folke/tokyonight.nvim
|
||||||
|
franlol/opencode-md-table-formatter
|
||||||
|
ggml-org/llama.cpp
|
||||||
|
ghoulr/opencode-websearch-cited.git
|
||||||
|
H2Shami/opencode-helicone-session
|
||||||
|
hosenur/portal
|
||||||
|
jamesmurdza/daytona
|
||||||
|
jenslys/opencode-gemini-auth
|
||||||
|
JRedeker/opencode-morph-fast-apply
|
||||||
|
JRedeker/opencode-shell-strategy
|
||||||
|
kdcokenny/ocx
|
||||||
|
kdcokenny/opencode-background-agents
|
||||||
|
kdcokenny/opencode-notify
|
||||||
|
kdcokenny/opencode-workspace
|
||||||
|
kdcokenny/opencode-worktree
|
||||||
|
login/device
|
||||||
|
mohak34/opencode-notifier
|
||||||
|
morhetz/gruvbox
|
||||||
|
mtymek/opencode-obsidian
|
||||||
|
NeuralNomadsAI/CodeNomad
|
||||||
|
nick-vi/opencode-type-inject
|
||||||
|
NickvanDyke/opencode.nvim
|
||||||
|
NoeFabris/opencode-antigravity-auth
|
||||||
|
nordtheme/nord
|
||||||
|
numman-ali/opencode-openai-codex-auth
|
||||||
|
olimorris/codecompanion.nvim
|
||||||
|
panta82/opencode-notificator
|
||||||
|
rebelot/kanagawa.nvim
|
||||||
|
remorses/kimaki
|
||||||
|
sainnhe/everforest
|
||||||
|
shekohex/opencode-google-antigravity-auth
|
||||||
|
shekohex/opencode-pty.git
|
||||||
|
spoons-and-mirrors/subtask2
|
||||||
|
sudo-tee/opencode.nvim
|
||||||
|
supermemoryai/opencode-supermemory
|
||||||
|
Tarquinen/opencode-dynamic-context-pruning
|
||||||
|
Th3Whit3Wolf/one-nvim
|
||||||
|
upstash/context7
|
||||||
|
vtemian/micode
|
||||||
|
vtemian/octto
|
||||||
|
yetone/avante.nvim
|
||||||
|
zenobi-us/opencode-plugin-template
|
||||||
|
zenobi-us/opencode-skillful
|
||||||
|
```
|
||||||
|
|
||||||
|
## Paths, filenames, globs, and URLs
|
||||||
|
|
||||||
|
```text
|
||||||
|
./.opencode/themes/*.json
|
||||||
|
./<project-slug>/storage/
|
||||||
|
./config/#custom-directory
|
||||||
|
./global/storage/
|
||||||
|
.agents/skills/*/SKILL.md
|
||||||
|
.agents/skills/<name>/SKILL.md
|
||||||
|
.clang-format
|
||||||
|
.claude
|
||||||
|
.claude/skills
|
||||||
|
.claude/skills/*/SKILL.md
|
||||||
|
.claude/skills/<name>/SKILL.md
|
||||||
|
.env
|
||||||
|
.github/workflows/opencode.yml
|
||||||
|
.gitignore
|
||||||
|
.gitlab-ci.yml
|
||||||
|
.ignore
|
||||||
|
.NET SDK
|
||||||
|
.npmrc
|
||||||
|
.ocamlformat
|
||||||
|
.opencode
|
||||||
|
.opencode/
|
||||||
|
.opencode/agents/
|
||||||
|
.opencode/commands/
|
||||||
|
.opencode/commands/test.md
|
||||||
|
.opencode/modes/
|
||||||
|
.opencode/plans/*.md
|
||||||
|
.opencode/plugins/
|
||||||
|
.opencode/skills/<name>/SKILL.md
|
||||||
|
.opencode/skills/git-release/SKILL.md
|
||||||
|
.opencode/tools/
|
||||||
|
.well-known/opencode
|
||||||
|
{ type: "raw" \| "patch", content: string }
|
||||||
|
{file:path/to/file}
|
||||||
|
**/*.js
|
||||||
|
%USERPROFILE%/intelephense/license.txt
|
||||||
|
%USERPROFILE%\.cache\opencode
|
||||||
|
%USERPROFILE%\.config\opencode\opencode.jsonc
|
||||||
|
%USERPROFILE%\.config\opencode\plugins
|
||||||
|
%USERPROFILE%\.local\share\opencode
|
||||||
|
%USERPROFILE%\.local\share\opencode\log
|
||||||
|
<project-root>/.opencode/themes/*.json
|
||||||
|
<providerId>/<modelId>
|
||||||
|
<your-project>/.opencode/plugins/
|
||||||
|
~
|
||||||
|
~/...
|
||||||
|
~/.agents/skills/*/SKILL.md
|
||||||
|
~/.agents/skills/<name>/SKILL.md
|
||||||
|
~/.aws/credentials
|
||||||
|
~/.bashrc
|
||||||
|
~/.cache/opencode
|
||||||
|
~/.cache/opencode/node_modules/
|
||||||
|
~/.claude/CLAUDE.md
|
||||||
|
~/.claude/skills/
|
||||||
|
~/.claude/skills/*/SKILL.md
|
||||||
|
~/.claude/skills/<name>/SKILL.md
|
||||||
|
~/.config/opencode
|
||||||
|
~/.config/opencode/AGENTS.md
|
||||||
|
~/.config/opencode/agents/
|
||||||
|
~/.config/opencode/commands/
|
||||||
|
~/.config/opencode/modes/
|
||||||
|
~/.config/opencode/opencode.json
|
||||||
|
~/.config/opencode/opencode.jsonc
|
||||||
|
~/.config/opencode/plugins/
|
||||||
|
~/.config/opencode/skills/*/SKILL.md
|
||||||
|
~/.config/opencode/skills/<name>/SKILL.md
|
||||||
|
~/.config/opencode/themes/*.json
|
||||||
|
~/.config/opencode/tools/
|
||||||
|
~/.config/zed/settings.json
|
||||||
|
~/.local/share
|
||||||
|
~/.local/share/opencode/
|
||||||
|
~/.local/share/opencode/auth.json
|
||||||
|
~/.local/share/opencode/log/
|
||||||
|
~/.local/share/opencode/mcp-auth.json
|
||||||
|
~/.local/share/opencode/opencode.jsonc
|
||||||
|
~/.npmrc
|
||||||
|
~/.zshrc
|
||||||
|
~/code/
|
||||||
|
~/Library/Application Support
|
||||||
|
~/projects/*
|
||||||
|
~/projects/personal/
|
||||||
|
${config.github}/blob/dev/packages/sdk/js/src/gen/types.gen.ts
|
||||||
|
$HOME/intelephense/license.txt
|
||||||
|
$HOME/projects/*
|
||||||
|
$XDG_CONFIG_HOME/opencode/themes/*.json
|
||||||
|
agent/
|
||||||
|
agents/
|
||||||
|
build/
|
||||||
|
commands/
|
||||||
|
dist/
|
||||||
|
http://<wsl-ip>:4096
|
||||||
|
http://127.0.0.1:8080/callback
|
||||||
|
http://localhost:<port>
|
||||||
|
http://localhost:4096
|
||||||
|
http://localhost:4096/doc
|
||||||
|
https://app.example.com
|
||||||
|
https://AZURE_COGNITIVE_SERVICES_RESOURCE_NAME.cognitiveservices.azure.com/
|
||||||
|
https://opencode.ai/zen/v1/chat/completions
|
||||||
|
https://opencode.ai/zen/v1/messages
|
||||||
|
https://opencode.ai/zen/v1/models/gemini-3-flash
|
||||||
|
https://opencode.ai/zen/v1/models/gemini-3-pro
|
||||||
|
https://opencode.ai/zen/v1/responses
|
||||||
|
https://RESOURCE_NAME.openai.azure.com/
|
||||||
|
laravel/pint
|
||||||
|
log/
|
||||||
|
model: "anthropic/claude-sonnet-4-5"
|
||||||
|
modes/
|
||||||
|
node_modules/
|
||||||
|
openai/gpt-4.1
|
||||||
|
opencode.ai/config.json
|
||||||
|
opencode/<model-id>
|
||||||
|
opencode/gpt-5.1-codex
|
||||||
|
opencode/gpt-5.2-codex
|
||||||
|
opencode/kimi-k2
|
||||||
|
openrouter/google/gemini-2.5-flash
|
||||||
|
opncd.ai/s/<share-id>
|
||||||
|
packages/*/AGENTS.md
|
||||||
|
plugins/
|
||||||
|
project/
|
||||||
|
provider_id/model_id
|
||||||
|
provider/model
|
||||||
|
provider/model-id
|
||||||
|
rm -rf ~/.cache/opencode
|
||||||
|
skills/
|
||||||
|
skills/*/SKILL.md
|
||||||
|
src/**/*.ts
|
||||||
|
themes/
|
||||||
|
tools/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Keybind strings
|
||||||
|
|
||||||
|
```text
|
||||||
|
alt+b
|
||||||
|
Alt+Ctrl+K
|
||||||
|
alt+d
|
||||||
|
alt+f
|
||||||
|
Cmd+Esc
|
||||||
|
Cmd+Option+K
|
||||||
|
Cmd+Shift+Esc
|
||||||
|
Cmd+Shift+G
|
||||||
|
Cmd+Shift+P
|
||||||
|
ctrl+a
|
||||||
|
ctrl+b
|
||||||
|
ctrl+d
|
||||||
|
ctrl+e
|
||||||
|
Ctrl+Esc
|
||||||
|
ctrl+f
|
||||||
|
ctrl+g
|
||||||
|
ctrl+k
|
||||||
|
Ctrl+Shift+Esc
|
||||||
|
Ctrl+Shift+P
|
||||||
|
ctrl+t
|
||||||
|
ctrl+u
|
||||||
|
ctrl+w
|
||||||
|
ctrl+x
|
||||||
|
DELETE
|
||||||
|
Shift+Enter
|
||||||
|
WIN+R
|
||||||
|
```
|
||||||
|
|
||||||
|
## Model ID strings referenced
|
||||||
|
|
||||||
|
```text
|
||||||
|
{env:OPENCODE_MODEL}
|
||||||
|
anthropic/claude-3-5-sonnet-20241022
|
||||||
|
anthropic/claude-haiku-4-20250514
|
||||||
|
anthropic/claude-haiku-4-5
|
||||||
|
anthropic/claude-sonnet-4-20250514
|
||||||
|
anthropic/claude-sonnet-4-5
|
||||||
|
gitlab/duo-chat-haiku-4-5
|
||||||
|
lmstudio/google/gemma-3n-e4b
|
||||||
|
openai/gpt-4.1
|
||||||
|
openai/gpt-5
|
||||||
|
opencode/gpt-5.1-codex
|
||||||
|
opencode/gpt-5.2-codex
|
||||||
|
opencode/kimi-k2
|
||||||
|
openrouter/google/gemini-2.5-flash
|
||||||
|
```
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
---
|
|
||||||
description: translate English to other languages
|
|
||||||
model: opencode/claude-opus-4-7
|
|
||||||
---
|
|
||||||
|
|
||||||
run git diff and translate changed english doc and UI copy files to other international languages. Translate all languages in parallel to save time.
|
|
||||||
|
|
||||||
Requirements:
|
|
||||||
|
|
||||||
- Preserve meaning, intent, tone, and formatting (including Markdown/MDX structure).
|
|
||||||
- Preserve all technical terms and artifacts exactly: product/company names, API names, identifiers, code, commands/flags, file paths, URLs, versions, error messages, config keys/values, and anything inside inline code or code blocks.
|
|
||||||
- Also preserve every term listed in the Do-Not-Translate glossary below.
|
|
||||||
- Also apply locale-specific guidance from `.opencode/glossary/<locale>.md` when available (for example, `zh-cn.md`).
|
|
||||||
- Do not modify fenced code blocks.
|
|
||||||
@@ -1,38 +1,21 @@
|
|||||||
---
|
---
|
||||||
name: effect
|
name: effect
|
||||||
description: Work with Effect v4 / effect-smol TypeScript code in this repo
|
description: Answer questions about the Effect framework
|
||||||
---
|
---
|
||||||
|
|
||||||
# Effect
|
# Effect
|
||||||
|
|
||||||
This codebase uses Effect for typed, composable TypeScript services, schemas, and workflows.
|
This codebase uses Effect, a framework for writing typescript.
|
||||||
|
|
||||||
## Source Of Truth
|
## How to Answer Effect Questions
|
||||||
|
|
||||||
Use the current Effect v4 / effect-smol source, not memory or older Effect v2/v3 examples.
|
1. Clone the Effect repository: `https://github.com/Effect-TS/effect-smol` to
|
||||||
|
`.opencode/references/effect-smol` in this project NOT the skill folder.
|
||||||
1. If `.opencode/references/effect-smol` is missing, clone `https://github.com/Effect-TS/effect-smol` there. Do this in the project, not in the skill folder.
|
2. Use the explore agent to search the codebase for answers about Effect patterns, APIs, and concepts
|
||||||
2. Search `.opencode/references/effect-smol` for exact APIs, examples, tests, and naming patterns before answering or implementing Effect-specific code.
|
3. Provide responses based on the actual Effect source code and documentation
|
||||||
3. Also inspect existing repo code for local house style before introducing new patterns.
|
|
||||||
4. Prefer answers and implementations backed by specific source files or nearby repo examples.
|
|
||||||
|
|
||||||
## Guidelines
|
## Guidelines
|
||||||
|
|
||||||
- Prefer current Effect v4 APIs and project-local patterns over old blog posts, examples, or package-memory guesses.
|
- Always use the explore agent with the cloned repository when answering Effect-related questions
|
||||||
- Use `Effect.gen(function* () { ... })` for multi-step workflows.
|
- Reference specific files and patterns found in the Effect codebase
|
||||||
- Use `Effect.fn("Name")` or `Effect.fnUntraced(...)` for named effects when adding reusable service methods or important workflows.
|
- Do not answer from memory - always verify against the source
|
||||||
- Prefer Effect `Schema` for API and domain data shapes. Use branded schemas for IDs and `Schema.TaggedErrorClass` for typed domain errors when modeling new error surfaces.
|
|
||||||
- Keep HTTP handlers thin: decode input, read request context, call services, and map transport errors. Put business rules in services.
|
|
||||||
- In Effect service code, prefer Effect-aware platform abstractions and dependencies over ad hoc promises where the surrounding code already does so.
|
|
||||||
- Keep layer composition explicit. Avoid broad hidden provisioning that makes missing dependencies hard to see.
|
|
||||||
- In tests, prefer the repo's existing Effect test helpers and live tests for filesystem, git, child process, locks, or timing behavior.
|
|
||||||
- Do not introduce `any`, non-null assertions, unchecked casts, or older Effect APIs just to satisfy types.
|
|
||||||
- Do not answer from memory. Verify against `.opencode/references/effect-smol` or nearby code first.
|
|
||||||
|
|
||||||
## Testing Patterns
|
|
||||||
|
|
||||||
- Use `testEffect(...)` from `packages/opencode/test/lib/effect.ts` for tests that exercise Effect services, layers, runtime context, scoped resources, or platform integrations.
|
|
||||||
- Use `it.live(...)` for filesystem, git repositories, HTTP servers, sockets, child processes, locks, real time, and other live platform behavior.
|
|
||||||
- Run tests from package directories such as `packages/opencode`; never run package tests from the repo root.
|
|
||||||
- Prefer explicit test layers over ad hoc managed runtimes. Keep dependency provisioning visible in the test file.
|
|
||||||
- Use scoped fixtures and finalizers for resources that must be cleaned up, including temporary directories, flags, databases, fibers, servers, and global state.
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { tool } from "@opencode-ai/plugin"
|
|||||||
const TEAM = {
|
const TEAM = {
|
||||||
desktop: ["adamdotdevin", "iamdavidhill", "Brendonovich", "nexxeln"],
|
desktop: ["adamdotdevin", "iamdavidhill", "Brendonovich", "nexxeln"],
|
||||||
zen: ["fwang", "MrMushrooooom"],
|
zen: ["fwang", "MrMushrooooom"],
|
||||||
tui: ["kommander", "rekram1-node", "simonklee"],
|
tui: ["thdxr", "kommander", "rekram1-node"],
|
||||||
core: ["kitlangton", "rekram1-node", "jlongster"],
|
core: ["thdxr", "rekram1-node", "jlongster"],
|
||||||
docs: ["R44VC0RP"],
|
docs: ["R44VC0RP"],
|
||||||
windows: ["Hona"],
|
windows: ["Hona"],
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
- To regenerate the JavaScript SDK, run `./packages/sdk/js/script/build.ts`.
|
# OpenCode Monorepo Agent Guide
|
||||||
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.
|
|
||||||
- The default branch in this repo is `dev`.
|
|
||||||
- Local `main` ref may not exist; use `dev` or `origin/dev` for diffs.
|
|
||||||
- Prefer automation: execute requested actions without confirmation unless blocked by missing info or safety/irreversibility.
|
|
||||||
|
|
||||||
## Style Guide
|
This file is for coding agents working in `/Users/ryanvogel/dev/opencode`.
|
||||||
|
|
||||||
### General Principles
|
## Scope And Precedence
|
||||||
|
|
||||||
- Keep things in one function unless composable or reusable
|
- Keep things in one function unless composable or reusable
|
||||||
- Avoid `try`/`catch` where possible
|
- Avoid `try`/`catch` where possible
|
||||||
@@ -56,48 +52,48 @@ else foo = 2
|
|||||||
|
|
||||||
### Control Flow
|
### Control Flow
|
||||||
|
|
||||||
Avoid `else` statements. Prefer early returns.
|
- Prefer early returns over nested `else` blocks.
|
||||||
|
- Keep functions focused; split only when it improves reuse or readability.
|
||||||
|
|
||||||
```ts
|
### Error Handling
|
||||||
// Good
|
|
||||||
function foo() {
|
|
||||||
if (condition) return 1
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bad
|
- Fail with actionable messages.
|
||||||
function foo() {
|
- Avoid swallowing errors silently.
|
||||||
if (condition) return 1
|
- Log enough context to debug production issues (IDs, env, status), but never secrets.
|
||||||
else return 2
|
- In UI code, degrade gracefully for missing capabilities.
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Schema Definitions (Drizzle)
|
### Data / DB
|
||||||
|
|
||||||
Use snake_case for field names so column names don't need to be redefined as strings.
|
- For Drizzle schema, use snake_case fields and columns.
|
||||||
|
- Keep migration and schema changes minimal and explicit.
|
||||||
|
- Follow package-specific DB guidance in `packages/opencode/AGENTS.md`.
|
||||||
|
|
||||||
```ts
|
### Testing Philosophy
|
||||||
// Good
|
|
||||||
const table = sqliteTable("session", {
|
|
||||||
id: text().primaryKey(),
|
|
||||||
project_id: text().notNull(),
|
|
||||||
created_at: integer().notNull(),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Bad
|
- Prefer testing real behavior over mocks.
|
||||||
const table = sqliteTable("session", {
|
- Add regression tests for bug fixes where practical.
|
||||||
id: text("id").primaryKey(),
|
- Keep fixtures small and focused.
|
||||||
projectID: text("project_id").notNull(),
|
|
||||||
createdAt: integer("created_at").notNull(),
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing
|
## Agent Workflow Tips
|
||||||
|
|
||||||
- Avoid mocks as much as possible
|
- Read existing code paths before introducing new abstractions.
|
||||||
- Test actual implementation, do not duplicate logic into tests
|
- Match local patterns first; do not impose a new style per file.
|
||||||
- Tests cannot run from repo root (guard: `do-not-run-tests-from-root`); run from package dirs like `packages/opencode`.
|
- If a package has its own `AGENTS.md`, review it before editing.
|
||||||
|
- For OpenCode Effect services, follow `packages/opencode/AGENTS.md` strictly.
|
||||||
|
|
||||||
## Type Checking
|
## Known Operational Notes
|
||||||
|
|
||||||
- Always run `bun typecheck` from package directories (e.g., `packages/opencode`), never `tsc` directly.
|
- `packages/app/AGENTS.md` says: never restart app/server processes during that package's debugging workflow.
|
||||||
|
- `packages/app/AGENTS.md` also documents local backend+web split for UI work.
|
||||||
|
- `packages/opencode/AGENTS.md` contains mandatory Effect and database conventions.
|
||||||
|
|
||||||
|
## Regeneration / Special Scripts
|
||||||
|
|
||||||
|
- Regenerate JS SDK with: `./packages/sdk/js/script/build.ts`
|
||||||
|
|
||||||
|
## Quick Checklist Before Finishing
|
||||||
|
|
||||||
|
- Ran relevant package checks.
|
||||||
|
- Updated docs/config when behavior changed.
|
||||||
|
- Avoided committing unrelated files.
|
||||||
|
- Kept edits minimal and aligned with local conventions.
|
||||||
|
|||||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1776683584,
|
"lastModified": 1773909469,
|
||||||
"narHash": "sha256-NuTLMrr10Tng72hurYG8jYQ4XKK8wnpJmOGcPiis96g=",
|
"narHash": "sha256-vglVrLfHjFIzIdV9A27Ugul6rh3I1qHbbitGW7dk420=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "9dd5558b06dbdacbf635a3dd36dce1b1a7ee3a89",
|
"rev": "7149c06513f335be57f26fcbbbe34afda923882b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -115,27 +115,6 @@ const zenLiteCouponFirstMonth100 = new stripe.Coupon("ZenLiteCouponFirstMonth100
|
|||||||
appliesToProducts: [zenLiteProduct.id],
|
appliesToProducts: [zenLiteProduct.id],
|
||||||
duration: "once",
|
duration: "once",
|
||||||
})
|
})
|
||||||
const zenLiteCouponThreeMonths100 = new stripe.Coupon("ZenLiteCoupon3Months100", {
|
|
||||||
name: "3 months 100% off",
|
|
||||||
percentOff: 100,
|
|
||||||
appliesToProducts: [zenLiteProduct.id],
|
|
||||||
duration: "repeating",
|
|
||||||
durationInMonths: 3,
|
|
||||||
})
|
|
||||||
const zenLiteCouponSixMonths100 = new stripe.Coupon("ZenLiteCoupon6Months100", {
|
|
||||||
name: "6 months 100% off",
|
|
||||||
percentOff: 100,
|
|
||||||
appliesToProducts: [zenLiteProduct.id],
|
|
||||||
duration: "repeating",
|
|
||||||
durationInMonths: 6,
|
|
||||||
})
|
|
||||||
const zenLiteCouponTwelveMonths100 = new stripe.Coupon("ZenLiteCoupon12Months100", {
|
|
||||||
name: "12 months 100% off",
|
|
||||||
percentOff: 100,
|
|
||||||
appliesToProducts: [zenLiteProduct.id],
|
|
||||||
duration: "repeating",
|
|
||||||
durationInMonths: 12,
|
|
||||||
})
|
|
||||||
const zenLitePrice = new stripe.Price("ZenLitePrice", {
|
const zenLitePrice = new stripe.Price("ZenLitePrice", {
|
||||||
product: zenLiteProduct.id,
|
product: zenLiteProduct.id,
|
||||||
currency: "usd",
|
currency: "usd",
|
||||||
@@ -152,9 +131,6 @@ const ZEN_LITE_PRICE = new sst.Linkable("ZEN_LITE_PRICE", {
|
|||||||
priceInr: 92900,
|
priceInr: 92900,
|
||||||
firstMonth50Coupon: zenLiteCouponFirstMonth50.id,
|
firstMonth50Coupon: zenLiteCouponFirstMonth50.id,
|
||||||
firstMonth100Coupon: zenLiteCouponFirstMonth100.id,
|
firstMonth100Coupon: zenLiteCouponFirstMonth100.id,
|
||||||
threeMonths100Coupon: zenLiteCouponThreeMonths100.id,
|
|
||||||
sixMonths100Coupon: zenLiteCouponSixMonths100.id,
|
|
||||||
twelveMonths100Coupon: zenLiteCouponTwelveMonths100.id,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-cBfg4pJ4mjsfS4MFFASBaZZykArgIoeo/3woOcSGy1U=",
|
"x86_64-linux": "sha256-i9TxYwWkJAR+kW6pbvhgQbRW9UYPtdrPQAGic4zPoa4=",
|
||||||
"aarch64-linux": "sha256-Q6cqUwfqbscdrPW0uHcfshhQINjJi0HiyURMSdOOCf4=",
|
"aarch64-linux": "sha256-RYc/OYlETXUwkWBRDas+/P4cBW6zde4FqxxnMARu5vs=",
|
||||||
"aarch64-darwin": "sha256-1AtfsD1D9YxWSEsecPJF9XsvsxsWTtVtkP5l6UW43og=",
|
"aarch64-darwin": "sha256-jIhUOIRIQEa2WT62TVIedmRIhl/edhK8sbiAFvU3yCM=",
|
||||||
"x86_64-darwin": "sha256-YS5/8YTf9LymAUbjXVrGDfxtKVJrpZbPnnCtsGHSHoU="
|
"x86_64-darwin": "sha256-xLGzaX7OofFlZzVgpORJR5QXD2u+54hp+t3cCfUtO84="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -64,7 +64,7 @@ stdenvNoCC.mkDerivation (finalAttrs: {
|
|||||||
[
|
[
|
||||||
ripgrep
|
ripgrep
|
||||||
]
|
]
|
||||||
# bun runs sysctl to detect if running on rosetta2
|
# bun runs sysctl to detect if dunning on rosetta2
|
||||||
++ lib.optional stdenvNoCC.hostPlatform.isDarwin sysctl
|
++ lib.optional stdenvNoCC.hostPlatform.isDarwin sysctl
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-9
@@ -4,7 +4,7 @@
|
|||||||
"description": "AI-powered development tool",
|
"description": "AI-powered development tool",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "bun@1.3.13",
|
"packageManager": "bun@1.3.11",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||||
"dev:desktop": "bun --cwd packages/desktop-electron dev",
|
"dev:desktop": "bun --cwd packages/desktop-electron dev",
|
||||||
@@ -27,15 +27,15 @@
|
|||||||
"packages/slack"
|
"packages/slack"
|
||||||
],
|
],
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"@effect/opentelemetry": "4.0.0-beta.57",
|
"@effect/opentelemetry": "4.0.0-beta.48",
|
||||||
"@effect/platform-node": "4.0.0-beta.57",
|
"@effect/platform-node": "4.0.0-beta.48",
|
||||||
"@npmcli/arborist": "9.4.0",
|
"@npmcli/arborist": "9.4.0",
|
||||||
"@types/bun": "1.3.12",
|
"@types/bun": "1.3.11",
|
||||||
"@types/cross-spawn": "6.0.6",
|
"@types/cross-spawn": "6.0.6",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
"@opentui/core": "0.2.0",
|
"@opentui/core": "0.1.99",
|
||||||
"@opentui/solid": "0.2.0",
|
"@opentui/solid": "0.1.99",
|
||||||
"ulid": "3.0.1",
|
"ulid": "3.0.1",
|
||||||
"@kobalte/core": "0.13.11",
|
"@kobalte/core": "0.13.11",
|
||||||
"@types/luxon": "3.7.1",
|
"@types/luxon": "3.7.1",
|
||||||
@@ -46,14 +46,13 @@
|
|||||||
"@cloudflare/workers-types": "4.20251008.0",
|
"@cloudflare/workers-types": "4.20251008.0",
|
||||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||||
"@pierre/diffs": "1.1.0-beta.18",
|
"@pierre/diffs": "1.1.0-beta.18",
|
||||||
"opentui-spinner": "0.0.6",
|
|
||||||
"@solid-primitives/storage": "4.3.3",
|
"@solid-primitives/storage": "4.3.3",
|
||||||
"@tailwindcss/vite": "4.1.11",
|
"@tailwindcss/vite": "4.1.11",
|
||||||
"diff": "8.0.2",
|
"diff": "8.0.2",
|
||||||
"dompurify": "3.3.1",
|
"dompurify": "3.3.1",
|
||||||
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
||||||
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
||||||
"effect": "4.0.0-beta.57",
|
"effect": "4.0.0-beta.48",
|
||||||
"ai": "6.0.168",
|
"ai": "6.0.168",
|
||||||
"cross-spawn": "7.0.6",
|
"cross-spawn": "7.0.6",
|
||||||
"hono": "4.10.7",
|
"hono": "4.10.7",
|
||||||
@@ -128,7 +127,6 @@
|
|||||||
"@types/node": "catalog:"
|
"@types/node": "catalog:"
|
||||||
},
|
},
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch",
|
|
||||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch"
|
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
PORT=8787
|
||||||
|
|
||||||
|
DATABASE_HOST=
|
||||||
|
DATABASE_USERNAME=
|
||||||
|
DATABASE_PASSWORD=
|
||||||
|
DATABASE_NAME=main
|
||||||
|
|
||||||
|
APNS_TEAM_ID=
|
||||||
|
APNS_KEY_ID=
|
||||||
|
APNS_PRIVATE_KEY=
|
||||||
|
APNS_DEFAULT_BUNDLE_ID=com.anomalyco.mobilevoice
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# apn-relay Agent Guide
|
||||||
|
|
||||||
|
This file defines package-specific guidance for agents working in `packages/apn-relay`.
|
||||||
|
|
||||||
|
## Scope And Precedence
|
||||||
|
|
||||||
|
- Follow root `AGENTS.md` first.
|
||||||
|
- This file provides stricter package-level conventions for relay service work.
|
||||||
|
- If future local guides are added, closest guide wins.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
- Minimal APNs relay service (Hono + Bun + PlanetScale via Drizzle).
|
||||||
|
- Core routes:
|
||||||
|
- `GET /health`
|
||||||
|
- `GET /`
|
||||||
|
- `POST /v1/device/register`
|
||||||
|
- `POST /v1/device/unregister`
|
||||||
|
- `POST /v1/event`
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
Run all commands from `packages/apn-relay`.
|
||||||
|
|
||||||
|
- Install deps: `bun install`
|
||||||
|
- Start relay locally: `bun run dev`
|
||||||
|
- Typecheck: `bun run typecheck`
|
||||||
|
- DB connectivity check: `bun run db:check`
|
||||||
|
|
||||||
|
## Build / Test Expectations
|
||||||
|
|
||||||
|
- There is no dedicated package test script currently.
|
||||||
|
- Required validation for behavior changes:
|
||||||
|
- `bun run typecheck`
|
||||||
|
- `bun run db:check` when DB/env changes are involved
|
||||||
|
- manual endpoint verification against `/health`, `/v1/device/register`, `/v1/event`
|
||||||
|
|
||||||
|
## Single-Test Guidance
|
||||||
|
|
||||||
|
- No single-test command exists for this package today.
|
||||||
|
- For focused checks, run endpoint-level manual tests against a local dev server.
|
||||||
|
|
||||||
|
## Code Style Guidelines
|
||||||
|
|
||||||
|
### Formatting / Structure
|
||||||
|
|
||||||
|
- Keep handlers compact and explicit.
|
||||||
|
- Prefer small local helpers for repeated route logic.
|
||||||
|
- Avoid broad refactors when a targeted fix is enough.
|
||||||
|
|
||||||
|
### Types / Validation
|
||||||
|
|
||||||
|
- Validate request bodies with `zod` at route boundaries.
|
||||||
|
- Keep payload and DB row shapes explicit and close to usage.
|
||||||
|
- Avoid `any`; narrow unknown input immediately after parsing.
|
||||||
|
|
||||||
|
### Naming
|
||||||
|
|
||||||
|
- Follow existing concise naming in this package (`reg`, `unreg`, `evt`, `row`, `key`).
|
||||||
|
- For DB columns, keep snake_case alignment with schema.
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
- Return clear JSON errors for invalid input.
|
||||||
|
- Keep handler failures observable via `app.onError` and structured logs.
|
||||||
|
- Do not leak secrets in responses or logs.
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
- Log delivery lifecycle at key checkpoints:
|
||||||
|
- registration/unregistration attempts
|
||||||
|
- event fanout start/end
|
||||||
|
- APNs send failures and retries
|
||||||
|
- Mask sensitive values; prefer token suffixes and metadata.
|
||||||
|
|
||||||
|
### APNs Environment Rules
|
||||||
|
|
||||||
|
- Keep APNs env explicit per registration (`sandbox` / `production`).
|
||||||
|
- For `BadEnvironmentKeyInToken`, retry once with flipped env and persist correction.
|
||||||
|
- Avoid infinite retry loops; one retry max per delivery attempt.
|
||||||
|
|
||||||
|
## Database Conventions
|
||||||
|
|
||||||
|
- Schema is in `src/schema.sql.ts`.
|
||||||
|
- Keep table/column names snake_case.
|
||||||
|
- Maintain index naming consistency with existing schema.
|
||||||
|
- For upserts, update only fields required by current behavior.
|
||||||
|
|
||||||
|
## API Behavior Expectations
|
||||||
|
|
||||||
|
- `register`/`unregister` must be idempotent.
|
||||||
|
- `event` should return success envelope even when no devices are registered.
|
||||||
|
- Delivery logs should capture per-attempt result and error payload.
|
||||||
|
|
||||||
|
## Operational Notes
|
||||||
|
|
||||||
|
- Ensure `APNS_PRIVATE_KEY` supports escaped newline format (`\n`) and raw multiline.
|
||||||
|
- Validate that `APNS_DEFAULT_BUNDLE_ID` matches mobile app bundle identifier.
|
||||||
|
- Avoid coupling route behavior to deployment platform specifics.
|
||||||
|
|
||||||
|
## Before Finishing
|
||||||
|
|
||||||
|
- Run `bun run typecheck`.
|
||||||
|
- If DB/env behavior changed, run `bun run db:check`.
|
||||||
|
- Manually exercise affected endpoints.
|
||||||
|
- Confirm logs are useful and secret-safe.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
FROM oven/bun:1.3.11-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json ./
|
||||||
|
COPY tsconfig.json ./
|
||||||
|
COPY drizzle.config.ts ./
|
||||||
|
RUN bun install --production
|
||||||
|
|
||||||
|
COPY src ./src
|
||||||
|
|
||||||
|
EXPOSE 8787
|
||||||
|
|
||||||
|
CMD ["bun", "run", "src/index.ts"]
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# APN Relay
|
||||||
|
|
||||||
|
Minimal APNs relay for OpenCode mobile background notifications.
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
- Registers iOS device tokens for a shared secret.
|
||||||
|
- Receives OpenCode event posts (`complete`, `permission`, `error`).
|
||||||
|
- Sends APNs notifications to mapped devices.
|
||||||
|
- Stores delivery rows in PlanetScale.
|
||||||
|
|
||||||
|
## Routes
|
||||||
|
|
||||||
|
- `GET /health`
|
||||||
|
- `GET /` (simple dashboard)
|
||||||
|
- `POST /v1/device/register`
|
||||||
|
- `POST /v1/device/unregister`
|
||||||
|
- `POST /v1/event`
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
Use `.env.example` as a starting point.
|
||||||
|
|
||||||
|
- `DATABASE_HOST`
|
||||||
|
- `DATABASE_USERNAME`
|
||||||
|
- `DATABASE_PASSWORD`
|
||||||
|
- `APNS_TEAM_ID`
|
||||||
|
- `APNS_KEY_ID`
|
||||||
|
- `APNS_PRIVATE_KEY`
|
||||||
|
- `APNS_DEFAULT_BUNDLE_ID`
|
||||||
|
|
||||||
|
## Run locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
bun run src/index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
Build from this directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t apn-relay .
|
||||||
|
docker run --rm -p 8787:8787 --env-file .env apn-relay
|
||||||
|
```
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { defineConfig } from "drizzle-kit"
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
out: "./migration",
|
||||||
|
strict: true,
|
||||||
|
schema: ["./src/**/*.sql.ts"],
|
||||||
|
dialect: "mysql",
|
||||||
|
dbCredentials: {
|
||||||
|
host: process.env.DATABASE_HOST ?? "",
|
||||||
|
user: process.env.DATABASE_USERNAME ?? "",
|
||||||
|
password: process.env.DATABASE_PASSWORD ?? "",
|
||||||
|
database: process.env.DATABASE_NAME ?? "main",
|
||||||
|
ssl: {
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
|
"name": "@opencode-ai/apn-relay",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun run src/index.ts",
|
||||||
|
"db:check": "bun run --env-file .env src/check.ts",
|
||||||
|
"typecheck": "tsgo --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@planetscale/database": "1.19.0",
|
||||||
|
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
||||||
|
"hono": "4.10.7",
|
||||||
|
"jose": "6.0.11",
|
||||||
|
"zod": "4.1.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tsconfig/bun": "1.0.9",
|
||||||
|
"@types/bun": "1.3.11",
|
||||||
|
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||||
|
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
||||||
|
"typescript": "5.8.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import { connect } from "node:http2"
|
||||||
|
import { SignJWT, importPKCS8 } from "jose"
|
||||||
|
import { env } from "./env"
|
||||||
|
|
||||||
|
export type PushEnv = "sandbox" | "production"
|
||||||
|
|
||||||
|
type PushInput = {
|
||||||
|
token: string
|
||||||
|
bundle: string
|
||||||
|
env: PushEnv
|
||||||
|
title: string
|
||||||
|
body: string
|
||||||
|
data: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
type PushResult = {
|
||||||
|
ok: boolean
|
||||||
|
code: number
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenSuffix(input: string) {
|
||||||
|
return input.length > 8 ? input.slice(-8) : input
|
||||||
|
}
|
||||||
|
|
||||||
|
let jwt = ""
|
||||||
|
let exp = 0
|
||||||
|
let pk: Awaited<ReturnType<typeof importPKCS8>> | undefined
|
||||||
|
|
||||||
|
function host(input: PushEnv) {
|
||||||
|
if (input === "sandbox") return "api.sandbox.push.apple.com"
|
||||||
|
return "api.push.apple.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
function key() {
|
||||||
|
if (env.APNS_PRIVATE_KEY.includes("\\n")) return env.APNS_PRIVATE_KEY.replace(/\\n/g, "\n")
|
||||||
|
return env.APNS_PRIVATE_KEY
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sign() {
|
||||||
|
if (!pk) pk = await importPKCS8(key(), "ES256")
|
||||||
|
const now = Math.floor(Date.now() / 1000)
|
||||||
|
if (jwt && now < exp) return jwt
|
||||||
|
jwt = await new SignJWT({})
|
||||||
|
.setProtectedHeader({ alg: "ES256", kid: env.APNS_KEY_ID })
|
||||||
|
.setIssuer(env.APNS_TEAM_ID)
|
||||||
|
.setIssuedAt(now)
|
||||||
|
.sign(pk)
|
||||||
|
exp = now + 50 * 60
|
||||||
|
return jwt
|
||||||
|
}
|
||||||
|
|
||||||
|
function post(input: {
|
||||||
|
host: string
|
||||||
|
token: string
|
||||||
|
auth: string
|
||||||
|
bundle: string
|
||||||
|
payload: string
|
||||||
|
}): Promise<{ code: number; body: string }> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const cli = connect(`https://${input.host}`)
|
||||||
|
let done = false
|
||||||
|
let code = 0
|
||||||
|
let body = ""
|
||||||
|
|
||||||
|
const stop = (fn: () => void) => {
|
||||||
|
if (done) return
|
||||||
|
done = true
|
||||||
|
fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
cli.on("error", (err) => {
|
||||||
|
stop(() => reject(err))
|
||||||
|
cli.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
const req = cli.request({
|
||||||
|
":method": "POST",
|
||||||
|
":path": `/3/device/${input.token}`,
|
||||||
|
authorization: `bearer ${input.auth}`,
|
||||||
|
"apns-topic": input.bundle,
|
||||||
|
"apns-push-type": "alert",
|
||||||
|
"apns-priority": "10",
|
||||||
|
"content-type": "application/json",
|
||||||
|
})
|
||||||
|
|
||||||
|
req.setEncoding("utf8")
|
||||||
|
req.on("response", (headers) => {
|
||||||
|
code = Number(headers[":status"] ?? 0)
|
||||||
|
})
|
||||||
|
req.on("data", (chunk) => {
|
||||||
|
body += chunk
|
||||||
|
})
|
||||||
|
req.on("end", () => {
|
||||||
|
stop(() => resolve({ code, body }))
|
||||||
|
cli.close()
|
||||||
|
})
|
||||||
|
req.on("error", (err) => {
|
||||||
|
stop(() => reject(err))
|
||||||
|
cli.close()
|
||||||
|
})
|
||||||
|
req.end(input.payload)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function send(input: PushInput): Promise<PushResult> {
|
||||||
|
const apnsHost = host(input.env)
|
||||||
|
const suffix = tokenSuffix(input.token)
|
||||||
|
|
||||||
|
console.log("[ APN RELAY ] push:start", {
|
||||||
|
env: input.env,
|
||||||
|
host: apnsHost,
|
||||||
|
bundle: input.bundle,
|
||||||
|
tokenSuffix: suffix,
|
||||||
|
})
|
||||||
|
|
||||||
|
const auth = await sign().catch((err) => {
|
||||||
|
return `error:${String(err)}`
|
||||||
|
})
|
||||||
|
if (auth.startsWith("error:")) {
|
||||||
|
console.log("[ APN RELAY ] push:auth-failed", {
|
||||||
|
env: input.env,
|
||||||
|
host: apnsHost,
|
||||||
|
bundle: input.bundle,
|
||||||
|
tokenSuffix: suffix,
|
||||||
|
error: auth,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
code: 0,
|
||||||
|
error: auth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
aps: {
|
||||||
|
alert: {
|
||||||
|
title: input.title,
|
||||||
|
body: input.body,
|
||||||
|
},
|
||||||
|
sound: "alert.wav",
|
||||||
|
},
|
||||||
|
...input.data,
|
||||||
|
})
|
||||||
|
|
||||||
|
const out = await post({
|
||||||
|
host: apnsHost,
|
||||||
|
token: input.token,
|
||||||
|
auth,
|
||||||
|
bundle: input.bundle,
|
||||||
|
payload,
|
||||||
|
}).catch((err) => ({
|
||||||
|
code: 0,
|
||||||
|
body: String(err),
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (out.code === 200) {
|
||||||
|
console.log("[ APN RELAY ] push:sent", {
|
||||||
|
env: input.env,
|
||||||
|
host: apnsHost,
|
||||||
|
bundle: input.bundle,
|
||||||
|
tokenSuffix: suffix,
|
||||||
|
code: out.code,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
code: 200,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[ APN RELAY ] push:failed", {
|
||||||
|
env: input.env,
|
||||||
|
host: apnsHost,
|
||||||
|
bundle: input.bundle,
|
||||||
|
tokenSuffix: suffix,
|
||||||
|
code: out.code,
|
||||||
|
error: out.body,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
code: out.code,
|
||||||
|
error: out.body,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { sql } from "drizzle-orm"
|
||||||
|
import { db } from "./db"
|
||||||
|
import { env } from "./env"
|
||||||
|
import { delivery_log, device_registration } from "./schema.sql"
|
||||||
|
import { setup } from "./setup"
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
console.log(`[apn-relay] DB host: ${env.DATABASE_HOST}`)
|
||||||
|
|
||||||
|
await db.execute(sql`SELECT 1`)
|
||||||
|
console.log("[apn-relay] DB connection OK")
|
||||||
|
|
||||||
|
await setup()
|
||||||
|
console.log("[apn-relay] Setup migration OK")
|
||||||
|
|
||||||
|
const [a] = await db.select({ value: sql<number>`count(*)` }).from(device_registration)
|
||||||
|
const [b] = await db.select({ value: sql<number>`count(*)` }).from(delivery_log)
|
||||||
|
|
||||||
|
console.log(`[apn-relay] device_registration rows: ${Number(a?.value ?? 0)}`)
|
||||||
|
console.log(`[apn-relay] delivery_log rows: ${Number(b?.value ?? 0)}`)
|
||||||
|
console.log("[apn-relay] DB check passed")
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error("[apn-relay] DB check failed")
|
||||||
|
console.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Client } from "@planetscale/database"
|
||||||
|
import { drizzle } from "drizzle-orm/planetscale-serverless"
|
||||||
|
import { env } from "./env"
|
||||||
|
|
||||||
|
const client = new Client({
|
||||||
|
host: env.DATABASE_HOST,
|
||||||
|
username: env.DATABASE_USERNAME,
|
||||||
|
password: env.DATABASE_PASSWORD,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const db = drizzle({ client })
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
const bad = new Set(["undefined", "null"])
|
||||||
|
const txt = z
|
||||||
|
.string()
|
||||||
|
.transform((input) => input.trim())
|
||||||
|
.refine((input) => input.length > 0 && !bad.has(input.toLowerCase()))
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
PORT: z.coerce.number().int().positive().default(8787),
|
||||||
|
DATABASE_HOST: txt,
|
||||||
|
DATABASE_USERNAME: txt,
|
||||||
|
DATABASE_PASSWORD: txt,
|
||||||
|
APNS_TEAM_ID: txt,
|
||||||
|
APNS_KEY_ID: txt,
|
||||||
|
APNS_PRIVATE_KEY: txt,
|
||||||
|
APNS_DEFAULT_BUNDLE_ID: txt,
|
||||||
|
})
|
||||||
|
|
||||||
|
const req = [
|
||||||
|
"DATABASE_HOST",
|
||||||
|
"DATABASE_USERNAME",
|
||||||
|
"DATABASE_PASSWORD",
|
||||||
|
"APNS_TEAM_ID",
|
||||||
|
"APNS_KEY_ID",
|
||||||
|
"APNS_PRIVATE_KEY",
|
||||||
|
"APNS_DEFAULT_BUNDLE_ID",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const out = schema.safeParse(process.env)
|
||||||
|
|
||||||
|
if (!out.success) {
|
||||||
|
const miss = req.filter((key) => !process.env[key]?.trim())
|
||||||
|
const bad = out.error.issues
|
||||||
|
.map((item) => item.path[0])
|
||||||
|
.filter((key): key is string => typeof key === "string")
|
||||||
|
.filter((key) => !miss.includes(key as (typeof req)[number]))
|
||||||
|
|
||||||
|
console.error("[apn-relay] Invalid startup configuration")
|
||||||
|
if (miss.length) console.error(`[apn-relay] Missing required env vars: ${miss.join(", ")}`)
|
||||||
|
if (bad.length) console.error(`[apn-relay] Invalid env vars: ${Array.from(new Set(bad)).join(", ")}`)
|
||||||
|
console.error("[apn-relay] Check .env.example and restart")
|
||||||
|
|
||||||
|
throw new Error("Startup configuration invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
export const env = out.data
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { createHash } from "node:crypto"
|
||||||
|
|
||||||
|
export function hash(input: string) {
|
||||||
|
return createHash("sha256").update(input).digest("hex")
|
||||||
|
}
|
||||||
@@ -0,0 +1,448 @@
|
|||||||
|
import { randomUUID } from "node:crypto"
|
||||||
|
import { and, desc, eq, sql } from "drizzle-orm"
|
||||||
|
import { Hono } from "hono"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { send } from "./apns"
|
||||||
|
import { db } from "./db"
|
||||||
|
import { env } from "./env"
|
||||||
|
import { hash } from "./hash"
|
||||||
|
import { delivery_log, device_registration } from "./schema.sql"
|
||||||
|
import { setup } from "./setup"
|
||||||
|
|
||||||
|
function bad(input?: string) {
|
||||||
|
if (!input) return false
|
||||||
|
return input.includes("BadEnvironmentKeyInToken")
|
||||||
|
}
|
||||||
|
|
||||||
|
function flip(input: "sandbox" | "production") {
|
||||||
|
if (input === "sandbox") return "production"
|
||||||
|
return "sandbox"
|
||||||
|
}
|
||||||
|
|
||||||
|
function tail(input: string) {
|
||||||
|
return input.slice(-8)
|
||||||
|
}
|
||||||
|
|
||||||
|
function esc(input: unknown) {
|
||||||
|
return String(input ?? "")
|
||||||
|
.replaceAll("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">")
|
||||||
|
.replaceAll('"', """)
|
||||||
|
.replaceAll("'", "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmt(input: number) {
|
||||||
|
return new Date(input).toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
const reg = z.object({
|
||||||
|
secret: z.string().min(1),
|
||||||
|
deviceToken: z.string().min(1),
|
||||||
|
bundleId: z.string().min(1).optional(),
|
||||||
|
apnsEnv: z.enum(["sandbox", "production"]).default("production"),
|
||||||
|
})
|
||||||
|
|
||||||
|
const unreg = z.object({
|
||||||
|
secret: z.string().min(1),
|
||||||
|
deviceToken: z.string().min(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
const evt = z.object({
|
||||||
|
secret: z.string().min(1),
|
||||||
|
serverID: z.string().min(1).optional(),
|
||||||
|
eventType: z.enum(["complete", "permission", "error"]),
|
||||||
|
sessionID: z.string().min(1),
|
||||||
|
title: z.string().min(1).optional(),
|
||||||
|
body: z.string().min(1).optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
function title(input: z.infer<typeof evt>["eventType"]) {
|
||||||
|
if (input === "complete") return "Session complete"
|
||||||
|
if (input === "permission") return "Action needed"
|
||||||
|
return "Session error"
|
||||||
|
}
|
||||||
|
|
||||||
|
function body(input: z.infer<typeof evt>["eventType"]) {
|
||||||
|
if (input === "complete") return "OpenCode finished your session."
|
||||||
|
if (input === "permission") return "OpenCode needs your permission decision."
|
||||||
|
return "OpenCode reported an error for your session."
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = new Hono()
|
||||||
|
|
||||||
|
app.onError((err, c) => {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
ok: false,
|
||||||
|
error: err.message,
|
||||||
|
},
|
||||||
|
500,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.notFound((c) => {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
ok: false,
|
||||||
|
error: "Not found",
|
||||||
|
},
|
||||||
|
404,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get("/health", async (c) => {
|
||||||
|
const [a] = await db.select({ value: sql<number>`count(*)` }).from(device_registration)
|
||||||
|
const [b] = await db.select({ value: sql<number>`count(*)` }).from(delivery_log)
|
||||||
|
return c.json({
|
||||||
|
ok: true,
|
||||||
|
devices: Number(a?.value ?? 0),
|
||||||
|
deliveries: Number(b?.value ?? 0),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get("/", async (c) => {
|
||||||
|
const [a] = await db.select({ value: sql<number>`count(*)` }).from(device_registration)
|
||||||
|
const [b] = await db.select({ value: sql<number>`count(*)` }).from(delivery_log)
|
||||||
|
const devices = await db.select().from(device_registration).orderBy(desc(device_registration.updated_at)).limit(100)
|
||||||
|
const byBundle = await db
|
||||||
|
.select({
|
||||||
|
bundle: device_registration.bundle_id,
|
||||||
|
env: device_registration.apns_env,
|
||||||
|
value: sql<number>`count(*)`,
|
||||||
|
})
|
||||||
|
.from(device_registration)
|
||||||
|
.groupBy(device_registration.bundle_id, device_registration.apns_env)
|
||||||
|
.orderBy(desc(sql<number>`count(*)`))
|
||||||
|
const rows = await db.select().from(delivery_log).orderBy(desc(delivery_log.created_at)).limit(20)
|
||||||
|
|
||||||
|
const html = `<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>APN Relay</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: ui-sans-serif, system-ui, sans-serif; margin: 24px; color: #111827; }
|
||||||
|
h1 { margin: 0 0 12px 0; }
|
||||||
|
h2 { margin: 22px 0 10px 0; font-size: 16px; }
|
||||||
|
.stats { display: flex; gap: 16px; margin: 0 0 18px 0; }
|
||||||
|
.card { border: 1px solid #e5e7eb; border-radius: 8px; padding: 10px 12px; min-width: 160px; }
|
||||||
|
.muted { color: #6b7280; font-size: 12px; }
|
||||||
|
.small { font-size: 11px; color: #6b7280; }
|
||||||
|
table { border-collapse: collapse; width: 100%; }
|
||||||
|
th, td { border: 1px solid #e5e7eb; text-align: left; padding: 8px; font-size: 12px; }
|
||||||
|
th { background: #f9fafb; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>APN Relay</h1>
|
||||||
|
<p class="muted">MVP dashboard</p>
|
||||||
|
<div class="stats">
|
||||||
|
<div class="card">
|
||||||
|
<div class="muted">Registered devices</div>
|
||||||
|
<div>${Number(a?.value ?? 0)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="muted">Delivery log rows</div>
|
||||||
|
<div>${Number(b?.value ?? 0)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2>Registered devices</h2>
|
||||||
|
<p class="small">Most recent 100 registrations. Token values are masked to suffix only.</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>updated</th>
|
||||||
|
<th>created</th>
|
||||||
|
<th>token suffix</th>
|
||||||
|
<th>env</th>
|
||||||
|
<th>bundle</th>
|
||||||
|
<th>secret hash</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${
|
||||||
|
devices.length
|
||||||
|
? devices
|
||||||
|
.map(
|
||||||
|
(row) => `<tr>
|
||||||
|
<td>${esc(fmt(row.updated_at))}</td>
|
||||||
|
<td>${esc(fmt(row.created_at))}</td>
|
||||||
|
<td>${esc(tail(row.device_token))}</td>
|
||||||
|
<td>${esc(row.apns_env)}</td>
|
||||||
|
<td>${esc(row.bundle_id)}</td>
|
||||||
|
<td>${esc(`${row.secret_hash.slice(0, 12)}…`)}</td>
|
||||||
|
</tr>`,
|
||||||
|
)
|
||||||
|
.join("")
|
||||||
|
: `<tr><td colspan="6" class="muted">No devices registered.</td></tr>`
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h2>Bundle breakdown</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>bundle</th>
|
||||||
|
<th>env</th>
|
||||||
|
<th>count</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${
|
||||||
|
byBundle.length
|
||||||
|
? byBundle
|
||||||
|
.map(
|
||||||
|
(row) => `<tr>
|
||||||
|
<td>${esc(row.bundle)}</td>
|
||||||
|
<td>${esc(row.env)}</td>
|
||||||
|
<td>${esc(Number(row.value ?? 0))}</td>
|
||||||
|
</tr>`,
|
||||||
|
)
|
||||||
|
.join("")
|
||||||
|
: `<tr><td colspan="3" class="muted">No device data.</td></tr>`
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h2>Recent deliveries</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>time</th>
|
||||||
|
<th>event</th>
|
||||||
|
<th>session</th>
|
||||||
|
<th>status</th>
|
||||||
|
<th>error</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${rows
|
||||||
|
.map(
|
||||||
|
(row) => `<tr>
|
||||||
|
<td>${esc(fmt(row.created_at))}</td>
|
||||||
|
<td>${esc(row.event_type)}</td>
|
||||||
|
<td>${esc(row.session_id)}</td>
|
||||||
|
<td>${esc(row.status)}</td>
|
||||||
|
<td>${esc(row.error ?? "")}</td>
|
||||||
|
</tr>`,
|
||||||
|
)
|
||||||
|
.join("")}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
|
||||||
|
return c.html(html)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post("/v1/device/register", async (c) => {
|
||||||
|
const raw = await c.req.json().catch(() => undefined)
|
||||||
|
const check = reg.safeParse(raw)
|
||||||
|
if (!check.success) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
ok: false,
|
||||||
|
error: "Invalid request body",
|
||||||
|
},
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
const key = hash(check.data.secret)
|
||||||
|
const row = {
|
||||||
|
id: randomUUID(),
|
||||||
|
secret_hash: key,
|
||||||
|
device_token: check.data.deviceToken,
|
||||||
|
bundle_id: check.data.bundleId ?? env.APNS_DEFAULT_BUNDLE_ID,
|
||||||
|
apns_env: check.data.apnsEnv,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[relay] register", {
|
||||||
|
token: tail(row.device_token),
|
||||||
|
env: row.apns_env,
|
||||||
|
bundle: row.bundle_id,
|
||||||
|
secretHash: `${key.slice(0, 12)}...`,
|
||||||
|
})
|
||||||
|
|
||||||
|
await db
|
||||||
|
.insert(device_registration)
|
||||||
|
.values(row)
|
||||||
|
.onDuplicateKeyUpdate({
|
||||||
|
set: {
|
||||||
|
bundle_id: row.bundle_id,
|
||||||
|
apns_env: row.apns_env,
|
||||||
|
updated_at: now,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return c.json({ ok: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post("/v1/device/unregister", async (c) => {
|
||||||
|
const raw = await c.req.json().catch(() => undefined)
|
||||||
|
const check = unreg.safeParse(raw)
|
||||||
|
if (!check.success) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
ok: false,
|
||||||
|
error: "Invalid request body",
|
||||||
|
},
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = hash(check.data.secret)
|
||||||
|
|
||||||
|
console.log("[relay] unregister", {
|
||||||
|
token: tail(check.data.deviceToken),
|
||||||
|
secretHash: `${key.slice(0, 12)}...`,
|
||||||
|
})
|
||||||
|
|
||||||
|
await db
|
||||||
|
.delete(device_registration)
|
||||||
|
.where(and(eq(device_registration.secret_hash, key), eq(device_registration.device_token, check.data.deviceToken)))
|
||||||
|
|
||||||
|
return c.json({ ok: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post("/v1/event", async (c) => {
|
||||||
|
const raw = await c.req.json().catch(() => undefined)
|
||||||
|
const check = evt.safeParse(raw)
|
||||||
|
if (!check.success) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
ok: false,
|
||||||
|
error: "Invalid request body",
|
||||||
|
},
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = hash(check.data.secret)
|
||||||
|
const list = await db.select().from(device_registration).where(eq(device_registration.secret_hash, key))
|
||||||
|
console.log("[relay] event", {
|
||||||
|
type: check.data.eventType,
|
||||||
|
serverID: check.data.serverID,
|
||||||
|
session: check.data.sessionID,
|
||||||
|
secretHash: `${key.slice(0, 12)}...`,
|
||||||
|
devices: list.length,
|
||||||
|
})
|
||||||
|
if (!list.length) {
|
||||||
|
const [total] = await db.select({ value: sql<number>`count(*)` }).from(device_registration)
|
||||||
|
console.log("[relay] event:no-matching-devices", {
|
||||||
|
type: check.data.eventType,
|
||||||
|
serverID: check.data.serverID,
|
||||||
|
session: check.data.sessionID,
|
||||||
|
secretHash: `${key.slice(0, 12)}...`,
|
||||||
|
totalDevices: Number(total?.value ?? 0),
|
||||||
|
})
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
ok: true,
|
||||||
|
sent: 0,
|
||||||
|
failed: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const out = await Promise.all(
|
||||||
|
list.map(async (row) => {
|
||||||
|
const env = row.apns_env === "sandbox" ? "sandbox" : "production"
|
||||||
|
const payload = {
|
||||||
|
token: row.device_token,
|
||||||
|
bundle: row.bundle_id,
|
||||||
|
title: check.data.title ?? title(check.data.eventType),
|
||||||
|
body: check.data.body ?? body(check.data.eventType),
|
||||||
|
data: {
|
||||||
|
serverID: check.data.serverID,
|
||||||
|
eventType: check.data.eventType,
|
||||||
|
sessionID: check.data.sessionID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const first = await send({ ...payload, env })
|
||||||
|
if (first.ok || !bad(first.error)) {
|
||||||
|
if (!first.ok) {
|
||||||
|
console.log("[relay] send:error", {
|
||||||
|
token: tail(row.device_token),
|
||||||
|
env,
|
||||||
|
error: first.error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return first
|
||||||
|
}
|
||||||
|
|
||||||
|
const alt = flip(env)
|
||||||
|
console.log("[relay] send:retry-env", {
|
||||||
|
token: tail(row.device_token),
|
||||||
|
from: env,
|
||||||
|
to: alt,
|
||||||
|
})
|
||||||
|
const second = await send({ ...payload, env: alt })
|
||||||
|
if (!second.ok) {
|
||||||
|
console.log("[relay] send:error", {
|
||||||
|
token: tail(row.device_token),
|
||||||
|
env: alt,
|
||||||
|
error: second.error,
|
||||||
|
})
|
||||||
|
return second
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(device_registration)
|
||||||
|
.set({ apns_env: alt, updated_at: Date.now() })
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(device_registration.secret_hash, row.secret_hash),
|
||||||
|
eq(device_registration.device_token, row.device_token),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log("[relay] send:env-updated", {
|
||||||
|
token: tail(row.device_token),
|
||||||
|
env: alt,
|
||||||
|
})
|
||||||
|
return second
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
await db.insert(delivery_log).values(
|
||||||
|
out.map((item) => ({
|
||||||
|
id: randomUUID(),
|
||||||
|
secret_hash: key,
|
||||||
|
event_type: check.data.eventType,
|
||||||
|
session_id: check.data.sessionID,
|
||||||
|
status: item.ok ? "sent" : "failed",
|
||||||
|
error: item.error,
|
||||||
|
created_at: now,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
const sent = out.filter((item) => item.ok).length
|
||||||
|
console.log("[relay] event:done", {
|
||||||
|
type: check.data.eventType,
|
||||||
|
session: check.data.sessionID,
|
||||||
|
sent,
|
||||||
|
failed: out.length - sent,
|
||||||
|
})
|
||||||
|
return c.json({
|
||||||
|
ok: true,
|
||||||
|
sent,
|
||||||
|
failed: out.length - sent,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await setup()
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
Bun.serve({
|
||||||
|
port: env.PORT,
|
||||||
|
fetch: app.fetch,
|
||||||
|
})
|
||||||
|
console.log(`apn-relay listening on http://0.0.0.0:${env.PORT}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { app }
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { bigint, index, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
|
||||||
|
|
||||||
|
export const device_registration = mysqlTable(
|
||||||
|
"device_registration",
|
||||||
|
{
|
||||||
|
id: varchar("id", { length: 36 }).primaryKey(),
|
||||||
|
secret_hash: varchar("secret_hash", { length: 64 }).notNull(),
|
||||||
|
device_token: varchar("device_token", { length: 255 }).notNull(),
|
||||||
|
bundle_id: varchar("bundle_id", { length: 255 }).notNull(),
|
||||||
|
apns_env: varchar("apns_env", { length: 16 }).notNull().default("production"),
|
||||||
|
created_at: bigint("created_at", { mode: "number" }).notNull(),
|
||||||
|
updated_at: bigint("updated_at", { mode: "number" }).notNull(),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
uniqueIndex("device_registration_secret_token_idx").on(table.secret_hash, table.device_token),
|
||||||
|
index("device_registration_secret_hash_idx").on(table.secret_hash),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
export const delivery_log = mysqlTable(
|
||||||
|
"delivery_log",
|
||||||
|
{
|
||||||
|
id: varchar("id", { length: 36 }).primaryKey(),
|
||||||
|
secret_hash: varchar("secret_hash", { length: 64 }).notNull(),
|
||||||
|
event_type: varchar("event_type", { length: 32 }).notNull(),
|
||||||
|
session_id: varchar("session_id", { length: 255 }).notNull(),
|
||||||
|
status: varchar("status", { length: 16 }).notNull(),
|
||||||
|
error: varchar("error", { length: 1024 }),
|
||||||
|
created_at: bigint("created_at", { mode: "number" }).notNull(),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
index("delivery_log_secret_hash_idx").on(table.secret_hash),
|
||||||
|
index("delivery_log_created_at_idx").on(table.created_at),
|
||||||
|
],
|
||||||
|
)
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { sql } from "drizzle-orm"
|
||||||
|
import { db } from "./db"
|
||||||
|
|
||||||
|
export async function setup() {
|
||||||
|
await db.execute(sql`
|
||||||
|
CREATE TABLE IF NOT EXISTS device_registration (
|
||||||
|
id varchar(36) NOT NULL,
|
||||||
|
secret_hash varchar(64) NOT NULL,
|
||||||
|
device_token varchar(255) NOT NULL,
|
||||||
|
bundle_id varchar(255) NOT NULL,
|
||||||
|
apns_env varchar(16) NOT NULL DEFAULT 'production',
|
||||||
|
created_at bigint NOT NULL,
|
||||||
|
updated_at bigint NOT NULL,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY device_registration_secret_token_idx (secret_hash, device_token),
|
||||||
|
KEY device_registration_secret_hash_idx (secret_hash)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
`)
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
CREATE TABLE IF NOT EXISTS delivery_log (
|
||||||
|
id varchar(36) NOT NULL,
|
||||||
|
secret_hash varchar(64) NOT NULL,
|
||||||
|
event_type varchar(32) NOT NULL,
|
||||||
|
session_id varchar(255) NOT NULL,
|
||||||
|
status varchar(16) NOT NULL,
|
||||||
|
error varchar(1024) NULL,
|
||||||
|
created_at bigint NOT NULL,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY delivery_log_secret_hash_idx (secret_hash),
|
||||||
|
KEY delivery_log_created_at_idx (created_at)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
`)
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"extends": "@tsconfig/bun/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||||
|
"noUncheckedIndexedAccess": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.14.30",
|
"version": "1.14.18",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/shared": "workspace:*",
|
||||||
"@shikijs/transformers": "3.9.2",
|
"@shikijs/transformers": "3.9.2",
|
||||||
"@solid-primitives/active-element": "2.1.3",
|
"@solid-primitives/active-element": "2.1.3",
|
||||||
"@solid-primitives/audio": "1.4.2",
|
"@solid-primitives/audio": "1.4.2",
|
||||||
|
|||||||
+22
-30
@@ -82,15 +82,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function QueryProvider(props: ParentProps) {
|
function QueryProvider(props: ParentProps) {
|
||||||
const client = new QueryClient({
|
const client = new QueryClient()
|
||||||
defaultOptions: {
|
|
||||||
queries: {
|
|
||||||
refetchOnReconnect: false,
|
|
||||||
refetchOnMount: false,
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return <QueryClientProvider client={client}>{props.children}</QueryClientProvider>
|
return <QueryClientProvider client={client}>{props.children}</QueryClientProvider>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,11 +141,13 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
|
|||||||
<LanguageProvider locale={props.locale}>
|
<LanguageProvider locale={props.locale}>
|
||||||
<UiI18nBridge>
|
<UiI18nBridge>
|
||||||
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
|
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
|
||||||
<DialogProvider>
|
<QueryProvider>
|
||||||
<MarkedProvider>
|
<DialogProvider>
|
||||||
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
|
<MarkedProvider>
|
||||||
</MarkedProvider>
|
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
|
||||||
</DialogProvider>
|
</MarkedProvider>
|
||||||
|
</DialogProvider>
|
||||||
|
</QueryProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</UiI18nBridge>
|
</UiI18nBridge>
|
||||||
</LanguageProvider>
|
</LanguageProvider>
|
||||||
@@ -299,22 +293,20 @@ export function AppInterface(props: {
|
|||||||
>
|
>
|
||||||
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
|
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
|
||||||
<ServerKey>
|
<ServerKey>
|
||||||
<QueryProvider>
|
<GlobalSDKProvider>
|
||||||
<GlobalSDKProvider>
|
<GlobalSyncProvider>
|
||||||
<GlobalSyncProvider>
|
<Dynamic
|
||||||
<Dynamic
|
component={props.router ?? Router}
|
||||||
component={props.router ?? Router}
|
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
|
||||||
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
|
>
|
||||||
>
|
<Route path="/" component={HomeRoute} />
|
||||||
<Route path="/" component={HomeRoute} />
|
<Route path="/:dir" component={DirectoryLayout}>
|
||||||
<Route path="/:dir" component={DirectoryLayout}>
|
<Route path="/" component={SessionIndexRoute} />
|
||||||
<Route path="/" component={SessionIndexRoute} />
|
<Route path="/session/:id?" component={SessionRoute} />
|
||||||
<Route path="/session/:id?" component={SessionRoute} />
|
</Route>
|
||||||
</Route>
|
</Dynamic>
|
||||||
</Dynamic>
|
</GlobalSyncProvider>
|
||||||
</GlobalSyncProvider>
|
</GlobalSDKProvider>
|
||||||
</GlobalSDKProvider>
|
|
||||||
</QueryProvider>
|
|
||||||
</ServerKey>
|
</ServerKey>
|
||||||
</ConnectionGate>
|
</ConnectionGate>
|
||||||
</ServerProvider>
|
</ServerProvider>
|
||||||
|
|||||||
@@ -9,10 +9,9 @@ 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"
|
||||||
import { type LocalProject, getAvatarColors } from "@/context/layout"
|
import { type LocalProject, getAvatarColors } from "@/context/layout"
|
||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
import { getFilename } from "@opencode-ai/shared/util/path"
|
||||||
import { Avatar } from "@opencode-ai/ui/avatar"
|
import { Avatar } from "@opencode-ai/ui/avatar"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { getProjectAvatarSource } from "@/pages/layout/sidebar-items"
|
|
||||||
|
|
||||||
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
|
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
|
||||||
|
|
||||||
@@ -27,8 +26,8 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
|
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
name: defaultName(),
|
name: defaultName(),
|
||||||
color: props.project.icon?.color,
|
color: props.project.icon?.color || "pink",
|
||||||
iconOverride: props.project.icon?.override,
|
iconUrl: props.project.icon?.override || "",
|
||||||
startup: props.project.commands?.start ?? "",
|
startup: props.project.commands?.start ?? "",
|
||||||
dragOver: false,
|
dragOver: false,
|
||||||
iconHover: false,
|
iconHover: false,
|
||||||
@@ -40,7 +39,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
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("iconOverride", e.target?.result as string)
|
setStore("iconUrl", e.target?.result as string)
|
||||||
setStore("iconHover", false)
|
setStore("iconHover", false)
|
||||||
}
|
}
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file)
|
||||||
@@ -69,7 +68,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearIcon() {
|
function clearIcon() {
|
||||||
setStore("iconOverride", "")
|
setStore("iconUrl", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveMutation = useMutation(() => ({
|
const saveMutation = useMutation(() => ({
|
||||||
@@ -82,17 +81,17 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
projectID: props.project.id,
|
projectID: props.project.id,
|
||||||
directory: props.project.worktree,
|
directory: props.project.worktree,
|
||||||
name,
|
name,
|
||||||
icon: { color: store.color || "", override: store.iconOverride || "" },
|
icon: { color: store.color, override: store.iconUrl },
|
||||||
commands: { start },
|
commands: { start },
|
||||||
})
|
})
|
||||||
globalSync.project.icon(props.project.worktree, store.iconOverride || undefined)
|
globalSync.project.icon(props.project.worktree, store.iconUrl || undefined)
|
||||||
dialog.close()
|
dialog.close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
globalSync.project.meta(props.project.worktree, {
|
globalSync.project.meta(props.project.worktree, {
|
||||||
name,
|
name,
|
||||||
icon: { color: store.color || undefined, override: store.iconOverride || undefined },
|
icon: { color: store.color, override: store.iconUrl || undefined },
|
||||||
commands: { start: start || undefined },
|
commands: { start: start || undefined },
|
||||||
})
|
})
|
||||||
dialog.close()
|
dialog.close()
|
||||||
@@ -131,13 +130,13 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
classList={{
|
classList={{
|
||||||
"border-text-interactive-base bg-surface-info-base/20": store.dragOver,
|
"border-text-interactive-base bg-surface-info-base/20": store.dragOver,
|
||||||
"border-border-base hover:border-border-strong": !store.dragOver,
|
"border-border-base hover:border-border-strong": !store.dragOver,
|
||||||
"overflow-hidden": !!store.iconOverride,
|
"overflow-hidden": !!store.iconUrl,
|
||||||
}}
|
}}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (store.iconOverride && store.iconHover) {
|
if (store.iconUrl && store.iconHover) {
|
||||||
clearIcon()
|
clearIcon()
|
||||||
} else {
|
} else {
|
||||||
iconInput?.click()
|
iconInput?.click()
|
||||||
@@ -145,11 +144,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show
|
<Show
|
||||||
when={getProjectAvatarSource(props.project.id, {
|
when={store.iconUrl}
|
||||||
color: store.color,
|
|
||||||
url: props.project.icon?.url,
|
|
||||||
override: store.iconOverride,
|
|
||||||
})}
|
|
||||||
fallback={
|
fallback={
|
||||||
<div class="size-full flex items-center justify-center">
|
<div class="size-full flex items-center justify-center">
|
||||||
<Avatar
|
<Avatar
|
||||||
@@ -160,20 +155,18 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(src) => (
|
<img
|
||||||
<img
|
src={store.iconUrl}
|
||||||
src={src()}
|
alt={language.t("dialog.project.edit.icon.alt")}
|
||||||
alt={language.t("dialog.project.edit.icon.alt")}
|
class="size-full object-cover"
|
||||||
class="size-full object-cover"
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
||||||
classList={{
|
classList={{
|
||||||
"opacity-100": store.iconHover && !store.iconOverride,
|
"opacity-100": store.iconHover && !store.iconUrl,
|
||||||
"opacity-0": !(store.iconHover && !store.iconOverride),
|
"opacity-0": !(store.iconHover && !store.iconUrl),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon name="cloud-upload" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
|
<Icon name="cloud-upload" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
|
||||||
@@ -181,8 +174,8 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
<div
|
<div
|
||||||
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
||||||
classList={{
|
classList={{
|
||||||
"opacity-100": store.iconHover && !!store.iconOverride,
|
"opacity-100": store.iconHover && !!store.iconUrl,
|
||||||
"opacity-0": !(store.iconHover && !!store.iconOverride),
|
"opacity-0": !(store.iconHover && !!store.iconUrl),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon name="trash" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
|
<Icon name="trash" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
|
||||||
@@ -205,7 +198,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={!store.iconOverride}>
|
<Show when={!store.iconUrl}>
|
||||||
<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.color")}</label>
|
<label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.color")}</label>
|
||||||
<div class="flex gap-1.5">
|
<div class="flex gap-1.5">
|
||||||
@@ -222,10 +215,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
"bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base":
|
"bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base":
|
||||||
store.color !== color,
|
store.color !== color,
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => setStore("color", color)}
|
||||||
if (store.color === color && !props.project.icon?.url) return
|
|
||||||
setStore("color", store.color === color ? undefined : color)
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
fallback={store.name || defaultName()}
|
fallback={store.name || defaultName()}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { List } from "@opencode-ai/ui/list"
|
|||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { extractPromptFromParts } from "@/utils/prompt"
|
import { extractPromptFromParts } from "@/utils/prompt"
|
||||||
import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client"
|
import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
|
||||||
interface ForkableMessage {
|
interface ForkableMessage {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Dialog } from "@opencode-ai/ui/dialog"
|
|||||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import type { ListRef } from "@opencode-ai/ui/list"
|
import type { ListRef } from "@opencode-ai/ui/list"
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/shared/util/path"
|
||||||
import fuzzysort from "fuzzysort"
|
import fuzzysort from "fuzzysort"
|
||||||
import { createMemo, createResource, createSignal } from "solid-js"
|
import { createMemo, createResource, createSignal } from "solid-js"
|
||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { FileIcon } from "@opencode-ai/ui/file-icon"
|
|||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
import { Keybind } from "@opencode-ai/ui/keybind"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/shared/util/path"
|
||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import { createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js"
|
import { createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js"
|
||||||
import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
|
import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { useMutation, useQueryClient } from "@tanstack/solid-query"
|
import { useMutation } from "@tanstack/solid-query"
|
||||||
import { Component, createMemo, Show } from "solid-js"
|
import { Component, createEffect, createMemo, on, Show } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { Switch } from "@opencode-ai/ui/switch"
|
||||||
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { loadMcpQuery } from "@/context/global-sync"
|
|
||||||
|
|
||||||
const statusLabels = {
|
const statusLabels = {
|
||||||
connected: "mcp.status.connected",
|
connected: "mcp.status.connected",
|
||||||
@@ -19,7 +20,48 @@ export const DialogSelectMcp: Component = () => {
|
|||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const queryClient = useQueryClient()
|
const [state, setState] = createStore({
|
||||||
|
done: false,
|
||||||
|
loading: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
() => sync.data.mcp_ready,
|
||||||
|
(ready, prev) => {
|
||||||
|
if (!ready && prev) setState("done", false)
|
||||||
|
},
|
||||||
|
{ defer: true },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (state.done || state.loading) return
|
||||||
|
if (sync.data.mcp_ready) {
|
||||||
|
setState("done", true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setState("loading", true)
|
||||||
|
void sdk.client.mcp
|
||||||
|
.status()
|
||||||
|
.then((result) => {
|
||||||
|
sync.set("mcp", result.data ?? {})
|
||||||
|
sync.set("mcp_ready", true)
|
||||||
|
setState("done", true)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setState("done", true)
|
||||||
|
showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: language.t("common.requestFailed"),
|
||||||
|
description: err instanceof Error ? err.message : String(err),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setState("loading", false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const items = createMemo(() =>
|
const items = createMemo(() =>
|
||||||
Object.entries(sync.data.mcp ?? {})
|
Object.entries(sync.data.mcp ?? {})
|
||||||
@@ -29,10 +71,16 @@ export const DialogSelectMcp: Component = () => {
|
|||||||
|
|
||||||
const toggle = useMutation(() => ({
|
const toggle = useMutation(() => ({
|
||||||
mutationFn: async (name: string) => {
|
mutationFn: async (name: string) => {
|
||||||
if (sync.data.mcp[name]?.status === "connected") await sdk.client.mcp.disconnect({ name })
|
const status = sync.data.mcp[name]
|
||||||
else await sdk.client.mcp.connect({ name })
|
if (status?.status === "connected") {
|
||||||
|
await sdk.client.mcp.disconnect({ name })
|
||||||
|
} else {
|
||||||
|
await sdk.client.mcp.connect({ name })
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await sdk.client.mcp.status()
|
||||||
|
if (result.data) sync.set("mcp", result.data)
|
||||||
},
|
},
|
||||||
onSuccess: () => queryClient.refetchQueries({ queryKey: loadMcpQuery(sync.directory).queryKey }),
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length)
|
const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length)
|
||||||
|
|||||||
@@ -504,7 +504,7 @@ export function DialogSelectServer() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog title={formTitle()}>
|
<Dialog title={formTitle()}>
|
||||||
<div class="flex flex-1 min-h-0 flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<Show
|
<Show
|
||||||
when={!isFormMode()}
|
when={!isFormMode()}
|
||||||
fallback={
|
fallback={
|
||||||
@@ -539,7 +539,7 @@ export function DialogSelectServer() {
|
|||||||
if (x) void select(x)
|
if (x) void select(x)
|
||||||
}}
|
}}
|
||||||
divider={true}
|
divider={true}
|
||||||
class="flex-1 min-h-0 px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:min-h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent"
|
class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:min-h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent"
|
||||||
>
|
>
|
||||||
{(i) => {
|
{(i) => {
|
||||||
const key = ServerConnection.key(i)
|
const key = ServerConnection.key(i)
|
||||||
@@ -619,7 +619,7 @@ export function DialogSelectServer() {
|
|||||||
</List>
|
</List>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div class="shrink-0 px-5 pb-5">
|
<div class="px-5 pb-5">
|
||||||
<Show
|
<Show
|
||||||
when={isFormMode()}
|
when={isFormMode()}
|
||||||
fallback={
|
fallback={
|
||||||
|
|||||||
@@ -8,14 +8,20 @@ import { SettingsGeneral } from "./settings-general"
|
|||||||
import { SettingsKeybinds } from "./settings-keybinds"
|
import { SettingsKeybinds } from "./settings-keybinds"
|
||||||
import { SettingsProviders } from "./settings-providers"
|
import { SettingsProviders } from "./settings-providers"
|
||||||
import { SettingsModels } from "./settings-models"
|
import { SettingsModels } from "./settings-models"
|
||||||
|
import { SettingsPair } from "./settings-pair"
|
||||||
|
|
||||||
export const DialogSettings: Component = () => {
|
export const DialogSettings: Component<{ defaultTab?: string }> = (props) => {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog size="x-large" transition>
|
<Dialog size="x-large" transition>
|
||||||
<Tabs orientation="vertical" variant="settings" defaultValue="general" class="h-full settings-dialog">
|
<Tabs
|
||||||
|
orientation="vertical"
|
||||||
|
variant="settings"
|
||||||
|
defaultValue={props.defaultTab ?? "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">
|
||||||
<div class="flex flex-col gap-3 w-full pt-3">
|
<div class="flex flex-col gap-3 w-full pt-3">
|
||||||
@@ -45,6 +51,10 @@ export const DialogSettings: Component = () => {
|
|||||||
<Icon name="models" />
|
<Icon name="models" />
|
||||||
{language.t("settings.models.title")}
|
{language.t("settings.models.title")}
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger value="pair">
|
||||||
|
<Icon name="link" />
|
||||||
|
{language.t("settings.pair.title")}
|
||||||
|
</Tabs.Trigger>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -67,6 +77,9 @@ export const DialogSettings: Component = () => {
|
|||||||
<Tabs.Content value="models" class="no-scrollbar">
|
<Tabs.Content value="models" class="no-scrollbar">
|
||||||
<SettingsModels />
|
<SettingsModels />
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
|
<Tabs.Content value="pair" class="no-scrollbar">
|
||||||
|
<SettingsPair />
|
||||||
|
</Tabs.Content>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
||||||
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
||||||
import { createEffect, on, Component, Show, onCleanup, createMemo, createSignal, createResource } from "solid-js"
|
import { createEffect, on, Component, Show, onCleanup, createMemo, createSignal } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useLocal } from "@/context/local"
|
import { useLocal } from "@/context/local"
|
||||||
import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file"
|
import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file"
|
||||||
@@ -54,7 +54,7 @@ import { PromptImageAttachments } from "./prompt-input/image-attachments"
|
|||||||
import { PromptDragOverlay } from "./prompt-input/drag-overlay"
|
import { PromptDragOverlay } from "./prompt-input/drag-overlay"
|
||||||
import { promptPlaceholder } from "./prompt-input/placeholder"
|
import { promptPlaceholder } from "./prompt-input/placeholder"
|
||||||
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
||||||
import { useQueries } from "@tanstack/solid-query"
|
import { useQuery } from "@tanstack/solid-query"
|
||||||
import { loadAgentsQuery, loadProvidersQuery } from "@/context/global-sync/bootstrap"
|
import { loadAgentsQuery, loadProvidersQuery } from "@/context/global-sync/bootstrap"
|
||||||
|
|
||||||
interface PromptInputProps {
|
interface PromptInputProps {
|
||||||
@@ -270,7 +270,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 })
|
const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 })
|
||||||
const motion = (value: number) => ({
|
const motion = (value: number) => ({
|
||||||
opacity: value,
|
opacity: value,
|
||||||
transform: `scale(${0.98 + value * 0.02})`,
|
transform: `scale(${0.95 + value * 0.05})`,
|
||||||
filter: `blur(${(1 - value) * 2}px)`,
|
filter: `blur(${(1 - value) * 2}px)`,
|
||||||
"pointer-events": value > 0.5 ? ("auto" as const) : ("none" as const),
|
"pointer-events": value > 0.5 ? ("auto" as const) : ("none" as const),
|
||||||
})
|
})
|
||||||
@@ -345,7 +345,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
promptPlaceholder({
|
promptPlaceholder({
|
||||||
mode: store.mode,
|
mode: store.mode,
|
||||||
commentCount: commentCount(),
|
commentCount: commentCount(),
|
||||||
example: suggest() ? (store.mode === "shell" ? "git status" : language.t(EXAMPLES[store.placeholder])) : "",
|
example: suggest() ? language.t(EXAMPLES[store.placeholder]) : "",
|
||||||
suggest: suggest(),
|
suggest: suggest(),
|
||||||
t: (key, params) => language.t(key as Parameters<typeof language.t>[0], params as never),
|
t: (key, params) => language.t(key as Parameters<typeof language.t>[0], params as never),
|
||||||
}),
|
}),
|
||||||
@@ -1252,23 +1252,16 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [agentsQuery, globalProvidersQuery, providersQuery] = useQueries(() => ({
|
const agentsQuery = useQuery(() => loadAgentsQuery(sdk.directory))
|
||||||
queries: [loadAgentsQuery(sdk.directory), loadProvidersQuery(null), loadProvidersQuery(sdk.directory)],
|
|
||||||
}))
|
|
||||||
|
|
||||||
const agentsLoading = () => agentsQuery.isLoading
|
const agentsLoading = () => agentsQuery.isLoading
|
||||||
const agentsShouldFadeIn = createMemo((prev) => prev ?? agentsLoading())
|
|
||||||
const providersLoading = () => agentsLoading() || providersQuery.isLoading || globalProvidersQuery.isLoading
|
|
||||||
const providersShouldFadeIn = createMemo((prev) => prev ?? providersLoading())
|
|
||||||
|
|
||||||
const [promptReady] = createResource(
|
const globalProvidersQuery = useQuery(() => loadProvidersQuery(null))
|
||||||
() => prompt.ready().promise,
|
const providersQuery = useQuery(() => loadProvidersQuery(sdk.directory))
|
||||||
(p) => p,
|
|
||||||
)
|
const providersLoading = () => agentsLoading() || providersQuery.isLoading || globalProvidersQuery.isLoading
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="relative size-full _max-h-[320px] flex flex-col gap-0">
|
<div class="relative size-full _max-h-[320px] flex flex-col gap-0">
|
||||||
{(promptReady(), null)}
|
|
||||||
<PromptPopover
|
<PromptPopover
|
||||||
popover={store.popover}
|
popover={store.popover}
|
||||||
setSlashPopoverRef={(el) => (slashPopoverRef = el)}
|
setSlashPopoverRef={(el) => (slashPopoverRef = el)}
|
||||||
@@ -1365,13 +1358,15 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
}}
|
}}
|
||||||
style={{ "padding-bottom": space }}
|
style={{ "padding-bottom": space }}
|
||||||
/>
|
/>
|
||||||
<div
|
<Show when={!prompt.dirty()}>
|
||||||
class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate"
|
<div
|
||||||
classList={{ "font-mono!": store.mode === "shell" }}
|
class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate"
|
||||||
style={{ "padding-bottom": space, display: prompt.dirty() ? "none" : undefined }}
|
classList={{ "font-mono!": store.mode === "shell" }}
|
||||||
>
|
style={{ "padding-bottom": space }}
|
||||||
{placeholder()}
|
>
|
||||||
</div>
|
{placeholder()}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -1403,11 +1398,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
<IconButton
|
<IconButton
|
||||||
data-action="prompt-submit"
|
data-action="prompt-submit"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!working() && blank()}
|
disabled={store.mode !== "normal" || (!working() && blank())}
|
||||||
tabIndex={store.mode === "normal" ? undefined : -1}
|
tabIndex={store.mode === "normal" ? undefined : -1}
|
||||||
icon={stopping() ? "stop" : store.mode === "shell" ? "arrow-undo-down" : "arrow-up"}
|
icon={stopping() ? "stop" : "arrow-up"}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
class="size-8"
|
class="size-8"
|
||||||
|
style={buttons()}
|
||||||
aria-label={stopping() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
|
aria-label={stopping() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -1450,31 +1446,18 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
<div class="px-1.75 pt-5.5 pb-2 flex items-center gap-2 min-w-0">
|
<div class="px-1.75 pt-5.5 pb-2 flex items-center gap-2 min-w-0">
|
||||||
<div class="flex items-center gap-1.5 min-w-0 flex-1 relative">
|
<div class="flex items-center gap-1.5 min-w-0 flex-1 relative">
|
||||||
<div
|
<div
|
||||||
class="h-7 flex items-center gap-1.5 min-w-0 absolute inset-0"
|
class="h-7 flex items-center gap-1.5 max-w-[160px] min-w-0 absolute inset-y-0 left-0"
|
||||||
style={{
|
style={{
|
||||||
padding: "0 0px 0 8px",
|
padding: "0 4px 0 8px",
|
||||||
...shell(),
|
...shell(),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon name="console" />
|
<span class="truncate text-13-medium text-text-strong">{language.t("prompt.mode.shell")}</span>
|
||||||
<span class="truncate text-13-medium text-text-base">{language.t("prompt.mode.shell")}</span>
|
<div class="size-4 shrink-0" />
|
||||||
<div class="flex-1" />
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
class="text-text-base"
|
|
||||||
onClick={() => {
|
|
||||||
setStore("mode", "normal")
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{language.t("common.cancel")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1.5 min-w-0 flex-1 h-7">
|
<div class="flex items-center gap-1.5 min-w-0 flex-1 h-7">
|
||||||
<Show when={!agentsLoading()}>
|
<Show when={!agentsLoading()}>
|
||||||
<div
|
<div data-component="prompt-agent-control">
|
||||||
data-component="prompt-agent-control"
|
|
||||||
style={agentsShouldFadeIn() ? { animation: "fade-in 0.3s" } : undefined}
|
|
||||||
>
|
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
placement="top"
|
placement="top"
|
||||||
gutter={4}
|
gutter={4}
|
||||||
@@ -1500,10 +1483,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
</Show>
|
</Show>
|
||||||
<Show when={!providersLoading()}>
|
<Show when={!providersLoading()}>
|
||||||
<Show when={store.mode !== "shell"}>
|
<Show when={store.mode !== "shell"}>
|
||||||
<div
|
<div data-component="prompt-model-control">
|
||||||
data-component="prompt-model-control"
|
|
||||||
style={providersShouldFadeIn() ? { animation: "fade-in 0.3s" } : undefined}
|
|
||||||
>
|
|
||||||
<Show
|
<Show
|
||||||
when={providers.paid().length > 0}
|
when={providers.paid().length > 0}
|
||||||
fallback={
|
fallback={
|
||||||
@@ -1574,35 +1554,30 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Show when={variants().length > 2}>
|
<div data-component="prompt-variant-control">
|
||||||
<div
|
<TooltipKeybind
|
||||||
data-component="prompt-variant-control"
|
placement="top"
|
||||||
style={providersShouldFadeIn() ? { animation: "fade-in 0.3s" } : undefined}
|
gutter={4}
|
||||||
|
title={language.t("command.model.variant.cycle")}
|
||||||
|
keybind={command.keybind("model.variant.cycle")}
|
||||||
>
|
>
|
||||||
<TooltipKeybind
|
<Select
|
||||||
placement="top"
|
size="normal"
|
||||||
gutter={4}
|
options={variants()}
|
||||||
title={language.t("command.model.variant.cycle")}
|
current={local.model.variant.current() ?? "default"}
|
||||||
keybind={command.keybind("model.variant.cycle")}
|
label={(x) => (x === "default" ? language.t("common.default") : x)}
|
||||||
>
|
onSelect={(value) => {
|
||||||
<Select
|
local.model.variant.set(value === "default" ? undefined : value)
|
||||||
size="normal"
|
restoreFocus()
|
||||||
options={variants()}
|
}}
|
||||||
current={local.model.variant.current() ?? "default"}
|
class="capitalize max-w-[160px] text-text-base"
|
||||||
label={(x) => (x === "default" ? language.t("common.default") : x)}
|
valueClass="truncate text-13-regular text-text-base"
|
||||||
onSelect={(value) => {
|
triggerStyle={control()}
|
||||||
local.model.variant.set(value === "default" ? undefined : value)
|
triggerProps={{ "data-action": "prompt-model-variant" }}
|
||||||
restoreFocus()
|
variant="ghost"
|
||||||
}}
|
/>
|
||||||
class="capitalize max-w-[160px] text-text-base"
|
</TooltipKeybind>
|
||||||
valueClass="truncate text-13-regular text-text-base"
|
</div>
|
||||||
triggerStyle={control()}
|
|
||||||
triggerProps={{ "data-action": "prompt-model-variant" }}
|
|
||||||
variant="ghost"
|
|
||||||
/>
|
|
||||||
</TooltipKeybind>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
import { getFilename } from "@opencode-ai/shared/util/path"
|
||||||
import { type AgentPartInput, type FilePartInput, type Part, type TextPartInput } from "@opencode-ai/sdk/v2/client"
|
import { type AgentPartInput, type FilePartInput, type Part, type TextPartInput } from "@opencode-ai/sdk/v2/client"
|
||||||
import type { FileSelection } from "@/context/file"
|
import type { FileSelection } from "@/context/file"
|
||||||
import { encodeFilePath } from "@/context/file/path"
|
import { encodeFilePath } from "@/context/file/path"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Component, For, Show } from "solid-js"
|
|||||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { getDirectory, getFilename, getFilenameTruncated } from "@opencode-ai/core/util/path"
|
import { getDirectory, getFilename, getFilenameTruncated } from "@opencode-ai/shared/util/path"
|
||||||
import type { ContextItem } from "@/context/prompt"
|
import type { ContextItem } from "@/context/prompt"
|
||||||
|
|
||||||
type PromptContextItem = ContextItem & { key: string }
|
type PromptContextItem = ContextItem & { key: string }
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ describe("promptPlaceholder", () => {
|
|||||||
suggest: true,
|
suggest: true,
|
||||||
t,
|
t,
|
||||||
})
|
})
|
||||||
expect(value).toBe("prompt.placeholder.shell:example")
|
expect(value).toBe("prompt.placeholder.shell")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("returns summarize placeholders for comment context", () => {
|
test("returns summarize placeholders for comment context", () => {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ type PromptPlaceholderInput = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function promptPlaceholder(input: PromptPlaceholderInput) {
|
export function promptPlaceholder(input: PromptPlaceholderInput) {
|
||||||
if (input.mode === "shell") return input.t("prompt.placeholder.shell", { example: input.example })
|
if (input.mode === "shell") return input.t("prompt.placeholder.shell")
|
||||||
if (input.commentCount > 1) return input.t("prompt.placeholder.summarizeComments")
|
if (input.commentCount > 1) return input.t("prompt.placeholder.summarizeComments")
|
||||||
if (input.commentCount === 1) return input.t("prompt.placeholder.summarizeComment")
|
if (input.commentCount === 1) return input.t("prompt.placeholder.summarizeComment")
|
||||||
if (!input.suggest) return input.t("prompt.placeholder.simple")
|
if (!input.suggest) return input.t("prompt.placeholder.simple")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component, For, Match, Show, Switch } from "solid-js"
|
import { Component, For, Match, Show, Switch } from "solid-js"
|
||||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/shared/util/path"
|
||||||
|
|
||||||
export type AtOption =
|
export type AtOption =
|
||||||
| { type: "agent"; name: string; display: string }
|
| { type: "agent"; name: string; display: string }
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ beforeAll(async () => {
|
|||||||
showToast: () => 0,
|
showToast: () => 0,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
mock.module("@opencode-ai/core/util/encode", () => ({
|
mock.module("@opencode-ai/shared/util/encode", () => ({
|
||||||
base64Encode: (value: string) => value,
|
base64Encode: (value: string) => value,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Message, Session } from "@opencode-ai/sdk/v2/client"
|
import type { Message, Session } from "@opencode-ai/sdk/v2/client"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
||||||
import { Binary } from "@opencode-ai/core/util/binary"
|
import { Binary } from "@opencode-ai/shared/util/binary"
|
||||||
import { useNavigate, useParams } from "@solidjs/router"
|
import { useNavigate, useParams } from "@solidjs/router"
|
||||||
import { batch, type Accessor } from "solid-js"
|
import { batch, type Accessor } from "solid-js"
|
||||||
import type { FileSelection } from "@/context/file"
|
import type { FileSelection } from "@/context/file"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { createMemo, createEffect, on, onCleanup, For, Show } from "solid-js"
|
import { createMemo, createEffect, on, onCleanup, For, Show } from "solid-js"
|
||||||
import type { JSX } from "solid-js"
|
import type { JSX } from "solid-js"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { checksum } from "@opencode-ai/core/util/encode"
|
import { checksum } from "@opencode-ai/shared/util/encode"
|
||||||
import { findLast } from "@opencode-ai/core/util/array"
|
import { findLast } from "@opencode-ai/shared/util/array"
|
||||||
import { same } from "@/utils/same"
|
import { same } from "@/utils/same"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Accordion } from "@opencode-ai/ui/accordion"
|
import { Accordion } from "@opencode-ai/ui/accordion"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Keybind } from "@opencode-ai/ui/keybind"
|
|||||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
import { getFilename } from "@opencode-ai/shared/util/path"
|
||||||
import { createEffect, createMemo, createSignal, For, onMount, Show } from "solid-js"
|
import { createEffect, createMemo, createSignal, For, onMount, Show } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { Portal } from "solid-js/web"
|
import { Portal } from "solid-js/web"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useSDK } from "@/context/sdk"
|
|||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Mark } from "@opencode-ai/ui/logo"
|
import { Mark } from "@opencode-ai/ui/logo"
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/shared/util/path"
|
||||||
|
|
||||||
const MAIN_WORKTREE = "main"
|
const MAIN_WORKTREE = "main"
|
||||||
const CREATE_WORKTREE = "create"
|
const CREATE_WORKTREE = "create"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { FileIcon } from "@opencode-ai/ui/file-icon"
|
|||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
import { getFilename } from "@opencode-ai/shared/util/path"
|
||||||
import { useFile } from "@/context/file"
|
import { useFile } from "@/context/file"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ import { showToast } from "@opencode-ai/ui/toast"
|
|||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
import { usePlatform, type DisplayBackend } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
|
||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
|
||||||
import {
|
import {
|
||||||
monoDefault,
|
monoDefault,
|
||||||
monoFontFamily,
|
monoFontFamily,
|
||||||
@@ -42,18 +40,6 @@ type ThemeOption = {
|
|||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ShellOption = {
|
|
||||||
path: string
|
|
||||||
name: string
|
|
||||||
acceptable: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
type ShellSelectOption = {
|
|
||||||
id: string
|
|
||||||
value: string
|
|
||||||
label: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// To prevent audio from overlapping/playing very quickly when navigating the settings menus,
|
// To prevent audio from overlapping/playing very quickly when navigating the settings menus,
|
||||||
// delay the playback by 100ms during quick selection changes and pause existing sounds.
|
// delay the playback by 100ms during quick selection changes and pause existing sounds.
|
||||||
const stopDemoSound = () => {
|
const stopDemoSound = () => {
|
||||||
@@ -89,6 +75,10 @@ export const SettingsGeneral: Component = () => {
|
|||||||
const params = useParams()
|
const params = useParams()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
void theme.loadThemes()
|
||||||
|
})
|
||||||
|
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
checking: false,
|
checking: false,
|
||||||
})
|
})
|
||||||
@@ -138,25 +128,27 @@ export const SettingsGeneral: Component = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const actions = platform.updateAndRestart
|
const actions =
|
||||||
? [
|
platform.update && platform.restart
|
||||||
{
|
? [
|
||||||
label: language.t("toast.update.action.installRestart"),
|
{
|
||||||
onClick: async () => {
|
label: language.t("toast.update.action.installRestart"),
|
||||||
await platform.updateAndRestart!()
|
onClick: async () => {
|
||||||
|
await platform.update!()
|
||||||
|
await platform.restart!()
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
label: language.t("toast.update.action.notYet"),
|
||||||
label: language.t("toast.update.action.notYet"),
|
onClick: "dismiss" as const,
|
||||||
onClick: "dismiss" as const,
|
},
|
||||||
},
|
]
|
||||||
]
|
: [
|
||||||
: [
|
{
|
||||||
{
|
label: language.t("toast.update.action.notYet"),
|
||||||
label: language.t("toast.update.action.notYet"),
|
onClick: "dismiss" as const,
|
||||||
onClick: "dismiss" as const,
|
},
|
||||||
},
|
]
|
||||||
]
|
|
||||||
|
|
||||||
showToast({
|
showToast({
|
||||||
persistent: true,
|
persistent: true,
|
||||||
@@ -175,70 +167,6 @@ export const SettingsGeneral: Component = () => {
|
|||||||
|
|
||||||
const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
|
const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
|
||||||
|
|
||||||
const globalSync = useGlobalSync()
|
|
||||||
const globalSdk = useGlobalSDK()
|
|
||||||
|
|
||||||
const [shells] = createResource(
|
|
||||||
() =>
|
|
||||||
globalSdk.client.pty
|
|
||||||
.shells()
|
|
||||||
.then((res) => res.data ?? [])
|
|
||||||
.catch(() => [] as ShellOption[]),
|
|
||||||
{ initialValue: [] as ShellOption[] },
|
|
||||||
)
|
|
||||||
|
|
||||||
const [displayBackend, { refetch: refetchDisplayBackend }] = createResource(
|
|
||||||
() => (linux() && platform.getDisplayBackend ? true : false),
|
|
||||||
() => Promise.resolve(platform.getDisplayBackend?.() ?? null).catch(() => null as DisplayBackend | null),
|
|
||||||
{ initialValue: null as DisplayBackend | null },
|
|
||||||
)
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
void theme.loadThemes()
|
|
||||||
})
|
|
||||||
|
|
||||||
const autoOption = { id: "auto", value: "", label: language.t("settings.general.row.shell.autoDefault") }
|
|
||||||
const currentShell = createMemo(() => globalSync.data.config.shell ?? "")
|
|
||||||
|
|
||||||
const shellOptions = createMemo<ShellSelectOption[]>(() => {
|
|
||||||
const list = shells.latest
|
|
||||||
const current = globalSync.data.config.shell
|
|
||||||
|
|
||||||
const nameCounts = new Map<string, number>()
|
|
||||||
for (const s of list) {
|
|
||||||
nameCounts.set(s.name, (nameCounts.get(s.name) || 0) + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = [
|
|
||||||
autoOption,
|
|
||||||
...list.map((s) => {
|
|
||||||
const ambiguousName = (nameCounts.get(s.name) || 0) > 1
|
|
||||||
const text = ambiguousName ? s.path : s.name
|
|
||||||
const label = s.acceptable ? text : `${text} (${language.t("settings.general.row.shell.terminalOnly")})`
|
|
||||||
return {
|
|
||||||
id: s.path,
|
|
||||||
// Prefer name over path - "bash" is much cleaner than the explicit full route even when it may change due to PATH.
|
|
||||||
value: ambiguousName ? s.path : s.name,
|
|
||||||
label,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
|
|
||||||
if (current && !options.some((o) => o.value === current)) {
|
|
||||||
options.push({ id: current, value: current, label: current })
|
|
||||||
}
|
|
||||||
|
|
||||||
return options
|
|
||||||
})
|
|
||||||
|
|
||||||
const onDisplayBackendChange = (checked: boolean) => {
|
|
||||||
const update = platform.setDisplayBackend?.(checked ? "wayland" : "auto")
|
|
||||||
if (!update) return
|
|
||||||
void update.finally(() => {
|
|
||||||
void refetchDisplayBackend()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const colorSchemeOptions = createMemo((): { value: ColorScheme; label: string }[] => [
|
const colorSchemeOptions = createMemo((): { value: ColorScheme; label: string }[] => [
|
||||||
{ value: "system", label: language.t("theme.scheme.system") },
|
{ value: "system", label: language.t("theme.scheme.system") },
|
||||||
{ value: "light", label: language.t("theme.scheme.light") },
|
{ value: "light", label: language.t("theme.scheme.light") },
|
||||||
@@ -317,27 +245,6 @@ export const SettingsGeneral: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
|
||||||
<SettingsRow
|
|
||||||
title={language.t("settings.general.row.shell.title")}
|
|
||||||
description={language.t("settings.general.row.shell.description")}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
data-action="settings-shell"
|
|
||||||
options={shellOptions()}
|
|
||||||
current={shellOptions().find((o) => o.value === currentShell()) ?? autoOption}
|
|
||||||
value={(o) => o.id}
|
|
||||||
label={(o) => o.label}
|
|
||||||
onSelect={(option) => {
|
|
||||||
if (!option) return
|
|
||||||
globalSync.updateConfig({ shell: option.value })
|
|
||||||
}}
|
|
||||||
variant="secondary"
|
|
||||||
size="small"
|
|
||||||
triggerVariant="settings"
|
|
||||||
triggerStyle={{ "min-width": "180px" }}
|
|
||||||
/>
|
|
||||||
</SettingsRow>
|
|
||||||
|
|
||||||
<SettingsRow
|
<SettingsRow
|
||||||
title={language.t("settings.general.row.reasoningSummaries.title")}
|
title={language.t("settings.general.row.reasoningSummaries.title")}
|
||||||
description={language.t("settings.general.row.reasoningSummaries.description")}
|
description={language.t("settings.general.row.reasoningSummaries.description")}
|
||||||
@@ -373,18 +280,6 @@ export const SettingsGeneral: Component = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
|
||||||
<SettingsRow
|
|
||||||
title={language.t("settings.general.row.showSessionProgressBar.title")}
|
|
||||||
description={language.t("settings.general.row.showSessionProgressBar.description")}
|
|
||||||
>
|
|
||||||
<div data-action="settings-show-session-progress-bar">
|
|
||||||
<Switch
|
|
||||||
checked={settings.general.showSessionProgressBar()}
|
|
||||||
onChange={(checked) => settings.general.setShowSessionProgressBar(checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SettingsRow>
|
|
||||||
</SettingsList>
|
</SettingsList>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -746,32 +641,70 @@ export const SettingsGeneral: Component = () => {
|
|||||||
|
|
||||||
<SoundsSection />
|
<SoundsSection />
|
||||||
|
|
||||||
|
{/*<Show when={platform.platform === "desktop" && platform.os === "windows" && platform.getWslEnabled}>
|
||||||
|
{(_) => {
|
||||||
|
const [enabledResource, actions] = createResource(() => platform.getWslEnabled?.())
|
||||||
|
const enabled = () => (enabledResource.state === "pending" ? undefined : enabledResource.latest)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.desktop.section.wsl")}</h3>
|
||||||
|
|
||||||
|
<SettingsList>
|
||||||
|
<SettingsRow
|
||||||
|
title={language.t("settings.desktop.wsl.title")}
|
||||||
|
description={language.t("settings.desktop.wsl.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-wsl">
|
||||||
|
<Switch
|
||||||
|
checked={enabled() ?? false}
|
||||||
|
disabled={enabledResource.state === "pending"}
|
||||||
|
onChange={(checked) => platform.setWslEnabled?.(checked)?.finally(() => actions.refetch())}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRow>
|
||||||
|
</SettingsList>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Show>*/}
|
||||||
|
|
||||||
<UpdatesSection />
|
<UpdatesSection />
|
||||||
|
|
||||||
<Show when={linux()}>
|
<Show when={linux()}>
|
||||||
<div class="flex flex-col gap-1">
|
{(_) => {
|
||||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.display")}</h3>
|
const [valueResource, actions] = createResource(() => platform.getDisplayBackend?.())
|
||||||
|
const value = () => (valueResource.state === "pending" ? undefined : valueResource.latest)
|
||||||
|
|
||||||
<SettingsList>
|
const onChange = (checked: boolean) =>
|
||||||
<SettingsRow
|
platform.setDisplayBackend?.(checked ? "wayland" : "auto").finally(() => actions.refetch())
|
||||||
title={
|
|
||||||
<div class="flex items-center gap-2">
|
return (
|
||||||
<span>{language.t("settings.general.row.wayland.title")}</span>
|
<div class="flex flex-col gap-1">
|
||||||
<Tooltip value={language.t("settings.general.row.wayland.tooltip")} placement="top">
|
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.display")}</h3>
|
||||||
<span class="text-text-weak">
|
|
||||||
<Icon name="help" size="small" />
|
<SettingsList>
|
||||||
</span>
|
<SettingsRow
|
||||||
</Tooltip>
|
title={
|
||||||
</div>
|
<div class="flex items-center gap-2">
|
||||||
}
|
<span>{language.t("settings.general.row.wayland.title")}</span>
|
||||||
description={language.t("settings.general.row.wayland.description")}
|
<Tooltip value={language.t("settings.general.row.wayland.tooltip")} placement="top">
|
||||||
>
|
<span class="text-text-weak">
|
||||||
<div data-action="settings-wayland">
|
<Icon name="help" size="small" />
|
||||||
<Switch checked={displayBackend.latest === "wayland"} onChange={onDisplayBackendChange} />
|
</span>
|
||||||
</div>
|
</Tooltip>
|
||||||
</SettingsRow>
|
</div>
|
||||||
</SettingsList>
|
}
|
||||||
</div>
|
description={language.t("settings.general.row.wayland.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-wayland">
|
||||||
|
<Switch checked={value() === "wayland"} onChange={onChange} />
|
||||||
|
</div>
|
||||||
|
</SettingsRow>
|
||||||
|
</SettingsList>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={desktop() && import.meta.env.VITE_OPENCODE_CHANNEL === "beta"}>
|
<Show when={desktop() && import.meta.env.VITE_OPENCODE_CHANNEL === "beta"}>
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import { type Component, createResource, Show } from "solid-js"
|
||||||
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
|
import { useServer } from "@/context/server"
|
||||||
|
import { usePlatform } from "@/context/platform"
|
||||||
|
import { SettingsList } from "./settings-list"
|
||||||
|
|
||||||
|
type PairResult =
|
||||||
|
| { enabled: false }
|
||||||
|
| {
|
||||||
|
enabled: true
|
||||||
|
hosts: string[]
|
||||||
|
relayURL?: string
|
||||||
|
serverID?: string
|
||||||
|
relaySecretHash?: string
|
||||||
|
link: string
|
||||||
|
qr: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SettingsPair: Component = () => {
|
||||||
|
const language = useLanguage()
|
||||||
|
const globalSDK = useGlobalSDK()
|
||||||
|
const server = useServer()
|
||||||
|
const platform = usePlatform()
|
||||||
|
|
||||||
|
const [data] = createResource(async () => {
|
||||||
|
const url = `${globalSDK.url}/experimental/push/pair`
|
||||||
|
console.debug("[settings-pair] fetching pair data", {
|
||||||
|
serverUrl: globalSDK.url,
|
||||||
|
serverName: server.name,
|
||||||
|
serverKey: server.key,
|
||||||
|
})
|
||||||
|
const f = platform.fetch ?? fetch
|
||||||
|
const res = await f(url)
|
||||||
|
if (!res.ok) {
|
||||||
|
console.debug("[settings-pair] pair endpoint returned non-ok", {
|
||||||
|
status: res.status,
|
||||||
|
serverUrl: globalSDK.url,
|
||||||
|
})
|
||||||
|
return { enabled: false as const }
|
||||||
|
}
|
||||||
|
const result = (await res.json()) as PairResult
|
||||||
|
console.debug("[settings-pair] pair data received", {
|
||||||
|
enabled: result.enabled,
|
||||||
|
serverUrl: globalSDK.url,
|
||||||
|
serverName: server.name,
|
||||||
|
...(result.enabled
|
||||||
|
? {
|
||||||
|
relayURL: result.relayURL,
|
||||||
|
serverID: result.serverID,
|
||||||
|
relaySecretHash: result.relaySecretHash,
|
||||||
|
hostCount: result.hosts.length,
|
||||||
|
hosts: result.hosts,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="flex flex-col gap-6 py-4 px-5">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<h2 class="text-16-semibold text-text-strong">{language.t("settings.pair.title")}</h2>
|
||||||
|
<p class="text-13-regular text-text-weak">{language.t("settings.pair.description")}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={data.loading}>
|
||||||
|
<SettingsList>
|
||||||
|
<div class="flex items-center justify-center py-12">
|
||||||
|
<span class="text-14-regular text-text-weak">{language.t("settings.pair.loading")}</span>
|
||||||
|
</div>
|
||||||
|
</SettingsList>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={data.error}>
|
||||||
|
<SettingsList>
|
||||||
|
<div class="flex flex-col items-center justify-center py-12 gap-3 text-center">
|
||||||
|
<Icon name="warning" size="large" />
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<span class="text-14-medium text-text-strong">{language.t("settings.pair.error.title")}</span>
|
||||||
|
<span class="text-13-regular text-text-weak max-w-md">
|
||||||
|
{language.t("settings.pair.error.description")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SettingsList>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={!data.loading && !data.error && data()}>
|
||||||
|
{(result) => (
|
||||||
|
<Show
|
||||||
|
when={result().enabled && result()}
|
||||||
|
fallback={
|
||||||
|
<SettingsList>
|
||||||
|
<div class="flex flex-col items-center justify-center py-12 gap-3 text-center">
|
||||||
|
<Icon name="link" size="large" />
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<span class="text-14-medium text-text-strong">{language.t("settings.pair.disabled.title")}</span>
|
||||||
|
<span class="text-13-regular text-text-weak max-w-md">
|
||||||
|
{language.t("settings.pair.disabled.description")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<code class="text-12-regular text-text-weak bg-surface-inset px-3 py-1.5 rounded mt-1">
|
||||||
|
opencode serve --relay-url <url> --relay-secret <secret>
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</SettingsList>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(pair) => {
|
||||||
|
const p = pair() as PairResult & { enabled: true }
|
||||||
|
return (
|
||||||
|
<SettingsList>
|
||||||
|
<div class="flex flex-col items-center py-8 gap-4">
|
||||||
|
<Show when={server.list.length > 1 || p.relayURL}>
|
||||||
|
<div class="flex flex-col gap-1.5 w-full max-w-sm text-left">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-12-medium text-text-weak shrink-0">
|
||||||
|
{language.t("settings.pair.server.label")}
|
||||||
|
</span>
|
||||||
|
<code class="text-12-regular text-text-default bg-surface-inset px-2 py-0.5 rounded truncate">
|
||||||
|
{server.name}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
<Show when={p.relayURL}>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-12-medium text-text-weak shrink-0">
|
||||||
|
{language.t("settings.pair.relay.label")}
|
||||||
|
</span>
|
||||||
|
<code class="text-12-regular text-text-default bg-surface-inset px-2 py-0.5 rounded truncate">
|
||||||
|
{p.relayURL}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={p.relaySecretHash}>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-12-medium text-text-weak shrink-0">
|
||||||
|
{language.t("settings.pair.secret.label")}
|
||||||
|
</span>
|
||||||
|
<code class="text-12-regular text-text-default bg-surface-inset px-2 py-0.5 rounded truncate">
|
||||||
|
{p.relaySecretHash}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<img src={p.qr} alt="Pairing QR code" class="w-64 h-64" />
|
||||||
|
<div class="flex flex-col gap-1 text-center max-w-sm">
|
||||||
|
<span class="text-14-medium text-text-strong">
|
||||||
|
{language.t("settings.pair.instructions.title")}
|
||||||
|
</span>
|
||||||
|
<span class="text-13-regular text-text-weak">
|
||||||
|
{language.t("settings.pair.instructions.description")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SettingsList>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Show>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { Switch } from "@opencode-ai/ui/switch"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { useMutation, useQueryClient } from "@tanstack/solid-query"
|
import { useMutation } from "@tanstack/solid-query"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import { type Accessor, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js"
|
import { type Accessor, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js"
|
||||||
@@ -15,7 +15,6 @@ import { useSDK } from "@/context/sdk"
|
|||||||
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
|
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health"
|
import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health"
|
||||||
import { loadMcpQuery } from "@/context/global-sync"
|
|
||||||
|
|
||||||
const pollMs = 10_000
|
const pollMs = 10_000
|
||||||
|
|
||||||
@@ -138,14 +137,14 @@ const useMcpToggleMutation = () => {
|
|||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const queryClient = useQueryClient()
|
|
||||||
|
|
||||||
return useMutation(() => ({
|
return useMutation(() => ({
|
||||||
mutationFn: async (name: string) => {
|
mutationFn: async (name: string) => {
|
||||||
const status = sync.data.mcp[name]
|
const status = sync.data.mcp[name]
|
||||||
await (status?.status === "connected" ? sdk.client.mcp.disconnect({ name }) : sdk.client.mcp.connect({ name }))
|
await (status?.status === "connected" ? sdk.client.mcp.disconnect({ name }) : sdk.client.mcp.connect({ name }))
|
||||||
|
const result = await sdk.client.mcp.status()
|
||||||
|
if (result.data) sync.set("mcp", result.data)
|
||||||
},
|
},
|
||||||
onSuccess: () => queryClient.refetchQueries({ queryKey: loadMcpQuery(sync.directory).queryKey }),
|
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
showToast({
|
showToast({
|
||||||
variant: "error",
|
variant: "error",
|
||||||
@@ -163,6 +162,14 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
|||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const sdk = useSDK()
|
||||||
|
|
||||||
|
const [load, setLoad] = createStore({
|
||||||
|
lspDone: false,
|
||||||
|
lspLoading: false,
|
||||||
|
mcpDone: false,
|
||||||
|
mcpLoading: false,
|
||||||
|
})
|
||||||
|
|
||||||
const fail = (err: unknown) => {
|
const fail = (err: unknown) => {
|
||||||
showToast({
|
showToast({
|
||||||
@@ -174,6 +181,40 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
|||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!props.shown()) return
|
if (!props.shown()) return
|
||||||
|
|
||||||
|
if (!sync.data.mcp_ready && !load.mcpDone && !load.mcpLoading) {
|
||||||
|
setLoad("mcpLoading", true)
|
||||||
|
void sdk.client.mcp
|
||||||
|
.status()
|
||||||
|
.then((result) => {
|
||||||
|
sync.set("mcp", result.data ?? {})
|
||||||
|
sync.set("mcp_ready", true)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setLoad("mcpDone", true)
|
||||||
|
fail(err)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoad("mcpLoading", false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sync.data.lsp_ready && !load.lspDone && !load.lspLoading) {
|
||||||
|
setLoad("lspLoading", true)
|
||||||
|
void sdk.client.lsp
|
||||||
|
.status()
|
||||||
|
.then((result) => {
|
||||||
|
sync.set("lsp", result.data ?? [])
|
||||||
|
sync.set("lsp_ready", true)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setLoad("lspDone", true)
|
||||||
|
fail(err)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoad("lspLoading", false)
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let dialogRun = 0
|
let dialogRun = 0
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { createStore, produce, reconcile } from "solid-js/store"
|
|||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
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/core/util/path"
|
import { getFilename } from "@opencode-ai/shared/util/path"
|
||||||
import { useSDK } from "./sdk"
|
import { useSDK } from "./sdk"
|
||||||
import { useSync } from "./sync"
|
import { useSync } from "./sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
|||||||
@@ -8,32 +8,25 @@ import type {
|
|||||||
Todo,
|
Todo,
|
||||||
} from "@opencode-ai/sdk/v2/client"
|
} from "@opencode-ai/sdk/v2/client"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
import { getFilename } from "@opencode-ai/shared/util/path"
|
||||||
import { batch, createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js"
|
import { createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js"
|
||||||
import { createStore, produce, reconcile } from "solid-js/store"
|
import { createStore, produce, reconcile } from "solid-js/store"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import type { InitError } from "../pages/error"
|
import type { InitError } from "../pages/error"
|
||||||
import { useGlobalSDK } from "./global-sdk"
|
import { useGlobalSDK } from "./global-sdk"
|
||||||
import {
|
import { bootstrapDirectory, bootstrapGlobal, clearProviderRev } from "./global-sync/bootstrap"
|
||||||
bootstrapDirectory,
|
|
||||||
bootstrapGlobal,
|
|
||||||
clearProviderRev,
|
|
||||||
loadGlobalConfigQuery,
|
|
||||||
loadPathQuery,
|
|
||||||
loadProjectsQuery,
|
|
||||||
loadProvidersQuery,
|
|
||||||
} from "./global-sync/bootstrap"
|
|
||||||
import { createChildStoreManager } from "./global-sync/child-store"
|
import { createChildStoreManager } from "./global-sync/child-store"
|
||||||
import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } from "./global-sync/event-reducer"
|
import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } from "./global-sync/event-reducer"
|
||||||
|
import { createRefreshQueue } from "./global-sync/queue"
|
||||||
import { clearSessionPrefetchDirectory } from "./global-sync/session-prefetch"
|
import { clearSessionPrefetchDirectory } from "./global-sync/session-prefetch"
|
||||||
import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global-sync/session-load"
|
import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global-sync/session-load"
|
||||||
import { trimSessions } from "./global-sync/session-trim"
|
import { trimSessions } from "./global-sync/session-trim"
|
||||||
import type { ProjectMeta } from "./global-sync/types"
|
import type { ProjectMeta } from "./global-sync/types"
|
||||||
import { SESSION_RECENT_LIMIT } from "./global-sync/types"
|
import { SESSION_RECENT_LIMIT } from "./global-sync/types"
|
||||||
|
import { sanitizeProject } from "./global-sync/utils"
|
||||||
import { formatServerError } from "@/utils/server-errors"
|
import { formatServerError } from "@/utils/server-errors"
|
||||||
import { queryOptions, skipToken, useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/solid-query"
|
import { queryOptions, skipToken, useQueryClient } from "@tanstack/solid-query"
|
||||||
import { createRefreshQueue } from "./global-sync/queue"
|
|
||||||
import { directoryKey } from "./global-sync/utils"
|
|
||||||
|
|
||||||
type GlobalStore = {
|
type GlobalStore = {
|
||||||
ready: boolean
|
ready: boolean
|
||||||
@@ -52,18 +45,6 @@ type GlobalStore = {
|
|||||||
export const loadSessionsQuery = (directory: string) =>
|
export const loadSessionsQuery = (directory: string) =>
|
||||||
queryOptions<null>({ queryKey: [directory, "loadSessions"], queryFn: skipToken })
|
queryOptions<null>({ queryKey: [directory, "loadSessions"], queryFn: skipToken })
|
||||||
|
|
||||||
export const loadMcpQuery = (directory: string, sdk?: OpencodeClient) =>
|
|
||||||
queryOptions({
|
|
||||||
queryKey: [directory, "mcp"],
|
|
||||||
queryFn: sdk ? () => sdk.mcp.status().then((r) => r.data ?? {}) : skipToken,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const loadLspQuery = (directory: string, sdk?: OpencodeClient) =>
|
|
||||||
queryOptions({
|
|
||||||
queryKey: [directory, "lsp"],
|
|
||||||
queryFn: sdk ? () => sdk.lsp.status().then((r) => r.data ?? []) : skipToken,
|
|
||||||
})
|
|
||||||
|
|
||||||
function createGlobalSync() {
|
function createGlobalSync() {
|
||||||
const globalSDK = useGlobalSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
@@ -75,49 +56,54 @@ 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 [configQuery, providerQuery, pathQuery] = useQueries(() => ({
|
const [projectCache, setProjectCache, projectInit] = persisted(
|
||||||
queries: [loadGlobalConfigQuery(), loadProvidersQuery(null), loadPathQuery(null), loadProjectsQuery()],
|
Persist.global("globalSync.project", ["globalSync.project.v1"]),
|
||||||
}))
|
createStore({ value: [] as Project[] }),
|
||||||
|
)
|
||||||
|
|
||||||
const [globalStore, setGlobalStore] = createStore<GlobalStore>({
|
const [globalStore, setGlobalStore] = createStore<GlobalStore>({
|
||||||
get ready() {
|
ready: false,
|
||||||
return bootstrap.isPending
|
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
||||||
},
|
project: projectCache.value,
|
||||||
project: [],
|
|
||||||
session_todo: {},
|
session_todo: {},
|
||||||
|
provider: { all: [], connected: [], default: {} },
|
||||||
provider_auth: {},
|
provider_auth: {},
|
||||||
get path() {
|
config: {},
|
||||||
const EMPTY = { state: "", config: "", worktree: "", directory: "", home: "" }
|
reload: undefined,
|
||||||
if (pathQuery.isLoading) return EMPTY
|
|
||||||
return pathQuery.data ?? EMPTY
|
|
||||||
},
|
|
||||||
get provider() {
|
|
||||||
const EMPTY = { all: [], connected: [], default: {} }
|
|
||||||
if (providerQuery.isLoading) return EMPTY
|
|
||||||
return providerQuery.data ?? EMPTY
|
|
||||||
},
|
|
||||||
get config() {
|
|
||||||
if (configQuery.isLoading) return {}
|
|
||||||
return configQuery.data ?? {}
|
|
||||||
},
|
|
||||||
get reload() {
|
|
||||||
return updateConfigMutation.isPending ? "pending" : undefined
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
let active = true
|
||||||
|
let projectWritten = false
|
||||||
let bootedAt = 0
|
let bootedAt = 0
|
||||||
let bootingRoot = false
|
let bootingRoot = false
|
||||||
let eventFrame: number | undefined
|
let eventFrame: number | undefined
|
||||||
let eventTimer: ReturnType<typeof setTimeout> | undefined
|
let eventTimer: ReturnType<typeof setTimeout> | undefined
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
active = false
|
||||||
|
})
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
if (eventFrame !== undefined) cancelAnimationFrame(eventFrame)
|
if (eventFrame !== undefined) cancelAnimationFrame(eventFrame)
|
||||||
if (eventTimer !== undefined) clearTimeout(eventTimer)
|
if (eventTimer !== undefined) clearTimeout(eventTimer)
|
||||||
})
|
})
|
||||||
|
|
||||||
const setProjects = (next: Project[] | ((draft: Project[]) => Project[])) => {
|
const cacheProjects = () => {
|
||||||
|
setProjectCache(
|
||||||
|
"value",
|
||||||
|
untrack(() => globalStore.project.map(sanitizeProject)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setProjects = (next: Project[] | ((draft: Project[]) => void)) => {
|
||||||
|
projectWritten = true
|
||||||
|
if (typeof next === "function") {
|
||||||
|
setGlobalStore("project", produce(next))
|
||||||
|
cacheProjects()
|
||||||
|
return
|
||||||
|
}
|
||||||
setGlobalStore("project", next)
|
setGlobalStore("project", next)
|
||||||
|
cacheProjects()
|
||||||
}
|
}
|
||||||
|
|
||||||
const setBootStore = ((...input: unknown[]) => {
|
const setBootStore = ((...input: unknown[]) => {
|
||||||
@@ -128,30 +114,24 @@ function createGlobalSync() {
|
|||||||
return (setGlobalStore as (...args: unknown[]) => unknown)(...input)
|
return (setGlobalStore as (...args: unknown[]) => unknown)(...input)
|
||||||
}) as typeof setGlobalStore
|
}) as typeof setGlobalStore
|
||||||
|
|
||||||
const bootstrap = useQuery(() => ({
|
|
||||||
queryKey: ["bootstrap"],
|
|
||||||
queryFn: async () => {
|
|
||||||
await bootstrapGlobal({
|
|
||||||
globalSDK: globalSDK.client,
|
|
||||||
requestFailedTitle: language.t("common.requestFailed"),
|
|
||||||
translate: language.t,
|
|
||||||
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
|
|
||||||
setGlobalStore: setBootStore,
|
|
||||||
queryClient,
|
|
||||||
})
|
|
||||||
bootedAt = Date.now()
|
|
||||||
return bootedAt
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
const set = ((...input: unknown[]) => {
|
const set = ((...input: unknown[]) => {
|
||||||
if (input[0] === "project" && (Array.isArray(input[1]) || typeof input[1] === "function")) {
|
if (input[0] === "project" && (Array.isArray(input[1]) || typeof input[1] === "function")) {
|
||||||
setProjects(input[1] as Project[] | ((draft: Project[]) => Project[]))
|
setProjects(input[1] as Project[] | ((draft: Project[]) => void))
|
||||||
return input[1]
|
return input[1]
|
||||||
}
|
}
|
||||||
return (setGlobalStore as (...args: unknown[]) => unknown)(...input)
|
return (setGlobalStore as (...args: unknown[]) => unknown)(...input)
|
||||||
}) as typeof setGlobalStore
|
}) as typeof setGlobalStore
|
||||||
|
|
||||||
|
if (projectInit instanceof Promise) {
|
||||||
|
void projectInit.then(() => {
|
||||||
|
if (!active) return
|
||||||
|
if (projectWritten) return
|
||||||
|
const cached = projectCache.value
|
||||||
|
if (cached.length === 0) return
|
||||||
|
setGlobalStore("project", cached)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const setSessionTodo = (sessionID: string, todos: Todo[] | undefined) => {
|
const setSessionTodo = (sessionID: string, todos: Todo[] | undefined) => {
|
||||||
if (!sessionID) return
|
if (!sessionID) return
|
||||||
if (!todos) {
|
if (!todos) {
|
||||||
@@ -170,23 +150,10 @@ function createGlobalSync() {
|
|||||||
|
|
||||||
const queue = createRefreshQueue({
|
const queue = createRefreshQueue({
|
||||||
paused,
|
paused,
|
||||||
key: directoryKey,
|
bootstrap,
|
||||||
bootstrap: () => queryClient.fetchQuery({ queryKey: ["bootstrap"] }),
|
|
||||||
bootstrapInstance,
|
bootstrapInstance,
|
||||||
})
|
})
|
||||||
|
|
||||||
const sdkFor = (directory: string) => {
|
|
||||||
const key = directoryKey(directory)
|
|
||||||
const cached = sdkCache.get(key)
|
|
||||||
if (cached) return cached
|
|
||||||
const sdk = globalSDK.createClient({
|
|
||||||
directory,
|
|
||||||
throwOnError: true,
|
|
||||||
})
|
|
||||||
sdkCache.set(key, sdk)
|
|
||||||
return sdk
|
|
||||||
}
|
|
||||||
|
|
||||||
const children = createChildStoreManager({
|
const children = createChildStoreManager({
|
||||||
owner,
|
owner,
|
||||||
isBooting: (directory) => booting.has(directory),
|
isBooting: (directory) => booting.has(directory),
|
||||||
@@ -195,25 +162,33 @@ function createGlobalSync() {
|
|||||||
void bootstrapInstance(directory)
|
void bootstrapInstance(directory)
|
||||||
},
|
},
|
||||||
onDispose: (directory) => {
|
onDispose: (directory) => {
|
||||||
const key = directoryKey(directory)
|
queue.clear(directory)
|
||||||
queue.clear(key)
|
sessionMeta.delete(directory)
|
||||||
sessionMeta.delete(key)
|
sdkCache.delete(directory)
|
||||||
sdkCache.delete(key)
|
clearProviderRev(directory)
|
||||||
clearProviderRev(key)
|
clearSessionPrefetchDirectory(directory)
|
||||||
clearSessionPrefetchDirectory(key)
|
|
||||||
},
|
},
|
||||||
translate: language.t,
|
translate: language.t,
|
||||||
getSdk: sdkFor,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const sdkFor = (directory: string) => {
|
||||||
|
const cached = sdkCache.get(directory)
|
||||||
|
if (cached) return cached
|
||||||
|
const sdk = globalSDK.createClient({
|
||||||
|
directory,
|
||||||
|
throwOnError: true,
|
||||||
|
})
|
||||||
|
sdkCache.set(directory, sdk)
|
||||||
|
return sdk
|
||||||
|
}
|
||||||
|
|
||||||
async function loadSessions(directory: string) {
|
async function loadSessions(directory: string) {
|
||||||
const key = directoryKey(directory)
|
const pending = sessionLoads.get(directory)
|
||||||
const pending = sessionLoads.get(key)
|
|
||||||
if (pending) return pending
|
if (pending) return pending
|
||||||
|
|
||||||
children.pin(key)
|
children.pin(directory)
|
||||||
const [store, setStore] = children.child(directory, { bootstrap: false })
|
const [store, setStore] = children.child(directory, { bootstrap: false })
|
||||||
const meta = sessionMeta.get(key)
|
const meta = sessionMeta.get(directory)
|
||||||
if (meta && meta.limit >= store.limit) {
|
if (meta && meta.limit >= store.limit) {
|
||||||
const next = trimSessions(store.session, {
|
const next = trimSessions(store.session, {
|
||||||
limit: store.limit,
|
limit: store.limit,
|
||||||
@@ -223,14 +198,14 @@ function createGlobalSync() {
|
|||||||
setStore("session", reconcile(next, { key: "id" }))
|
setStore("session", reconcile(next, { key: "id" }))
|
||||||
cleanupDroppedSessionCaches(store, setStore, next, setSessionTodo)
|
cleanupDroppedSessionCaches(store, setStore, next, setSessionTodo)
|
||||||
}
|
}
|
||||||
children.unpin(key)
|
children.unpin(directory)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT)
|
const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT)
|
||||||
const promise = queryClient
|
const promise = queryClient
|
||||||
.fetchQuery({
|
.fetchQuery({
|
||||||
...loadSessionsQuery(key),
|
...loadSessionsQuery(directory),
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
loadRootSessionsWithFallback({
|
loadRootSessionsWithFallback({
|
||||||
directory,
|
directory,
|
||||||
@@ -248,19 +223,17 @@ function createGlobalSync() {
|
|||||||
limit,
|
limit,
|
||||||
permission: store.permission,
|
permission: store.permission,
|
||||||
})
|
})
|
||||||
batch(() => {
|
setStore(
|
||||||
setStore(
|
"sessionTotal",
|
||||||
"sessionTotal",
|
estimateRootSessionTotal({
|
||||||
estimateRootSessionTotal({
|
count: nonArchived.length,
|
||||||
count: nonArchived.length,
|
limit: x.limit,
|
||||||
limit: x.limit,
|
limited: x.limited,
|
||||||
limited: x.limited,
|
}),
|
||||||
}),
|
)
|
||||||
)
|
setStore("session", reconcile(sessions, { key: "id" }))
|
||||||
setStore("session", reconcile(sessions, { key: "id" }))
|
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
|
||||||
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
|
sessionMeta.set(directory, { limit })
|
||||||
})
|
|
||||||
sessionMeta.set(key, { limit })
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error("Failed to load sessions", err)
|
console.error("Failed to load sessions", err)
|
||||||
@@ -275,24 +248,23 @@ function createGlobalSync() {
|
|||||||
})
|
})
|
||||||
.then(() => {})
|
.then(() => {})
|
||||||
|
|
||||||
sessionLoads.set(key, promise)
|
sessionLoads.set(directory, promise)
|
||||||
void promise.finally(() => {
|
void promise.finally(() => {
|
||||||
sessionLoads.delete(key)
|
sessionLoads.delete(directory)
|
||||||
children.unpin(key)
|
children.unpin(directory)
|
||||||
})
|
})
|
||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bootstrapInstance(directory: string) {
|
async function bootstrapInstance(directory: string) {
|
||||||
const key = directoryKey(directory)
|
if (!directory) return
|
||||||
if (!key) return
|
const pending = booting.get(directory)
|
||||||
const pending = booting.get(key)
|
|
||||||
if (pending) return pending
|
if (pending) return pending
|
||||||
|
|
||||||
children.pin(key)
|
children.pin(directory)
|
||||||
const promise = Promise.resolve().then(async () => {
|
const promise = Promise.resolve().then(async () => {
|
||||||
const child = children.ensureChild(directory)
|
const child = children.ensureChild(directory)
|
||||||
const cache = children.vcsCache.get(key)
|
const cache = children.vcsCache.get(directory)
|
||||||
if (!cache) return
|
if (!cache) return
|
||||||
const sdk = sdkFor(directory)
|
const sdk = sdkFor(directory)
|
||||||
await bootstrapDirectory({
|
await bootstrapDirectory({
|
||||||
@@ -313,17 +285,16 @@ function createGlobalSync() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
booting.set(key, promise)
|
booting.set(directory, promise)
|
||||||
void promise.finally(() => {
|
void promise.finally(() => {
|
||||||
booting.delete(key)
|
booting.delete(directory)
|
||||||
children.unpin(key)
|
children.unpin(directory)
|
||||||
})
|
})
|
||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
const unsub = globalSDK.event.listen((e) => {
|
const unsub = globalSDK.event.listen((e) => {
|
||||||
const directory = e.name
|
const directory = e.name
|
||||||
const key = directoryKey(directory)
|
|
||||||
const event = e.details
|
const event = e.details
|
||||||
const recent = bootingRoot || Date.now() - bootedAt < 1500
|
const recent = bootingRoot || Date.now() - bootedAt < 1500
|
||||||
|
|
||||||
@@ -333,7 +304,7 @@ function createGlobalSync() {
|
|||||||
project: globalStore.project,
|
project: globalStore.project,
|
||||||
refresh: () => {
|
refresh: () => {
|
||||||
if (recent) return
|
if (recent) return
|
||||||
bootstrap.refetch()
|
queue.refresh()
|
||||||
},
|
},
|
||||||
setGlobalProject: setProjects,
|
setGlobalProject: setProjects,
|
||||||
})
|
})
|
||||||
@@ -346,9 +317,9 @@ function createGlobalSync() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const existing = children.children[key]
|
const existing = children.children[directory]
|
||||||
if (!existing) return
|
if (!existing) return
|
||||||
children.mark(key)
|
children.mark(directory)
|
||||||
const [store, setStore] = existing
|
const [store, setStore] = existing
|
||||||
applyDirectoryEvent({
|
applyDirectoryEvent({
|
||||||
event,
|
event,
|
||||||
@@ -357,9 +328,14 @@ function createGlobalSync() {
|
|||||||
setStore,
|
setStore,
|
||||||
push: queue.push,
|
push: queue.push,
|
||||||
setSessionTodo,
|
setSessionTodo,
|
||||||
vcsCache: children.vcsCache.get(key),
|
vcsCache: children.vcsCache.get(directory),
|
||||||
loadLsp: () => {
|
loadLsp: () => {
|
||||||
void queryClient.fetchQuery(loadLspQuery(key, sdkFor(directory)))
|
void sdkFor(directory)
|
||||||
|
.lsp.status()
|
||||||
|
.then((x) => {
|
||||||
|
setStore("lsp", x.data ?? [])
|
||||||
|
setStore("lsp_ready", true)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -370,10 +346,27 @@ function createGlobalSync() {
|
|||||||
})
|
})
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
for (const directory of Object.keys(children.children)) {
|
for (const directory of Object.keys(children.children)) {
|
||||||
children.disposeDirectory(directoryKey(directory))
|
children.disposeDirectory(directory)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
bootingRoot = true
|
||||||
|
try {
|
||||||
|
await bootstrapGlobal({
|
||||||
|
globalSDK: globalSDK.client,
|
||||||
|
requestFailedTitle: language.t("common.requestFailed"),
|
||||||
|
translate: language.t,
|
||||||
|
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
|
||||||
|
setGlobalStore: setBootStore,
|
||||||
|
queryClient,
|
||||||
|
})
|
||||||
|
bootedAt = Date.now()
|
||||||
|
} finally {
|
||||||
|
bootingRoot = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (typeof requestAnimationFrame === "function") {
|
if (typeof requestAnimationFrame === "function") {
|
||||||
eventFrame = requestAnimationFrame(() => {
|
eventFrame = requestAnimationFrame(() => {
|
||||||
@@ -389,6 +382,7 @@ function createGlobalSync() {
|
|||||||
void globalSDK.event.start()
|
void globalSDK.event.start()
|
||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
|
void bootstrap()
|
||||||
})
|
})
|
||||||
|
|
||||||
const projectApi = {
|
const projectApi = {
|
||||||
@@ -401,10 +395,21 @@ function createGlobalSync() {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateConfigMutation = useMutation(() => ({
|
const updateConfig = async (config: Config) => {
|
||||||
mutationFn: (config: Config) => globalSDK.client.global.config.update({ config }),
|
setGlobalStore("reload", "pending")
|
||||||
onSuccess: () => bootstrap.refetch(),
|
return globalSDK.client.global.config
|
||||||
}))
|
.update({ config })
|
||||||
|
.then(bootstrap)
|
||||||
|
.then(() => {
|
||||||
|
queue.refresh()
|
||||||
|
setGlobalStore("reload", undefined)
|
||||||
|
queue.refresh()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setGlobalStore("reload", undefined)
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: globalStore,
|
data: globalStore,
|
||||||
@@ -417,8 +422,8 @@ function createGlobalSync() {
|
|||||||
},
|
},
|
||||||
child: children.child,
|
child: children.child,
|
||||||
peek: children.peek,
|
peek: children.peek,
|
||||||
// bootstrap,
|
bootstrap,
|
||||||
updateConfig: updateConfigMutation.mutateAsync,
|
updateConfig,
|
||||||
project: projectApi,
|
project: projectApi,
|
||||||
todo: {
|
todo: {
|
||||||
set: setSessionTodo,
|
set: setSessionTodo,
|
||||||
|
|||||||
@@ -11,15 +11,15 @@ import type {
|
|||||||
Todo,
|
Todo,
|
||||||
} from "@opencode-ai/sdk/v2/client"
|
} from "@opencode-ai/sdk/v2/client"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
import { getFilename } from "@opencode-ai/shared/util/path"
|
||||||
import { retry } from "@opencode-ai/core/util/retry"
|
import { retry } from "@opencode-ai/shared/util/retry"
|
||||||
import { batch } from "solid-js"
|
import { batch } from "solid-js"
|
||||||
import { reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
import { reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
||||||
import type { State, VcsCache } from "./types"
|
import type { State, VcsCache } from "./types"
|
||||||
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
|
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
|
||||||
import { formatServerError } from "@/utils/server-errors"
|
import { formatServerError } from "@/utils/server-errors"
|
||||||
import { QueryClient, queryOptions, skipToken } from "@tanstack/solid-query"
|
import { QueryClient, queryOptions, skipToken } from "@tanstack/solid-query"
|
||||||
import { loadMcpQuery } from "../global-sync"
|
import { loadSessionsQuery } from "../global-sync"
|
||||||
|
|
||||||
type GlobalStore = {
|
type GlobalStore = {
|
||||||
ready: boolean
|
ready: boolean
|
||||||
@@ -67,62 +67,6 @@ function runAll(list: Array<() => Promise<unknown>>) {
|
|||||||
return Promise.allSettled(list.map((item) => item()))
|
return Promise.allSettled(list.map((item) => item()))
|
||||||
}
|
}
|
||||||
|
|
||||||
function showErrors(input: {
|
|
||||||
errors: unknown[]
|
|
||||||
title: string
|
|
||||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
|
||||||
formatMoreCount: (count: number) => string
|
|
||||||
}) {
|
|
||||||
if (input.errors.length === 0) return
|
|
||||||
const message = formatServerError(input.errors[0], input.translate)
|
|
||||||
const more = input.errors.length > 1 ? input.formatMoreCount(input.errors.length - 1) : ""
|
|
||||||
showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: input.title,
|
|
||||||
description: message + more,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const loadGlobalConfigQuery = (
|
|
||||||
sdk?: OpencodeClient,
|
|
||||||
transform?: (x: Awaited<ReturnType<OpencodeClient["global"]["config"]["get"]>>) => void,
|
|
||||||
) =>
|
|
||||||
queryOptions({
|
|
||||||
queryKey: ["config"],
|
|
||||||
queryFn: sdk
|
|
||||||
? () =>
|
|
||||||
retry(() =>
|
|
||||||
sdk.global.config.get().then((x) => {
|
|
||||||
transform?.(x)
|
|
||||||
return x.data!
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
: skipToken,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const loadProjectsQuery = (
|
|
||||||
sdk?: OpencodeClient,
|
|
||||||
transform?: (x: Awaited<ReturnType<OpencodeClient["project"]["list"]>>["data"]) => void,
|
|
||||||
) =>
|
|
||||||
queryOptions({
|
|
||||||
queryKey: ["project"],
|
|
||||||
queryFn: sdk
|
|
||||||
? () =>
|
|
||||||
retry(() =>
|
|
||||||
sdk.project
|
|
||||||
.list()
|
|
||||||
.then((x) => {
|
|
||||||
return (x.data ?? [])
|
|
||||||
.filter((p) => !!p?.id)
|
|
||||||
.filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
|
|
||||||
.slice()
|
|
||||||
.sort((a, b) => cmp(a.id, b.id))
|
|
||||||
})
|
|
||||||
.then(transform),
|
|
||||||
)
|
|
||||||
: skipToken,
|
|
||||||
})
|
|
||||||
|
|
||||||
export async function bootstrapGlobal(input: {
|
export async function bootstrapGlobal(input: {
|
||||||
globalSDK: OpencodeClient
|
globalSDK: OpencodeClient
|
||||||
requestFailedTitle: string
|
requestFailedTitle: string
|
||||||
@@ -131,15 +75,53 @@ export async function bootstrapGlobal(input: {
|
|||||||
setGlobalStore: SetStoreFunction<GlobalStore>
|
setGlobalStore: SetStoreFunction<GlobalStore>
|
||||||
queryClient: QueryClient
|
queryClient: QueryClient
|
||||||
}) {
|
}) {
|
||||||
const slow = [
|
const fast = [
|
||||||
() => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.globalSDK)),
|
|
||||||
() => input.queryClient.fetchQuery(loadProvidersQuery(null, input.globalSDK)),
|
|
||||||
() => input.queryClient.fetchQuery(loadPathQuery(null, input.globalSDK)),
|
|
||||||
() =>
|
() =>
|
||||||
input.queryClient.fetchQuery(
|
retry(() =>
|
||||||
loadProjectsQuery(input.globalSDK, (data) => input.setGlobalStore("project", data ?? [])),
|
input.globalSDK.global.config.get().then((x) => {
|
||||||
|
input.setGlobalStore("config", x.data!)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
() =>
|
||||||
|
input.queryClient.fetchQuery({
|
||||||
|
...loadProvidersQuery(null),
|
||||||
|
queryFn: () =>
|
||||||
|
retry(() =>
|
||||||
|
input.globalSDK.provider.list().then((x) => {
|
||||||
|
input.setGlobalStore("provider", normalizeProviderList(x.data!))
|
||||||
|
return null
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
const slow = [
|
||||||
|
() =>
|
||||||
|
retry(() =>
|
||||||
|
input.globalSDK.path.get().then((x) => {
|
||||||
|
input.setGlobalStore("path", x.data!)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
() =>
|
||||||
|
retry(() =>
|
||||||
|
input.globalSDK.project.list().then((x) => {
|
||||||
|
const projects = (x.data ?? [])
|
||||||
|
.filter((p) => !!p?.id)
|
||||||
|
.filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => cmp(a.id, b.id))
|
||||||
|
input.setGlobalStore("project", projects)
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
await runAll(fast)
|
||||||
|
// showErrors({
|
||||||
|
// errors: errors(await runAll(fast)),
|
||||||
|
// title: input.requestFailedTitle,
|
||||||
|
// translate: input.translate,
|
||||||
|
// formatMoreCount: input.formatMoreCount,
|
||||||
|
// })
|
||||||
|
await waitForPaint()
|
||||||
await runAll(slow)
|
await runAll(slow)
|
||||||
// showErrors({
|
// showErrors({
|
||||||
// errors: errors(),
|
// errors: errors(),
|
||||||
@@ -147,6 +129,7 @@ export async function bootstrapGlobal(input: {
|
|||||||
// translate: input.translate,
|
// translate: input.translate,
|
||||||
// formatMoreCount: input.formatMoreCount,
|
// formatMoreCount: input.formatMoreCount,
|
||||||
// })
|
// })
|
||||||
|
input.setGlobalStore("ready", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupBySession<T extends { id: string; sessionID: string }>(input: T[]) {
|
function groupBySession<T extends { id: string; sessionID: string }>(input: T[]) {
|
||||||
@@ -197,47 +180,11 @@ function warmSessions(input: {
|
|||||||
).then(() => undefined)
|
).then(() => undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadProvidersQuery = (directory: string | null, sdk?: OpencodeClient) =>
|
export const loadProvidersQuery = (directory: string | null) =>
|
||||||
queryOptions({
|
queryOptions<null>({ queryKey: [directory, "providers"], queryFn: skipToken })
|
||||||
queryKey: [directory, "providers"],
|
|
||||||
queryFn: sdk ? () => retry(() => sdk.provider.list().then((x) => normalizeProviderList(x.data!))) : skipToken,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const loadAgentsQuery = (
|
export const loadAgentsQuery = (directory: string | null) =>
|
||||||
directory: string | null,
|
queryOptions<null>({ queryKey: [directory, "agents"], queryFn: skipToken })
|
||||||
sdk?: OpencodeClient,
|
|
||||||
transform?: (x: Awaited<ReturnType<OpencodeClient["app"]["agents"]>>) => void,
|
|
||||||
) =>
|
|
||||||
queryOptions({
|
|
||||||
queryKey: [directory, "agents"],
|
|
||||||
queryFn: sdk
|
|
||||||
? () =>
|
|
||||||
retry(() =>
|
|
||||||
sdk.app.agents().then((x) => {
|
|
||||||
transform?.(x)
|
|
||||||
return x.data!
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
: skipToken,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const loadPathQuery = (
|
|
||||||
directory: string | null,
|
|
||||||
sdk?: OpencodeClient,
|
|
||||||
transform?: (x: Awaited<ReturnType<OpencodeClient["path"]["get"]>>) => void,
|
|
||||||
) =>
|
|
||||||
queryOptions<Path>({
|
|
||||||
queryKey: [directory, "path"],
|
|
||||||
queryFn: sdk
|
|
||||||
? () =>
|
|
||||||
retry(() =>
|
|
||||||
sdk.path.get().then(async (x) => {
|
|
||||||
transform?.(x)
|
|
||||||
return x.data!
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
: skipToken,
|
|
||||||
})
|
|
||||||
|
|
||||||
export async function bootstrapDirectory(input: {
|
export async function bootstrapDirectory(input: {
|
||||||
directory: string
|
directory: string
|
||||||
@@ -264,32 +211,56 @@ export async function bootstrapDirectory(input: {
|
|||||||
input.setStore("provider", input.global.provider)
|
input.setStore("provider", input.global.provider)
|
||||||
}
|
}
|
||||||
if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) {
|
if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) {
|
||||||
input.setStore("config", reconcile(input.global.config, { merge: false }))
|
input.setStore("config", input.global.config)
|
||||||
}
|
}
|
||||||
|
if (loading || input.store.provider.all.length === 0) {
|
||||||
|
input.setStore("provider_ready", false)
|
||||||
|
}
|
||||||
|
input.setStore("mcp_ready", false)
|
||||||
|
input.setStore("mcp", {})
|
||||||
|
input.setStore("lsp_ready", false)
|
||||||
|
input.setStore("lsp", [])
|
||||||
if (loading) input.setStore("status", "partial")
|
if (loading) input.setStore("status", "partial")
|
||||||
|
|
||||||
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
const fast = [() => Promise.resolve(input.loadSessions(input.directory))]
|
||||||
providerRev.set(input.directory, rev)
|
|
||||||
|
const errs = errors(await runAll(fast))
|
||||||
|
if (errs.length > 0) {
|
||||||
|
console.error("Failed to bootstrap instance", errs[0])
|
||||||
|
const project = getFilename(input.directory)
|
||||||
|
showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||||
|
description: formatServerError(errs[0], input.translate),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
;(async () => {
|
;(async () => {
|
||||||
const slow = [
|
const slow = [
|
||||||
() => Promise.resolve(input.loadSessions(input.directory)),
|
|
||||||
() =>
|
() =>
|
||||||
input.queryClient.ensureQueryData(
|
input.queryClient.ensureQueryData({
|
||||||
loadAgentsQuery(input.directory, input.sdk, (x) => input.setStore("agent", normalizeAgentList(x.data))),
|
...loadAgentsQuery(input.directory),
|
||||||
),
|
queryFn: () =>
|
||||||
() =>
|
retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))).then(
|
||||||
retry(() => input.sdk.config.get().then((x) => input.setStore("config", reconcile(x.data!, { merge: false })))),
|
() => null,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
() => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
|
||||||
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
|
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
|
||||||
!seededProject &&
|
() =>
|
||||||
(() => retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id))),
|
seededProject
|
||||||
!seededPath &&
|
? Promise.resolve()
|
||||||
(() =>
|
: retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)),
|
||||||
input.queryClient.ensureQueryData(
|
() =>
|
||||||
loadPathQuery(input.directory, input.sdk, (x) => {
|
seededPath
|
||||||
const next = projectID(x.data?.directory ?? input.directory, input.global.project)
|
? Promise.resolve()
|
||||||
if (next) input.setStore("project", next)
|
: retry(() =>
|
||||||
}),
|
input.sdk.path.get().then((x) => {
|
||||||
)),
|
input.setStore("path", x.data!)
|
||||||
|
const next = projectID(x.data?.directory ?? input.directory, input.global.project)
|
||||||
|
if (next) input.setStore("project", next)
|
||||||
|
}),
|
||||||
|
),
|
||||||
() =>
|
() =>
|
||||||
retry(() =>
|
retry(() =>
|
||||||
input.sdk.vcs.get().then((x) => {
|
input.sdk.vcs.get().then((x) => {
|
||||||
@@ -352,17 +323,14 @@ export async function bootstrapDirectory(input: {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
() => Promise.resolve(input.loadSessions(input.directory)),
|
() => Promise.resolve(input.loadSessions(input.directory)),
|
||||||
() => input.queryClient.fetchQuery(loadMcpQuery(input.directory, input.sdk)),
|
|
||||||
() =>
|
() =>
|
||||||
input.queryClient.fetchQuery(loadProvidersQuery(input.directory, input.sdk)).catch((err) => {
|
retry(() =>
|
||||||
const project = getFilename(input.directory)
|
input.sdk.mcp.status().then((x) => {
|
||||||
showToast({
|
input.setStore("mcp", x.data!)
|
||||||
variant: "error",
|
input.setStore("mcp_ready", true)
|
||||||
title: input.translate("toast.project.reloadFailed.title", { project }),
|
}),
|
||||||
description: formatServerError(err, input.translate),
|
),
|
||||||
})
|
]
|
||||||
}),
|
|
||||||
].filter(Boolean) as (() => Promise<any>)[]
|
|
||||||
|
|
||||||
await waitForPaint()
|
await waitForPaint()
|
||||||
const slowErrs = errors(await runAll(slow))
|
const slowErrs = errors(await runAll(slow))
|
||||||
@@ -376,6 +344,29 @@ export async function bootstrapDirectory(input: {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading && slowErrs.length === 0) input.setStore("status", "complete")
|
if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete")
|
||||||
|
|
||||||
|
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
||||||
|
providerRev.set(input.directory, rev)
|
||||||
|
void input.queryClient.ensureQueryData({
|
||||||
|
...loadSessionsQuery(input.directory),
|
||||||
|
queryFn: () =>
|
||||||
|
retry(() => input.sdk.provider.list())
|
||||||
|
.then((x) => {
|
||||||
|
if (providerRev.get(input.directory) !== rev) return
|
||||||
|
input.setStore("provider", normalizeProviderList(x.data!))
|
||||||
|
input.setStore("provider_ready", true)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (providerRev.get(input.directory) !== rev) console.error("Failed to refresh provider list", err)
|
||||||
|
const project = getFilename(input.directory)
|
||||||
|
showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||||
|
description: formatServerError(err, input.translate),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(() => null),
|
||||||
|
})
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ describe("createChildStoreManager", () => {
|
|||||||
onBootstrap() {},
|
onBootstrap() {},
|
||||||
onDispose() {},
|
onDispose() {},
|
||||||
translate: (key) => key,
|
translate: (key) => key,
|
||||||
getSdk: () => null!,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => {
|
Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createRoot, getOwner, onCleanup, runWithOwner, type Owner } from "solid-js"
|
import { createRoot, getOwner, onCleanup, runWithOwner, type Owner } from "solid-js"
|
||||||
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
|
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import type { OpencodeClient, VcsInfo } from "@opencode-ai/sdk/v2/client"
|
import type { VcsInfo } from "@opencode-ai/sdk/v2/client"
|
||||||
import {
|
import {
|
||||||
DIR_IDLE_TTL_MS,
|
DIR_IDLE_TTL_MS,
|
||||||
MAX_DIR_STORES,
|
MAX_DIR_STORES,
|
||||||
@@ -14,10 +14,6 @@ import {
|
|||||||
type VcsCache,
|
type VcsCache,
|
||||||
} from "./types"
|
} from "./types"
|
||||||
import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
|
import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
|
||||||
import { useQueries } from "@tanstack/solid-query"
|
|
||||||
import { loadPathQuery, loadProvidersQuery } from "./bootstrap"
|
|
||||||
import { loadLspQuery, loadMcpQuery } from "../global-sync"
|
|
||||||
import { directoryKey, type DirectoryKey } from "./utils"
|
|
||||||
|
|
||||||
export function createChildStoreManager(input: {
|
export function createChildStoreManager(input: {
|
||||||
owner: Owner
|
owner: Owner
|
||||||
@@ -26,7 +22,6 @@ export function createChildStoreManager(input: {
|
|||||||
onBootstrap: (directory: string) => void
|
onBootstrap: (directory: string) => void
|
||||||
onDispose: (directory: string) => void
|
onDispose: (directory: string) => void
|
||||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||||
getSdk: (directory: string) => OpencodeClient
|
|
||||||
}) {
|
}) {
|
||||||
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
|
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
|
||||||
const vcsCache = new Map<string, VcsCache>()
|
const vcsCache = new Map<string, VcsCache>()
|
||||||
@@ -37,37 +32,30 @@ export function createChildStoreManager(input: {
|
|||||||
const ownerPins = new WeakMap<object, Set<string>>()
|
const ownerPins = new WeakMap<object, Set<string>>()
|
||||||
const disposers = new Map<string, () => void>()
|
const disposers = new Map<string, () => void>()
|
||||||
|
|
||||||
const markKey = (key: DirectoryKey) => {
|
|
||||||
if (!key) return
|
|
||||||
lifecycle.set(key, { lastAccessAt: Date.now() })
|
|
||||||
runEviction(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
const mark = (directory: string) => {
|
const mark = (directory: string) => {
|
||||||
const key = directoryKey(directory)
|
if (!directory) return
|
||||||
markKey(key)
|
lifecycle.set(directory, { lastAccessAt: Date.now() })
|
||||||
|
runEviction(directory)
|
||||||
}
|
}
|
||||||
|
|
||||||
const pin = (directory: string) => {
|
const pin = (directory: string) => {
|
||||||
const key = directoryKey(directory)
|
if (!directory) return
|
||||||
if (!key) return
|
pins.set(directory, (pins.get(directory) ?? 0) + 1)
|
||||||
pins.set(key, (pins.get(key) ?? 0) + 1)
|
mark(directory)
|
||||||
markKey(key)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const unpin = (directory: string) => {
|
const unpin = (directory: string) => {
|
||||||
const key = directoryKey(directory)
|
if (!directory) return
|
||||||
if (!key) return
|
const next = (pins.get(directory) ?? 0) - 1
|
||||||
const next = (pins.get(key) ?? 0) - 1
|
|
||||||
if (next > 0) {
|
if (next > 0) {
|
||||||
pins.set(key, next)
|
pins.set(directory, next)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pins.delete(key)
|
pins.delete(directory)
|
||||||
runEviction()
|
runEviction()
|
||||||
}
|
}
|
||||||
|
|
||||||
const pinned = (directory: string) => (pins.get(directoryKey(directory)) ?? 0) > 0
|
const pinned = (directory: string) => (pins.get(directory) ?? 0) > 0
|
||||||
|
|
||||||
const pinForOwner = (directory: string) => {
|
const pinForOwner = (directory: string) => {
|
||||||
const current = getOwner()
|
const current = getOwner()
|
||||||
@@ -89,31 +77,30 @@ export function createChildStoreManager(input: {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function disposeDirectory(directory: DirectoryKey) {
|
function disposeDirectory(directory: string) {
|
||||||
const key = directory
|
|
||||||
if (
|
if (
|
||||||
!canDisposeDirectory({
|
!canDisposeDirectory({
|
||||||
directory: key,
|
directory,
|
||||||
hasStore: !!children[key],
|
hasStore: !!children[directory],
|
||||||
pinned: pinned(key),
|
pinned: pinned(directory),
|
||||||
booting: input.isBooting(key),
|
booting: input.isBooting(directory),
|
||||||
loadingSessions: input.isLoadingSessions(key),
|
loadingSessions: input.isLoadingSessions(directory),
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
vcsCache.delete(key)
|
vcsCache.delete(directory)
|
||||||
metaCache.delete(key)
|
metaCache.delete(directory)
|
||||||
iconCache.delete(key)
|
iconCache.delete(directory)
|
||||||
lifecycle.delete(key)
|
lifecycle.delete(directory)
|
||||||
const dispose = disposers.get(key)
|
const dispose = disposers.get(directory)
|
||||||
if (dispose) {
|
if (dispose) {
|
||||||
dispose()
|
dispose()
|
||||||
disposers.delete(key)
|
disposers.delete(directory)
|
||||||
}
|
}
|
||||||
delete children[key]
|
delete children[directory]
|
||||||
input.onDispose(key)
|
input.onDispose(directory)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,14 +117,13 @@ export function createChildStoreManager(input: {
|
|||||||
}).filter((directory) => directory !== skip)
|
}).filter((directory) => directory !== skip)
|
||||||
if (list.length === 0) return
|
if (list.length === 0) return
|
||||||
for (const directory of list) {
|
for (const directory of list) {
|
||||||
if (!disposeDirectory(directoryKey(directory))) continue
|
if (!disposeDirectory(directory)) continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureChild(directory: string) {
|
function ensureChild(directory: string) {
|
||||||
const key = directoryKey(directory)
|
if (!directory) console.error("No directory provided")
|
||||||
if (!key) console.error("No directory provided")
|
if (!children[directory]) {
|
||||||
if (!children[key]) {
|
|
||||||
const vcs = runWithOwner(input.owner, () =>
|
const vcs = runWithOwner(input.owner, () =>
|
||||||
persisted(
|
persisted(
|
||||||
Persist.workspace(directory, "vcs", ["vcs.v1"]),
|
Persist.workspace(directory, "vcs", ["vcs.v1"]),
|
||||||
@@ -146,7 +132,7 @@ export function createChildStoreManager(input: {
|
|||||||
)
|
)
|
||||||
if (!vcs) throw new Error(input.translate("error.childStore.persistedCacheCreateFailed"))
|
if (!vcs) throw new Error(input.translate("error.childStore.persistedCacheCreateFailed"))
|
||||||
const vcsStore = vcs[0]
|
const vcsStore = vcs[0]
|
||||||
vcsCache.set(key, { store: vcsStore, setStore: vcs[1], ready: vcs[3] })
|
vcsCache.set(directory, { store: vcsStore, setStore: vcs[1], ready: vcs[3] })
|
||||||
|
|
||||||
const meta = runWithOwner(input.owner, () =>
|
const meta = runWithOwner(input.owner, () =>
|
||||||
persisted(
|
persisted(
|
||||||
@@ -155,7 +141,7 @@ export function createChildStoreManager(input: {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
if (!meta) throw new Error(input.translate("error.childStore.persistedProjectMetadataCreateFailed"))
|
if (!meta) throw new Error(input.translate("error.childStore.persistedProjectMetadataCreateFailed"))
|
||||||
metaCache.set(key, { store: meta[0], setStore: meta[1], ready: meta[3] })
|
metaCache.set(directory, { store: meta[0], setStore: meta[1], ready: meta[3] })
|
||||||
|
|
||||||
const icon = runWithOwner(input.owner, () =>
|
const icon = runWithOwner(input.owner, () =>
|
||||||
persisted(
|
persisted(
|
||||||
@@ -164,38 +150,20 @@ export function createChildStoreManager(input: {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
if (!icon) throw new Error(input.translate("error.childStore.persistedProjectIconCreateFailed"))
|
if (!icon) throw new Error(input.translate("error.childStore.persistedProjectIconCreateFailed"))
|
||||||
iconCache.set(key, { store: icon[0], setStore: icon[1], ready: icon[3] })
|
iconCache.set(directory, { store: icon[0], setStore: icon[1], ready: icon[3] })
|
||||||
|
|
||||||
const init = () =>
|
const init = () =>
|
||||||
createRoot((dispose) => {
|
createRoot((dispose) => {
|
||||||
const sdk = input.getSdk(directory)
|
|
||||||
|
|
||||||
const initialMeta = meta[0].value
|
const initialMeta = meta[0].value
|
||||||
const initialIcon = icon[0].value
|
const initialIcon = icon[0].value
|
||||||
|
|
||||||
const [pathQuery, mcpQuery, lspQuery, providerQuery] = useQueries(() => ({
|
|
||||||
queries: [
|
|
||||||
loadPathQuery(key, sdk),
|
|
||||||
loadMcpQuery(key, sdk),
|
|
||||||
loadLspQuery(key, sdk),
|
|
||||||
loadProvidersQuery(key, sdk),
|
|
||||||
],
|
|
||||||
}))
|
|
||||||
|
|
||||||
const child = createStore<State>({
|
const child = createStore<State>({
|
||||||
project: "",
|
project: "",
|
||||||
projectMeta: initialMeta,
|
projectMeta: initialMeta,
|
||||||
icon: initialIcon,
|
icon: initialIcon,
|
||||||
get provider_ready() {
|
provider_ready: false,
|
||||||
return !providerQuery.isLoading
|
|
||||||
},
|
|
||||||
provider: { all: [], connected: [], default: {} },
|
provider: { all: [], connected: [], default: {} },
|
||||||
config: {},
|
config: {},
|
||||||
get path() {
|
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
||||||
if (pathQuery.isLoading || !pathQuery.data)
|
|
||||||
return { state: "", config: "", worktree: "", directory: "", home: "" }
|
|
||||||
return pathQuery.data
|
|
||||||
},
|
|
||||||
status: "loading" as const,
|
status: "loading" as const,
|
||||||
agent: [],
|
agent: [],
|
||||||
command: [],
|
command: [],
|
||||||
@@ -206,30 +174,22 @@ export function createChildStoreManager(input: {
|
|||||||
todo: {},
|
todo: {},
|
||||||
permission: {},
|
permission: {},
|
||||||
question: {},
|
question: {},
|
||||||
get mcp_ready() {
|
mcp_ready: false,
|
||||||
return !mcpQuery.isLoading
|
mcp: {},
|
||||||
},
|
lsp_ready: false,
|
||||||
get mcp() {
|
lsp: [],
|
||||||
return mcpQuery.isLoading ? {} : (mcpQuery.data ?? {})
|
|
||||||
},
|
|
||||||
get lsp_ready() {
|
|
||||||
return !lspQuery.isLoading
|
|
||||||
},
|
|
||||||
get lsp() {
|
|
||||||
return lspQuery.isLoading ? [] : (lspQuery.data ?? [])
|
|
||||||
},
|
|
||||||
vcs: vcsStore.value,
|
vcs: vcsStore.value,
|
||||||
limit: 5,
|
limit: 5,
|
||||||
message: {},
|
message: {},
|
||||||
part: {},
|
part: {},
|
||||||
})
|
})
|
||||||
children[key] = child
|
children[directory] = child
|
||||||
disposers.set(key, dispose)
|
disposers.set(directory, dispose)
|
||||||
|
|
||||||
const onPersistedInit = (init: Promise<string> | string | null, run: () => void) => {
|
const onPersistedInit = (init: Promise<string> | string | null, run: () => void) => {
|
||||||
if (!(init instanceof Promise)) return
|
if (!(init instanceof Promise)) return
|
||||||
void init.then(() => {
|
void init.then(() => {
|
||||||
if (children[key] !== child) return
|
if (children[directory] !== child) return
|
||||||
run()
|
run()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -253,16 +213,15 @@ export function createChildStoreManager(input: {
|
|||||||
|
|
||||||
runWithOwner(input.owner, init)
|
runWithOwner(input.owner, init)
|
||||||
}
|
}
|
||||||
markKey(key)
|
mark(directory)
|
||||||
const childStore = children[key]
|
const childStore = children[directory]
|
||||||
if (!childStore) throw new Error(input.translate("error.childStore.storeCreateFailed"))
|
if (!childStore) throw new Error(input.translate("error.childStore.storeCreateFailed"))
|
||||||
return childStore
|
return childStore
|
||||||
}
|
}
|
||||||
|
|
||||||
function child(directory: string, options: ChildOptions = {}) {
|
function child(directory: string, options: ChildOptions = {}) {
|
||||||
const key = directoryKey(directory)
|
|
||||||
const childStore = ensureChild(directory)
|
const childStore = ensureChild(directory)
|
||||||
pinForOwner(key)
|
pinForOwner(directory)
|
||||||
const shouldBootstrap = options.bootstrap ?? true
|
const shouldBootstrap = options.bootstrap ?? true
|
||||||
if (shouldBootstrap && childStore[0].status === "loading") {
|
if (shouldBootstrap && childStore[0].status === "loading") {
|
||||||
input.onBootstrap(directory)
|
input.onBootstrap(directory)
|
||||||
@@ -271,7 +230,6 @@ export function createChildStoreManager(input: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function peek(directory: string, options: ChildOptions = {}) {
|
function peek(directory: string, options: ChildOptions = {}) {
|
||||||
const key = directoryKey(directory)
|
|
||||||
const childStore = ensureChild(directory)
|
const childStore = ensureChild(directory)
|
||||||
const shouldBootstrap = options.bootstrap ?? true
|
const shouldBootstrap = options.bootstrap ?? true
|
||||||
if (shouldBootstrap && childStore[0].status === "loading") {
|
if (shouldBootstrap && childStore[0].status === "loading") {
|
||||||
@@ -281,9 +239,8 @@ export function createChildStoreManager(input: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function projectMeta(directory: string, patch: ProjectMeta) {
|
function projectMeta(directory: string, patch: ProjectMeta) {
|
||||||
const key = directoryKey(directory)
|
|
||||||
const [store, setStore] = ensureChild(directory)
|
const [store, setStore] = ensureChild(directory)
|
||||||
const cached = metaCache.get(key)
|
const cached = metaCache.get(directory)
|
||||||
if (!cached) return
|
if (!cached) return
|
||||||
const previous = store.projectMeta ?? {}
|
const previous = store.projectMeta ?? {}
|
||||||
const icon = patch.icon ? { ...previous.icon, ...patch.icon } : previous.icon
|
const icon = patch.icon ? { ...previous.icon, ...patch.icon } : previous.icon
|
||||||
@@ -299,9 +256,8 @@ export function createChildStoreManager(input: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function projectIcon(directory: string, value: string | undefined) {
|
function projectIcon(directory: string, value: string | undefined) {
|
||||||
const key = directoryKey(directory)
|
|
||||||
const [store, setStore] = ensureChild(directory)
|
const [store, setStore] = ensureChild(directory)
|
||||||
const cached = iconCache.get(key)
|
const cached = iconCache.get(directory)
|
||||||
if (!cached) return
|
if (!cached) return
|
||||||
if (store.icon === value) return
|
if (store.icon === value) return
|
||||||
cached.setStore("value", value)
|
cached.setStore("value", value)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Binary } from "@opencode-ai/core/util/binary"
|
import { Binary } from "@opencode-ai/shared/util/binary"
|
||||||
import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
||||||
import type {
|
import type {
|
||||||
Message,
|
Message,
|
||||||
@@ -21,7 +21,7 @@ const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
|||||||
export function applyGlobalEvent(input: {
|
export function applyGlobalEvent(input: {
|
||||||
event: { type: string; properties?: unknown }
|
event: { type: string; properties?: unknown }
|
||||||
project: Project[]
|
project: Project[]
|
||||||
setGlobalProject: (next: Project[] | ((draft: Project[]) => Project[])) => void
|
setGlobalProject: (next: Project[] | ((draft: Project[]) => void)) => void
|
||||||
refresh: () => void
|
refresh: () => void
|
||||||
}) {
|
}) {
|
||||||
if (input.event.type === "global.disposed" || input.event.type === "server.connected") {
|
if (input.event.type === "global.disposed" || input.event.type === "server.connected") {
|
||||||
@@ -33,18 +33,14 @@ export function applyGlobalEvent(input: {
|
|||||||
const properties = input.event.properties as Project
|
const properties = input.event.properties as Project
|
||||||
const result = Binary.search(input.project, properties.id, (s) => s.id)
|
const result = Binary.search(input.project, properties.id, (s) => s.id)
|
||||||
if (result.found) {
|
if (result.found) {
|
||||||
input.setGlobalProject(
|
input.setGlobalProject((draft) => {
|
||||||
produce((draft) => {
|
draft[result.index] = { ...draft[result.index], ...properties }
|
||||||
draft[result.index] = { ...draft[result.index], ...properties }
|
})
|
||||||
}),
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
input.setGlobalProject(
|
input.setGlobalProject((draft) => {
|
||||||
produce((draft) => {
|
draft.splice(result.index, 0, properties)
|
||||||
draft.splice(result.index, 0, properties)
|
})
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanupSessionCaches(
|
function cleanupSessionCaches(
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
|
||||||
import { createRefreshQueue } from "./queue"
|
|
||||||
import { directoryKey } from "./utils"
|
|
||||||
|
|
||||||
const tick = () => new Promise((resolve) => setTimeout(resolve, 10))
|
|
||||||
|
|
||||||
describe("createRefreshQueue", () => {
|
|
||||||
test("clears queued directories by normalized key", async () => {
|
|
||||||
const calls: string[] = []
|
|
||||||
const queue = createRefreshQueue({
|
|
||||||
paused: () => false,
|
|
||||||
key: directoryKey,
|
|
||||||
bootstrap: async () => {},
|
|
||||||
bootstrapInstance: (directory) => {
|
|
||||||
calls.push(directory)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
queue.push("C:\\tmp\\demo")
|
|
||||||
queue.clear("C:/tmp/demo")
|
|
||||||
|
|
||||||
await tick()
|
|
||||||
|
|
||||||
expect(calls).toEqual([])
|
|
||||||
queue.dispose()
|
|
||||||
})
|
|
||||||
|
|
||||||
test("passes the original directory to bootstrapInstance", async () => {
|
|
||||||
const calls: string[] = []
|
|
||||||
const queue = createRefreshQueue({
|
|
||||||
paused: () => false,
|
|
||||||
key: directoryKey,
|
|
||||||
bootstrap: async () => {},
|
|
||||||
bootstrapInstance: (directory) => {
|
|
||||||
calls.push(directory)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
queue.push("C:\\tmp\\demo")
|
|
||||||
|
|
||||||
await tick()
|
|
||||||
|
|
||||||
expect(calls).toEqual(["C:\\tmp\\demo"])
|
|
||||||
queue.dispose()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -2,25 +2,22 @@ type QueueInput = {
|
|||||||
paused: () => boolean
|
paused: () => boolean
|
||||||
bootstrap: () => Promise<void>
|
bootstrap: () => Promise<void>
|
||||||
bootstrapInstance: (directory: string) => Promise<void> | void
|
bootstrapInstance: (directory: string) => Promise<void> | void
|
||||||
key?: (directory: string) => string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createRefreshQueue(input: QueueInput) {
|
export function createRefreshQueue(input: QueueInput) {
|
||||||
const queued = new Map<string, string>()
|
const queued = new Set<string>()
|
||||||
let root = false
|
let root = false
|
||||||
let running = false
|
let running = false
|
||||||
let timer: ReturnType<typeof setTimeout> | undefined
|
let timer: ReturnType<typeof setTimeout> | undefined
|
||||||
|
|
||||||
const key = input.key ?? ((directory: string) => directory)
|
|
||||||
|
|
||||||
const tick = () => new Promise<void>((resolve) => setTimeout(resolve, 0))
|
const tick = () => new Promise<void>((resolve) => setTimeout(resolve, 0))
|
||||||
|
|
||||||
const take = (count: number) => {
|
const take = (count: number) => {
|
||||||
if (queued.size === 0) return [] as string[]
|
if (queued.size === 0) return [] as string[]
|
||||||
const items: string[] = []
|
const items: string[] = []
|
||||||
for (const [id, directory] of queued) {
|
for (const item of queued) {
|
||||||
queued.delete(id)
|
queued.delete(item)
|
||||||
items.push(directory)
|
items.push(item)
|
||||||
if (items.length >= count) break
|
if (items.length >= count) break
|
||||||
}
|
}
|
||||||
return items
|
return items
|
||||||
@@ -36,7 +33,7 @@ export function createRefreshQueue(input: QueueInput) {
|
|||||||
|
|
||||||
const push = (directory: string) => {
|
const push = (directory: string) => {
|
||||||
if (!directory) return
|
if (!directory) return
|
||||||
queued.set(key(directory), directory)
|
queued.add(directory)
|
||||||
if (input.paused()) return
|
if (input.paused()) return
|
||||||
schedule()
|
schedule()
|
||||||
}
|
}
|
||||||
@@ -76,7 +73,7 @@ export function createRefreshQueue(input: QueueInput) {
|
|||||||
push,
|
push,
|
||||||
refresh,
|
refresh,
|
||||||
clear(directory: string) {
|
clear(directory: string) {
|
||||||
queued.delete(key(directory))
|
queued.delete(directory)
|
||||||
},
|
},
|
||||||
dispose() {
|
dispose() {
|
||||||
if (!timer) return
|
if (!timer) return
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import type { Agent } from "@opencode-ai/sdk/v2/client"
|
import type { Agent } from "@opencode-ai/sdk/v2/client"
|
||||||
import { directoryKey, normalizeAgentList } from "./utils"
|
import { normalizeAgentList } from "./utils"
|
||||||
|
|
||||||
const agent = (name = "build") =>
|
const agent = (name = "build") =>
|
||||||
({
|
({
|
||||||
@@ -33,20 +33,3 @@ describe("normalizeAgentList", () => {
|
|||||||
expect(normalizeAgentList([{ name: "build" }, agent("docs")])).toEqual([agent("docs")])
|
expect(normalizeAgentList([{ name: "build" }, agent("docs")])).toEqual([agent("docs")])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("directoryKey", () => {
|
|
||||||
test("normalizes slashes", () => {
|
|
||||||
expect(String(directoryKey("C:\\Repos\\sst\\opencode"))).toBe("C:/Repos/sst/opencode")
|
|
||||||
expect(String(directoryKey("C:/Repos/sst/opencode"))).toBe("C:/Repos/sst/opencode")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("preserves backslashes in posix paths", () => {
|
|
||||||
expect(String(directoryKey("/tmp/foo\\bar"))).toBe("/tmp/foo\\bar")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("trims trailing slashes without breaking roots", () => {
|
|
||||||
expect(String(directoryKey("C:/Repos/sst/opencode/"))).toBe("C:/Repos/sst/opencode")
|
|
||||||
expect(String(directoryKey("C:/"))).toBe("C:/")
|
|
||||||
expect(String(directoryKey("/"))).toBe("/")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { Agent, Project, ProviderListResponse } from "@opencode-ai/sdk/v2/client"
|
import type { Agent, Project, ProviderListResponse } from "@opencode-ai/sdk/v2/client"
|
||||||
export { pathKey as directoryKey, type PathKey as DirectoryKey } from "@/utils/path-key"
|
|
||||||
|
|
||||||
export const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
export const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
||||||
|
|
||||||
|
|||||||
@@ -391,14 +391,37 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
? globalSync.data.project.find((x) => x.id === projectID)
|
? globalSync.data.project.find((x) => x.id === projectID)
|
||||||
: globalSync.data.project.find((x) => x.worktree === project.worktree)
|
: globalSync.data.project.find((x) => x.worktree === project.worktree)
|
||||||
|
|
||||||
// Preserve local icon override from per-workspace localStorage cache (childStore.icon).
|
const local = childStore.projectMeta
|
||||||
// Without this, different subdirectories of the same git repo would share the same
|
const localOverride =
|
||||||
// icon from the database instead of using their individual overrides.
|
local?.name !== undefined ||
|
||||||
const base = { ...metadata, ...project }
|
local?.commands?.start !== undefined ||
|
||||||
if (childStore.icon) {
|
local?.icon?.override !== undefined ||
|
||||||
return { ...base, icon: { ...base.icon, override: childStore.icon } }
|
local?.icon?.color !== undefined
|
||||||
|
|
||||||
|
const base = {
|
||||||
|
...metadata,
|
||||||
|
...project,
|
||||||
|
icon: {
|
||||||
|
url: metadata?.icon?.url,
|
||||||
|
override: metadata?.icon?.override ?? childStore.icon,
|
||||||
|
color: metadata?.icon?.color,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const isGlobal = projectID === "global" || (metadata?.id === undefined && localOverride)
|
||||||
|
if (!isGlobal) return base
|
||||||
|
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
id: base.id ?? "global",
|
||||||
|
name: local?.name,
|
||||||
|
commands: local?.commands,
|
||||||
|
icon: {
|
||||||
|
url: base.icon?.url,
|
||||||
|
override: local?.icon?.override,
|
||||||
|
color: local?.icon?.color,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return base
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const roots = createMemo(() => {
|
const roots = createMemo(() => {
|
||||||
@@ -493,7 +516,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const project of projects) {
|
for (const project of projects) {
|
||||||
if (project.icon?.color || project.icon?.override || project.icon?.url) continue
|
if (project.icon?.color) continue
|
||||||
const worktree = project.worktree
|
const worktree = project.worktree
|
||||||
const existing = colors[worktree]
|
const existing = colors[worktree]
|
||||||
const color = existing ?? pickAvailableColor(used)
|
const color = existing ?? pickAvailableColor(used)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { batch, createEffect, createMemo } from "solid-js"
|
import { batch, createEffect, createMemo } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
@@ -382,7 +382,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
setSaved("session", session, {
|
setSaved("session", session, {
|
||||||
agent: msg.agent,
|
agent: msg.agent,
|
||||||
model: msg.model,
|
model: msg.model,
|
||||||
variant: msg.model?.variant ?? null,
|
variant: msg.model.variant ?? null,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import { useGlobalSync } from "./global-sync"
|
|||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { Binary } from "@opencode-ai/core/util/binary"
|
import { Binary } from "@opencode-ai/shared/util/binary"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
import { EventSessionError } from "@opencode-ai/sdk/v2"
|
import { EventSessionError } from "@opencode-ai/sdk/v2"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import type { PermissionRequest, Session } from "@opencode-ai/sdk/v2/client"
|
import type { PermissionRequest, Session } from "@opencode-ai/sdk/v2/client"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
||||||
import { autoRespondsPermission, isDirectoryAutoAccepting } from "./permission-auto-respond"
|
import { autoRespondsPermission, isDirectoryAutoAccepting } from "./permission-auto-respond"
|
||||||
|
|
||||||
const session = (input: { id: string; parentID?: string }) =>
|
const session = (input: { id: string; parentID?: string }) =>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
||||||
|
|
||||||
export function acceptKey(sessionID: string, directory?: string) {
|
export function acceptKey(sessionID: string, directory?: string) {
|
||||||
if (!directory) return sessionID
|
if (!directory) return sessionID
|
||||||
|
|||||||
@@ -49,11 +49,11 @@ export type Platform = {
|
|||||||
/** Storage mechanism, defaults to localStorage */
|
/** Storage mechanism, defaults to localStorage */
|
||||||
storage?: (name?: string) => SyncStorage | AsyncStorage
|
storage?: (name?: string) => SyncStorage | AsyncStorage
|
||||||
|
|
||||||
/** Check for a downloadable desktop update */
|
/** Check for updates (Tauri only) */
|
||||||
checkUpdate?(): Promise<UpdateInfo>
|
checkUpdate?(): Promise<UpdateInfo>
|
||||||
|
|
||||||
/** Install the downloaded update using the platform restart flow */
|
/** Install updates (Tauri only) */
|
||||||
updateAndRestart?(): Promise<void>
|
update?(): Promise<void>
|
||||||
|
|
||||||
/** Fetch override */
|
/** Fetch override */
|
||||||
fetch?: typeof fetch
|
fetch?: typeof fetch
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { checksum } from "@opencode-ai/core/util/encode"
|
import { checksum } from "@opencode-ai/shared/util/encode"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { batch, createMemo, createRoot, getOwner, onCleanup } from "solid-js"
|
import { batch, createMemo, createRoot, getOwner, onCleanup } from "solid-js"
|
||||||
import { createStore, type SetStoreFunction } from "solid-js/store"
|
import { createStore, type SetStoreFunction } from "solid-js/store"
|
||||||
@@ -185,9 +185,9 @@ function createPromptSession(dir: string, id: string | undefined) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
ready,
|
ready,
|
||||||
current: () => store.prompt,
|
current: createMemo(() => store.prompt),
|
||||||
cursor: createMemo(() => store.cursor),
|
cursor: createMemo(() => store.cursor),
|
||||||
dirty: () => !isPromptEqual(store.prompt, DEFAULT_PROMPT),
|
dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
|
||||||
context: {
|
context: {
|
||||||
items: createMemo(() => store.context.items),
|
items: createMemo(() => store.context.items),
|
||||||
add(item: ContextItem) {
|
add(item: ContextItem) {
|
||||||
@@ -277,7 +277,7 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
|
|||||||
const pick = (scope?: Scope) => (scope ? load(scope.dir, scope.id) : session())
|
const pick = (scope?: Scope) => (scope ? load(scope.dir, scope.id) : session())
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ready: () => session().ready,
|
ready: () => session().ready(),
|
||||||
current: () => session().current(),
|
current: () => session().current(),
|
||||||
cursor: () => session().cursor(),
|
cursor: () => session().cursor(),
|
||||||
dirty: () => session().dirty(),
|
dirty: () => session().dirty(),
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ export interface Settings {
|
|||||||
showReasoningSummaries: boolean
|
showReasoningSummaries: boolean
|
||||||
shellToolPartsExpanded: boolean
|
shellToolPartsExpanded: boolean
|
||||||
editToolPartsExpanded: boolean
|
editToolPartsExpanded: boolean
|
||||||
showSessionProgressBar: boolean
|
|
||||||
}
|
}
|
||||||
updates: {
|
updates: {
|
||||||
startup: boolean
|
startup: boolean
|
||||||
@@ -116,7 +115,6 @@ const defaultSettings: Settings = {
|
|||||||
showReasoningSummaries: false,
|
showReasoningSummaries: false,
|
||||||
shellToolPartsExpanded: false,
|
shellToolPartsExpanded: false,
|
||||||
editToolPartsExpanded: false,
|
editToolPartsExpanded: false,
|
||||||
showSessionProgressBar: true,
|
|
||||||
},
|
},
|
||||||
updates: {
|
updates: {
|
||||||
startup: true,
|
startup: true,
|
||||||
@@ -229,13 +227,6 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
|||||||
setEditToolPartsExpanded(value: boolean) {
|
setEditToolPartsExpanded(value: boolean) {
|
||||||
setStore("general", "editToolPartsExpanded", value)
|
setStore("general", "editToolPartsExpanded", value)
|
||||||
},
|
},
|
||||||
showSessionProgressBar: withFallback(
|
|
||||||
() => store.general?.showSessionProgressBar,
|
|
||||||
defaultSettings.general.showSessionProgressBar,
|
|
||||||
),
|
|
||||||
setShowSessionProgressBar(value: boolean) {
|
|
||||||
setStore("general", "showSessionProgressBar", value)
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
updates: {
|
updates: {
|
||||||
startup: withFallback(() => store.updates?.startup, defaultSettings.updates.startup),
|
startup: withFallback(() => store.updates?.startup, defaultSettings.updates.startup),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { batch, createMemo } from "solid-js"
|
import { batch, createMemo } from "solid-js"
|
||||||
import { createStore, produce, reconcile } from "solid-js/store"
|
import { createStore, produce, reconcile } from "solid-js/store"
|
||||||
import { Binary } from "@opencode-ai/core/util/binary"
|
import { Binary } from "@opencode-ai/shared/util/binary"
|
||||||
import { retry } from "@opencode-ai/core/util/retry"
|
import { retry } from "@opencode-ai/shared/util/retry"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import {
|
import {
|
||||||
clearSessionPrefetch,
|
clearSessionPrefetch,
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ export const dict = {
|
|||||||
"common.saving": "جارٍ الحفظ...",
|
"common.saving": "جارٍ الحفظ...",
|
||||||
"common.default": "افتراضي",
|
"common.default": "افتراضي",
|
||||||
"common.attachment": "مرفق",
|
"common.attachment": "مرفق",
|
||||||
"prompt.placeholder.shell": "أدخل أمر shell... {{example}}",
|
"prompt.placeholder.shell": "أدخل أمر shell...",
|
||||||
"prompt.placeholder.normal": 'اسأل أي شيء... "{{example}}"',
|
"prompt.placeholder.normal": 'اسأل أي شيء... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "اسأل أي شيء...",
|
"prompt.placeholder.simple": "اسأل أي شيء...",
|
||||||
"prompt.placeholder.summarizeComments": "لخّص التعليقات…",
|
"prompt.placeholder.summarizeComments": "لخّص التعليقات…",
|
||||||
@@ -582,8 +582,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "توسيع أجزاء أداة edit",
|
"settings.general.row.editToolPartsExpanded.title": "توسيع أجزاء أداة edit",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"إظهار أجزاء أدوات edit و write و patch موسعة بشكل افتراضي في الشريط الزمني",
|
"إظهار أجزاء أدوات edit و write و patch موسعة بشكل افتراضي في الشريط الزمني",
|
||||||
"settings.general.row.showSessionProgressBar.title": "إظهار شريط تقدم الجلسة",
|
|
||||||
"settings.general.row.showSessionProgressBar.description": "عرض شريط التقدم المتحرك أعلى الجلسة أثناء عمل الوكيل",
|
|
||||||
"settings.general.row.wayland.title": "استخدام Wayland الأصلي",
|
"settings.general.row.wayland.title": "استخدام Wayland الأصلي",
|
||||||
"settings.general.row.wayland.description": "تعطيل التراجع إلى X11 على Wayland. يتطلب إعادة التشغيل.",
|
"settings.general.row.wayland.description": "تعطيل التراجع إلى X11 على Wayland. يتطلب إعادة التشغيل.",
|
||||||
"settings.general.row.wayland.tooltip":
|
"settings.general.row.wayland.tooltip":
|
||||||
@@ -721,6 +719,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.webfetch.description": "جلب محتوى من عنوان URL",
|
"settings.permissions.tool.webfetch.description": "جلب محتوى من عنوان URL",
|
||||||
"settings.permissions.tool.websearch.title": "بحث الويب",
|
"settings.permissions.tool.websearch.title": "بحث الويب",
|
||||||
"settings.permissions.tool.websearch.description": "البحث في الويب",
|
"settings.permissions.tool.websearch.description": "البحث في الويب",
|
||||||
|
"settings.permissions.tool.codesearch.title": "بحث الكود",
|
||||||
|
"settings.permissions.tool.codesearch.description": "البحث عن كود على الويب",
|
||||||
"settings.permissions.tool.external_directory.title": "دليل خارجي",
|
"settings.permissions.tool.external_directory.title": "دليل خارجي",
|
||||||
"settings.permissions.tool.external_directory.description": "الوصول إلى الملفات خارج دليل المشروع",
|
"settings.permissions.tool.external_directory.description": "الوصول إلى الملفات خارج دليل المشروع",
|
||||||
"settings.permissions.tool.doom_loop.title": "حلقة الموت",
|
"settings.permissions.tool.doom_loop.title": "حلقة الموت",
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ export const dict = {
|
|||||||
"common.saving": "Salvando...",
|
"common.saving": "Salvando...",
|
||||||
"common.default": "Padrão",
|
"common.default": "Padrão",
|
||||||
"common.attachment": "anexo",
|
"common.attachment": "anexo",
|
||||||
"prompt.placeholder.shell": "Digite comando do shell... {{example}}",
|
"prompt.placeholder.shell": "Digite comando do shell...",
|
||||||
"prompt.placeholder.normal": 'Pergunte qualquer coisa... "{{example}}"',
|
"prompt.placeholder.normal": 'Pergunte qualquer coisa... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "Pergunte qualquer coisa...",
|
"prompt.placeholder.simple": "Pergunte qualquer coisa...",
|
||||||
"prompt.placeholder.summarizeComments": "Resumir comentários…",
|
"prompt.placeholder.summarizeComments": "Resumir comentários…",
|
||||||
@@ -590,9 +590,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "Expandir partes da ferramenta de edição",
|
"settings.general.row.editToolPartsExpanded.title": "Expandir partes da ferramenta de edição",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"Mostrar partes das ferramentas de edição, escrita e patch expandidas por padrão na linha do tempo",
|
"Mostrar partes das ferramentas de edição, escrita e patch expandidas por padrão na linha do tempo",
|
||||||
"settings.general.row.showSessionProgressBar.title": "Mostrar barra de progresso da sessão",
|
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
|
||||||
"Exibir a barra de progresso animada no topo da sessão quando o agente estiver trabalhando",
|
|
||||||
"settings.general.row.wayland.title": "Usar Wayland nativo",
|
"settings.general.row.wayland.title": "Usar Wayland nativo",
|
||||||
"settings.general.row.wayland.description": "Desabilitar fallback X11 no Wayland. Requer reinicialização.",
|
"settings.general.row.wayland.description": "Desabilitar fallback X11 no Wayland. Requer reinicialização.",
|
||||||
"settings.general.row.wayland.tooltip":
|
"settings.general.row.wayland.tooltip":
|
||||||
@@ -732,6 +729,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.webfetch.description": "Buscar conteúdo de uma URL",
|
"settings.permissions.tool.webfetch.description": "Buscar conteúdo de uma URL",
|
||||||
"settings.permissions.tool.websearch.title": "Pesquisa Web",
|
"settings.permissions.tool.websearch.title": "Pesquisa Web",
|
||||||
"settings.permissions.tool.websearch.description": "Pesquisar na web",
|
"settings.permissions.tool.websearch.description": "Pesquisar na web",
|
||||||
|
"settings.permissions.tool.codesearch.title": "Pesquisa de Código",
|
||||||
|
"settings.permissions.tool.codesearch.description": "Pesquisar código na web",
|
||||||
"settings.permissions.tool.external_directory.title": "Diretório Externo",
|
"settings.permissions.tool.external_directory.title": "Diretório Externo",
|
||||||
"settings.permissions.tool.external_directory.description": "Acessar arquivos fora do diretório do projeto",
|
"settings.permissions.tool.external_directory.description": "Acessar arquivos fora do diretório do projeto",
|
||||||
"settings.permissions.tool.doom_loop.title": "Loop Infinito",
|
"settings.permissions.tool.doom_loop.title": "Loop Infinito",
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ export const dict = {
|
|||||||
"common.default": "Podrazumijevano",
|
"common.default": "Podrazumijevano",
|
||||||
"common.attachment": "prilog",
|
"common.attachment": "prilog",
|
||||||
|
|
||||||
"prompt.placeholder.shell": "Unesi shell naredbu... {{example}}",
|
"prompt.placeholder.shell": "Unesi shell naredbu...",
|
||||||
"prompt.placeholder.normal": 'Pitaj bilo šta... "{{example}}"',
|
"prompt.placeholder.normal": 'Pitaj bilo šta... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "Pitaj bilo šta...",
|
"prompt.placeholder.simple": "Pitaj bilo šta...",
|
||||||
"prompt.placeholder.summarizeComments": "Sažmi komentare…",
|
"prompt.placeholder.summarizeComments": "Sažmi komentare…",
|
||||||
@@ -655,9 +655,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "Proširi dijelove alata za uređivanje",
|
"settings.general.row.editToolPartsExpanded.title": "Proširi dijelove alata za uređivanje",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"Prikaži dijelove alata za uređivanje, pisanje i patch podrazumijevano proširene na vremenskoj traci",
|
"Prikaži dijelove alata za uređivanje, pisanje i patch podrazumijevano proširene na vremenskoj traci",
|
||||||
"settings.general.row.showSessionProgressBar.title": "Prikaži traku napretka sesije",
|
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
|
||||||
"Prikaži animiranu traku napretka na vrhu sesije kada agent radi",
|
|
||||||
"settings.general.row.wayland.title": "Koristi nativni Wayland",
|
"settings.general.row.wayland.title": "Koristi nativni Wayland",
|
||||||
"settings.general.row.wayland.description": "Onemogući X11 fallback na Waylandu. Zahtijeva restart.",
|
"settings.general.row.wayland.description": "Onemogući X11 fallback na Waylandu. Zahtijeva restart.",
|
||||||
"settings.general.row.wayland.tooltip":
|
"settings.general.row.wayland.tooltip":
|
||||||
@@ -806,6 +803,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.webfetch.description": "Preuzmi sadržaj sa URL-a",
|
"settings.permissions.tool.webfetch.description": "Preuzmi sadržaj sa URL-a",
|
||||||
"settings.permissions.tool.websearch.title": "Web pretraga",
|
"settings.permissions.tool.websearch.title": "Web pretraga",
|
||||||
"settings.permissions.tool.websearch.description": "Pretražuj web",
|
"settings.permissions.tool.websearch.description": "Pretražuj web",
|
||||||
|
"settings.permissions.tool.codesearch.title": "Pretraga koda",
|
||||||
|
"settings.permissions.tool.codesearch.description": "Pretraži kod na webu",
|
||||||
"settings.permissions.tool.external_directory.title": "Vanjski direktorij",
|
"settings.permissions.tool.external_directory.title": "Vanjski direktorij",
|
||||||
"settings.permissions.tool.external_directory.description": "Pristup datotekama izvan direktorija projekta",
|
"settings.permissions.tool.external_directory.description": "Pristup datotekama izvan direktorija projekta",
|
||||||
"settings.permissions.tool.doom_loop.title": "Beskonačna petlja",
|
"settings.permissions.tool.doom_loop.title": "Beskonačna petlja",
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ export const dict = {
|
|||||||
"common.default": "Standard",
|
"common.default": "Standard",
|
||||||
"common.attachment": "vedhæftning",
|
"common.attachment": "vedhæftning",
|
||||||
|
|
||||||
"prompt.placeholder.shell": "Indtast shell-kommando... {{example}}",
|
"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.simple": "Spørg om hvad som helst...",
|
"prompt.placeholder.simple": "Spørg om hvad som helst...",
|
||||||
"prompt.placeholder.summarizeComments": "Opsummér kommentarer…",
|
"prompt.placeholder.summarizeComments": "Opsummér kommentarer…",
|
||||||
@@ -649,9 +649,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "Udvid edit-værktøjsdele",
|
"settings.general.row.editToolPartsExpanded.title": "Udvid edit-værktøjsdele",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"Vis edit-, write- og patch-værktøjsdele udvidet som standard i tidslinjen",
|
"Vis edit-, write- og patch-værktøjsdele udvidet som standard i tidslinjen",
|
||||||
"settings.general.row.showSessionProgressBar.title": "Vis sessionens fremdriftslinje",
|
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
|
||||||
"Vis den animerede fremdriftslinje øverst i sessionen, når agenten arbejder",
|
|
||||||
"settings.general.row.wayland.title": "Brug native Wayland",
|
"settings.general.row.wayland.title": "Brug native Wayland",
|
||||||
"settings.general.row.wayland.description": "Deaktiver X11-fallback på Wayland. Kræver genstart.",
|
"settings.general.row.wayland.description": "Deaktiver X11-fallback på Wayland. Kræver genstart.",
|
||||||
"settings.general.row.wayland.tooltip":
|
"settings.general.row.wayland.tooltip":
|
||||||
@@ -800,6 +797,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.webfetch.description": "Hent indhold fra en URL",
|
"settings.permissions.tool.webfetch.description": "Hent indhold fra en URL",
|
||||||
"settings.permissions.tool.websearch.title": "Websøgning",
|
"settings.permissions.tool.websearch.title": "Websøgning",
|
||||||
"settings.permissions.tool.websearch.description": "Søg på nettet",
|
"settings.permissions.tool.websearch.description": "Søg på nettet",
|
||||||
|
"settings.permissions.tool.codesearch.title": "Kodesøgning",
|
||||||
|
"settings.permissions.tool.codesearch.description": "Søg kode på nettet",
|
||||||
"settings.permissions.tool.external_directory.title": "Ekstern mappe",
|
"settings.permissions.tool.external_directory.title": "Ekstern mappe",
|
||||||
"settings.permissions.tool.external_directory.description": "Få adgang til filer uden for projektmappen",
|
"settings.permissions.tool.external_directory.description": "Få adgang til filer uden for projektmappen",
|
||||||
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ export const dict = {
|
|||||||
"common.saving": "Speichert...",
|
"common.saving": "Speichert...",
|
||||||
"common.default": "Standard",
|
"common.default": "Standard",
|
||||||
"common.attachment": "Anhang",
|
"common.attachment": "Anhang",
|
||||||
"prompt.placeholder.shell": "Shell-Befehl eingeben... {{example}}",
|
"prompt.placeholder.shell": "Shell-Befehl eingeben...",
|
||||||
"prompt.placeholder.normal": 'Fragen Sie alles... "{{example}}"',
|
"prompt.placeholder.normal": 'Fragen Sie alles... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "Fragen Sie alles...",
|
"prompt.placeholder.simple": "Fragen Sie alles...",
|
||||||
"prompt.placeholder.summarizeComments": "Kommentare zusammenfassen…",
|
"prompt.placeholder.summarizeComments": "Kommentare zusammenfassen…",
|
||||||
@@ -601,9 +601,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "Edit-Tool-Abschnitte ausklappen",
|
"settings.general.row.editToolPartsExpanded.title": "Edit-Tool-Abschnitte ausklappen",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"Edit-, Write- und Patch-Tool-Abschnitte standardmäßig in der Timeline ausgeklappt anzeigen",
|
"Edit-, Write- und Patch-Tool-Abschnitte standardmäßig in der Timeline ausgeklappt anzeigen",
|
||||||
"settings.general.row.showSessionProgressBar.title": "Sitzungsfortschrittsleiste anzeigen",
|
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
|
||||||
"Die animierte Fortschrittsleiste oben in der Sitzung anzeigen, wenn der Agent arbeitet",
|
|
||||||
"settings.general.row.wayland.title": "Natives Wayland verwenden",
|
"settings.general.row.wayland.title": "Natives Wayland verwenden",
|
||||||
"settings.general.row.wayland.description": "X11-Fallback unter Wayland deaktivieren. Erfordert Neustart.",
|
"settings.general.row.wayland.description": "X11-Fallback unter Wayland deaktivieren. Erfordert Neustart.",
|
||||||
"settings.general.row.wayland.tooltip":
|
"settings.general.row.wayland.tooltip":
|
||||||
@@ -743,6 +740,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.webfetch.description": "Inhalt von einer URL abrufen",
|
"settings.permissions.tool.webfetch.description": "Inhalt von einer URL abrufen",
|
||||||
"settings.permissions.tool.websearch.title": "Web-Suche",
|
"settings.permissions.tool.websearch.title": "Web-Suche",
|
||||||
"settings.permissions.tool.websearch.description": "Das Web durchsuchen",
|
"settings.permissions.tool.websearch.description": "Das Web durchsuchen",
|
||||||
|
"settings.permissions.tool.codesearch.title": "Code-Suche",
|
||||||
|
"settings.permissions.tool.codesearch.description": "Code im Web durchsuchen",
|
||||||
"settings.permissions.tool.external_directory.title": "Externes Verzeichnis",
|
"settings.permissions.tool.external_directory.title": "Externes Verzeichnis",
|
||||||
"settings.permissions.tool.external_directory.description": "Zugriff auf Dateien außerhalb des Projektverzeichnisses",
|
"settings.permissions.tool.external_directory.description": "Zugriff auf Dateien außerhalb des Projektverzeichnisses",
|
||||||
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export const dict = {
|
|||||||
"command.provider.connect": "Connect provider",
|
"command.provider.connect": "Connect provider",
|
||||||
"command.server.switch": "Switch server",
|
"command.server.switch": "Switch server",
|
||||||
"command.settings.open": "Open settings",
|
"command.settings.open": "Open settings",
|
||||||
|
"command.pair.show": "Pair mobile device",
|
||||||
"command.session.previous": "Previous session",
|
"command.session.previous": "Previous session",
|
||||||
"command.session.next": "Next session",
|
"command.session.next": "Next session",
|
||||||
"command.session.previous.unseen": "Previous unread session",
|
"command.session.previous.unseen": "Previous unread session",
|
||||||
@@ -230,7 +231,7 @@ export const dict = {
|
|||||||
"common.default": "Default",
|
"common.default": "Default",
|
||||||
"common.attachment": "attachment",
|
"common.attachment": "attachment",
|
||||||
|
|
||||||
"prompt.placeholder.shell": "Enter shell command... {{example}}",
|
"prompt.placeholder.shell": "Enter shell command...",
|
||||||
"prompt.placeholder.normal": 'Ask anything... "{{example}}"',
|
"prompt.placeholder.normal": 'Ask anything... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "Ask anything...",
|
"prompt.placeholder.simple": "Ask anything...",
|
||||||
"prompt.placeholder.summarizeComments": "Summarize comments…",
|
"prompt.placeholder.summarizeComments": "Summarize comments…",
|
||||||
@@ -728,11 +729,6 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.general.row.language.title": "Language",
|
"settings.general.row.language.title": "Language",
|
||||||
"settings.general.row.language.description": "Change the display language for OpenCode",
|
"settings.general.row.language.description": "Change the display language for OpenCode",
|
||||||
"settings.general.row.shell.title": "Terminal Shell",
|
|
||||||
"settings.general.row.shell.description":
|
|
||||||
"Choose the shell used for your terminal. Compatible shells are also used for agent tool calls.",
|
|
||||||
"settings.general.row.shell.autoDefault": "Auto (Default)",
|
|
||||||
"settings.general.row.shell.terminalOnly": "terminal only",
|
|
||||||
"settings.general.row.appearance.title": "Appearance",
|
"settings.general.row.appearance.title": "Appearance",
|
||||||
"settings.general.row.appearance.description": "Customise how OpenCode looks on your device",
|
"settings.general.row.appearance.description": "Customise how OpenCode looks on your device",
|
||||||
"settings.general.row.colorScheme.title": "Color scheme",
|
"settings.general.row.colorScheme.title": "Color scheme",
|
||||||
@@ -767,9 +763,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "Expand edit tool parts",
|
"settings.general.row.editToolPartsExpanded.title": "Expand edit tool parts",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"Show edit, write, and patch tool parts expanded by default in the timeline",
|
"Show edit, write, and patch tool parts expanded by default in the timeline",
|
||||||
"settings.general.row.showSessionProgressBar.title": "Show session progress bar",
|
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
|
||||||
"Display the animated progress bar at the top of the session when the agent is working",
|
|
||||||
|
|
||||||
"settings.general.row.wayland.title": "Use native Wayland",
|
"settings.general.row.wayland.title": "Use native Wayland",
|
||||||
"settings.general.row.wayland.description": "Disable X11 fallback on Wayland. Requires restart.",
|
"settings.general.row.wayland.description": "Disable X11 fallback on Wayland. Requires restart.",
|
||||||
@@ -878,6 +871,20 @@ export const dict = {
|
|||||||
"settings.providers.tag.config": "Config",
|
"settings.providers.tag.config": "Config",
|
||||||
"settings.providers.tag.custom": "Custom",
|
"settings.providers.tag.custom": "Custom",
|
||||||
"settings.providers.tag.other": "Other",
|
"settings.providers.tag.other": "Other",
|
||||||
|
"settings.pair.title": "Pair",
|
||||||
|
"settings.pair.description": "Pair a mobile device for push notifications.",
|
||||||
|
"settings.pair.loading": "Loading pairing info...",
|
||||||
|
"settings.pair.error.title": "Could not load pairing info",
|
||||||
|
"settings.pair.error.description": "Check that the server is reachable and try again.",
|
||||||
|
"settings.pair.disabled.title": "Push relay is not enabled",
|
||||||
|
"settings.pair.disabled.description": "Start the server with push relay options to enable mobile pairing.",
|
||||||
|
"settings.pair.server.label": "Server",
|
||||||
|
"settings.pair.relay.label": "Relay",
|
||||||
|
"settings.pair.secret.label": "Secret",
|
||||||
|
"settings.pair.instructions.title": "Scan with the OpenCode Control app",
|
||||||
|
"settings.pair.instructions.description":
|
||||||
|
"Open the OpenCode Control app and scan this QR code to pair your device for push notifications.",
|
||||||
|
|
||||||
"settings.models.title": "Models",
|
"settings.models.title": "Models",
|
||||||
"settings.models.description": "Model settings will be configurable here.",
|
"settings.models.description": "Model settings will be configurable here.",
|
||||||
"settings.agents.title": "Agents",
|
"settings.agents.title": "Agents",
|
||||||
@@ -920,6 +927,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.webfetch.description": "Fetch content from a URL",
|
"settings.permissions.tool.webfetch.description": "Fetch content from a URL",
|
||||||
"settings.permissions.tool.websearch.title": "Web Search",
|
"settings.permissions.tool.websearch.title": "Web Search",
|
||||||
"settings.permissions.tool.websearch.description": "Search the web",
|
"settings.permissions.tool.websearch.description": "Search the web",
|
||||||
|
"settings.permissions.tool.codesearch.title": "Code Search",
|
||||||
|
"settings.permissions.tool.codesearch.description": "Search code on the web",
|
||||||
"settings.permissions.tool.external_directory.title": "External Directory",
|
"settings.permissions.tool.external_directory.title": "External Directory",
|
||||||
"settings.permissions.tool.external_directory.description": "Access files outside the project directory",
|
"settings.permissions.tool.external_directory.description": "Access files outside the project directory",
|
||||||
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ export const dict = {
|
|||||||
"common.default": "Predeterminado",
|
"common.default": "Predeterminado",
|
||||||
"common.attachment": "adjunto",
|
"common.attachment": "adjunto",
|
||||||
|
|
||||||
"prompt.placeholder.shell": "Introduce comando de shell... {{example}}",
|
"prompt.placeholder.shell": "Introduce comando de shell...",
|
||||||
"prompt.placeholder.normal": 'Pregunta cualquier cosa... "{{example}}"',
|
"prompt.placeholder.normal": 'Pregunta cualquier cosa... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "Pregunta cualquier cosa...",
|
"prompt.placeholder.simple": "Pregunta cualquier cosa...",
|
||||||
"prompt.placeholder.summarizeComments": "Resumir comentarios…",
|
"prompt.placeholder.summarizeComments": "Resumir comentarios…",
|
||||||
@@ -659,9 +659,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "Expandir partes de la herramienta de edición",
|
"settings.general.row.editToolPartsExpanded.title": "Expandir partes de la herramienta de edición",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"Mostrar las partes de las herramientas de edición, escritura y parcheado expandidas por defecto en la línea de tiempo",
|
"Mostrar las partes de las herramientas de edición, escritura y parcheado expandidas por defecto en la línea de tiempo",
|
||||||
"settings.general.row.showSessionProgressBar.title": "Mostrar barra de progreso de la sesión",
|
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
|
||||||
"Mostrar la barra de progreso animada en la parte superior de la sesión cuando el agente esté trabajando",
|
|
||||||
"settings.general.row.wayland.title": "Usar Wayland nativo",
|
"settings.general.row.wayland.title": "Usar Wayland nativo",
|
||||||
"settings.general.row.wayland.description": "Deshabilitar fallback a X11 en Wayland. Requiere reinicio.",
|
"settings.general.row.wayland.description": "Deshabilitar fallback a X11 en Wayland. Requiere reinicio.",
|
||||||
"settings.general.row.wayland.tooltip":
|
"settings.general.row.wayland.tooltip":
|
||||||
@@ -813,6 +810,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.webfetch.description": "Obtener contenido de una URL",
|
"settings.permissions.tool.webfetch.description": "Obtener contenido de una URL",
|
||||||
"settings.permissions.tool.websearch.title": "Búsqueda Web",
|
"settings.permissions.tool.websearch.title": "Búsqueda Web",
|
||||||
"settings.permissions.tool.websearch.description": "Buscar en la web",
|
"settings.permissions.tool.websearch.description": "Buscar en la web",
|
||||||
|
"settings.permissions.tool.codesearch.title": "Búsqueda de Código",
|
||||||
|
"settings.permissions.tool.codesearch.description": "Buscar código en la web",
|
||||||
"settings.permissions.tool.external_directory.title": "Directorio Externo",
|
"settings.permissions.tool.external_directory.title": "Directorio Externo",
|
||||||
"settings.permissions.tool.external_directory.description": "Acceder a archivos fuera del directorio del proyecto",
|
"settings.permissions.tool.external_directory.description": "Acceder a archivos fuera del directorio del proyecto",
|
||||||
"settings.permissions.tool.doom_loop.title": "Bucle Infinito",
|
"settings.permissions.tool.doom_loop.title": "Bucle Infinito",
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ export const dict = {
|
|||||||
"common.saving": "Enregistrement...",
|
"common.saving": "Enregistrement...",
|
||||||
"common.default": "Défaut",
|
"common.default": "Défaut",
|
||||||
"common.attachment": "pièce jointe",
|
"common.attachment": "pièce jointe",
|
||||||
"prompt.placeholder.shell": "Entrez une commande shell... {{example}}",
|
"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.simple": "Demandez n'importe quoi...",
|
"prompt.placeholder.simple": "Demandez n'importe quoi...",
|
||||||
"prompt.placeholder.summarizeComments": "Résumer les commentaires…",
|
"prompt.placeholder.summarizeComments": "Résumer les commentaires…",
|
||||||
@@ -598,9 +598,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "Développer les parties de l'outil edit",
|
"settings.general.row.editToolPartsExpanded.title": "Développer les parties de l'outil edit",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"Afficher les parties des outils edit, write et patch développées par défaut dans la chronologie",
|
"Afficher les parties des outils edit, write et patch développées par défaut dans la chronologie",
|
||||||
"settings.general.row.showSessionProgressBar.title": "Afficher la barre de progression de la session",
|
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
|
||||||
"Afficher la barre de progression animée en haut de la session lorsque l'agent travaille",
|
|
||||||
"settings.general.row.wayland.title": "Utiliser Wayland natif",
|
"settings.general.row.wayland.title": "Utiliser Wayland natif",
|
||||||
"settings.general.row.wayland.description": "Désactiver le repli X11 sur Wayland. Nécessite un redémarrage.",
|
"settings.general.row.wayland.description": "Désactiver le repli X11 sur Wayland. Nécessite un redémarrage.",
|
||||||
"settings.general.row.wayland.tooltip":
|
"settings.general.row.wayland.tooltip":
|
||||||
@@ -741,6 +738,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.webfetch.description": "Récupérer le contenu d'une URL",
|
"settings.permissions.tool.webfetch.description": "Récupérer le contenu d'une URL",
|
||||||
"settings.permissions.tool.websearch.title": "Recherche Web",
|
"settings.permissions.tool.websearch.title": "Recherche Web",
|
||||||
"settings.permissions.tool.websearch.description": "Rechercher sur le web",
|
"settings.permissions.tool.websearch.description": "Rechercher sur le web",
|
||||||
|
"settings.permissions.tool.codesearch.title": "Recherche de code",
|
||||||
|
"settings.permissions.tool.codesearch.description": "Rechercher du code sur le web",
|
||||||
"settings.permissions.tool.external_directory.title": "Répertoire externe",
|
"settings.permissions.tool.external_directory.title": "Répertoire externe",
|
||||||
"settings.permissions.tool.external_directory.description": "Accéder aux fichiers en dehors du répertoire du projet",
|
"settings.permissions.tool.external_directory.description": "Accéder aux fichiers en dehors du répertoire du projet",
|
||||||
"settings.permissions.tool.doom_loop.title": "Boucle infernale",
|
"settings.permissions.tool.doom_loop.title": "Boucle infernale",
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ export const dict = {
|
|||||||
"common.saving": "保存中...",
|
"common.saving": "保存中...",
|
||||||
"common.default": "デフォルト",
|
"common.default": "デフォルト",
|
||||||
"common.attachment": "添付ファイル",
|
"common.attachment": "添付ファイル",
|
||||||
"prompt.placeholder.shell": "シェルコマンドを入力... {{example}}",
|
"prompt.placeholder.shell": "シェルコマンドを入力...",
|
||||||
"prompt.placeholder.normal": '何でも聞いてください... "{{example}}"',
|
"prompt.placeholder.normal": '何でも聞いてください... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "何でも聞いてください...",
|
"prompt.placeholder.simple": "何でも聞いてください...",
|
||||||
"prompt.placeholder.summarizeComments": "コメントを要約…",
|
"prompt.placeholder.summarizeComments": "コメントを要約…",
|
||||||
@@ -587,9 +587,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "edit ツールパーツを展開",
|
"settings.general.row.editToolPartsExpanded.title": "edit ツールパーツを展開",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"タイムラインで edit、write、patch ツールパーツをデフォルトで展開して表示します",
|
"タイムラインで edit、write、patch ツールパーツをデフォルトで展開して表示します",
|
||||||
"settings.general.row.showSessionProgressBar.title": "セッション進行状況バーを表示",
|
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
|
||||||
"エージェントの作業中に、セッション上部にアニメーション付きの進行状況バーを表示します",
|
|
||||||
"settings.general.row.wayland.title": "ネイティブWaylandを使用",
|
"settings.general.row.wayland.title": "ネイティブWaylandを使用",
|
||||||
"settings.general.row.wayland.description": "WaylandでのX11フォールバックを無効にします。再起動が必要です。",
|
"settings.general.row.wayland.description": "WaylandでのX11フォールバックを無効にします。再起動が必要です。",
|
||||||
"settings.general.row.wayland.tooltip":
|
"settings.general.row.wayland.tooltip":
|
||||||
@@ -727,6 +724,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.webfetch.description": "URLからコンテンツを取得",
|
"settings.permissions.tool.webfetch.description": "URLからコンテンツを取得",
|
||||||
"settings.permissions.tool.websearch.title": "Web検索",
|
"settings.permissions.tool.websearch.title": "Web検索",
|
||||||
"settings.permissions.tool.websearch.description": "ウェブを検索",
|
"settings.permissions.tool.websearch.description": "ウェブを検索",
|
||||||
|
"settings.permissions.tool.codesearch.title": "コード検索",
|
||||||
|
"settings.permissions.tool.codesearch.description": "ウェブ上のコードを検索",
|
||||||
"settings.permissions.tool.external_directory.title": "外部ディレクトリ",
|
"settings.permissions.tool.external_directory.title": "外部ディレクトリ",
|
||||||
"settings.permissions.tool.external_directory.description": "プロジェクトディレクトリ外のファイルへのアクセス",
|
"settings.permissions.tool.external_directory.description": "プロジェクトディレクトリ外のファイルへのアクセス",
|
||||||
"settings.permissions.tool.doom_loop.title": "無限ループ",
|
"settings.permissions.tool.doom_loop.title": "無限ループ",
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ export const dict = {
|
|||||||
"common.saving": "저장 중...",
|
"common.saving": "저장 중...",
|
||||||
"common.default": "기본값",
|
"common.default": "기본값",
|
||||||
"common.attachment": "첨부 파일",
|
"common.attachment": "첨부 파일",
|
||||||
"prompt.placeholder.shell": "셸 명령어 입력... {{example}}",
|
"prompt.placeholder.shell": "셸 명령어 입력...",
|
||||||
"prompt.placeholder.normal": '무엇이든 물어보세요... "{{example}}"',
|
"prompt.placeholder.normal": '무엇이든 물어보세요... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "무엇이든 물어보세요...",
|
"prompt.placeholder.simple": "무엇이든 물어보세요...",
|
||||||
"prompt.placeholder.summarizeComments": "댓글 요약…",
|
"prompt.placeholder.summarizeComments": "댓글 요약…",
|
||||||
@@ -583,9 +583,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "edit 도구 파트 펼치기",
|
"settings.general.row.editToolPartsExpanded.title": "edit 도구 파트 펼치기",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"타임라인에서 기본적으로 edit, write, patch 도구 파트를 펼친 상태로 표시합니다",
|
"타임라인에서 기본적으로 edit, write, patch 도구 파트를 펼친 상태로 표시합니다",
|
||||||
"settings.general.row.showSessionProgressBar.title": "세션 진행 표시줄 표시",
|
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
|
||||||
"에이전트가 작업 중일 때 세션 상단에 애니메이션 진행 표시줄을 표시합니다",
|
|
||||||
"settings.general.row.wayland.title": "네이티브 Wayland 사용",
|
"settings.general.row.wayland.title": "네이티브 Wayland 사용",
|
||||||
"settings.general.row.wayland.description": "Wayland에서 X11 폴백을 비활성화합니다. 다시 시작해야 합니다.",
|
"settings.general.row.wayland.description": "Wayland에서 X11 폴백을 비활성화합니다. 다시 시작해야 합니다.",
|
||||||
"settings.general.row.wayland.tooltip":
|
"settings.general.row.wayland.tooltip":
|
||||||
@@ -722,6 +719,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.webfetch.description": "URL에서 콘텐츠 가져오기",
|
"settings.permissions.tool.webfetch.description": "URL에서 콘텐츠 가져오기",
|
||||||
"settings.permissions.tool.websearch.title": "웹 검색",
|
"settings.permissions.tool.websearch.title": "웹 검색",
|
||||||
"settings.permissions.tool.websearch.description": "웹 검색",
|
"settings.permissions.tool.websearch.description": "웹 검색",
|
||||||
|
"settings.permissions.tool.codesearch.title": "코드 검색",
|
||||||
|
"settings.permissions.tool.codesearch.description": "웹에서 코드 검색",
|
||||||
"settings.permissions.tool.external_directory.title": "외부 디렉터리",
|
"settings.permissions.tool.external_directory.title": "외부 디렉터리",
|
||||||
"settings.permissions.tool.external_directory.description": "프로젝트 디렉터리 외부의 파일에 액세스",
|
"settings.permissions.tool.external_directory.description": "프로젝트 디렉터리 외부의 파일에 액세스",
|
||||||
"settings.permissions.tool.doom_loop.title": "무한 반복",
|
"settings.permissions.tool.doom_loop.title": "무한 반복",
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ export const dict = {
|
|||||||
"common.default": "Standard",
|
"common.default": "Standard",
|
||||||
"common.attachment": "vedlegg",
|
"common.attachment": "vedlegg",
|
||||||
|
|
||||||
"prompt.placeholder.shell": "Skriv inn shell-kommando... {{example}}",
|
"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.simple": "Spør om hva som helst...",
|
"prompt.placeholder.simple": "Spør om hva som helst...",
|
||||||
"prompt.placeholder.summarizeComments": "Oppsummer kommentarer…",
|
"prompt.placeholder.summarizeComments": "Oppsummer kommentarer…",
|
||||||
@@ -656,9 +656,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "Utvid edit-verktøydeler",
|
"settings.general.row.editToolPartsExpanded.title": "Utvid edit-verktøydeler",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"Vis edit-, write- og patch-verktøydeler utvidet som standard i tidslinjen",
|
"Vis edit-, write- og patch-verktøydeler utvidet som standard i tidslinjen",
|
||||||
"settings.general.row.showSessionProgressBar.title": "Vis fremdriftslinje for sesjonen",
|
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
|
||||||
"Vis den animerte fremdriftslinjen øverst i sesjonen når agenten jobber",
|
|
||||||
"settings.general.row.wayland.title": "Bruk innebygd Wayland",
|
"settings.general.row.wayland.title": "Bruk innebygd Wayland",
|
||||||
"settings.general.row.wayland.description": "Deaktiver X11-fallback på Wayland. Krever omstart.",
|
"settings.general.row.wayland.description": "Deaktiver X11-fallback på Wayland. Krever omstart.",
|
||||||
"settings.general.row.wayland.tooltip":
|
"settings.general.row.wayland.tooltip":
|
||||||
@@ -807,6 +804,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.webfetch.description": "Hent innhold fra en URL",
|
"settings.permissions.tool.webfetch.description": "Hent innhold fra en URL",
|
||||||
"settings.permissions.tool.websearch.title": "Websøk",
|
"settings.permissions.tool.websearch.title": "Websøk",
|
||||||
"settings.permissions.tool.websearch.description": "Søk på nettet",
|
"settings.permissions.tool.websearch.description": "Søk på nettet",
|
||||||
|
"settings.permissions.tool.codesearch.title": "Kodesøk",
|
||||||
|
"settings.permissions.tool.codesearch.description": "Søk etter kode på nettet",
|
||||||
"settings.permissions.tool.external_directory.title": "Ekstern mappe",
|
"settings.permissions.tool.external_directory.title": "Ekstern mappe",
|
||||||
"settings.permissions.tool.external_directory.description": "Få tilgang til filer utenfor prosjektmappen",
|
"settings.permissions.tool.external_directory.description": "Få tilgang til filer utenfor prosjektmappen",
|
||||||
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ export const dict = {
|
|||||||
"common.saving": "Zapisywanie...",
|
"common.saving": "Zapisywanie...",
|
||||||
"common.default": "Domyślny",
|
"common.default": "Domyślny",
|
||||||
"common.attachment": "załącznik",
|
"common.attachment": "załącznik",
|
||||||
"prompt.placeholder.shell": "Wpisz polecenie terminala... {{example}}",
|
"prompt.placeholder.shell": "Wpisz polecenie terminala...",
|
||||||
"prompt.placeholder.normal": 'Zapytaj o cokolwiek... "{{example}}"',
|
"prompt.placeholder.normal": 'Zapytaj o cokolwiek... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "Zapytaj o cokolwiek...",
|
"prompt.placeholder.simple": "Zapytaj o cokolwiek...",
|
||||||
"prompt.placeholder.summarizeComments": "Podsumuj komentarze…",
|
"prompt.placeholder.summarizeComments": "Podsumuj komentarze…",
|
||||||
@@ -588,9 +588,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "Rozwijaj elementy narzędzia edit",
|
"settings.general.row.editToolPartsExpanded.title": "Rozwijaj elementy narzędzia edit",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"Domyślnie pokazuj rozwinięte elementy narzędzi edit, write i patch na osi czasu",
|
"Domyślnie pokazuj rozwinięte elementy narzędzi edit, write i patch na osi czasu",
|
||||||
"settings.general.row.showSessionProgressBar.title": "Pokazuj pasek postępu sesji",
|
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
|
||||||
"Wyświetlaj animowany pasek postępu u góry sesji, gdy agent pracuje",
|
|
||||||
"settings.general.row.wayland.title": "Użyj natywnego Wayland",
|
"settings.general.row.wayland.title": "Użyj natywnego Wayland",
|
||||||
"settings.general.row.wayland.description": "Wyłącz fallback X11 na Wayland. Wymaga restartu.",
|
"settings.general.row.wayland.description": "Wyłącz fallback X11 na Wayland. Wymaga restartu.",
|
||||||
"settings.general.row.wayland.tooltip":
|
"settings.general.row.wayland.tooltip":
|
||||||
@@ -729,6 +726,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.webfetch.description": "Pobieranie zawartości z adresu URL",
|
"settings.permissions.tool.webfetch.description": "Pobieranie zawartości z adresu URL",
|
||||||
"settings.permissions.tool.websearch.title": "Wyszukiwanie w sieci",
|
"settings.permissions.tool.websearch.title": "Wyszukiwanie w sieci",
|
||||||
"settings.permissions.tool.websearch.description": "Przeszukiwanie sieci",
|
"settings.permissions.tool.websearch.description": "Przeszukiwanie sieci",
|
||||||
|
"settings.permissions.tool.codesearch.title": "Wyszukiwanie kodu",
|
||||||
|
"settings.permissions.tool.codesearch.description": "Przeszukiwanie kodu w sieci",
|
||||||
"settings.permissions.tool.external_directory.title": "Katalog zewnętrzny",
|
"settings.permissions.tool.external_directory.title": "Katalog zewnętrzny",
|
||||||
"settings.permissions.tool.external_directory.description": "Dostęp do plików poza katalogiem projektu",
|
"settings.permissions.tool.external_directory.description": "Dostęp do plików poza katalogiem projektu",
|
||||||
"settings.permissions.tool.doom_loop.title": "Zapętlenie",
|
"settings.permissions.tool.doom_loop.title": "Zapętlenie",
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ export const dict = {
|
|||||||
"common.default": "По умолчанию",
|
"common.default": "По умолчанию",
|
||||||
"common.attachment": "вложение",
|
"common.attachment": "вложение",
|
||||||
|
|
||||||
"prompt.placeholder.shell": "Введите команду оболочки... {{example}}",
|
"prompt.placeholder.shell": "Введите команду оболочки...",
|
||||||
"prompt.placeholder.normal": 'Спросите что угодно... "{{example}}"',
|
"prompt.placeholder.normal": 'Спросите что угодно... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "Спросите что угодно...",
|
"prompt.placeholder.simple": "Спросите что угодно...",
|
||||||
"prompt.placeholder.summarizeComments": "Суммировать комментарии…",
|
"prompt.placeholder.summarizeComments": "Суммировать комментарии…",
|
||||||
@@ -656,9 +656,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "Разворачивать элементы инструмента edit",
|
"settings.general.row.editToolPartsExpanded.title": "Разворачивать элементы инструмента edit",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"Показывать элементы инструментов edit, write и patch в ленте развернутыми по умолчанию",
|
"Показывать элементы инструментов edit, write и patch в ленте развернутыми по умолчанию",
|
||||||
"settings.general.row.showSessionProgressBar.title": "Показывать индикатор прогресса сессии",
|
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
|
||||||
"Показывать анимированный индикатор прогресса вверху сессии, когда агент работает",
|
|
||||||
"settings.general.row.wayland.title": "Использовать нативный Wayland",
|
"settings.general.row.wayland.title": "Использовать нативный Wayland",
|
||||||
"settings.general.row.wayland.description": "Отключить X11 fallback на Wayland. Требуется перезапуск.",
|
"settings.general.row.wayland.description": "Отключить X11 fallback на Wayland. Требуется перезапуск.",
|
||||||
"settings.general.row.wayland.tooltip":
|
"settings.general.row.wayland.tooltip":
|
||||||
@@ -808,6 +805,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.webfetch.description": "Получение контента по URL",
|
"settings.permissions.tool.webfetch.description": "Получение контента по URL",
|
||||||
"settings.permissions.tool.websearch.title": "Web Search",
|
"settings.permissions.tool.websearch.title": "Web Search",
|
||||||
"settings.permissions.tool.websearch.description": "Поиск в интернете",
|
"settings.permissions.tool.websearch.description": "Поиск в интернете",
|
||||||
|
"settings.permissions.tool.codesearch.title": "Code Search",
|
||||||
|
"settings.permissions.tool.codesearch.description": "Поиск кода в интернете",
|
||||||
"settings.permissions.tool.external_directory.title": "Внешняя директория",
|
"settings.permissions.tool.external_directory.title": "Внешняя директория",
|
||||||
"settings.permissions.tool.external_directory.description": "Доступ к файлам вне директории проекта",
|
"settings.permissions.tool.external_directory.description": "Доступ к файлам вне директории проекта",
|
||||||
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ export const dict = {
|
|||||||
"common.default": "ค่าเริ่มต้น",
|
"common.default": "ค่าเริ่มต้น",
|
||||||
"common.attachment": "ไฟล์แนบ",
|
"common.attachment": "ไฟล์แนบ",
|
||||||
|
|
||||||
"prompt.placeholder.shell": "ป้อนคำสั่งเชลล์... {{example}}",
|
"prompt.placeholder.shell": "ป้อนคำสั่งเชลล์...",
|
||||||
"prompt.placeholder.normal": 'ถามอะไรก็ได้... "{{example}}"',
|
"prompt.placeholder.normal": 'ถามอะไรก็ได้... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "ถามอะไรก็ได้...",
|
"prompt.placeholder.simple": "ถามอะไรก็ได้...",
|
||||||
"prompt.placeholder.summarizeComments": "สรุปความคิดเห็น…",
|
"prompt.placeholder.summarizeComments": "สรุปความคิดเห็น…",
|
||||||
@@ -647,9 +647,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "ขยายส่วนเครื่องมือ edit",
|
"settings.general.row.editToolPartsExpanded.title": "ขยายส่วนเครื่องมือ edit",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"แสดงส่วนเครื่องมือ edit, write และ patch แบบขยายตามค่าเริ่มต้นในไทม์ไลน์",
|
"แสดงส่วนเครื่องมือ edit, write และ patch แบบขยายตามค่าเริ่มต้นในไทม์ไลน์",
|
||||||
"settings.general.row.showSessionProgressBar.title": "แสดงแถบความคืบหน้าของเซสชัน",
|
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
|
||||||
"แสดงแถบความคืบหน้าแบบเคลื่อนไหวที่ด้านบนของเซสชันเมื่อเอเจนต์กำลังทำงาน",
|
|
||||||
"settings.general.row.wayland.title": "ใช้ Wayland แบบเนทีฟ",
|
"settings.general.row.wayland.title": "ใช้ Wayland แบบเนทีฟ",
|
||||||
"settings.general.row.wayland.description": "ปิดใช้งาน X11 fallback บน Wayland ต้องรีสตาร์ท",
|
"settings.general.row.wayland.description": "ปิดใช้งาน X11 fallback บน Wayland ต้องรีสตาร์ท",
|
||||||
"settings.general.row.wayland.tooltip": "บน Linux ที่มีจอภาพรีเฟรชเรตแบบผสม Wayland แบบเนทีฟอาจเสถียรกว่า",
|
"settings.general.row.wayland.tooltip": "บน Linux ที่มีจอภาพรีเฟรชเรตแบบผสม Wayland แบบเนทีฟอาจเสถียรกว่า",
|
||||||
@@ -796,6 +793,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.webfetch.description": "ดึงเนื้อหาจาก URL",
|
"settings.permissions.tool.webfetch.description": "ดึงเนื้อหาจาก URL",
|
||||||
"settings.permissions.tool.websearch.title": "ค้นหาเว็บ",
|
"settings.permissions.tool.websearch.title": "ค้นหาเว็บ",
|
||||||
"settings.permissions.tool.websearch.description": "ค้นหาบนเว็บ",
|
"settings.permissions.tool.websearch.description": "ค้นหาบนเว็บ",
|
||||||
|
"settings.permissions.tool.codesearch.title": "ค้นหาโค้ด",
|
||||||
|
"settings.permissions.tool.codesearch.description": "ค้นหาโค้ดบนเว็บ",
|
||||||
"settings.permissions.tool.external_directory.title": "ไดเรกทอรีภายนอก",
|
"settings.permissions.tool.external_directory.title": "ไดเรกทอรีภายนอก",
|
||||||
"settings.permissions.tool.external_directory.description": "เข้าถึงไฟล์นอกไดเรกทอรีโปรเจกต์",
|
"settings.permissions.tool.external_directory.description": "เข้าถึงไฟล์นอกไดเรกทอรีโปรเจกต์",
|
||||||
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ export const dict = {
|
|||||||
"common.default": "Varsayılan",
|
"common.default": "Varsayılan",
|
||||||
"common.attachment": "ek",
|
"common.attachment": "ek",
|
||||||
|
|
||||||
"prompt.placeholder.shell": "Kabuk komutu girin... {{example}}",
|
"prompt.placeholder.shell": "Kabuk komutu girin...",
|
||||||
"prompt.placeholder.normal": 'Bir şeyler sorun... "{{example}}"',
|
"prompt.placeholder.normal": 'Bir şeyler sorun... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "Bir şeyler sorun...",
|
"prompt.placeholder.simple": "Bir şeyler sorun...",
|
||||||
"prompt.placeholder.summarizeComments": "Yorumları özetle…",
|
"prompt.placeholder.summarizeComments": "Yorumları özetle…",
|
||||||
@@ -663,10 +663,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"Zaman çizelgesinde düzenleme, yazma ve yama araç bileşenlerini varsayılan olarak genişletilmiş göster",
|
"Zaman çizelgesinde düzenleme, yazma ve yama araç bileşenlerini varsayılan olarak genişletilmiş göster",
|
||||||
|
|
||||||
"settings.general.row.showSessionProgressBar.title": "Oturum ilerleme çubuğunu göster",
|
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
|
||||||
"Ajan çalışırken oturumun üst kısmında animasyonlu ilerleme çubuğunu göster",
|
|
||||||
|
|
||||||
"settings.general.row.wayland.title": "Yerel Wayland kullan",
|
"settings.general.row.wayland.title": "Yerel Wayland kullan",
|
||||||
"settings.general.row.wayland.description":
|
"settings.general.row.wayland.description":
|
||||||
"Wayland'da X11 geri dönüşünü devre dışı bırak. Yeniden başlatma gerektirir.",
|
"Wayland'da X11 geri dönüşünü devre dışı bırak. Yeniden başlatma gerektirir.",
|
||||||
@@ -816,6 +812,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.webfetch.description": "Bir URL'den içerik getir",
|
"settings.permissions.tool.webfetch.description": "Bir URL'den içerik getir",
|
||||||
"settings.permissions.tool.websearch.title": "Web Ara",
|
"settings.permissions.tool.websearch.title": "Web Ara",
|
||||||
"settings.permissions.tool.websearch.description": "Web'de ara",
|
"settings.permissions.tool.websearch.description": "Web'de ara",
|
||||||
|
"settings.permissions.tool.codesearch.title": "Kod Ara",
|
||||||
|
"settings.permissions.tool.codesearch.description": "Web'de kod ara",
|
||||||
"settings.permissions.tool.external_directory.title": "Harici Dizin",
|
"settings.permissions.tool.external_directory.title": "Harici Dizin",
|
||||||
"settings.permissions.tool.external_directory.description": "Proje dizini dışındaki dosyalara eriş",
|
"settings.permissions.tool.external_directory.description": "Proje dizini dışındaki dosyalara eriş",
|
||||||
"settings.permissions.tool.doom_loop.title": "Sonsuz Döngü",
|
"settings.permissions.tool.doom_loop.title": "Sonsuz Döngü",
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ export const dict = {
|
|||||||
"common.default": "默认",
|
"common.default": "默认",
|
||||||
"common.attachment": "附件",
|
"common.attachment": "附件",
|
||||||
|
|
||||||
"prompt.placeholder.shell": "输入 shell 命令... {{example}}",
|
"prompt.placeholder.shell": "输入 shell 命令...",
|
||||||
"prompt.placeholder.normal": '随便问点什么... "{{example}}"',
|
"prompt.placeholder.normal": '随便问点什么... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "随便问点什么...",
|
"prompt.placeholder.simple": "随便问点什么...",
|
||||||
"prompt.placeholder.summarizeComments": "总结评论…",
|
"prompt.placeholder.summarizeComments": "总结评论…",
|
||||||
@@ -646,8 +646,6 @@ export const dict = {
|
|||||||
"settings.general.row.shellToolPartsExpanded.description": "默认在时间线中展开 shell 工具部分",
|
"settings.general.row.shellToolPartsExpanded.description": "默认在时间线中展开 shell 工具部分",
|
||||||
"settings.general.row.editToolPartsExpanded.title": "展开编辑工具部分",
|
"settings.general.row.editToolPartsExpanded.title": "展开编辑工具部分",
|
||||||
"settings.general.row.editToolPartsExpanded.description": "默认在时间线中展开 edit、write 和 patch 工具部分",
|
"settings.general.row.editToolPartsExpanded.description": "默认在时间线中展开 edit、write 和 patch 工具部分",
|
||||||
"settings.general.row.showSessionProgressBar.title": "显示会话进度条",
|
|
||||||
"settings.general.row.showSessionProgressBar.description": "当智能体正在工作时,在会话顶部显示动画进度条",
|
|
||||||
"settings.general.row.wayland.title": "使用原生 Wayland",
|
"settings.general.row.wayland.title": "使用原生 Wayland",
|
||||||
"settings.general.row.wayland.description": "在 Wayland 上禁用 X11 回退。需要重启。",
|
"settings.general.row.wayland.description": "在 Wayland 上禁用 X11 回退。需要重启。",
|
||||||
"settings.general.row.wayland.tooltip": "在混合刷新率显示器的 Linux 系统上,原生 Wayland 可能更稳定。",
|
"settings.general.row.wayland.tooltip": "在混合刷新率显示器的 Linux 系统上,原生 Wayland 可能更稳定。",
|
||||||
@@ -793,6 +791,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.webfetch.description": "从 URL 获取内容",
|
"settings.permissions.tool.webfetch.description": "从 URL 获取内容",
|
||||||
"settings.permissions.tool.websearch.title": "网页搜索",
|
"settings.permissions.tool.websearch.title": "网页搜索",
|
||||||
"settings.permissions.tool.websearch.description": "搜索网页",
|
"settings.permissions.tool.websearch.description": "搜索网页",
|
||||||
|
"settings.permissions.tool.codesearch.title": "代码搜索",
|
||||||
|
"settings.permissions.tool.codesearch.description": "在网上搜索代码",
|
||||||
"settings.permissions.tool.external_directory.title": "外部目录",
|
"settings.permissions.tool.external_directory.title": "外部目录",
|
||||||
"settings.permissions.tool.external_directory.description": "访问项目目录之外的文件",
|
"settings.permissions.tool.external_directory.description": "访问项目目录之外的文件",
|
||||||
"settings.permissions.tool.doom_loop.title": "死循环",
|
"settings.permissions.tool.doom_loop.title": "死循环",
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ export const dict = {
|
|||||||
"common.default": "預設",
|
"common.default": "預設",
|
||||||
"common.attachment": "附件",
|
"common.attachment": "附件",
|
||||||
|
|
||||||
"prompt.placeholder.shell": "輸入 shell 命令... {{example}}",
|
"prompt.placeholder.shell": "輸入 shell 命令...",
|
||||||
"prompt.placeholder.normal": '隨便問點什麼... "{{example}}"',
|
"prompt.placeholder.normal": '隨便問點什麼... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "隨便問點什麼...",
|
"prompt.placeholder.simple": "隨便問點什麼...",
|
||||||
"prompt.placeholder.summarizeComments": "摘要評論…",
|
"prompt.placeholder.summarizeComments": "摘要評論…",
|
||||||
@@ -642,8 +642,6 @@ export const dict = {
|
|||||||
"settings.general.row.shellToolPartsExpanded.description": "在時間軸中預設展開 shell 工具區塊",
|
"settings.general.row.shellToolPartsExpanded.description": "在時間軸中預設展開 shell 工具區塊",
|
||||||
"settings.general.row.editToolPartsExpanded.title": "展開 edit 工具區塊",
|
"settings.general.row.editToolPartsExpanded.title": "展開 edit 工具區塊",
|
||||||
"settings.general.row.editToolPartsExpanded.description": "在時間軸中預設展開 edit、write 和 patch 工具區塊",
|
"settings.general.row.editToolPartsExpanded.description": "在時間軸中預設展開 edit、write 和 patch 工具區塊",
|
||||||
"settings.general.row.showSessionProgressBar.title": "顯示工作階段進度列",
|
|
||||||
"settings.general.row.showSessionProgressBar.description": "當代理程式正在運作時,在工作階段頂部顯示動畫進度列",
|
|
||||||
"settings.general.row.wayland.title": "使用原生 Wayland",
|
"settings.general.row.wayland.title": "使用原生 Wayland",
|
||||||
"settings.general.row.wayland.description": "在 Wayland 上停用 X11 後備模式。需要重新啟動。",
|
"settings.general.row.wayland.description": "在 Wayland 上停用 X11 後備模式。需要重新啟動。",
|
||||||
"settings.general.row.wayland.tooltip": "在混合更新率螢幕的 Linux 系統上,原生 Wayland 可能更穩定。",
|
"settings.general.row.wayland.tooltip": "在混合更新率螢幕的 Linux 系統上,原生 Wayland 可能更穩定。",
|
||||||
@@ -789,6 +787,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.webfetch.description": "從 URL 取得內容",
|
"settings.permissions.tool.webfetch.description": "從 URL 取得內容",
|
||||||
"settings.permissions.tool.websearch.title": "Web Search",
|
"settings.permissions.tool.websearch.title": "Web Search",
|
||||||
"settings.permissions.tool.websearch.description": "搜尋網頁",
|
"settings.permissions.tool.websearch.description": "搜尋網頁",
|
||||||
|
"settings.permissions.tool.codesearch.title": "Code Search",
|
||||||
|
"settings.permissions.tool.codesearch.description": "在網路上搜尋程式碼",
|
||||||
"settings.permissions.tool.external_directory.title": "外部目錄",
|
"settings.permissions.tool.external_directory.title": "外部目錄",
|
||||||
"settings.permissions.tool.external_directory.description": "存取專案目錄之外的檔案",
|
"settings.permissions.tool.external_directory.description": "存取專案目錄之外的檔案",
|
||||||
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
||||||
|
|||||||
@@ -73,13 +73,4 @@
|
|||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fade-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { DataProvider } from "@opencode-ai/ui/context"
|
import { DataProvider } from "@opencode-ai/ui/context"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
||||||
import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
||||||
import { createEffect, createMemo, createResource, type ParentProps, Show } from "solid-js"
|
import { createEffect, createMemo, type ParentProps, Show } from "solid-js"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { LocalProvider } from "@/context/local"
|
import { LocalProvider } from "@/context/local"
|
||||||
import { SDKProvider } from "@/context/sdk"
|
import { SDKProvider } from "@/context/sdk"
|
||||||
@@ -23,10 +23,11 @@ function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
|
|||||||
navigate(`/${base64Encode(next)}${path}${location.search}${location.hash}`, { replace: true })
|
navigate(`/${base64Encode(next)}${path}${location.search}${location.hash}`, { replace: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
createResource(
|
createEffect(() => {
|
||||||
() => params.id,
|
const id = params.id
|
||||||
(id) => sync.session.sync(id),
|
if (!id) return
|
||||||
)
|
void sync.session.sync(id)
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataProvider
|
<DataProvider
|
||||||
|
|||||||
@@ -244,9 +244,10 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function installUpdate() {
|
async function installUpdate() {
|
||||||
if (!platform.updateAndRestart) return
|
if (!platform.update || !platform.restart) return
|
||||||
await platform
|
await platform
|
||||||
.updateAndRestart()
|
.update()
|
||||||
|
.then(() => platform.restart!())
|
||||||
.then(() => setStore("actionError", undefined))
|
.then(() => setStore("actionError", undefined))
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
setStore("actionError", formatError(err, language.t))
|
setStore("actionError", formatError(err, language.t))
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Button } from "@opencode-ai/ui/button"
|
|||||||
import { Logo } from "@opencode-ai/ui/logo"
|
import { Logo } from "@opencode-ai/ui/logo"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { DateTime } from "luxon"
|
import { DateTime } from "luxon"
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
|||||||
import { useLayout, LocalProject } from "@/context/layout"
|
import { useLayout, LocalProject } from "@/context/layout"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
@@ -25,7 +25,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
|
|||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
import { getFilename } from "@opencode-ai/shared/util/path"
|
||||||
import { Session, type Message } from "@opencode-ai/sdk/v2/client"
|
import { Session, type Message } from "@opencode-ai/sdk/v2/client"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
@@ -48,8 +48,8 @@ import {
|
|||||||
} from "@/context/global-sync/session-prefetch"
|
} from "@/context/global-sync/session-prefetch"
|
||||||
import { useNotification } from "@/context/notification"
|
import { useNotification } from "@/context/notification"
|
||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
import { Binary } from "@opencode-ai/core/util/binary"
|
import { Binary } from "@opencode-ai/shared/util/binary"
|
||||||
import { retry } from "@opencode-ai/core/util/retry"
|
import { retry } from "@opencode-ai/shared/util/retry"
|
||||||
import { playSoundById } from "@/utils/sound"
|
import { playSoundById } from "@/utils/sound"
|
||||||
import { createAim } from "@/utils/aim"
|
import { createAim } from "@/utils/aim"
|
||||||
import { setNavigate } from "@/utils/notification-click"
|
import { setNavigate } from "@/utils/notification-click"
|
||||||
@@ -64,13 +64,13 @@ import { DebugBar } from "@/components/debug-bar"
|
|||||||
import { Titlebar } from "@/components/titlebar"
|
import { Titlebar } from "@/components/titlebar"
|
||||||
import { useServer } from "@/context/server"
|
import { useServer } from "@/context/server"
|
||||||
import { useLanguage, type Locale } from "@/context/language"
|
import { useLanguage, type Locale } from "@/context/language"
|
||||||
import { pathKey } from "@/utils/path-key"
|
|
||||||
import {
|
import {
|
||||||
displayName,
|
displayName,
|
||||||
effectiveWorkspaceOrder,
|
effectiveWorkspaceOrder,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
latestRootSession,
|
latestRootSession,
|
||||||
sortedRootSessions,
|
sortedRootSessions,
|
||||||
|
workspaceKey,
|
||||||
} from "./layout/helpers"
|
} from "./layout/helpers"
|
||||||
import {
|
import {
|
||||||
collectNewSessionDeepLinks,
|
collectNewSessionDeepLinks,
|
||||||
@@ -164,7 +164,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
const editor = createInlineEditorController()
|
const editor = createInlineEditorController()
|
||||||
const setBusy = (directory: string, value: boolean) => {
|
const setBusy = (directory: string, value: boolean) => {
|
||||||
const key = pathKey(directory)
|
const key = workspaceKey(directory)
|
||||||
if (value) {
|
if (value) {
|
||||||
setState("busyWorkspaces", key, true)
|
setState("busyWorkspaces", key, true)
|
||||||
return
|
return
|
||||||
@@ -176,7 +176,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const isBusy = (directory: string) => !!state.busyWorkspaces[pathKey(directory)]
|
const isBusy = (directory: string) => !!state.busyWorkspaces[workspaceKey(directory)]
|
||||||
const navLeave = { current: undefined as number | undefined }
|
const navLeave = { current: undefined as number | undefined }
|
||||||
const sortNow = () => state.sortNow
|
const sortNow = () => state.sortNow
|
||||||
let sizet: number | undefined
|
let sizet: number | undefined
|
||||||
@@ -366,7 +366,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
const useUpdatePolling = () =>
|
const useUpdatePolling = () =>
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!platform.checkUpdate || !platform.updateAndRestart) return
|
if (!platform.checkUpdate || !platform.update || !platform.restart) return
|
||||||
|
|
||||||
let toastId: number | undefined
|
let toastId: number | undefined
|
||||||
let interval: ReturnType<typeof setInterval> | undefined
|
let interval: ReturnType<typeof setInterval> | undefined
|
||||||
@@ -384,7 +384,8 @@ export default function Layout(props: ParentProps) {
|
|||||||
{
|
{
|
||||||
label: language.t("toast.update.action.installRestart"),
|
label: language.t("toast.update.action.installRestart"),
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
await platform.updateAndRestart!()
|
await platform.update!()
|
||||||
|
await platform.restart!()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -497,8 +498,8 @@ export default function Layout(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentSession = params.id
|
const currentSession = params.id
|
||||||
if (pathKey(directory) === pathKey(currentDir()) && props.sessionID === currentSession) return
|
if (workspaceKey(directory) === workspaceKey(currentDir()) && props.sessionID === currentSession) return
|
||||||
if (pathKey(directory) === pathKey(currentDir()) && session?.parentID === currentSession) return
|
if (workspaceKey(directory) === workspaceKey(currentDir()) && session?.parentID === currentSession) return
|
||||||
|
|
||||||
dismissSessionAlert(sessionKey)
|
dismissSessionAlert(sessionKey)
|
||||||
|
|
||||||
@@ -556,14 +557,14 @@ export default function Layout(props: ParentProps) {
|
|||||||
const currentProject = createMemo(() => {
|
const currentProject = createMemo(() => {
|
||||||
const directory = currentDir()
|
const directory = currentDir()
|
||||||
if (!directory) return
|
if (!directory) return
|
||||||
const key = pathKey(directory)
|
const key = workspaceKey(directory)
|
||||||
|
|
||||||
const projects = layout.projects.list()
|
const projects = layout.projects.list()
|
||||||
|
|
||||||
const sandbox = projects.find((p) => p.sandboxes?.some((item) => pathKey(item) === key))
|
const sandbox = projects.find((p) => p.sandboxes?.some((item) => workspaceKey(item) === key))
|
||||||
if (sandbox) return sandbox
|
if (sandbox) return sandbox
|
||||||
|
|
||||||
const direct = projects.find((p) => pathKey(p.worktree) === key)
|
const direct = projects.find((p) => workspaceKey(p.worktree) === key)
|
||||||
if (direct) return direct
|
if (direct) return direct
|
||||||
|
|
||||||
const [child] = globalSync.child(directory, { bootstrap: false })
|
const [child] = globalSync.child(directory, { bootstrap: false })
|
||||||
@@ -596,7 +597,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const workspaceName = (directory: string, projectId?: string, branch?: string) => {
|
const workspaceName = (directory: string, projectId?: string, branch?: string) => {
|
||||||
const key = pathKey(directory)
|
const key = workspaceKey(directory)
|
||||||
const direct = store.workspaceName[key] ?? store.workspaceName[directory]
|
const direct = store.workspaceName[key] ?? store.workspaceName[directory]
|
||||||
if (direct) return direct
|
if (direct) return direct
|
||||||
if (!projectId) return
|
if (!projectId) return
|
||||||
@@ -605,7 +606,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const setWorkspaceName = (directory: string, next: string, projectId?: string, branch?: string) => {
|
const setWorkspaceName = (directory: string, next: string, projectId?: string, branch?: string) => {
|
||||||
const key = pathKey(directory)
|
const key = workspaceKey(directory)
|
||||||
setStore("workspaceName", key, next)
|
setStore("workspaceName", key, next)
|
||||||
if (!projectId) return
|
if (!projectId) return
|
||||||
if (!branch) return
|
if (!branch) return
|
||||||
@@ -633,7 +634,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
const activeDir = currentDir()
|
const activeDir = currentDir()
|
||||||
return workspaceIds(project).filter((directory) => {
|
return workspaceIds(project).filter((directory) => {
|
||||||
const expanded = store.workspaceExpanded[directory] ?? directory === project.worktree
|
const expanded = store.workspaceExpanded[directory] ?? directory === project.worktree
|
||||||
const active = pathKey(directory) === pathKey(activeDir)
|
const active = workspaceKey(directory) === workspaceKey(activeDir)
|
||||||
return expanded || active
|
return expanded || active
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -644,9 +645,10 @@ export default function Layout(props: ParentProps) {
|
|||||||
const projects = layout.projects.list()
|
const projects = layout.projects.list()
|
||||||
for (const [directory, expanded] of Object.entries(store.workspaceExpanded)) {
|
for (const [directory, expanded] of Object.entries(store.workspaceExpanded)) {
|
||||||
if (!expanded) continue
|
if (!expanded) continue
|
||||||
const key = pathKey(directory)
|
const key = workspaceKey(directory)
|
||||||
const project = projects.find(
|
const project = projects.find(
|
||||||
(item) => pathKey(item.worktree) === key || item.sandboxes?.some((sandbox) => pathKey(sandbox) === key),
|
(item) =>
|
||||||
|
workspaceKey(item.worktree) === key || item.sandboxes?.some((sandbox) => workspaceKey(sandbox) === key),
|
||||||
)
|
)
|
||||||
if (!project) continue
|
if (!project) continue
|
||||||
if (project.vcs === "git" && layout.sidebar.workspaces(project.worktree)()) continue
|
if (project.vcs === "git" && layout.sidebar.workspaces(project.worktree)()) continue
|
||||||
@@ -699,7 +701,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
seen: lru,
|
seen: lru,
|
||||||
keep: sessionID,
|
keep: sessionID,
|
||||||
limit: PREFETCH_MAX_SESSIONS_PER_DIR,
|
limit: PREFETCH_MAX_SESSIONS_PER_DIR,
|
||||||
preserve: params.id && pathKey(directory) === pathKey(currentDir()) ? [params.id] : undefined,
|
preserve: params.id && workspaceKey(directory) === workspaceKey(currentDir()) ? [params.id] : undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1059,6 +1061,13 @@ export default function Layout(props: ParentProps) {
|
|||||||
keybind: "mod+comma",
|
keybind: "mod+comma",
|
||||||
onSelect: () => openSettings(),
|
onSelect: () => openSettings(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "pair.show",
|
||||||
|
title: language.t("command.pair.show"),
|
||||||
|
category: language.t("command.category.settings"),
|
||||||
|
slash: "pair",
|
||||||
|
onSelect: () => openSettings("pair"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "session.previous",
|
id: "session.previous",
|
||||||
title: language.t("command.session.previous"),
|
title: language.t("command.session.previous"),
|
||||||
@@ -1211,23 +1220,26 @@ export default function Layout(props: ParentProps) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function openSettings() {
|
function openSettings(defaultTab?: string) {
|
||||||
const run = ++dialogRun
|
const run = ++dialogRun
|
||||||
void import("@/components/dialog-settings").then((x) => {
|
void import("@/components/dialog-settings").then((x) => {
|
||||||
if (dialogDead || dialogRun !== run) return
|
if (dialogDead || dialogRun !== run) return
|
||||||
dialog.show(() => <x.DialogSettings />)
|
dialog.show(() => <x.DialogSettings defaultTab={defaultTab} />)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function projectRoot(directory: string) {
|
function projectRoot(directory: string) {
|
||||||
const key = pathKey(directory)
|
const key = workspaceKey(directory)
|
||||||
const project = layout.projects
|
const project = layout.projects
|
||||||
.list()
|
.list()
|
||||||
.find((item) => pathKey(item.worktree) === key || item.sandboxes?.some((sandbox) => pathKey(sandbox) === key))
|
.find(
|
||||||
|
(item) =>
|
||||||
|
workspaceKey(item.worktree) === key || item.sandboxes?.some((sandbox) => workspaceKey(sandbox) === key),
|
||||||
|
)
|
||||||
if (project) return project.worktree
|
if (project) return project.worktree
|
||||||
|
|
||||||
const known = Object.entries(store.workspaceOrder).find(
|
const known = Object.entries(store.workspaceOrder).find(
|
||||||
([root, dirs]) => pathKey(root) === key || dirs.some((item) => pathKey(item) === key),
|
([root, dirs]) => workspaceKey(root) === key || dirs.some((item) => workspaceKey(item) === key),
|
||||||
)
|
)
|
||||||
if (known) return known[0]
|
if (known) return known[0]
|
||||||
|
|
||||||
@@ -1279,7 +1291,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
: [root]
|
: [root]
|
||||||
const canOpen = (value: string | undefined) => {
|
const canOpen = (value: string | undefined) => {
|
||||||
if (!value) return false
|
if (!value) return false
|
||||||
return dirs.some((item) => pathKey(item) === pathKey(value))
|
return dirs.some((item) => workspaceKey(item) === workspaceKey(value))
|
||||||
}
|
}
|
||||||
const refreshDirs = async (target?: string) => {
|
const refreshDirs = async (target?: string) => {
|
||||||
if (!target || target === root || canOpen(target)) return canOpen(target)
|
if (!target || target === root || canOpen(target)) return canOpen(target)
|
||||||
@@ -1405,9 +1417,9 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
function closeProject(directory: string) {
|
function closeProject(directory: string) {
|
||||||
const list = layout.projects.list()
|
const list = layout.projects.list()
|
||||||
const key = pathKey(directory)
|
const key = workspaceKey(directory)
|
||||||
const index = list.findIndex((x) => pathKey(x.worktree) === key)
|
const index = list.findIndex((x) => workspaceKey(x.worktree) === key)
|
||||||
const active = pathKey(currentProject()?.worktree ?? "") === key
|
const active = workspaceKey(currentProject()?.worktree ?? "") === key
|
||||||
if (index === -1) return
|
if (index === -1) return
|
||||||
const next = list[index + 1]
|
const next = list[index + 1]
|
||||||
|
|
||||||
@@ -1481,8 +1493,8 @@ export default function Layout(props: ParentProps) {
|
|||||||
if (directory === root) return
|
if (directory === root) return
|
||||||
|
|
||||||
const current = currentDir()
|
const current = currentDir()
|
||||||
const currentKey = pathKey(current)
|
const currentKey = workspaceKey(current)
|
||||||
const deletedKey = pathKey(directory)
|
const deletedKey = workspaceKey(directory)
|
||||||
const shouldLeave = leaveDeletedWorkspace || (!!params.dir && currentKey === deletedKey)
|
const shouldLeave = leaveDeletedWorkspace || (!!params.dir && currentKey === deletedKey)
|
||||||
if (!leaveDeletedWorkspace && shouldLeave) {
|
if (!leaveDeletedWorkspace && shouldLeave) {
|
||||||
navigateWithSidebarReset(`/${base64Encode(root)}/session`)
|
navigateWithSidebarReset(`/${base64Encode(root)}/session`)
|
||||||
@@ -1505,7 +1517,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
if (!result) return
|
if (!result) return
|
||||||
|
|
||||||
if (pathKey(store.lastProjectSession[root]?.directory ?? "") === pathKey(directory)) {
|
if (workspaceKey(store.lastProjectSession[root]?.directory ?? "") === workspaceKey(directory)) {
|
||||||
clearLastProjectSession(root)
|
clearLastProjectSession(root)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1525,12 +1537,12 @@ export default function Layout(props: ParentProps) {
|
|||||||
if (shouldLeave) return
|
if (shouldLeave) return
|
||||||
|
|
||||||
const nextCurrent = currentDir()
|
const nextCurrent = currentDir()
|
||||||
const nextKey = pathKey(nextCurrent)
|
const nextKey = workspaceKey(nextCurrent)
|
||||||
const project = layout.projects.list().find((item) => item.worktree === root)
|
const project = layout.projects.list().find((item) => item.worktree === root)
|
||||||
const dirs = project
|
const dirs = project
|
||||||
? effectiveWorkspaceOrder(root, [root, ...(project.sandboxes ?? [])], store.workspaceOrder[root])
|
? effectiveWorkspaceOrder(root, [root, ...(project.sandboxes ?? [])], store.workspaceOrder[root])
|
||||||
: [root]
|
: [root]
|
||||||
const valid = dirs.some((item) => pathKey(item) === nextKey)
|
const valid = dirs.some((item) => workspaceKey(item) === nextKey)
|
||||||
|
|
||||||
if (params.dir && projectRoot(nextCurrent) === root && !valid) {
|
if (params.dir && projectRoot(nextCurrent) === root && !valid) {
|
||||||
navigateWithSidebarReset(`/${base64Encode(root)}/session`)
|
navigateWithSidebarReset(`/${base64Encode(root)}/session`)
|
||||||
@@ -1636,7 +1648,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
const leaveDeletedWorkspace = !!params.dir && pathKey(currentDir()) === pathKey(props.directory)
|
const leaveDeletedWorkspace = !!params.dir && workspaceKey(currentDir()) === workspaceKey(props.directory)
|
||||||
if (leaveDeletedWorkspace) {
|
if (leaveDeletedWorkspace) {
|
||||||
navigateWithSidebarReset(`/${base64Encode(props.root)}/session`)
|
navigateWithSidebarReset(`/${base64Encode(props.root)}/session`)
|
||||||
}
|
}
|
||||||
@@ -1863,9 +1875,11 @@ export default function Layout(props: ParentProps) {
|
|||||||
const local = project.worktree
|
const local = project.worktree
|
||||||
const dirs = [local, ...(project.sandboxes ?? [])]
|
const dirs = [local, ...(project.sandboxes ?? [])]
|
||||||
const active = currentProject()
|
const active = currentProject()
|
||||||
const directory = pathKey(active?.worktree ?? "") === pathKey(project.worktree) ? currentDir() : undefined
|
const directory = workspaceKey(active?.worktree ?? "") === workspaceKey(project.worktree) ? currentDir() : undefined
|
||||||
const extra =
|
const extra =
|
||||||
directory && pathKey(directory) !== pathKey(local) && !dirs.some((item) => pathKey(item) === pathKey(directory))
|
directory &&
|
||||||
|
workspaceKey(directory) !== workspaceKey(local) &&
|
||||||
|
!dirs.some((item) => workspaceKey(item) === workspaceKey(directory))
|
||||||
? directory
|
? directory
|
||||||
: undefined
|
: undefined
|
||||||
const pending = extra ? WorktreeState.get(extra)?.status === "pending" : false
|
const pending = extra ? WorktreeState.get(extra)?.status === "pending" : false
|
||||||
@@ -1910,7 +1924,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
setStore(
|
setStore(
|
||||||
"workspaceOrder",
|
"workspaceOrder",
|
||||||
project.worktree,
|
project.worktree,
|
||||||
result.filter((directory) => pathKey(directory) !== pathKey(project.worktree)),
|
result.filter((directory) => workspaceKey(directory) !== workspaceKey(project.worktree)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1936,8 +1950,8 @@ export default function Layout(props: ParentProps) {
|
|||||||
setWorkspaceName(created.directory, created.branch, project.id, created.branch)
|
setWorkspaceName(created.directory, created.branch, project.id, created.branch)
|
||||||
|
|
||||||
const local = project.worktree
|
const local = project.worktree
|
||||||
const key = pathKey(created.directory)
|
const key = workspaceKey(created.directory)
|
||||||
const root = pathKey(local)
|
const root = workspaceKey(local)
|
||||||
|
|
||||||
setBusy(created.directory, true)
|
setBusy(created.directory, true)
|
||||||
WorktreeState.pending(created.directory)
|
WorktreeState.pending(created.directory)
|
||||||
@@ -1948,7 +1962,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
setStore("workspaceOrder", project.worktree, (prev) => {
|
setStore("workspaceOrder", project.worktree, (prev) => {
|
||||||
const existing = prev ?? []
|
const existing = prev ?? []
|
||||||
const next = existing.filter((item) => {
|
const next = existing.filter((item) => {
|
||||||
const id = pathKey(item)
|
const id = workspaceKey(item)
|
||||||
return id !== root && id !== key
|
return id !== root && id !== key
|
||||||
})
|
})
|
||||||
return [created.directory, ...next]
|
return [created.directory, ...next]
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import {
|
|||||||
errorMessage,
|
errorMessage,
|
||||||
hasProjectPermissions,
|
hasProjectPermissions,
|
||||||
latestRootSession,
|
latestRootSession,
|
||||||
|
workspaceKey,
|
||||||
} from "./helpers"
|
} from "./helpers"
|
||||||
import { pathKey } from "@/utils/path-key"
|
|
||||||
|
|
||||||
const session = (input: Partial<Session> & Pick<Session, "id" | "directory">) =>
|
const session = (input: Partial<Session> & Pick<Session, "id" | "directory">) =>
|
||||||
({
|
({
|
||||||
@@ -104,16 +104,16 @@ describe("layout deep links", () => {
|
|||||||
|
|
||||||
describe("layout workspace helpers", () => {
|
describe("layout workspace helpers", () => {
|
||||||
test("normalizes trailing slash in workspace key", () => {
|
test("normalizes trailing slash in workspace key", () => {
|
||||||
expect(String(pathKey("/tmp/demo///"))).toBe("/tmp/demo")
|
expect(workspaceKey("/tmp/demo///")).toBe("/tmp/demo")
|
||||||
expect(String(pathKey("C:\\tmp\\demo\\\\"))).toBe("C:/tmp/demo")
|
expect(workspaceKey("C:\\tmp\\demo\\\\")).toBe("C:/tmp/demo")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("preserves posix and drive roots in workspace key", () => {
|
test("preserves posix and drive roots in workspace key", () => {
|
||||||
expect(String(pathKey("/"))).toBe("/")
|
expect(workspaceKey("/")).toBe("/")
|
||||||
expect(String(pathKey("///"))).toBe("/")
|
expect(workspaceKey("///")).toBe("/")
|
||||||
expect(String(pathKey("C:\\"))).toBe("C:/")
|
expect(workspaceKey("C:\\")).toBe("C:/")
|
||||||
expect(String(pathKey("C://"))).toBe("C:/")
|
expect(workspaceKey("C://")).toBe("C:/")
|
||||||
expect(String(pathKey("C:///"))).toBe("C:/")
|
expect(workspaceKey("C:///")).toBe("C:/")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("keeps local first while preserving known order", () => {
|
test("keeps local first while preserving known order", () => {
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
import { getFilename } from "@opencode-ai/shared/util/path"
|
||||||
import { type Session } from "@opencode-ai/sdk/v2/client"
|
import { type Session } from "@opencode-ai/sdk/v2/client"
|
||||||
import { pathKey } from "@/utils/path-key"
|
|
||||||
|
|
||||||
type SessionStore = {
|
type SessionStore = {
|
||||||
session?: Session[]
|
session?: Session[]
|
||||||
path: { directory: string }
|
path: { directory: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const workspaceKey = (directory: string) => {
|
||||||
|
const value = directory.replaceAll("\\", "/")
|
||||||
|
const drive = value.match(/^([A-Za-z]:)\/+$/)
|
||||||
|
if (drive) return `${drive[1]}/`
|
||||||
|
if (/^\/+$/i.test(value)) return "/"
|
||||||
|
return value.replace(/\/+$/, "")
|
||||||
|
}
|
||||||
|
|
||||||
function sortSessions(now: number) {
|
function sortSessions(now: number) {
|
||||||
const oneMinuteAgo = now - 60 * 1000
|
const oneMinuteAgo = now - 60 * 1000
|
||||||
return (a: Session, b: Session) => {
|
return (a: Session, b: Session) => {
|
||||||
@@ -22,9 +29,9 @@ function sortSessions(now: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isRootVisibleSession = (session: Session, directory: string) =>
|
const isRootVisibleSession = (session: Session, directory: string) =>
|
||||||
pathKey(session.directory) === pathKey(directory) && !session.parentID && !session.time?.archived
|
workspaceKey(session.directory) === workspaceKey(directory) && !session.parentID && !session.time?.archived
|
||||||
|
|
||||||
export const roots = (store: SessionStore) =>
|
const roots = (store: SessionStore) =>
|
||||||
(store.session ?? []).filter((session) => isRootVisibleSession(session, store.path.directory))
|
(store.session ?? []).filter((session) => isRootVisibleSession(session, store.path.directory))
|
||||||
|
|
||||||
export const sortedRootSessions = (store: SessionStore, now: number) => roots(store).sort(sortSessions(now))
|
export const sortedRootSessions = (store: SessionStore, now: number) => roots(store).sort(sortSessions(now))
|
||||||
@@ -65,11 +72,11 @@ export const errorMessage = (err: unknown, fallback: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const effectiveWorkspaceOrder = (local: string, dirs: string[], persisted?: string[]) => {
|
export const effectiveWorkspaceOrder = (local: string, dirs: string[], persisted?: string[]) => {
|
||||||
const root = pathKey(local)
|
const root = workspaceKey(local)
|
||||||
const live = new Map<string, string>()
|
const live = new Map<string, string>()
|
||||||
|
|
||||||
for (const dir of dirs) {
|
for (const dir of dirs) {
|
||||||
const key = pathKey(dir)
|
const key = workspaceKey(dir)
|
||||||
if (key === root) continue
|
if (key === root) continue
|
||||||
if (!live.has(key)) live.set(key, dir)
|
if (!live.has(key)) live.set(key, dir)
|
||||||
}
|
}
|
||||||
@@ -78,7 +85,7 @@ export const effectiveWorkspaceOrder = (local: string, dirs: string[], persisted
|
|||||||
|
|
||||||
const result = [local]
|
const result = [local]
|
||||||
for (const dir of persisted) {
|
for (const dir of persisted) {
|
||||||
const key = pathKey(dir)
|
const key = workspaceKey(dir)
|
||||||
if (key === root) continue
|
if (key === root) continue
|
||||||
const match = live.get(key)
|
const match = live.get(key)
|
||||||
if (!match) continue
|
if (!match) continue
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user