Compare commits
377
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b3d027e3b | ||
|
|
ce209e22a2 | ||
|
|
33b2795cc8 | ||
|
|
10bd044c55 | ||
|
|
c09bcfe531 | ||
|
|
83227be0ca | ||
|
|
8ee47a0533 | ||
|
|
a546e88f37 | ||
|
|
e998c9e9cb | ||
|
|
889087c966 | ||
|
|
7f3b64c7c4 | ||
|
|
e60a6e3a82 | ||
|
|
135c8f0e99 | ||
|
|
f02504bb80 | ||
|
|
40834fdf2f | ||
|
|
fc0588954b | ||
|
|
75960e3bf3 | ||
|
|
f14ac472a3 | ||
|
|
9ed93715ef | ||
|
|
b34ca44abe | ||
|
|
40ba8f3570 | ||
|
|
e543acf923 | ||
|
|
d183568644 | ||
|
|
f27eb8f09e | ||
|
|
ad0545335a | ||
|
|
cfbbae7323 | ||
|
|
940f971ca0 | ||
|
|
78ca49a1bc | ||
|
|
1d54b0e540 | ||
|
|
7e971d8302 | ||
|
|
54b3b3fe05 | ||
|
|
9d012b0621 | ||
|
|
fbb0a93e12 | ||
|
|
e2e7a8d722 | ||
|
|
ce7923adaf | ||
|
|
a26d53151b | ||
|
|
5eaef6b758 | ||
|
|
c5c38cad9c | ||
|
|
9918f389e7 | ||
|
|
dd8c424806 | ||
|
|
078d8a07cf | ||
|
|
1ee712e549 | ||
|
|
55315bdffa | ||
|
|
882b8e1e75 | ||
|
|
95edbc0ae6 | ||
|
|
11cd4fb639 | ||
|
|
9c16bd1e30 | ||
|
|
5e9d5c734e | ||
|
|
b382d1a467 | ||
|
|
23f31475e7 | ||
|
|
c0eab9e442 | ||
|
|
8a1e85d0c8 | ||
|
|
2793502db2 | ||
|
|
9f7bd0246c | ||
|
|
a6a4350d10 | ||
|
|
471b9f4dc4 | ||
|
|
24fb9b1296 | ||
|
|
3573019916 | ||
|
|
fc5b353144 | ||
|
|
1dd257b76a | ||
|
|
5fa1673341 | ||
|
|
daaa1c7e26 | ||
|
|
1fae784b81 | ||
|
|
81b7b58a5e | ||
|
|
866188a643 | ||
|
|
e6fd57165e | ||
|
|
a5d99e7a3c | ||
|
|
a92c75e5f4 | ||
|
|
826fd3350c | ||
|
|
23a2d01282 | ||
|
|
5181f9b4e1 | ||
|
|
f52ae28432 | ||
|
|
36119ff173 | ||
|
|
bb90f3bbf9 | ||
|
|
05cdb7c107 | ||
|
|
b493dabfe6 | ||
|
|
c4816f944e | ||
|
|
211136e3a8 | ||
|
|
cf0a53c501 | ||
|
|
2899984819 | ||
|
|
eafbe5c57c | ||
|
|
7b98f544ff | ||
|
|
b5aba5807c | ||
|
|
d5c4c26b4b | ||
|
|
a35b8a95c2 | ||
|
|
56fa267e09 | ||
|
|
2b73a08916 | ||
|
|
b0190116a7 | ||
|
|
11c0ad24aa | ||
|
|
4cfe8a8bf8 | ||
|
|
754951bbbd | ||
|
|
cded68a2e2 | ||
|
|
0068ccec35 | ||
|
|
89e8994fd1 | ||
|
|
5980b0a5ee | ||
|
|
89029a20ef | ||
|
|
ce69bd97b9 | ||
|
|
999d8651aa | ||
|
|
ed0f022502 | ||
|
|
b1307d5c2a | ||
|
|
dc16013b4f | ||
|
|
e7686dbd64 | ||
|
|
47f553f9ba | ||
|
|
d11268ece7 | ||
|
|
650a13a690 | ||
|
|
54435325b6 | ||
|
|
11fa257549 | ||
|
|
6af8ab0df2 | ||
|
|
984f5ed6eb | ||
|
|
c2061c6bbf | ||
|
|
b708e8431e | ||
|
|
9b6c397171 | ||
|
|
9b0659d4f9 | ||
|
|
f83cecaaf6 | ||
|
|
aa05b9abe5 | ||
|
|
68834cfcc3 | ||
|
|
5621373bc2 | ||
|
|
88582566bf | ||
|
|
d6e1362fee | ||
|
|
b275b8580d | ||
|
|
467be08e67 | ||
|
|
bbb422d125 | ||
|
|
38d4d03ba8 | ||
|
|
b1f076558c | ||
|
|
992435aaf8 | ||
|
|
2f73e73e9d | ||
|
|
4c30a78cd9 | ||
|
|
a8c78fc005 | ||
|
|
fcb473ff64 | ||
|
|
797953c88d | ||
|
|
ce0cfb0ea5 | ||
|
|
13dfe569ef | ||
|
|
c491161c0c | ||
|
|
fde3d9133b | ||
|
|
0d582f9d3f | ||
|
|
1a59133168 | ||
|
|
803d9eb7ad | ||
|
|
a27d3c1623 | ||
|
|
551216a452 | ||
|
|
38cd3979f2 | ||
|
|
3fe602cda3 | ||
|
|
3a4b49095c | ||
|
|
ac5b395c5d | ||
|
|
8fbbca5f4b | ||
|
|
2415820ecd | ||
|
|
20103eb97b | ||
|
|
10c4ab9a3d | ||
|
|
7e39c9b950 | ||
|
|
cc063d4c32 | ||
|
|
3707e4a49c | ||
|
|
cb425ac927 | ||
|
|
0f80c827ed | ||
|
|
fffc496f41 | ||
|
|
06ae43920b | ||
|
|
e78d75a003 | ||
|
|
ec3ac0c4b0 | ||
|
|
c57c5315c1 | ||
|
|
a726530735 | ||
|
|
d9950598d0 | ||
|
|
81f0885879 | ||
|
|
65b2a10e97 | ||
|
|
7605acff65 | ||
|
|
e7f8f7fa3b | ||
|
|
72d7cb717d | ||
|
|
f0caeb9b25 | ||
|
|
76a141090e | ||
|
|
4bd5a158a5 | ||
|
|
dfaae14544 | ||
|
|
79e9baf55a | ||
|
|
9ee89f7868 | ||
|
|
67dbb3cf18 | ||
|
|
4260c40efa | ||
|
|
0bedea52b1 | ||
|
|
fbbab9d6c8 | ||
|
|
cccb907a9b | ||
|
|
ee7339f2c6 | ||
|
|
c51f3e35ca | ||
|
|
7b3bb9a761 | ||
|
|
dc38f22bd8 | ||
|
|
220e3e9a2b | ||
|
|
f135c0b5ee | ||
|
|
ebe6ea580d | ||
|
|
ee708040f6 | ||
|
|
61c4815a37 | ||
|
|
01bb54a94d | ||
|
|
f592c3846b | ||
|
|
c026e25088 | ||
|
|
8ba73bed23 | ||
|
|
4f8986aa48 | ||
|
|
9c87a144e8 | ||
|
|
5b9fa32255 | ||
|
|
f13778215a | ||
|
|
326471a25c | ||
|
|
6405e3a7b1 | ||
|
|
8afb625bab | ||
|
|
c59df636cc | ||
|
|
94878d76f8 | ||
|
|
5022895e2b | ||
|
|
54046e0b98 | ||
|
|
d2cb1613ac | ||
|
|
266fb93422 | ||
|
|
51d8219c46 | ||
|
|
d6af5a686c | ||
|
|
39342b0e75 | ||
|
|
54078c4cae | ||
|
|
c0bfccc15e | ||
|
|
53dc7b1649 | ||
|
|
635970b0a1 | ||
|
|
059b32c212 | ||
|
|
2704ad9110 | ||
|
|
06d247c709 | ||
|
|
974fa1b8b1 | ||
|
|
fb02744460 | ||
|
|
79732ab175 | ||
|
|
f6dbb2f3e0 | ||
|
|
fdd5b77bfd | ||
|
|
cde105e7a8 | ||
|
|
1291e82bb4 | ||
|
|
19d15d9ff7 | ||
|
|
4e27804160 | ||
|
|
bae80af1b4 | ||
|
|
f9aa3d77cd | ||
|
|
5d47ea0918 | ||
|
|
c03fa36257 | ||
|
|
1089fa0415 | ||
|
|
715786bbf9 | ||
|
|
218eca7c2b | ||
|
|
30fc791480 | ||
|
|
e2d161dfdd | ||
|
|
23d48a7cf1 | ||
|
|
cb18f2ef40 | ||
|
|
dbe2ff52b2 | ||
|
|
9db40996cc | ||
|
|
9f201d6370 | ||
|
|
0e86466f99 | ||
|
|
32548bcb4a | ||
|
|
86c54c5acc | ||
|
|
ae584332b3 | ||
|
|
1694c5bfe1 | ||
|
|
cdfbb26c00 | ||
|
|
610c036ef1 | ||
|
|
2638e2acfa | ||
|
|
49bbea5aed | ||
|
|
5fccdc9fc7 | ||
|
|
664b2c36e8 | ||
|
|
964474a1b1 | ||
|
|
ab15fc1575 | ||
|
|
99d392a4fb | ||
|
|
ae9a696607 | ||
|
|
bd51a0d35b | ||
|
|
8c191b10c2 | ||
|
|
cb6a9253fe | ||
|
|
23f97ac49d | ||
|
|
021ab50fb1 | ||
|
|
3fe906f517 | ||
|
|
a8d8a35cd3 | ||
|
|
9b77430d0d | ||
|
|
1045a43603 | ||
|
|
26af77cd1e | ||
|
|
25a9de301a | ||
|
|
e0d71f124e | ||
|
|
1c33b866ba | ||
|
|
5e650fd9e2 | ||
|
|
76275fc3ab | ||
|
|
6c3b28db64 | ||
|
|
2fe9d94470 | ||
|
|
219b473e66 | ||
|
|
7c1b30291c | ||
|
|
47e0e2342c | ||
|
|
bf4c107829 | ||
|
|
9afbdc102c | ||
|
|
370770122c | ||
|
|
143817d44e | ||
|
|
c60862fc9e | ||
|
|
bee5f919fc | ||
|
|
cefa7f04c6 | ||
|
|
03e20e6ac1 | ||
|
|
c5deeee8c7 | ||
|
|
8b1f0e2d90 | ||
|
|
9bf2dfea35 | ||
|
|
33bb847a1d | ||
|
|
bfffc3c2c6 | ||
|
|
b28956f0db | ||
|
|
d82bc3a421 | ||
|
|
06afd33291 | ||
|
|
305460b25f | ||
|
|
8c0205a84a | ||
|
|
378c05f202 | ||
|
|
cc7acd90ab | ||
|
|
a200f6fb8b | ||
|
|
2b1696f1d1 | ||
|
|
8ab17f5ce0 | ||
|
|
6ce481e95b | ||
|
|
162da4bd97 | ||
|
|
18563363da | ||
|
|
7341718f92 | ||
|
|
ef90b93205 | ||
|
|
3f7df08be9 | ||
|
|
ef6c26c730 | ||
|
|
8b3b608ba9 | ||
|
|
97918500d4 | ||
|
|
e2c0803962 | ||
|
|
f418fd5632 | ||
|
|
675a46e23e | ||
|
|
150ab07a83 | ||
|
|
6b20838981 | ||
|
|
c8af8f96ce | ||
|
|
5011465c81 | ||
|
|
f6cc228684 | ||
|
|
9f4b73b6a3 | ||
|
|
bd29004831 | ||
|
|
8aa0f9fe95 | ||
|
|
c802695ee9 | ||
|
|
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 |
@@ -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 }}
|
||||||
@@ -594,7 +594,6 @@ OPENCODE_DISABLE_CLAUDE_CODE
|
|||||||
OPENCODE_DISABLE_CLAUDE_CODE_PROMPT
|
OPENCODE_DISABLE_CLAUDE_CODE_PROMPT
|
||||||
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS
|
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS
|
||||||
OPENCODE_DISABLE_DEFAULT_PLUGINS
|
OPENCODE_DISABLE_DEFAULT_PLUGINS
|
||||||
OPENCODE_DISABLE_FILETIME_CHECK
|
|
||||||
OPENCODE_DISABLE_LSP_DOWNLOAD
|
OPENCODE_DISABLE_LSP_DOWNLOAD
|
||||||
OPENCODE_DISABLE_MODELS_FETCH
|
OPENCODE_DISABLE_MODELS_FETCH
|
||||||
OPENCODE_DISABLE_PRUNE
|
OPENCODE_DISABLE_PRUNE
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://opencode.ai/config.json",
|
"$schema": "https://opencode.ai/config.json",
|
||||||
"provider": {
|
"provider": {},
|
||||||
"opencode": {
|
|
||||||
"options": {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"permission": {
|
"permission": {
|
||||||
"edit": {
|
"edit": {
|
||||||
"packages/opencode/migration/*": "deny",
|
"packages/opencode/migration/*": "deny",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ async function githubFetch(endpoint: string, options: RequestInit = {}) {
|
|||||||
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
|
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
|
||||||
Accept: "application/vnd.github+json",
|
Accept: "application/vnd.github+json",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...options.headers,
|
...(options.headers instanceof Headers ? Object.fromEntries(options.headers.entries()) : options.headers),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ async function githubFetch(endpoint: string, options: RequestInit = {}) {
|
|||||||
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
|
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
|
||||||
Accept: "application/vnd.github+json",
|
Accept: "application/vnd.github+json",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...options.headers,
|
...(options.headers instanceof Headers ? Object.fromEntries(options.headers.entries()) : options.headers),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
+11
-2
@@ -1,9 +1,13 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/nicolo-ribaudo/oxc-project.github.io/refs/heads/json-schema/src/public/.oxlintrc.schema.json",
|
"$schema": "https://raw.githubusercontent.com/nicolo-ribaudo/oxc-project.github.io/refs/heads/json-schema/src/public/.oxlintrc.schema.json",
|
||||||
|
"options": {
|
||||||
|
"typeAware": true
|
||||||
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"suspicious": "warn"
|
"suspicious": "warn"
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
|
"typescript/no-base-to-string": "warn",
|
||||||
// Effect uses `function*` with Effect.gen/Effect.fnUntraced that don't always yield
|
// Effect uses `function*` with Effect.gen/Effect.fnUntraced that don't always yield
|
||||||
"require-yield": "off",
|
"require-yield": "off",
|
||||||
// SolidJS uses `let ref: T | undefined` for JSX ref bindings assigned at runtime
|
// SolidJS uses `let ref: T | undefined` for JSX ref bindings assigned at runtime
|
||||||
@@ -33,10 +37,15 @@
|
|||||||
"no-new": "off",
|
"no-new": "off",
|
||||||
|
|
||||||
// Type-aware: catch unhandled promises
|
// Type-aware: catch unhandled promises
|
||||||
"typescript/no-floating-promises": "warn"
|
"typescript/no-floating-promises": "warn",
|
||||||
|
// Warn when spreading non-plain objects (Headers, class instances, etc.)
|
||||||
|
"typescript/no-misused-spread": "warn"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"typeAware": true
|
"typeAware": true
|
||||||
},
|
},
|
||||||
"ignorePatterns": ["**/node_modules", "**/dist", "**/.build", "**/.sst", "**/*.d.ts"]
|
"options": {
|
||||||
|
"typeAware": true
|
||||||
|
},
|
||||||
|
"ignorePatterns": ["**/node_modules", "**/dist", "**/.build", "**/.sst", "**/*.d.ts", "**/sdk.gen.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -14,6 +10,7 @@
|
|||||||
- Use Bun APIs when possible, like `Bun.file()`
|
- Use Bun APIs when possible, like `Bun.file()`
|
||||||
- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity
|
- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity
|
||||||
- Prefer functional array methods (flatMap, filter, map) over for loops; use type guards on filter to maintain type inference downstream
|
- Prefer functional array methods (flatMap, filter, map) over for loops; use type guards on filter to maintain type inference downstream
|
||||||
|
- In `src/config`, follow the existing self-export pattern at the top of the file (for example `export * as ConfigAgent from "./agent"`) when adding a new config module.
|
||||||
|
|
||||||
Reduce total variable count by inlining when a value is only used once.
|
Reduce total variable count by inlining when a value is only used once.
|
||||||
|
|
||||||
@@ -55,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.
|
||||||
|
|||||||
@@ -236,7 +236,6 @@ new sst.cloudflare.x.SolidStart("Console", {
|
|||||||
SALESFORCE_INSTANCE_URL,
|
SALESFORCE_INSTANCE_URL,
|
||||||
ZEN_BLACK_PRICE,
|
ZEN_BLACK_PRICE,
|
||||||
ZEN_LITE_PRICE,
|
ZEN_LITE_PRICE,
|
||||||
new sst.Secret("ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES"),
|
|
||||||
new sst.Secret("ZEN_LIMITS"),
|
new sst.Secret("ZEN_LIMITS"),
|
||||||
new sst.Secret("ZEN_SESSION_SECRET"),
|
new sst.Secret("ZEN_SESSION_SECRET"),
|
||||||
...ZEN_MODELS,
|
...ZEN_MODELS,
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-VIgTxIjmZ4Bfwwdj/YFmRJdBpPHYhJSY31kh06EXX+0=",
|
"x86_64-linux": "sha256-i9TxYwWkJAR+kW6pbvhgQbRW9UYPtdrPQAGic4zPoa4=",
|
||||||
"aarch64-linux": "sha256-9118AS1ED0nrliURgZYBRuF/18RqXpUouhYJRlZ6jeA=",
|
"aarch64-linux": "sha256-RYc/OYlETXUwkWBRDas+/P4cBW6zde4FqxxnMARu5vs=",
|
||||||
"aarch64-darwin": "sha256-ppo3MfSIGKQHJCdYEZiLFRc61PtcJ9J0kAXH1pNIonA=",
|
"aarch64-darwin": "sha256-jIhUOIRIQEa2WT62TVIedmRIhl/edhK8sbiAFvU3yCM=",
|
||||||
"x86_64-darwin": "sha256-m+CZSOglBCTfNzbdBX6hXdDqqOzHNMzAddVp6BZVDtU="
|
"x86_64-darwin": "sha256-xLGzaX7OofFlZzVgpORJR5QXD2u+54hp+t3cCfUtO84="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ stdenvNoCC.mkDerivation {
|
|||||||
--filter './packages/opencode' \
|
--filter './packages/opencode' \
|
||||||
--filter './packages/desktop' \
|
--filter './packages/desktop' \
|
||||||
--filter './packages/app' \
|
--filter './packages/app' \
|
||||||
|
--filter './packages/shared' \
|
||||||
--frozen-lockfile \
|
--frozen-lockfile \
|
||||||
--ignore-scripts \
|
--ignore-scripts \
|
||||||
--no-progress
|
--no-progress
|
||||||
|
|||||||
+18
-17
@@ -7,6 +7,7 @@
|
|||||||
sysctl,
|
sysctl,
|
||||||
makeBinaryWrapper,
|
makeBinaryWrapper,
|
||||||
models-dev,
|
models-dev,
|
||||||
|
ripgrep,
|
||||||
installShellFiles,
|
installShellFiles,
|
||||||
versionCheckHook,
|
versionCheckHook,
|
||||||
writableTmpDirAsHomeHook,
|
writableTmpDirAsHomeHook,
|
||||||
@@ -51,25 +52,25 @@ stdenvNoCC.mkDerivation (finalAttrs: {
|
|||||||
runHook postBuild
|
runHook postBuild
|
||||||
'';
|
'';
|
||||||
|
|
||||||
installPhase =
|
installPhase = ''
|
||||||
''
|
runHook preInstall
|
||||||
runHook preInstall
|
|
||||||
|
|
||||||
install -Dm755 dist/opencode-*/bin/opencode $out/bin/opencode
|
install -Dm755 dist/opencode-*/bin/opencode $out/bin/opencode
|
||||||
install -Dm644 schema.json $out/share/opencode/schema.json
|
install -Dm644 schema.json $out/share/opencode/schema.json
|
||||||
''
|
|
||||||
# bun runs sysctl to detect if dunning on rosetta2
|
wrapProgram $out/bin/opencode \
|
||||||
+ lib.optionalString stdenvNoCC.hostPlatform.isDarwin ''
|
--prefix PATH : ${
|
||||||
wrapProgram $out/bin/opencode \
|
lib.makeBinPath (
|
||||||
--prefix PATH : ${
|
[
|
||||||
lib.makeBinPath [
|
ripgrep
|
||||||
sysctl
|
|
||||||
]
|
]
|
||||||
}
|
# bun runs sysctl to detect if dunning on rosetta2
|
||||||
''
|
++ lib.optional stdenvNoCC.hostPlatform.isDarwin sysctl
|
||||||
+ ''
|
)
|
||||||
runHook postInstall
|
}
|
||||||
'';
|
|
||||||
|
runHook postInstall
|
||||||
|
'';
|
||||||
|
|
||||||
postInstall = lib.optionalString (stdenvNoCC.buildPlatform.canExecute stdenvNoCC.hostPlatform) ''
|
postInstall = lib.optionalString (stdenvNoCC.buildPlatform.canExecute stdenvNoCC.hostPlatform) ''
|
||||||
# trick yargs into also generating zsh completions
|
# trick yargs into also generating zsh completions
|
||||||
|
|||||||
+4
-2
@@ -7,7 +7,7 @@
|
|||||||
"packageManager": "bun@1.3.11",
|
"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 tauri dev",
|
"dev:desktop": "bun --cwd packages/desktop-electron dev",
|
||||||
"dev:web": "bun --cwd packages/app dev",
|
"dev:web": "bun --cwd packages/app dev",
|
||||||
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
|
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
|
||||||
"dev:storybook": "bun --cwd packages/storybook storybook",
|
"dev:storybook": "bun --cwd packages/storybook storybook",
|
||||||
@@ -34,6 +34,8 @@
|
|||||||
"@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.1.99",
|
||||||
|
"@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",
|
||||||
@@ -51,7 +53,7 @@
|
|||||||
"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.48",
|
"effect": "4.0.0-beta.48",
|
||||||
"ai": "6.0.158",
|
"ai": "6.0.168",
|
||||||
"cross-spawn": "7.0.6",
|
"cross-spawn": "7.0.6",
|
||||||
"hono": "4.10.7",
|
"hono": "4.10.7",
|
||||||
"hono-openapi": "1.1.2",
|
"hono-openapi": "1.1.2",
|
||||||
|
|||||||
@@ -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.4.6",
|
"version": "1.14.18",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
Binary file not shown.
@@ -121,10 +121,10 @@ function SessionProviders(props: ParentProps) {
|
|||||||
function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
|
function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
|
||||||
return (
|
return (
|
||||||
<AppShellProviders>
|
<AppShellProviders>
|
||||||
<Suspense fallback={<Loading />}>
|
{/*<Suspense fallback={<Loading />}>*/}
|
||||||
{props.appChildren}
|
{props.appChildren}
|
||||||
{props.children}
|
{props.children}
|
||||||
</Suspense>
|
{/*</Suspense>*/}
|
||||||
</AppShellProviders>
|
</AppShellProviders>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -184,14 +184,22 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show
|
<Suspense
|
||||||
when={checkMode() === "blocking" ? !startupHealthCheck.loading : startupHealthCheck.state !== "pending"}
|
|
||||||
fallback={
|
fallback={
|
||||||
<div class="h-dvh w-screen flex flex-col items-center justify-center bg-background-base">
|
<div class="h-dvh w-screen flex flex-col items-center justify-center bg-background-base">
|
||||||
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
|
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{/*<Show
|
||||||
|
when={checkMode() === "blocking" ? !startupHealthCheck.loading : startupHealthCheck.state !== "pending"}
|
||||||
|
fallback={
|
||||||
|
<div class="h-dvh w-screen flex flex-col items-center justify-center bg-background-base">
|
||||||
|
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>*/}
|
||||||
|
{checkMode() === "blocking" ? startupHealthCheck() : startupHealthCheck.latest}
|
||||||
<Show
|
<Show
|
||||||
when={startupHealthCheck()}
|
when={startupHealthCheck()}
|
||||||
fallback={
|
fallback={
|
||||||
@@ -209,7 +217,8 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
|
|||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
{/*</Show>*/}
|
||||||
|
</Suspense>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ 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 { useQuery } from "@tanstack/solid-query"
|
||||||
|
import { loadAgentsQuery, loadProvidersQuery } from "@/context/global-sync/bootstrap"
|
||||||
|
|
||||||
interface PromptInputProps {
|
interface PromptInputProps {
|
||||||
class?: string
|
class?: string
|
||||||
@@ -100,6 +102,7 @@ const NON_EMPTY_TEXT = /[^\s\u200B]/
|
|||||||
|
|
||||||
export const PromptInput: Component<PromptInputProps> = (props) => {
|
export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
|
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
const files = useFile()
|
const files = useFile()
|
||||||
@@ -1249,6 +1252,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const agentsQuery = useQuery(() => loadAgentsQuery(sdk.directory))
|
||||||
|
const agentsLoading = () => agentsQuery.isLoading
|
||||||
|
|
||||||
|
const globalProvidersQuery = useQuery(() => loadProvidersQuery(null))
|
||||||
|
const providersQuery = useQuery(() => loadProvidersQuery(sdk.directory))
|
||||||
|
|
||||||
|
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">
|
||||||
<PromptPopover
|
<PromptPopover
|
||||||
@@ -1444,53 +1455,89 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
<span class="truncate text-13-medium text-text-strong">{language.t("prompt.mode.shell")}</span>
|
<span class="truncate text-13-medium text-text-strong">{language.t("prompt.mode.shell")}</span>
|
||||||
<div class="size-4 shrink-0" />
|
<div class="size-4 shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1.5 min-w-0 flex-1">
|
<div class="flex items-center gap-1.5 min-w-0 flex-1 h-7">
|
||||||
<div data-component="prompt-agent-control">
|
<Show when={!agentsLoading()}>
|
||||||
<TooltipKeybind
|
<div data-component="prompt-agent-control">
|
||||||
placement="top"
|
<TooltipKeybind
|
||||||
gutter={4}
|
placement="top"
|
||||||
title={language.t("command.agent.cycle")}
|
gutter={4}
|
||||||
keybind={command.keybind("agent.cycle")}
|
title={language.t("command.agent.cycle")}
|
||||||
>
|
keybind={command.keybind("agent.cycle")}
|
||||||
<Select
|
>
|
||||||
size="normal"
|
<Select
|
||||||
options={agentNames()}
|
size="normal"
|
||||||
current={local.agent.current()?.name ?? ""}
|
options={agentNames()}
|
||||||
onSelect={(value) => {
|
current={local.agent.current()?.name ?? ""}
|
||||||
local.agent.set(value)
|
onSelect={(value) => {
|
||||||
restoreFocus()
|
local.agent.set(value)
|
||||||
}}
|
restoreFocus()
|
||||||
class="capitalize max-w-[160px] text-text-base"
|
}}
|
||||||
valueClass="truncate text-13-regular text-text-base"
|
class="capitalize max-w-[160px] text-text-base"
|
||||||
triggerStyle={control()}
|
valueClass="truncate text-13-regular text-text-base"
|
||||||
triggerProps={{ "data-action": "prompt-agent" }}
|
triggerStyle={control()}
|
||||||
variant="ghost"
|
triggerProps={{ "data-action": "prompt-agent" }}
|
||||||
/>
|
variant="ghost"
|
||||||
</TooltipKeybind>
|
/>
|
||||||
</div>
|
</TooltipKeybind>
|
||||||
<Show when={store.mode !== "shell"}>
|
</div>
|
||||||
<div data-component="prompt-model-control">
|
</Show>
|
||||||
<Show
|
<Show when={!providersLoading()}>
|
||||||
when={providers.paid().length > 0}
|
<Show when={store.mode !== "shell"}>
|
||||||
fallback={
|
<div data-component="prompt-model-control">
|
||||||
|
<Show
|
||||||
|
when={providers.paid().length > 0}
|
||||||
|
fallback={
|
||||||
|
<TooltipKeybind
|
||||||
|
placement="top"
|
||||||
|
gutter={4}
|
||||||
|
title={language.t("command.model.choose")}
|
||||||
|
keybind={command.keybind("model.choose")}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
data-action="prompt-model"
|
||||||
|
as="div"
|
||||||
|
variant="ghost"
|
||||||
|
size="normal"
|
||||||
|
class="min-w-0 max-w-[320px] text-13-regular text-text-base group"
|
||||||
|
style={control()}
|
||||||
|
onClick={() => {
|
||||||
|
void import("@/components/dialog-select-model-unpaid").then((x) => {
|
||||||
|
dialog.show(() => <x.DialogSelectModelUnpaid model={local.model} />)
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Show when={local.model.current()?.provider?.id}>
|
||||||
|
<ProviderIcon
|
||||||
|
id={local.model.current()?.provider?.id ?? ""}
|
||||||
|
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
||||||
|
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
<span class="truncate">
|
||||||
|
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
||||||
|
</span>
|
||||||
|
<Icon name="chevron-down" size="small" class="shrink-0" />
|
||||||
|
</Button>
|
||||||
|
</TooltipKeybind>
|
||||||
|
}
|
||||||
|
>
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
placement="top"
|
placement="top"
|
||||||
gutter={4}
|
gutter={4}
|
||||||
title={language.t("command.model.choose")}
|
title={language.t("command.model.choose")}
|
||||||
keybind={command.keybind("model.choose")}
|
keybind={command.keybind("model.choose")}
|
||||||
>
|
>
|
||||||
<Button
|
<ModelSelectorPopover
|
||||||
data-action="prompt-model"
|
model={local.model}
|
||||||
as="div"
|
triggerAs={Button}
|
||||||
variant="ghost"
|
triggerProps={{
|
||||||
size="normal"
|
variant: "ghost",
|
||||||
class="min-w-0 max-w-[320px] text-13-regular text-text-base group"
|
size: "normal",
|
||||||
style={control()}
|
style: control(),
|
||||||
onClick={() => {
|
class: "min-w-0 max-w-[320px] text-13-regular text-text-base group",
|
||||||
void import("@/components/dialog-select-model-unpaid").then((x) => {
|
"data-action": "prompt-model",
|
||||||
dialog.show(() => <x.DialogSelectModelUnpaid model={local.model} />)
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
|
onClose={restoreFocus}
|
||||||
>
|
>
|
||||||
<Show when={local.model.current()?.provider?.id}>
|
<Show when={local.model.current()?.provider?.id}>
|
||||||
<ProviderIcon
|
<ProviderIcon
|
||||||
@@ -1503,67 +1550,35 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
||||||
</span>
|
</span>
|
||||||
<Icon name="chevron-down" size="small" class="shrink-0" />
|
<Icon name="chevron-down" size="small" class="shrink-0" />
|
||||||
</Button>
|
</ModelSelectorPopover>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
}
|
</Show>
|
||||||
>
|
</div>
|
||||||
|
<div data-component="prompt-variant-control">
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
placement="top"
|
placement="top"
|
||||||
gutter={4}
|
gutter={4}
|
||||||
title={language.t("command.model.choose")}
|
title={language.t("command.model.variant.cycle")}
|
||||||
keybind={command.keybind("model.choose")}
|
keybind={command.keybind("model.variant.cycle")}
|
||||||
>
|
>
|
||||||
<ModelSelectorPopover
|
<Select
|
||||||
model={local.model}
|
size="normal"
|
||||||
triggerAs={Button}
|
options={variants()}
|
||||||
triggerProps={{
|
current={local.model.variant.current() ?? "default"}
|
||||||
variant: "ghost",
|
label={(x) => (x === "default" ? language.t("common.default") : x)}
|
||||||
size: "normal",
|
onSelect={(value) => {
|
||||||
style: control(),
|
local.model.variant.set(value === "default" ? undefined : value)
|
||||||
class: "min-w-0 max-w-[320px] text-13-regular text-text-base group",
|
restoreFocus()
|
||||||
"data-action": "prompt-model",
|
|
||||||
}}
|
}}
|
||||||
onClose={restoreFocus}
|
class="capitalize max-w-[160px] text-text-base"
|
||||||
>
|
valueClass="truncate text-13-regular text-text-base"
|
||||||
<Show when={local.model.current()?.provider?.id}>
|
triggerStyle={control()}
|
||||||
<ProviderIcon
|
triggerProps={{ "data-action": "prompt-model-variant" }}
|
||||||
id={local.model.current()?.provider?.id ?? ""}
|
variant="ghost"
|
||||||
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
/>
|
||||||
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
<span class="truncate">
|
|
||||||
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
|
||||||
</span>
|
|
||||||
<Icon name="chevron-down" size="small" class="shrink-0" />
|
|
||||||
</ModelSelectorPopover>
|
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</Show>
|
</div>
|
||||||
</div>
|
</Show>
|
||||||
<div data-component="prompt-variant-control">
|
|
||||||
<TooltipKeybind
|
|
||||||
placement="top"
|
|
||||||
gutter={4}
|
|
||||||
title={language.t("command.model.variant.cycle")}
|
|
||||||
keybind={command.keybind("model.variant.cycle")}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
size="normal"
|
|
||||||
options={variants()}
|
|
||||||
current={local.model.variant.current() ?? "default"}
|
|
||||||
label={(x) => (x === "default" ? language.t("common.default") : x)}
|
|
||||||
onSelect={(value) => {
|
|
||||||
local.model.variant.set(value === "default" ? undefined : value)
|
|
||||||
restoreFocus()
|
|
||||||
}}
|
|
||||||
class="capitalize max-w-[160px] text-text-base"
|
|
||||||
valueClass="truncate text-13-regular text-text-base"
|
|
||||||
triggerStyle={control()}
|
|
||||||
triggerProps={{ "data-action": "prompt-model-variant" }}
|
|
||||||
variant="ghost"
|
|
||||||
/>
|
|
||||||
</TooltipKeybind>
|
|
||||||
</div>
|
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ 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/shared/util/path"
|
import { getFilename } from "@opencode-ai/shared/util/path"
|
||||||
import { createEffect, createMemo, For, 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"
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
@@ -16,6 +16,7 @@ import { useLanguage } from "@/context/language"
|
|||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useServer } from "@/context/server"
|
import { useServer } from "@/context/server"
|
||||||
|
import { useSettings } from "@/context/settings"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useTerminal } from "@/context/terminal"
|
import { useTerminal } from "@/context/terminal"
|
||||||
import { focusTerminalById } from "@/pages/session/helpers"
|
import { focusTerminalById } from "@/pages/session/helpers"
|
||||||
@@ -134,6 +135,7 @@ export function SessionHeader() {
|
|||||||
const server = useServer()
|
const server = useServer()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
const settings = useSettings()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const terminal = useTerminal()
|
const terminal = useTerminal()
|
||||||
const { params, view } = useSessionLayout()
|
const { params, view } = useSessionLayout()
|
||||||
@@ -151,6 +153,11 @@ export function SessionHeader() {
|
|||||||
})
|
})
|
||||||
const hotkey = createMemo(() => command.keybind("file.open"))
|
const hotkey = createMemo(() => command.keybind("file.open"))
|
||||||
const os = createMemo(() => detectOS(platform))
|
const os = createMemo(() => detectOS(platform))
|
||||||
|
const isDesktopBeta = platform.platform === "desktop" && import.meta.env.VITE_OPENCODE_CHANNEL === "beta"
|
||||||
|
const search = createMemo(() => !isDesktopBeta || settings.general.showSearch())
|
||||||
|
const tree = createMemo(() => !isDesktopBeta || settings.general.showFileTree())
|
||||||
|
const term = createMemo(() => !isDesktopBeta || settings.general.showTerminal())
|
||||||
|
const status = createMemo(() => !isDesktopBeta || settings.general.showStatus())
|
||||||
|
|
||||||
const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({
|
const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({
|
||||||
finder: true,
|
finder: true,
|
||||||
@@ -262,12 +269,16 @@ export function SessionHeader() {
|
|||||||
.catch((err: unknown) => showRequestError(language, err))
|
.catch((err: unknown) => showRequestError(language, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center"))
|
const [centerMount, setCenterMount] = createSignal<HTMLElement | null>(null)
|
||||||
const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right"))
|
const [rightMount, setRightMount] = createSignal<HTMLElement | null>(null)
|
||||||
|
onMount(() => {
|
||||||
|
setCenterMount(document.getElementById("opencode-titlebar-center"))
|
||||||
|
setRightMount(document.getElementById("opencode-titlebar-right"))
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Show when={centerMount()}>
|
<Show when={search() && centerMount()}>
|
||||||
{(mount) => (
|
{(mount) => (
|
||||||
<Portal mount={mount()}>
|
<Portal mount={mount()}>
|
||||||
<Button
|
<Button
|
||||||
@@ -415,24 +426,28 @@ export function SessionHeader() {
|
|||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<Tooltip placement="bottom" value={language.t("status.popover.trigger")}>
|
<Show when={status()}>
|
||||||
<StatusPopover />
|
<Tooltip placement="bottom" value={language.t("status.popover.trigger")}>
|
||||||
</Tooltip>
|
<StatusPopover />
|
||||||
<TooltipKeybind
|
</Tooltip>
|
||||||
title={language.t("command.terminal.toggle")}
|
</Show>
|
||||||
keybind={command.keybind("terminal.toggle")}
|
<Show when={term()}>
|
||||||
>
|
<TooltipKeybind
|
||||||
<Button
|
title={language.t("command.terminal.toggle")}
|
||||||
variant="ghost"
|
keybind={command.keybind("terminal.toggle")}
|
||||||
class="group/terminal-toggle titlebar-icon w-8 h-6 p-0 box-border shrink-0"
|
|
||||||
onClick={toggleTerminal}
|
|
||||||
aria-label={language.t("command.terminal.toggle")}
|
|
||||||
aria-expanded={view().terminal.opened()}
|
|
||||||
aria-controls="terminal-panel"
|
|
||||||
>
|
>
|
||||||
<Icon size="small" name={view().terminal.opened() ? "terminal-active" : "terminal"} />
|
<Button
|
||||||
</Button>
|
variant="ghost"
|
||||||
</TooltipKeybind>
|
class="group/terminal-toggle titlebar-icon w-8 h-6 p-0 box-border shrink-0"
|
||||||
|
onClick={toggleTerminal}
|
||||||
|
aria-label={language.t("command.terminal.toggle")}
|
||||||
|
aria-expanded={view().terminal.opened()}
|
||||||
|
aria-controls="terminal-panel"
|
||||||
|
>
|
||||||
|
<Icon size="small" name={view().terminal.opened() ? "terminal-active" : "terminal"} />
|
||||||
|
</Button>
|
||||||
|
</TooltipKeybind>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<div class="hidden md:flex items-center gap-1 shrink-0">
|
<div class="hidden md:flex items-center gap-1 shrink-0">
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
@@ -451,30 +466,32 @@ export function SessionHeader() {
|
|||||||
</Button>
|
</Button>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
|
|
||||||
<TooltipKeybind
|
<Show when={tree()}>
|
||||||
title={language.t("command.fileTree.toggle")}
|
<TooltipKeybind
|
||||||
keybind={command.keybind("fileTree.toggle")}
|
title={language.t("command.fileTree.toggle")}
|
||||||
>
|
keybind={command.keybind("fileTree.toggle")}
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
class="titlebar-icon w-8 h-6 p-0 box-border"
|
|
||||||
onClick={() => layout.fileTree.toggle()}
|
|
||||||
aria-label={language.t("command.fileTree.toggle")}
|
|
||||||
aria-expanded={layout.fileTree.opened()}
|
|
||||||
aria-controls="file-tree-panel"
|
|
||||||
>
|
>
|
||||||
<div class="relative flex items-center justify-center size-4">
|
<Button
|
||||||
<Icon
|
variant="ghost"
|
||||||
size="small"
|
class="titlebar-icon w-8 h-6 p-0 box-border"
|
||||||
name={layout.fileTree.opened() ? "file-tree-active" : "file-tree"}
|
onClick={() => layout.fileTree.toggle()}
|
||||||
classList={{
|
aria-label={language.t("command.fileTree.toggle")}
|
||||||
"text-icon-strong": layout.fileTree.opened(),
|
aria-expanded={layout.fileTree.opened()}
|
||||||
"text-icon-weak": !layout.fileTree.opened(),
|
aria-controls="file-tree-panel"
|
||||||
}}
|
>
|
||||||
/>
|
<div class="relative flex items-center justify-center size-4">
|
||||||
</div>
|
<Icon
|
||||||
</Button>
|
size="small"
|
||||||
</TooltipKeybind>
|
name={layout.fileTree.opened() ? "file-tree-active" : "file-tree"}
|
||||||
|
classList={{
|
||||||
|
"text-icon-strong": layout.fileTree.opened(),
|
||||||
|
"text-icon-weak": !layout.fileTree.opened(),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</TooltipKeybind>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ import {
|
|||||||
sansDefault,
|
sansDefault,
|
||||||
sansFontFamily,
|
sansFontFamily,
|
||||||
sansInput,
|
sansInput,
|
||||||
|
terminalDefault,
|
||||||
|
terminalFontFamily,
|
||||||
|
terminalInput,
|
||||||
useSettings,
|
useSettings,
|
||||||
} from "@/context/settings"
|
} from "@/context/settings"
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
@@ -106,6 +109,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
|
|
||||||
permission.disableAutoAccept(params.id, value)
|
permission.disableAutoAccept(params.id, value)
|
||||||
}
|
}
|
||||||
|
const desktop = createMemo(() => platform.platform === "desktop")
|
||||||
|
|
||||||
const check = () => {
|
const check = () => {
|
||||||
if (!platform.checkUpdate) return
|
if (!platform.checkUpdate) return
|
||||||
@@ -180,6 +184,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
const soundOptions = [noneSound, ...SOUND_OPTIONS]
|
const soundOptions = [noneSound, ...SOUND_OPTIONS]
|
||||||
const mono = () => monoInput(settings.appearance.font())
|
const mono = () => monoInput(settings.appearance.font())
|
||||||
const sans = () => sansInput(settings.appearance.uiFont())
|
const sans = () => sansInput(settings.appearance.uiFont())
|
||||||
|
const terminal = () => terminalInput(settings.appearance.terminalFont())
|
||||||
|
|
||||||
const soundSelectProps = (
|
const soundSelectProps = (
|
||||||
enabled: () => boolean,
|
enabled: () => boolean,
|
||||||
@@ -279,6 +284,74 @@ export const SettingsGeneral: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const AdvancedSection = () => (
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.advanced")}</h3>
|
||||||
|
|
||||||
|
<SettingsList>
|
||||||
|
<SettingsRow
|
||||||
|
title={language.t("settings.general.row.showFileTree.title")}
|
||||||
|
description={language.t("settings.general.row.showFileTree.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-show-file-tree">
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.showFileTree()}
|
||||||
|
onChange={(checked) => settings.general.setShowFileTree(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRow>
|
||||||
|
|
||||||
|
<SettingsRow
|
||||||
|
title={language.t("settings.general.row.showNavigation.title")}
|
||||||
|
description={language.t("settings.general.row.showNavigation.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-show-navigation">
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.showNavigation()}
|
||||||
|
onChange={(checked) => settings.general.setShowNavigation(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRow>
|
||||||
|
|
||||||
|
<SettingsRow
|
||||||
|
title={language.t("settings.general.row.showSearch.title")}
|
||||||
|
description={language.t("settings.general.row.showSearch.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-show-search">
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.showSearch()}
|
||||||
|
onChange={(checked) => settings.general.setShowSearch(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRow>
|
||||||
|
|
||||||
|
<SettingsRow
|
||||||
|
title={language.t("settings.general.row.showTerminal.title")}
|
||||||
|
description={language.t("settings.general.row.showTerminal.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-show-terminal">
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.showTerminal()}
|
||||||
|
onChange={(checked) => settings.general.setShowTerminal(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRow>
|
||||||
|
|
||||||
|
<SettingsRow
|
||||||
|
title={language.t("settings.general.row.showStatus.title")}
|
||||||
|
description={language.t("settings.general.row.showStatus.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-show-status">
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.showStatus()}
|
||||||
|
onChange={(checked) => settings.general.setShowStatus(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRow>
|
||||||
|
</SettingsList>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
const AppearanceSection = () => (
|
const AppearanceSection = () => (
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.appearance")}</h3>
|
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.appearance")}</h3>
|
||||||
@@ -382,6 +455,29 @@ export const SettingsGeneral: Component = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
|
||||||
|
<SettingsRow
|
||||||
|
title={language.t("settings.general.row.terminalFont.title")}
|
||||||
|
description={language.t("settings.general.row.terminalFont.description")}
|
||||||
|
>
|
||||||
|
<div class="w-full sm:w-[220px]">
|
||||||
|
<TextField
|
||||||
|
data-action="settings-terminal-font"
|
||||||
|
label={language.t("settings.general.row.terminalFont.title")}
|
||||||
|
hideLabel
|
||||||
|
type="text"
|
||||||
|
value={terminal()}
|
||||||
|
onChange={(value) => settings.appearance.setTerminalFont(value)}
|
||||||
|
placeholder={terminalDefault}
|
||||||
|
spellcheck={false}
|
||||||
|
autocorrect="off"
|
||||||
|
autocomplete="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
class="text-12-regular"
|
||||||
|
style={{ "font-family": terminalFontFamily(settings.appearance.terminalFont()) }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRow>
|
||||||
</SettingsList>
|
</SettingsList>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -527,6 +623,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
console.log(import.meta.env)
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
||||||
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
||||||
@@ -609,6 +706,10 @@ export const SettingsGeneral: Component = () => {
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
<Show when={desktop() && import.meta.env.VITE_OPENCODE_CHANNEL === "beta"}>
|
||||||
|
<AdvancedSection />
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ import { useLanguage } from "@/context/language"
|
|||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { useServer } from "@/context/server"
|
import { useServer } from "@/context/server"
|
||||||
import { monoFontFamily, useSettings } from "@/context/settings"
|
import { terminalFontFamily, useSettings } from "@/context/settings"
|
||||||
import type { LocalPTY } from "@/context/terminal"
|
import type { LocalPTY } from "@/context/terminal"
|
||||||
import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/utils/runtime-adapters"
|
import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/utils/runtime-adapters"
|
||||||
import { terminalWriter } from "@/utils/terminal-writer"
|
import { terminalWriter } from "@/utils/terminal-writer"
|
||||||
@@ -191,7 +191,7 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
const scrollY = typeof local.pty.scrollY === "number" ? local.pty.scrollY : undefined
|
const scrollY = typeof local.pty.scrollY === "number" ? local.pty.scrollY : undefined
|
||||||
let ws: WebSocket | undefined
|
let ws: WebSocket | undefined
|
||||||
let term: Term | undefined
|
let term: Term | undefined
|
||||||
let ghostty: Ghostty
|
let _ghostty: Ghostty
|
||||||
let serializeAddon: SerializeAddon
|
let serializeAddon: SerializeAddon
|
||||||
let fitAddon: FitAddon
|
let fitAddon: FitAddon
|
||||||
let handleResize: () => void
|
let handleResize: () => void
|
||||||
@@ -300,7 +300,7 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const font = monoFontFamily(settings.appearance.font())
|
const font = terminalFontFamily(settings.appearance.terminalFont())
|
||||||
if (!term) return
|
if (!term) return
|
||||||
setOptionIfSupported(term, "fontFamily", font)
|
setOptionIfSupported(term, "fontFamily", font)
|
||||||
scheduleFit()
|
scheduleFit()
|
||||||
@@ -360,7 +360,7 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
cols: restoreSize?.cols,
|
cols: restoreSize?.cols,
|
||||||
rows: restoreSize?.rows,
|
rows: restoreSize?.rows,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontFamily: monoFontFamily(settings.appearance.font()),
|
fontFamily: terminalFontFamily(settings.appearance.terminalFont()),
|
||||||
allowTransparency: false,
|
allowTransparency: false,
|
||||||
convertEol: false,
|
convertEol: false,
|
||||||
theme: terminalColors(),
|
theme: terminalColors(),
|
||||||
@@ -372,7 +372,7 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
cleanup()
|
cleanup()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ghostty = g
|
_ghostty = g
|
||||||
term = t
|
term = t
|
||||||
output = terminalWriter((data, done) =>
|
output = terminalWriter((data, done) =>
|
||||||
t.write(data, () => {
|
t.write(data, () => {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { useLayout } from "@/context/layout"
|
|||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { useSettings } from "@/context/settings"
|
||||||
import { applyPath, backPath, forwardPath } from "./titlebar-history"
|
import { applyPath, backPath, forwardPath } from "./titlebar-history"
|
||||||
|
|
||||||
type TauriDesktopWindow = {
|
type TauriDesktopWindow = {
|
||||||
@@ -40,6 +41,7 @@ export function Titlebar() {
|
|||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
const settings = useSettings()
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
@@ -78,6 +80,7 @@ export function Titlebar() {
|
|||||||
const canBack = createMemo(() => history.index > 0)
|
const canBack = createMemo(() => history.index > 0)
|
||||||
const canForward = createMemo(() => history.index < history.stack.length - 1)
|
const canForward = createMemo(() => history.index < history.stack.length - 1)
|
||||||
const hasProjects = createMemo(() => layout.projects.list().length > 0)
|
const hasProjects = createMemo(() => layout.projects.list().length > 0)
|
||||||
|
const nav = createMemo(() => import.meta.env.VITE_OPENCODE_CHANNEL !== "beta" || settings.general.showNavigation())
|
||||||
|
|
||||||
const back = () => {
|
const back = () => {
|
||||||
const next = backPath(history)
|
const next = backPath(history)
|
||||||
@@ -252,41 +255,47 @@ export function Titlebar() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={hasProjects()}>
|
<div
|
||||||
<div
|
class="flex items-center shrink-0"
|
||||||
class="flex items-center gap-0 transition-transform"
|
classList={{
|
||||||
classList={{
|
"-translate-x-[36px]": layout.sidebar.opened() && !!params.dir,
|
||||||
"translate-x-0": !layout.sidebar.opened(),
|
"duration-180 ease-out": !layout.sidebar.opened(),
|
||||||
"-translate-x-[36px]": layout.sidebar.opened(),
|
"duration-180 ease-in": layout.sidebar.opened(),
|
||||||
"duration-180 ease-out": !layout.sidebar.opened(),
|
}}
|
||||||
"duration-180 ease-in": layout.sidebar.opened(),
|
>
|
||||||
}}
|
<Show when={hasProjects() && nav()}>
|
||||||
>
|
<div class="flex items-center gap-0 transition-transform">
|
||||||
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
|
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
icon="chevron-left"
|
icon="chevron-left"
|
||||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||||
disabled={!canBack()}
|
disabled={!canBack()}
|
||||||
onClick={back}
|
onClick={back}
|
||||||
aria-label={language.t("common.goBack")}
|
aria-label={language.t("common.goBack")}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
|
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
icon="chevron-right"
|
icon="chevron-right"
|
||||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||||
disabled={!canForward()}
|
disabled={!canForward()}
|
||||||
onClick={forward}
|
onClick={forward}
|
||||||
aria-label={language.t("common.goForward")}
|
aria-label={language.t("common.goForward")}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
|
||||||
|
{["beta", "dev"].includes(import.meta.env.VITE_OPENCODE_CHANNEL) && (
|
||||||
|
<div class="bg-icon-interactive-base text-[#FFF] font-medium px-2 rounded-sm uppercase font-mono">
|
||||||
|
{import.meta.env.VITE_OPENCODE_CHANNEL.toUpperCase()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="min-w-0 flex items-center justify-center pointer-events-none">
|
<div class="min-w-0 flex items-center justify-center pointer-events-none">
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ 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 { sanitizeProject } from "./global-sync/utils"
|
||||||
import { formatServerError } from "@/utils/server-errors"
|
import { formatServerError } from "@/utils/server-errors"
|
||||||
|
import { queryOptions, skipToken, useQueryClient } from "@tanstack/solid-query"
|
||||||
|
|
||||||
type GlobalStore = {
|
type GlobalStore = {
|
||||||
ready: boolean
|
ready: boolean
|
||||||
@@ -41,6 +42,9 @@ type GlobalStore = {
|
|||||||
reload: undefined | "pending" | "complete"
|
reload: undefined | "pending" | "complete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const loadSessionsQuery = (directory: string) =>
|
||||||
|
queryOptions<null>({ queryKey: [directory, "loadSessions"], queryFn: skipToken })
|
||||||
|
|
||||||
function createGlobalSync() {
|
function createGlobalSync() {
|
||||||
const globalSDK = useGlobalSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
@@ -67,6 +71,7 @@ function createGlobalSync() {
|
|||||||
config: {},
|
config: {},
|
||||||
reload: undefined,
|
reload: undefined,
|
||||||
})
|
})
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
let active = true
|
let active = true
|
||||||
let projectWritten = false
|
let projectWritten = false
|
||||||
@@ -198,43 +203,50 @@ function createGlobalSync() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 = loadRootSessionsWithFallback({
|
const promise = queryClient
|
||||||
directory,
|
.fetchQuery({
|
||||||
limit,
|
...loadSessionsQuery(directory),
|
||||||
list: (query) => globalSDK.client.session.list(query),
|
queryFn: () =>
|
||||||
})
|
loadRootSessionsWithFallback({
|
||||||
.then((x) => {
|
directory,
|
||||||
const nonArchived = (x.data ?? [])
|
limit,
|
||||||
.filter((s) => !!s?.id)
|
list: (query) => globalSDK.client.session.list(query),
|
||||||
.filter((s) => !s.time?.archived)
|
})
|
||||||
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
.then((x) => {
|
||||||
const limit = store.limit
|
const nonArchived = (x.data ?? [])
|
||||||
const childSessions = store.session.filter((s) => !!s.parentID)
|
.filter((s) => !!s?.id)
|
||||||
const sessions = trimSessions([...nonArchived, ...childSessions], {
|
.filter((s) => !s.time?.archived)
|
||||||
limit,
|
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
||||||
permission: store.permission,
|
const limit = store.limit
|
||||||
})
|
const childSessions = store.session.filter((s) => !!s.parentID)
|
||||||
setStore(
|
const sessions = trimSessions([...nonArchived, ...childSessions], {
|
||||||
"sessionTotal",
|
limit,
|
||||||
estimateRootSessionTotal({
|
permission: store.permission,
|
||||||
count: nonArchived.length,
|
})
|
||||||
limit: x.limit,
|
setStore(
|
||||||
limited: x.limited,
|
"sessionTotal",
|
||||||
}),
|
estimateRootSessionTotal({
|
||||||
)
|
count: nonArchived.length,
|
||||||
setStore("session", reconcile(sessions, { key: "id" }))
|
limit: x.limit,
|
||||||
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
|
limited: x.limited,
|
||||||
sessionMeta.set(directory, { limit })
|
}),
|
||||||
})
|
)
|
||||||
.catch((err) => {
|
setStore("session", reconcile(sessions, { key: "id" }))
|
||||||
console.error("Failed to load sessions", err)
|
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
|
||||||
const project = getFilename(directory)
|
sessionMeta.set(directory, { limit })
|
||||||
showToast({
|
})
|
||||||
variant: "error",
|
.catch((err) => {
|
||||||
title: language.t("toast.session.listFailed.title", { project }),
|
console.error("Failed to load sessions", err)
|
||||||
description: formatServerError(err, language.t),
|
const project = getFilename(directory)
|
||||||
})
|
showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: language.t("toast.session.listFailed.title", { project }),
|
||||||
|
description: formatServerError(err, language.t),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(() => null),
|
||||||
})
|
})
|
||||||
|
.then(() => {})
|
||||||
|
|
||||||
sessionLoads.set(directory, promise)
|
sessionLoads.set(directory, promise)
|
||||||
void promise.finally(() => {
|
void promise.finally(() => {
|
||||||
@@ -250,7 +262,7 @@ function createGlobalSync() {
|
|||||||
if (pending) return pending
|
if (pending) return pending
|
||||||
|
|
||||||
children.pin(directory)
|
children.pin(directory)
|
||||||
const promise = (async () => {
|
const promise = Promise.resolve().then(async () => {
|
||||||
const child = children.ensureChild(directory)
|
const child = children.ensureChild(directory)
|
||||||
const cache = children.vcsCache.get(directory)
|
const cache = children.vcsCache.get(directory)
|
||||||
if (!cache) return
|
if (!cache) return
|
||||||
@@ -269,8 +281,9 @@ function createGlobalSync() {
|
|||||||
vcsCache: cache,
|
vcsCache: cache,
|
||||||
loadSessions,
|
loadSessions,
|
||||||
translate: language.t,
|
translate: language.t,
|
||||||
|
queryClient,
|
||||||
})
|
})
|
||||||
})()
|
})
|
||||||
|
|
||||||
booting.set(directory, promise)
|
booting.set(directory, promise)
|
||||||
void promise.finally(() => {
|
void promise.finally(() => {
|
||||||
@@ -346,6 +359,7 @@ function createGlobalSync() {
|
|||||||
translate: language.t,
|
translate: language.t,
|
||||||
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
|
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
|
||||||
setGlobalStore: setBootStore,
|
setGlobalStore: setBootStore,
|
||||||
|
queryClient,
|
||||||
})
|
})
|
||||||
bootedAt = Date.now()
|
bootedAt = Date.now()
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ 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 { loadSessionsQuery } from "../global-sync"
|
||||||
|
|
||||||
type GlobalStore = {
|
type GlobalStore = {
|
||||||
ready: boolean
|
ready: boolean
|
||||||
@@ -71,6 +73,7 @@ export async function bootstrapGlobal(input: {
|
|||||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||||
formatMoreCount: (count: number) => string
|
formatMoreCount: (count: number) => string
|
||||||
setGlobalStore: SetStoreFunction<GlobalStore>
|
setGlobalStore: SetStoreFunction<GlobalStore>
|
||||||
|
queryClient: QueryClient
|
||||||
}) {
|
}) {
|
||||||
const fast = [
|
const fast = [
|
||||||
() =>
|
() =>
|
||||||
@@ -80,11 +83,16 @@ export async function bootstrapGlobal(input: {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
() =>
|
() =>
|
||||||
retry(() =>
|
input.queryClient.fetchQuery({
|
||||||
input.globalSDK.provider.list().then((x) => {
|
...loadProvidersQuery(null),
|
||||||
input.setGlobalStore("provider", normalizeProviderList(x.data!))
|
queryFn: () =>
|
||||||
}),
|
retry(() =>
|
||||||
),
|
input.globalSDK.provider.list().then((x) => {
|
||||||
|
input.setGlobalStore("provider", normalizeProviderList(x.data!))
|
||||||
|
return null
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
const slow = [
|
const slow = [
|
||||||
@@ -172,6 +180,12 @@ function warmSessions(input: {
|
|||||||
).then(() => undefined)
|
).then(() => undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const loadProvidersQuery = (directory: string | null) =>
|
||||||
|
queryOptions<null>({ queryKey: [directory, "providers"], queryFn: skipToken })
|
||||||
|
|
||||||
|
export const loadAgentsQuery = (directory: string | null) =>
|
||||||
|
queryOptions<null>({ queryKey: [directory, "agents"], queryFn: skipToken })
|
||||||
|
|
||||||
export async function bootstrapDirectory(input: {
|
export async function bootstrapDirectory(input: {
|
||||||
directory: string
|
directory: string
|
||||||
sdk: OpencodeClient
|
sdk: OpencodeClient
|
||||||
@@ -186,6 +200,7 @@ export async function bootstrapDirectory(input: {
|
|||||||
project: Project[]
|
project: Project[]
|
||||||
provider: ProviderListResponse
|
provider: ProviderListResponse
|
||||||
}
|
}
|
||||||
|
queryClient: QueryClient
|
||||||
}) {
|
}) {
|
||||||
const loading = input.store.status !== "complete"
|
const loading = input.store.status !== "complete"
|
||||||
const seededProject = projectID(input.directory, input.global.project)
|
const seededProject = projectID(input.directory, input.global.project)
|
||||||
@@ -207,97 +222,7 @@ export async function bootstrapDirectory(input: {
|
|||||||
input.setStore("lsp", [])
|
input.setStore("lsp", [])
|
||||||
if (loading) input.setStore("status", "partial")
|
if (loading) input.setStore("status", "partial")
|
||||||
|
|
||||||
const fast = [
|
const fast = [() => Promise.resolve(input.loadSessions(input.directory))]
|
||||||
() => retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))),
|
|
||||||
() => 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!))),
|
|
||||||
]
|
|
||||||
|
|
||||||
const slow = [
|
|
||||||
() =>
|
|
||||||
seededProject
|
|
||||||
? Promise.resolve()
|
|
||||||
: retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)),
|
|
||||||
() =>
|
|
||||||
seededPath
|
|
||||||
? Promise.resolve()
|
|
||||||
: 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(() =>
|
|
||||||
input.sdk.vcs.get().then((x) => {
|
|
||||||
const next = x.data ?? input.store.vcs
|
|
||||||
input.setStore("vcs", next)
|
|
||||||
if (next) input.vcsCache.setStore("value", next)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
() => retry(() => input.sdk.command.list().then((x) => input.setStore("command", x.data ?? []))),
|
|
||||||
() =>
|
|
||||||
retry(() =>
|
|
||||||
input.sdk.permission.list().then((x) => {
|
|
||||||
const ids = (x.data ?? []).map((perm) => perm?.sessionID).filter((id): id is string => !!id)
|
|
||||||
const grouped = groupBySession(
|
|
||||||
(x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID),
|
|
||||||
)
|
|
||||||
return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() =>
|
|
||||||
batch(() => {
|
|
||||||
for (const sessionID of Object.keys(input.store.permission)) {
|
|
||||||
if (grouped[sessionID]) continue
|
|
||||||
input.setStore("permission", sessionID, [])
|
|
||||||
}
|
|
||||||
for (const [sessionID, permissions] of Object.entries(grouped)) {
|
|
||||||
input.setStore(
|
|
||||||
"permission",
|
|
||||||
sessionID,
|
|
||||||
reconcile(
|
|
||||||
permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
|
|
||||||
{ key: "id" },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
() =>
|
|
||||||
retry(() =>
|
|
||||||
input.sdk.question.list().then((x) => {
|
|
||||||
const ids = (x.data ?? []).map((question) => question?.sessionID).filter((id): id is string => !!id)
|
|
||||||
const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID))
|
|
||||||
return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() =>
|
|
||||||
batch(() => {
|
|
||||||
for (const sessionID of Object.keys(input.store.question)) {
|
|
||||||
if (grouped[sessionID]) continue
|
|
||||||
input.setStore("question", sessionID, [])
|
|
||||||
}
|
|
||||||
for (const [sessionID, questions] of Object.entries(grouped)) {
|
|
||||||
input.setStore(
|
|
||||||
"question",
|
|
||||||
sessionID,
|
|
||||||
reconcile(
|
|
||||||
questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
|
|
||||||
{ key: "id" },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
() => Promise.resolve(input.loadSessions(input.directory)),
|
|
||||||
() =>
|
|
||||||
retry(() =>
|
|
||||||
input.sdk.mcp.status().then((x) => {
|
|
||||||
input.setStore("mcp", x.data!)
|
|
||||||
input.setStore("mcp_ready", true)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
const errs = errors(await runAll(fast))
|
const errs = errors(await runAll(fast))
|
||||||
if (errs.length > 0) {
|
if (errs.length > 0) {
|
||||||
@@ -310,36 +235,138 @@ export async function bootstrapDirectory(input: {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
await waitForPaint()
|
;(async () => {
|
||||||
const slowErrs = errors(await runAll(slow))
|
const slow = [
|
||||||
if (slowErrs.length > 0) {
|
() =>
|
||||||
console.error("Failed to finish bootstrap instance", slowErrs[0])
|
input.queryClient.ensureQueryData({
|
||||||
const project = getFilename(input.directory)
|
...loadAgentsQuery(input.directory),
|
||||||
showToast({
|
queryFn: () =>
|
||||||
variant: "error",
|
retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))).then(
|
||||||
title: input.translate("toast.project.reloadFailed.title", { project }),
|
() => null,
|
||||||
description: formatServerError(slowErrs[0], input.translate),
|
),
|
||||||
})
|
}),
|
||||||
}
|
() => 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!))),
|
||||||
|
() =>
|
||||||
|
seededProject
|
||||||
|
? Promise.resolve()
|
||||||
|
: retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)),
|
||||||
|
() =>
|
||||||
|
seededPath
|
||||||
|
? Promise.resolve()
|
||||||
|
: 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(() =>
|
||||||
|
input.sdk.vcs.get().then((x) => {
|
||||||
|
const next = x.data ?? input.store.vcs
|
||||||
|
input.setStore("vcs", next)
|
||||||
|
if (next) input.vcsCache.setStore("value", next)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
() => retry(() => input.sdk.command.list().then((x) => input.setStore("command", x.data ?? []))),
|
||||||
|
() =>
|
||||||
|
retry(() =>
|
||||||
|
input.sdk.permission.list().then((x) => {
|
||||||
|
const ids = (x.data ?? []).map((perm) => perm?.sessionID).filter((id): id is string => !!id)
|
||||||
|
const grouped = groupBySession(
|
||||||
|
(x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID),
|
||||||
|
)
|
||||||
|
return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() =>
|
||||||
|
batch(() => {
|
||||||
|
for (const sessionID of Object.keys(input.store.permission)) {
|
||||||
|
if (grouped[sessionID]) continue
|
||||||
|
input.setStore("permission", sessionID, [])
|
||||||
|
}
|
||||||
|
for (const [sessionID, permissions] of Object.entries(grouped)) {
|
||||||
|
input.setStore(
|
||||||
|
"permission",
|
||||||
|
sessionID,
|
||||||
|
reconcile(
|
||||||
|
permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
|
||||||
|
{ key: "id" },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
() =>
|
||||||
|
retry(() =>
|
||||||
|
input.sdk.question.list().then((x) => {
|
||||||
|
const ids = (x.data ?? []).map((question) => question?.sessionID).filter((id): id is string => !!id)
|
||||||
|
const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID))
|
||||||
|
return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() =>
|
||||||
|
batch(() => {
|
||||||
|
for (const sessionID of Object.keys(input.store.question)) {
|
||||||
|
if (grouped[sessionID]) continue
|
||||||
|
input.setStore("question", sessionID, [])
|
||||||
|
}
|
||||||
|
for (const [sessionID, questions] of Object.entries(grouped)) {
|
||||||
|
input.setStore(
|
||||||
|
"question",
|
||||||
|
sessionID,
|
||||||
|
reconcile(
|
||||||
|
questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
|
||||||
|
{ key: "id" },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
() => Promise.resolve(input.loadSessions(input.directory)),
|
||||||
|
() =>
|
||||||
|
retry(() =>
|
||||||
|
input.sdk.mcp.status().then((x) => {
|
||||||
|
input.setStore("mcp", x.data!)
|
||||||
|
input.setStore("mcp_ready", true)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete")
|
await waitForPaint()
|
||||||
|
const slowErrs = errors(await runAll(slow))
|
||||||
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
if (slowErrs.length > 0) {
|
||||||
providerRev.set(input.directory, rev)
|
console.error("Failed to finish bootstrap instance", slowErrs[0])
|
||||||
void 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) return
|
|
||||||
console.error("Failed to refresh provider list", err)
|
|
||||||
const project = getFilename(input.directory)
|
const project = getFilename(input.directory)
|
||||||
showToast({
|
showToast({
|
||||||
variant: "error",
|
variant: "error",
|
||||||
title: input.translate("toast.project.reloadFailed.title", { project }),
|
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||||
description: formatServerError(err, input.translate),
|
description: formatServerError(slowErrs[0], input.translate),
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
})
|
})
|
||||||
|
})()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ export interface Settings {
|
|||||||
autoSave: boolean
|
autoSave: boolean
|
||||||
releaseNotes: boolean
|
releaseNotes: boolean
|
||||||
followup: "queue" | "steer"
|
followup: "queue" | "steer"
|
||||||
|
showFileTree: boolean
|
||||||
|
showNavigation: boolean
|
||||||
|
showSearch: boolean
|
||||||
|
showStatus: boolean
|
||||||
|
showTerminal: boolean
|
||||||
showReasoningSummaries: boolean
|
showReasoningSummaries: boolean
|
||||||
shellToolPartsExpanded: boolean
|
shellToolPartsExpanded: boolean
|
||||||
editToolPartsExpanded: boolean
|
editToolPartsExpanded: boolean
|
||||||
@@ -34,6 +39,7 @@ export interface Settings {
|
|||||||
fontSize: number
|
fontSize: number
|
||||||
mono: string
|
mono: string
|
||||||
sans: string
|
sans: string
|
||||||
|
terminal: string
|
||||||
}
|
}
|
||||||
keybinds: Record<string, string>
|
keybinds: Record<string, string>
|
||||||
permissions: {
|
permissions: {
|
||||||
@@ -45,13 +51,17 @@ export interface Settings {
|
|||||||
|
|
||||||
export const monoDefault = "System Mono"
|
export const monoDefault = "System Mono"
|
||||||
export const sansDefault = "System Sans"
|
export const sansDefault = "System Sans"
|
||||||
|
export const terminalDefault = "JetBrainsMono Nerd Font Mono"
|
||||||
|
|
||||||
const monoFallback =
|
const monoFallback =
|
||||||
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
|
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
|
||||||
const sansFallback = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
|
const sansFallback = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
|
||||||
|
const terminalFallback =
|
||||||
|
'"JetBrainsMono Nerd Font Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
|
||||||
|
|
||||||
const monoBase = monoFallback
|
const monoBase = monoFallback
|
||||||
const sansBase = sansFallback
|
const sansBase = sansFallback
|
||||||
|
const terminalBase = terminalFallback
|
||||||
|
|
||||||
function input(font: string | undefined) {
|
function input(font: string | undefined) {
|
||||||
return font ?? ""
|
return font ?? ""
|
||||||
@@ -84,11 +94,24 @@ export function sansFontFamily(font: string | undefined) {
|
|||||||
return stack(font, sansBase)
|
return stack(font, sansBase)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function terminalInput(font: string | undefined) {
|
||||||
|
return input(font)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function terminalFontFamily(font: string | undefined) {
|
||||||
|
return stack(font, terminalBase)
|
||||||
|
}
|
||||||
|
|
||||||
const defaultSettings: Settings = {
|
const defaultSettings: Settings = {
|
||||||
general: {
|
general: {
|
||||||
autoSave: true,
|
autoSave: true,
|
||||||
releaseNotes: true,
|
releaseNotes: true,
|
||||||
followup: "steer",
|
followup: "steer",
|
||||||
|
showFileTree: false,
|
||||||
|
showNavigation: false,
|
||||||
|
showSearch: false,
|
||||||
|
showStatus: false,
|
||||||
|
showTerminal: false,
|
||||||
showReasoningSummaries: false,
|
showReasoningSummaries: false,
|
||||||
shellToolPartsExpanded: false,
|
shellToolPartsExpanded: false,
|
||||||
editToolPartsExpanded: false,
|
editToolPartsExpanded: false,
|
||||||
@@ -100,6 +123,7 @@ const defaultSettings: Settings = {
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
mono: "",
|
mono: "",
|
||||||
sans: "",
|
sans: "",
|
||||||
|
terminal: "",
|
||||||
},
|
},
|
||||||
keybinds: {},
|
keybinds: {},
|
||||||
permissions: {
|
permissions: {
|
||||||
@@ -162,6 +186,26 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
|||||||
setFollowup(value: "queue" | "steer") {
|
setFollowup(value: "queue" | "steer") {
|
||||||
setStore("general", "followup", value === "queue" ? "steer" : value)
|
setStore("general", "followup", value === "queue" ? "steer" : value)
|
||||||
},
|
},
|
||||||
|
showFileTree: withFallback(() => store.general?.showFileTree, defaultSettings.general.showFileTree),
|
||||||
|
setShowFileTree(value: boolean) {
|
||||||
|
setStore("general", "showFileTree", value)
|
||||||
|
},
|
||||||
|
showNavigation: withFallback(() => store.general?.showNavigation, defaultSettings.general.showNavigation),
|
||||||
|
setShowNavigation(value: boolean) {
|
||||||
|
setStore("general", "showNavigation", value)
|
||||||
|
},
|
||||||
|
showSearch: withFallback(() => store.general?.showSearch, defaultSettings.general.showSearch),
|
||||||
|
setShowSearch(value: boolean) {
|
||||||
|
setStore("general", "showSearch", value)
|
||||||
|
},
|
||||||
|
showStatus: withFallback(() => store.general?.showStatus, defaultSettings.general.showStatus),
|
||||||
|
setShowStatus(value: boolean) {
|
||||||
|
setStore("general", "showStatus", value)
|
||||||
|
},
|
||||||
|
showTerminal: withFallback(() => store.general?.showTerminal, defaultSettings.general.showTerminal),
|
||||||
|
setShowTerminal(value: boolean) {
|
||||||
|
setStore("general", "showTerminal", value)
|
||||||
|
},
|
||||||
showReasoningSummaries: withFallback(
|
showReasoningSummaries: withFallback(
|
||||||
() => store.general?.showReasoningSummaries,
|
() => store.general?.showReasoningSummaries,
|
||||||
defaultSettings.general.showReasoningSummaries,
|
defaultSettings.general.showReasoningSummaries,
|
||||||
@@ -203,6 +247,10 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
|||||||
setUIFont(value: string) {
|
setUIFont(value: string) {
|
||||||
setStore("appearance", "sans", value.trim() ? value : "")
|
setStore("appearance", "sans", value.trim() ? value : "")
|
||||||
},
|
},
|
||||||
|
terminalFont: withFallback(() => store.appearance?.terminal, defaultSettings.appearance.terminal),
|
||||||
|
setTerminalFont(value: string) {
|
||||||
|
setStore("appearance", "terminal", value.trim() ? value : "")
|
||||||
|
},
|
||||||
},
|
},
|
||||||
keybinds: {
|
keybinds: {
|
||||||
get: (action: string) => store.keybinds?.[action],
|
get: (action: string) => store.keybinds?.[action],
|
||||||
|
|||||||
Vendored
+2
-3
@@ -1,15 +1,14 @@
|
|||||||
import "solid-js"
|
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_OPENCODE_SERVER_HOST: string
|
readonly VITE_OPENCODE_SERVER_HOST: string
|
||||||
readonly VITE_OPENCODE_SERVER_PORT: string
|
readonly VITE_OPENCODE_SERVER_PORT: string
|
||||||
|
readonly VITE_OPENCODE_CHANNEL?: "dev" | "beta" | "prod"
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
readonly env: ImportMetaEnv
|
readonly env: ImportMetaEnv
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "solid-js" {
|
export declare module "solid-js" {
|
||||||
namespace JSX {
|
namespace JSX {
|
||||||
interface Directives {
|
interface Directives {
|
||||||
sortable: true
|
sortable: true
|
||||||
|
|||||||
@@ -565,7 +565,9 @@ export const dict = {
|
|||||||
"settings.general.row.theme.title": "السمة",
|
"settings.general.row.theme.title": "السمة",
|
||||||
"settings.general.row.theme.description": "تخصيص سمة OpenCode.",
|
"settings.general.row.theme.description": "تخصيص سمة OpenCode.",
|
||||||
"settings.general.row.font.title": "خط الكود",
|
"settings.general.row.font.title": "خط الكود",
|
||||||
"settings.general.row.font.description": "خصّص الخط المستخدم في كتل التعليمات البرمجية والطرفيات",
|
"settings.general.row.font.description": "خصّص الخط المستخدم في كتل التعليمات البرمجية",
|
||||||
|
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||||
|
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||||
"settings.general.row.uiFont.title": "خط الواجهة",
|
"settings.general.row.uiFont.title": "خط الواجهة",
|
||||||
"settings.general.row.uiFont.description": "خصّص الخط المستخدم في الواجهة بأكملها",
|
"settings.general.row.uiFont.description": "خصّص الخط المستخدم في الواجهة بأكملها",
|
||||||
"settings.general.row.followup.title": "سلوك المتابعة",
|
"settings.general.row.followup.title": "سلوك المتابعة",
|
||||||
|
|||||||
@@ -572,7 +572,9 @@ export const dict = {
|
|||||||
"settings.general.row.theme.title": "Tema",
|
"settings.general.row.theme.title": "Tema",
|
||||||
"settings.general.row.theme.description": "Personalize como o OpenCode é tematizado.",
|
"settings.general.row.theme.description": "Personalize como o OpenCode é tematizado.",
|
||||||
"settings.general.row.font.title": "Fonte de código",
|
"settings.general.row.font.title": "Fonte de código",
|
||||||
"settings.general.row.font.description": "Personalize a fonte usada em blocos de código e terminais",
|
"settings.general.row.font.description": "Personalize a fonte usada em blocos de código",
|
||||||
|
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||||
|
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||||
"settings.general.row.uiFont.title": "Fonte da interface",
|
"settings.general.row.uiFont.title": "Fonte da interface",
|
||||||
"settings.general.row.uiFont.description": "Personalize a fonte usada em toda a interface",
|
"settings.general.row.uiFont.description": "Personalize a fonte usada em toda a interface",
|
||||||
"settings.general.row.followup.title": "Comportamento de acompanhamento",
|
"settings.general.row.followup.title": "Comportamento de acompanhamento",
|
||||||
|
|||||||
@@ -637,7 +637,9 @@ export const dict = {
|
|||||||
"settings.general.row.theme.title": "Tema",
|
"settings.general.row.theme.title": "Tema",
|
||||||
"settings.general.row.theme.description": "Prilagodi temu OpenCode-a.",
|
"settings.general.row.theme.description": "Prilagodi temu OpenCode-a.",
|
||||||
"settings.general.row.font.title": "Font za kod",
|
"settings.general.row.font.title": "Font za kod",
|
||||||
"settings.general.row.font.description": "Prilagodi font koji se koristi u blokovima koda i terminalima",
|
"settings.general.row.font.description": "Prilagodi font koji se koristi u blokovima koda",
|
||||||
|
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||||
|
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||||
"settings.general.row.uiFont.title": "UI font",
|
"settings.general.row.uiFont.title": "UI font",
|
||||||
"settings.general.row.uiFont.description": "Prilagodi font koji se koristi u cijelom interfejsu",
|
"settings.general.row.uiFont.description": "Prilagodi font koji se koristi u cijelom interfejsu",
|
||||||
"settings.general.row.followup.title": "Ponašanje nadovezivanja",
|
"settings.general.row.followup.title": "Ponašanje nadovezivanja",
|
||||||
|
|||||||
@@ -632,7 +632,9 @@ export const dict = {
|
|||||||
"settings.general.row.theme.title": "Tema",
|
"settings.general.row.theme.title": "Tema",
|
||||||
"settings.general.row.theme.description": "Tilpas hvordan OpenCode er temabestemt.",
|
"settings.general.row.theme.description": "Tilpas hvordan OpenCode er temabestemt.",
|
||||||
"settings.general.row.font.title": "Kode-skrifttype",
|
"settings.general.row.font.title": "Kode-skrifttype",
|
||||||
"settings.general.row.font.description": "Tilpas skrifttypen, der bruges i kodeblokke og terminaler",
|
"settings.general.row.font.description": "Tilpas skrifttypen, der bruges i kodeblokke",
|
||||||
|
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||||
|
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||||
"settings.general.row.uiFont.title": "UI-skrifttype",
|
"settings.general.row.uiFont.title": "UI-skrifttype",
|
||||||
"settings.general.row.uiFont.description": "Tilpas skrifttypen, der bruges i hele brugerfladen",
|
"settings.general.row.uiFont.description": "Tilpas skrifttypen, der bruges i hele brugerfladen",
|
||||||
"settings.general.row.followup.title": "Opfølgningsadfærd",
|
"settings.general.row.followup.title": "Opfølgningsadfærd",
|
||||||
|
|||||||
@@ -582,7 +582,9 @@ export const dict = {
|
|||||||
"settings.general.row.theme.title": "Thema",
|
"settings.general.row.theme.title": "Thema",
|
||||||
"settings.general.row.theme.description": "Das Thema von OpenCode anpassen.",
|
"settings.general.row.theme.description": "Das Thema von OpenCode anpassen.",
|
||||||
"settings.general.row.font.title": "Code-Schriftart",
|
"settings.general.row.font.title": "Code-Schriftart",
|
||||||
"settings.general.row.font.description": "Die in Codeblöcken und Terminals verwendete Schriftart anpassen",
|
"settings.general.row.font.description": "Die in Codeblöcken verwendete Schriftart anpassen",
|
||||||
|
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||||
|
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||||
"settings.general.row.uiFont.title": "UI-Schriftart",
|
"settings.general.row.uiFont.title": "UI-Schriftart",
|
||||||
"settings.general.row.uiFont.description": "Die im gesamten Interface verwendete Schriftart anpassen",
|
"settings.general.row.uiFont.description": "Die im gesamten Interface verwendete Schriftart anpassen",
|
||||||
"settings.general.row.followup.title": "Verhalten bei Folgefragen",
|
"settings.general.row.followup.title": "Verhalten bei Folgefragen",
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -719,6 +720,7 @@ export const dict = {
|
|||||||
"settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.",
|
"settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.",
|
||||||
|
|
||||||
"settings.general.section.appearance": "Appearance",
|
"settings.general.section.appearance": "Appearance",
|
||||||
|
"settings.general.section.advanced": "Advanced",
|
||||||
"settings.general.section.notifications": "System notifications",
|
"settings.general.section.notifications": "System notifications",
|
||||||
"settings.general.section.updates": "Updates",
|
"settings.general.section.updates": "Updates",
|
||||||
"settings.general.section.sounds": "Sound effects",
|
"settings.general.section.sounds": "Sound effects",
|
||||||
@@ -734,13 +736,25 @@ export const dict = {
|
|||||||
"settings.general.row.theme.title": "Theme",
|
"settings.general.row.theme.title": "Theme",
|
||||||
"settings.general.row.theme.description": "Customise how OpenCode is themed.",
|
"settings.general.row.theme.description": "Customise how OpenCode is themed.",
|
||||||
"settings.general.row.font.title": "Code Font",
|
"settings.general.row.font.title": "Code Font",
|
||||||
"settings.general.row.font.description": "Customise the font used in code blocks and terminals",
|
"settings.general.row.font.description": "Customise the font used in code blocks",
|
||||||
|
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||||
|
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||||
"settings.general.row.uiFont.title": "UI Font",
|
"settings.general.row.uiFont.title": "UI Font",
|
||||||
"settings.general.row.uiFont.description": "Customise the font used throughout the interface",
|
"settings.general.row.uiFont.description": "Customise the font used throughout the interface",
|
||||||
"settings.general.row.followup.title": "Follow-up behavior",
|
"settings.general.row.followup.title": "Follow-up behavior",
|
||||||
"settings.general.row.followup.description": "Choose whether follow-up prompts steer immediately or wait in a queue",
|
"settings.general.row.followup.description": "Choose whether follow-up prompts steer immediately or wait in a queue",
|
||||||
"settings.general.row.followup.option.queue": "Queue",
|
"settings.general.row.followup.option.queue": "Queue",
|
||||||
"settings.general.row.followup.option.steer": "Steer",
|
"settings.general.row.followup.option.steer": "Steer",
|
||||||
|
"settings.general.row.showFileTree.title": "File tree",
|
||||||
|
"settings.general.row.showFileTree.description": "Show the file tree toggle and panel in desktop sessions",
|
||||||
|
"settings.general.row.showNavigation.title": "Navigation controls",
|
||||||
|
"settings.general.row.showNavigation.description": "Show the back and forward buttons in the desktop title bar",
|
||||||
|
"settings.general.row.showSearch.title": "Command palette",
|
||||||
|
"settings.general.row.showSearch.description": "Show the search and command palette button in the desktop title bar",
|
||||||
|
"settings.general.row.showTerminal.title": "Terminal",
|
||||||
|
"settings.general.row.showTerminal.description": "Show the terminal button in the desktop title bar",
|
||||||
|
"settings.general.row.showStatus.title": "Server status",
|
||||||
|
"settings.general.row.showStatus.description": "Show the server status button in the desktop title bar",
|
||||||
"settings.general.row.reasoningSummaries.title": "Show reasoning summaries",
|
"settings.general.row.reasoningSummaries.title": "Show reasoning summaries",
|
||||||
"settings.general.row.reasoningSummaries.description": "Display model reasoning summaries in the timeline",
|
"settings.general.row.reasoningSummaries.description": "Display model reasoning summaries in the timeline",
|
||||||
"settings.general.row.shellToolPartsExpanded.title": "Expand shell tool parts",
|
"settings.general.row.shellToolPartsExpanded.title": "Expand shell tool parts",
|
||||||
@@ -857,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",
|
||||||
|
|||||||
@@ -640,7 +640,9 @@ export const dict = {
|
|||||||
"settings.general.row.theme.title": "Tema",
|
"settings.general.row.theme.title": "Tema",
|
||||||
"settings.general.row.theme.description": "Personaliza el tema de OpenCode.",
|
"settings.general.row.theme.description": "Personaliza el tema de OpenCode.",
|
||||||
"settings.general.row.font.title": "Fuente de código",
|
"settings.general.row.font.title": "Fuente de código",
|
||||||
"settings.general.row.font.description": "Personaliza la fuente usada en bloques de código y terminales",
|
"settings.general.row.font.description": "Personaliza la fuente usada en bloques de código",
|
||||||
|
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||||
|
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||||
"settings.general.row.uiFont.title": "Fuente de la interfaz",
|
"settings.general.row.uiFont.title": "Fuente de la interfaz",
|
||||||
"settings.general.row.uiFont.description": "Personaliza la fuente usada en toda la interfaz",
|
"settings.general.row.uiFont.description": "Personaliza la fuente usada en toda la interfaz",
|
||||||
"settings.general.row.followup.title": "Comportamiento de seguimiento",
|
"settings.general.row.followup.title": "Comportamiento de seguimiento",
|
||||||
|
|||||||
@@ -579,7 +579,9 @@ export const dict = {
|
|||||||
"settings.general.row.theme.title": "Thème",
|
"settings.general.row.theme.title": "Thème",
|
||||||
"settings.general.row.theme.description": "Personnaliser le thème d'OpenCode.",
|
"settings.general.row.theme.description": "Personnaliser le thème d'OpenCode.",
|
||||||
"settings.general.row.font.title": "Police de code",
|
"settings.general.row.font.title": "Police de code",
|
||||||
"settings.general.row.font.description": "Personnaliser la police utilisée dans les blocs de code et les terminaux",
|
"settings.general.row.font.description": "Personnaliser la police utilisée dans les blocs de code",
|
||||||
|
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||||
|
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||||
"settings.general.row.uiFont.title": "Police de l'interface",
|
"settings.general.row.uiFont.title": "Police de l'interface",
|
||||||
"settings.general.row.uiFont.description": "Personnaliser la police utilisée dans toute l'interface",
|
"settings.general.row.uiFont.description": "Personnaliser la police utilisée dans toute l'interface",
|
||||||
"settings.general.row.followup.title": "Comportement de suivi",
|
"settings.general.row.followup.title": "Comportement de suivi",
|
||||||
|
|||||||
@@ -569,7 +569,9 @@ export const dict = {
|
|||||||
"settings.general.row.theme.title": "テーマ",
|
"settings.general.row.theme.title": "テーマ",
|
||||||
"settings.general.row.theme.description": "OpenCodeのテーマをカスタマイズします。",
|
"settings.general.row.theme.description": "OpenCodeのテーマをカスタマイズします。",
|
||||||
"settings.general.row.font.title": "コードフォント",
|
"settings.general.row.font.title": "コードフォント",
|
||||||
"settings.general.row.font.description": "コードブロックとターミナルで使用するフォントをカスタマイズします",
|
"settings.general.row.font.description": "コードブロックで使用するフォントをカスタマイズします",
|
||||||
|
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||||
|
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||||
"settings.general.row.uiFont.title": "UIフォント",
|
"settings.general.row.uiFont.title": "UIフォント",
|
||||||
"settings.general.row.uiFont.description": "インターフェース全体で使用するフォントをカスタマイズします",
|
"settings.general.row.uiFont.description": "インターフェース全体で使用するフォントをカスタマイズします",
|
||||||
"settings.general.row.followup.title": "フォローアップの動作",
|
"settings.general.row.followup.title": "フォローアップの動作",
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { dict as en } from "./en"
|
|
||||||
|
|
||||||
export const dict = {
|
export const dict = {
|
||||||
"command.category.suggested": "추천",
|
"command.category.suggested": "추천",
|
||||||
"command.category.view": "보기",
|
"command.category.view": "보기",
|
||||||
@@ -568,7 +566,9 @@ export const dict = {
|
|||||||
"settings.general.row.theme.title": "테마",
|
"settings.general.row.theme.title": "테마",
|
||||||
"settings.general.row.theme.description": "OpenCode 테마 사용자 지정",
|
"settings.general.row.theme.description": "OpenCode 테마 사용자 지정",
|
||||||
"settings.general.row.font.title": "코드 글꼴",
|
"settings.general.row.font.title": "코드 글꼴",
|
||||||
"settings.general.row.font.description": "코드 블록과 터미널에 사용되는 글꼴을 사용자 지정",
|
"settings.general.row.font.description": "코드 블록에 사용되는 글꼴을 사용자 지정",
|
||||||
|
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||||
|
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||||
"settings.general.row.uiFont.title": "UI 글꼴",
|
"settings.general.row.uiFont.title": "UI 글꼴",
|
||||||
"settings.general.row.uiFont.description": "인터페이스 전반에 사용되는 글꼴을 사용자 지정",
|
"settings.general.row.uiFont.description": "인터페이스 전반에 사용되는 글꼴을 사용자 지정",
|
||||||
"settings.general.row.followup.title": "후속 조치 동작",
|
"settings.general.row.followup.title": "후속 조치 동작",
|
||||||
|
|||||||
@@ -640,7 +640,9 @@ export const dict = {
|
|||||||
"settings.general.row.theme.title": "Tema",
|
"settings.general.row.theme.title": "Tema",
|
||||||
"settings.general.row.theme.description": "Tilpass hvordan OpenCode er tematisert.",
|
"settings.general.row.theme.description": "Tilpass hvordan OpenCode er tematisert.",
|
||||||
"settings.general.row.font.title": "Kodefont",
|
"settings.general.row.font.title": "Kodefont",
|
||||||
"settings.general.row.font.description": "Tilpass skrifttypen som brukes i kodeblokker og terminaler",
|
"settings.general.row.font.description": "Tilpass skrifttypen som brukes i kodeblokker",
|
||||||
|
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||||
|
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||||
"settings.general.row.uiFont.title": "UI-skrift",
|
"settings.general.row.uiFont.title": "UI-skrift",
|
||||||
"settings.general.row.uiFont.description": "Tilpass skrifttypen som brukes i hele grensesnittet",
|
"settings.general.row.uiFont.description": "Tilpass skrifttypen som brukes i hele grensesnittet",
|
||||||
"settings.general.row.followup.title": "Oppfølgingsadferd",
|
"settings.general.row.followup.title": "Oppfølgingsadferd",
|
||||||
|
|||||||
@@ -571,7 +571,9 @@ export const dict = {
|
|||||||
"settings.general.row.theme.title": "Motyw",
|
"settings.general.row.theme.title": "Motyw",
|
||||||
"settings.general.row.theme.description": "Dostosuj motyw OpenCode.",
|
"settings.general.row.theme.description": "Dostosuj motyw OpenCode.",
|
||||||
"settings.general.row.font.title": "Czcionka kodu",
|
"settings.general.row.font.title": "Czcionka kodu",
|
||||||
"settings.general.row.font.description": "Dostosuj czcionkę używaną w blokach kodu i terminalach",
|
"settings.general.row.font.description": "Dostosuj czcionkę używaną w blokach kodu",
|
||||||
|
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||||
|
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||||
"settings.general.row.uiFont.title": "Czcionka interfejsu",
|
"settings.general.row.uiFont.title": "Czcionka interfejsu",
|
||||||
"settings.general.row.uiFont.description": "Dostosuj czcionkę używaną w całym interfejsie",
|
"settings.general.row.uiFont.description": "Dostosuj czcionkę używaną w całym interfejsie",
|
||||||
"settings.general.row.followup.title": "Zachowanie kontynuacji",
|
"settings.general.row.followup.title": "Zachowanie kontynuacji",
|
||||||
|
|||||||
@@ -637,7 +637,9 @@ export const dict = {
|
|||||||
"settings.general.row.theme.title": "Тема",
|
"settings.general.row.theme.title": "Тема",
|
||||||
"settings.general.row.theme.description": "Настройте оформление OpenCode.",
|
"settings.general.row.theme.description": "Настройте оформление OpenCode.",
|
||||||
"settings.general.row.font.title": "Шрифт кода",
|
"settings.general.row.font.title": "Шрифт кода",
|
||||||
"settings.general.row.font.description": "Настройте шрифт, используемый в блоках кода и терминалах",
|
"settings.general.row.font.description": "Настройте шрифт, используемый в блоках кода",
|
||||||
|
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||||
|
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||||
"settings.general.row.uiFont.title": "Шрифт интерфейса",
|
"settings.general.row.uiFont.title": "Шрифт интерфейса",
|
||||||
"settings.general.row.uiFont.description": "Настройте шрифт, используемый во всем интерфейсе",
|
"settings.general.row.uiFont.description": "Настройте шрифт, используемый во всем интерфейсе",
|
||||||
"settings.general.row.followup.title": "Поведение уточняющих вопросов",
|
"settings.general.row.followup.title": "Поведение уточняющих вопросов",
|
||||||
|
|||||||
@@ -631,7 +631,9 @@ export const dict = {
|
|||||||
"settings.general.row.theme.title": "ธีม",
|
"settings.general.row.theme.title": "ธีม",
|
||||||
"settings.general.row.theme.description": "ปรับแต่งวิธีการที่ OpenCode มีธีม",
|
"settings.general.row.theme.description": "ปรับแต่งวิธีการที่ OpenCode มีธีม",
|
||||||
"settings.general.row.font.title": "ฟอนต์โค้ด",
|
"settings.general.row.font.title": "ฟอนต์โค้ด",
|
||||||
"settings.general.row.font.description": "ปรับแต่งฟอนต์ที่ใช้ในบล็อกโค้ดและเทอร์มินัล",
|
"settings.general.row.font.description": "ปรับแต่งฟอนต์ที่ใช้ในบล็อกโค้ด",
|
||||||
|
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||||
|
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||||
"settings.general.row.uiFont.title": "ฟอนต์ UI",
|
"settings.general.row.uiFont.title": "ฟอนต์ UI",
|
||||||
"settings.general.row.uiFont.description": "ปรับแต่งฟอนต์ที่ใช้ทั่วทั้งอินเทอร์เฟซ",
|
"settings.general.row.uiFont.description": "ปรับแต่งฟอนต์ที่ใช้ทั่วทั้งอินเทอร์เฟซ",
|
||||||
"settings.general.row.followup.title": "พฤติกรรมการติดตามผล",
|
"settings.general.row.followup.title": "พฤติกรรมการติดตามผล",
|
||||||
|
|||||||
@@ -644,7 +644,9 @@ export const dict = {
|
|||||||
"settings.general.row.theme.title": "Tema",
|
"settings.general.row.theme.title": "Tema",
|
||||||
"settings.general.row.theme.description": "OpenCode'un temasını özelleştirin.",
|
"settings.general.row.theme.description": "OpenCode'un temasını özelleştirin.",
|
||||||
"settings.general.row.font.title": "Kod Yazı Tipi",
|
"settings.general.row.font.title": "Kod Yazı Tipi",
|
||||||
"settings.general.row.font.description": "Kod bloklarında ve terminallerde kullanılan yazı tipini özelleştirin",
|
"settings.general.row.font.description": "Kod bloklarında kullanılan yazı tipini özelleştirin",
|
||||||
|
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||||
|
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||||
"settings.general.row.uiFont.title": "Arayüz Yazı Tipi",
|
"settings.general.row.uiFont.title": "Arayüz Yazı Tipi",
|
||||||
"settings.general.row.uiFont.description": "Arayüz genelinde kullanılan yazı tipini özelleştirin",
|
"settings.general.row.uiFont.description": "Arayüz genelinde kullanılan yazı tipini özelleştirin",
|
||||||
"settings.general.row.followup.title": "Takip davranışı",
|
"settings.general.row.followup.title": "Takip davranışı",
|
||||||
|
|||||||
@@ -631,7 +631,9 @@ export const dict = {
|
|||||||
"settings.general.row.theme.title": "主题",
|
"settings.general.row.theme.title": "主题",
|
||||||
"settings.general.row.theme.description": "自定义 OpenCode 的主题。",
|
"settings.general.row.theme.description": "自定义 OpenCode 的主题。",
|
||||||
"settings.general.row.font.title": "代码字体",
|
"settings.general.row.font.title": "代码字体",
|
||||||
"settings.general.row.font.description": "自定义代码块和终端使用的字体",
|
"settings.general.row.font.description": "自定义代码块使用的字体",
|
||||||
|
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||||
|
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||||
"settings.general.row.uiFont.title": "界面字体",
|
"settings.general.row.uiFont.title": "界面字体",
|
||||||
"settings.general.row.uiFont.description": "自定义整个界面使用的字体",
|
"settings.general.row.uiFont.description": "自定义整个界面使用的字体",
|
||||||
"settings.general.row.followup.title": "跟进消息行为",
|
"settings.general.row.followup.title": "跟进消息行为",
|
||||||
|
|||||||
@@ -626,7 +626,9 @@ export const dict = {
|
|||||||
"settings.general.row.theme.title": "主題",
|
"settings.general.row.theme.title": "主題",
|
||||||
"settings.general.row.theme.description": "自訂 OpenCode 的主題。",
|
"settings.general.row.theme.description": "自訂 OpenCode 的主題。",
|
||||||
"settings.general.row.font.title": "程式碼字型",
|
"settings.general.row.font.title": "程式碼字型",
|
||||||
"settings.general.row.font.description": "自訂程式碼區塊和終端機使用的字型",
|
"settings.general.row.font.description": "自訂程式碼區塊使用的字型",
|
||||||
|
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||||
|
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||||
"settings.general.row.uiFont.title": "介面字型",
|
"settings.general.row.uiFont.title": "介面字型",
|
||||||
"settings.general.row.uiFont.description": "自訂整個介面使用的字型",
|
"settings.general.row.uiFont.description": "自訂整個介面使用的字型",
|
||||||
"settings.general.row.followup.title": "後續追問行為",
|
"settings.general.row.followup.title": "後續追問行為",
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
@import "@opencode-ai/ui/styles/tailwind";
|
@import "@opencode-ai/ui/styles/tailwind";
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "JetBrainsMono Nerd Font Mono";
|
||||||
|
src: url("/assets/JetBrainsMonoNerdFontMono-Regular.woff2") format("woff2");
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
@keyframes session-progress-whip {
|
@keyframes session-progress-whip {
|
||||||
0% {
|
0% {
|
||||||
|
|||||||
+191
-178
@@ -13,7 +13,7 @@ import {
|
|||||||
type Accessor,
|
type Accessor,
|
||||||
} from "solid-js"
|
} from "solid-js"
|
||||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||||
import { useNavigate, useParams } from "@solidjs/router"
|
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"
|
||||||
@@ -127,14 +127,17 @@ export default function Layout(props: ParentProps) {
|
|||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const initialDirectory = decode64(params.dir)
|
const initialDirectory = decode64(params.dir)
|
||||||
|
const location = useLocation()
|
||||||
const route = createMemo(() => {
|
const route = createMemo(() => {
|
||||||
const slug = params.dir
|
const slug = params.dir
|
||||||
if (!slug) return { slug, dir: "" }
|
if (!slug) return { slug, dir: "" }
|
||||||
const dir = decode64(slug)
|
const dir = decode64(slug)
|
||||||
if (!dir) return { slug, dir: "" }
|
if (!dir) return { slug, dir: "" }
|
||||||
|
const store = globalSync.peek(dir, { bootstrap: false })
|
||||||
return {
|
return {
|
||||||
slug,
|
slug,
|
||||||
dir: globalSync.peek(dir, { bootstrap: false })[0].path.directory || dir,
|
store,
|
||||||
|
dir: store[0].path.directory || dir,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const availableThemeEntries = createMemo(() => theme.ids().map((id) => [id, theme.themes()[id]] as const))
|
const availableThemeEntries = createMemo(() => theme.ids().map((id) => [id, theme.themes()[id]] as const))
|
||||||
@@ -1058,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"),
|
||||||
@@ -1210,11 +1220,11 @@ 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} />)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2100,196 +2110,198 @@ export default function Layout(props: ParentProps) {
|
|||||||
</Show>
|
</Show>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<>
|
{(project) => (
|
||||||
<div class="shrink-0 pl-1 py-1">
|
<>
|
||||||
<div class="group/project flex items-start justify-between gap-2 py-2 pl-2 pr-0">
|
<div class="shrink-0 pl-1 py-1">
|
||||||
<div class="flex flex-col min-w-0">
|
<div class="group/project flex items-start justify-between gap-2 py-2 pl-2 pr-0">
|
||||||
<InlineEditor
|
<div class="flex flex-col min-w-0">
|
||||||
id={`project:${projectId()}`}
|
<InlineEditor
|
||||||
value={projectName}
|
id={`project:${projectId()}`}
|
||||||
onSave={(next) => {
|
value={projectName}
|
||||||
const item = project()
|
onSave={(next) => {
|
||||||
if (!item) return
|
const item = project()
|
||||||
void renameProject(item, next)
|
if (!item) return
|
||||||
}}
|
void renameProject(item, next)
|
||||||
class="text-14-medium text-text-strong truncate"
|
}}
|
||||||
displayClass="text-14-medium text-text-strong truncate"
|
class="text-14-medium text-text-strong truncate"
|
||||||
stopPropagation
|
displayClass="text-14-medium text-text-strong truncate"
|
||||||
/>
|
stopPropagation
|
||||||
|
/>
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
gutter={2}
|
gutter={2}
|
||||||
value={worktree()}
|
value={worktree()}
|
||||||
class="shrink-0"
|
class="shrink-0"
|
||||||
contentStyle={{
|
contentStyle={{
|
||||||
"max-width": "640px",
|
"max-width": "640px",
|
||||||
transform: "translate3d(52px, 0, 0)",
|
transform: "translate3d(52px, 0, 0)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span class="text-12-regular text-text-base truncate select-text">
|
<span class="text-12-regular text-text-base truncate select-text">
|
||||||
{worktree().replace(homedir(), "~")}
|
{worktree().replace(homedir(), "~")}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DropdownMenu modal={!sidebarHovering()}>
|
||||||
|
<DropdownMenu.Trigger
|
||||||
|
as={IconButton}
|
||||||
|
icon="dot-grid"
|
||||||
|
variant="ghost"
|
||||||
|
data-action="project-menu"
|
||||||
|
data-project={slug()}
|
||||||
|
class="shrink-0 size-6 rounded-md transition-opacity data-[expanded]:bg-surface-base-active"
|
||||||
|
classList={{
|
||||||
|
"opacity-100": panelProps.mobile || merged(),
|
||||||
|
"opacity-0 group-hover/project:opacity-100 group-focus-within/project:opacity-100 data-[expanded]:opacity-100":
|
||||||
|
!panelProps.mobile && !merged(),
|
||||||
|
}}
|
||||||
|
aria-label={language.t("common.moreOptions")}
|
||||||
|
/>
|
||||||
|
<DropdownMenu.Portal>
|
||||||
|
<DropdownMenu.Content class="mt-1">
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onSelect={() => {
|
||||||
|
const item = project()
|
||||||
|
if (!item) return
|
||||||
|
showEditProjectDialog(item)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
data-action="project-workspaces-toggle"
|
||||||
|
data-project={slug()}
|
||||||
|
disabled={!canToggle()}
|
||||||
|
onSelect={() => {
|
||||||
|
const item = project()
|
||||||
|
if (!item) return
|
||||||
|
toggleProjectWorkspaces(item)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemLabel>
|
||||||
|
{workspacesEnabled()
|
||||||
|
? language.t("sidebar.workspaces.disable")
|
||||||
|
: language.t("sidebar.workspaces.enable")}
|
||||||
|
</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
data-action="project-clear-notifications"
|
||||||
|
data-project={slug()}
|
||||||
|
disabled={unseenCount() === 0}
|
||||||
|
onSelect={clearNotifications}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemLabel>
|
||||||
|
{language.t("sidebar.project.clearNotifications")}
|
||||||
|
</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item
|
||||||
|
data-action="project-close-menu"
|
||||||
|
data-project={slug()}
|
||||||
|
onSelect={() => {
|
||||||
|
const dir = worktree()
|
||||||
|
if (!dir) return
|
||||||
|
closeProject(dir)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemLabel>{language.t("common.close")}</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Portal>
|
||||||
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DropdownMenu modal={!sidebarHovering()}>
|
|
||||||
<DropdownMenu.Trigger
|
|
||||||
as={IconButton}
|
|
||||||
icon="dot-grid"
|
|
||||||
variant="ghost"
|
|
||||||
data-action="project-menu"
|
|
||||||
data-project={slug()}
|
|
||||||
class="shrink-0 size-6 rounded-md transition-opacity data-[expanded]:bg-surface-base-active"
|
|
||||||
classList={{
|
|
||||||
"opacity-100": panelProps.mobile || merged(),
|
|
||||||
"opacity-0 group-hover/project:opacity-100 group-focus-within/project:opacity-100 data-[expanded]:opacity-100":
|
|
||||||
!panelProps.mobile && !merged(),
|
|
||||||
}}
|
|
||||||
aria-label={language.t("common.moreOptions")}
|
|
||||||
/>
|
|
||||||
<DropdownMenu.Portal>
|
|
||||||
<DropdownMenu.Content class="mt-1">
|
|
||||||
<DropdownMenu.Item
|
|
||||||
onSelect={() => {
|
|
||||||
const item = project()
|
|
||||||
if (!item) return
|
|
||||||
showEditProjectDialog(item)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item
|
|
||||||
data-action="project-workspaces-toggle"
|
|
||||||
data-project={slug()}
|
|
||||||
disabled={!canToggle()}
|
|
||||||
onSelect={() => {
|
|
||||||
const item = project()
|
|
||||||
if (!item) return
|
|
||||||
toggleProjectWorkspaces(item)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DropdownMenu.ItemLabel>
|
|
||||||
{workspacesEnabled()
|
|
||||||
? language.t("sidebar.workspaces.disable")
|
|
||||||
: language.t("sidebar.workspaces.enable")}
|
|
||||||
</DropdownMenu.ItemLabel>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item
|
|
||||||
data-action="project-clear-notifications"
|
|
||||||
data-project={slug()}
|
|
||||||
disabled={unseenCount() === 0}
|
|
||||||
onSelect={clearNotifications}
|
|
||||||
>
|
|
||||||
<DropdownMenu.ItemLabel>
|
|
||||||
{language.t("sidebar.project.clearNotifications")}
|
|
||||||
</DropdownMenu.ItemLabel>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Separator />
|
|
||||||
<DropdownMenu.Item
|
|
||||||
data-action="project-close-menu"
|
|
||||||
data-project={slug()}
|
|
||||||
onSelect={() => {
|
|
||||||
const dir = worktree()
|
|
||||||
if (!dir) return
|
|
||||||
closeProject(dir)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DropdownMenu.ItemLabel>{language.t("common.close")}</DropdownMenu.ItemLabel>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu.Portal>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-1 min-h-0 flex flex-col">
|
<div class="flex-1 min-h-0 flex flex-col">
|
||||||
<Show
|
<Show
|
||||||
when={workspacesEnabled()}
|
when={workspacesEnabled()}
|
||||||
fallback={
|
fallback={
|
||||||
|
<>
|
||||||
|
<div class="shrink-0 py-4">
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
icon="new-session"
|
||||||
|
class="w-full"
|
||||||
|
onClick={() => {
|
||||||
|
const dir = worktree()
|
||||||
|
if (!dir) return
|
||||||
|
navigateWithSidebarReset(`/${base64Encode(dir)}/session`)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{language.t("command.session.new")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-h-0">
|
||||||
|
<LocalWorkspace
|
||||||
|
ctx={workspaceSidebarCtx}
|
||||||
|
project={project()}
|
||||||
|
sortNow={sortNow}
|
||||||
|
mobile={panelProps.mobile}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<>
|
<>
|
||||||
<div class="shrink-0 py-4">
|
<div class="shrink-0 py-4">
|
||||||
<Button
|
<Button
|
||||||
size="large"
|
size="large"
|
||||||
icon="new-session"
|
icon="plus-small"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const dir = worktree()
|
const item = project()
|
||||||
if (!dir) return
|
if (!item) return
|
||||||
navigateWithSidebarReset(`/${base64Encode(dir)}/session`)
|
void createWorkspace(item)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{language.t("command.session.new")}
|
{language.t("workspace.new")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-h-0">
|
<div class="relative flex-1 min-h-0">
|
||||||
<LocalWorkspace
|
<DragDropProvider
|
||||||
ctx={workspaceSidebarCtx}
|
onDragStart={handleWorkspaceDragStart}
|
||||||
project={project()!}
|
onDragEnd={handleWorkspaceDragEnd}
|
||||||
sortNow={sortNow}
|
onDragOver={handleWorkspaceDragOver}
|
||||||
mobile={panelProps.mobile}
|
collisionDetector={closestCenter}
|
||||||
/>
|
>
|
||||||
|
<DragDropSensors />
|
||||||
|
<ConstrainDragXAxis />
|
||||||
|
<div
|
||||||
|
ref={(el) => {
|
||||||
|
if (!panelProps.mobile) scrollContainerRef = el
|
||||||
|
}}
|
||||||
|
class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar [overflow-anchor:none]"
|
||||||
|
>
|
||||||
|
<SortableProvider ids={workspaces()}>
|
||||||
|
<For each={workspaces()}>
|
||||||
|
{(directory) => (
|
||||||
|
<SortableWorkspace
|
||||||
|
ctx={workspaceSidebarCtx}
|
||||||
|
directory={directory}
|
||||||
|
project={project()}
|
||||||
|
sortNow={sortNow}
|
||||||
|
mobile={panelProps.mobile}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</SortableProvider>
|
||||||
|
</div>
|
||||||
|
<DragOverlay>
|
||||||
|
<WorkspaceDragOverlay
|
||||||
|
sidebarProject={sidebarProject}
|
||||||
|
activeWorkspace={() => store.activeWorkspace}
|
||||||
|
workspaceLabel={workspaceLabel}
|
||||||
|
/>
|
||||||
|
</DragOverlay>
|
||||||
|
</DragDropProvider>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
</Show>
|
||||||
>
|
</div>
|
||||||
<>
|
</>
|
||||||
<div class="shrink-0 py-4">
|
)}
|
||||||
<Button
|
|
||||||
size="large"
|
|
||||||
icon="plus-small"
|
|
||||||
class="w-full"
|
|
||||||
onClick={() => {
|
|
||||||
const item = project()
|
|
||||||
if (!item) return
|
|
||||||
void createWorkspace(item)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{language.t("workspace.new")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div class="relative flex-1 min-h-0">
|
|
||||||
<DragDropProvider
|
|
||||||
onDragStart={handleWorkspaceDragStart}
|
|
||||||
onDragEnd={handleWorkspaceDragEnd}
|
|
||||||
onDragOver={handleWorkspaceDragOver}
|
|
||||||
collisionDetector={closestCenter}
|
|
||||||
>
|
|
||||||
<DragDropSensors />
|
|
||||||
<ConstrainDragXAxis />
|
|
||||||
<div
|
|
||||||
ref={(el) => {
|
|
||||||
if (!panelProps.mobile) scrollContainerRef = el
|
|
||||||
}}
|
|
||||||
class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar [overflow-anchor:none]"
|
|
||||||
>
|
|
||||||
<SortableProvider ids={workspaces()}>
|
|
||||||
<For each={workspaces()}>
|
|
||||||
{(directory) => (
|
|
||||||
<SortableWorkspace
|
|
||||||
ctx={workspaceSidebarCtx}
|
|
||||||
directory={directory}
|
|
||||||
project={project()!}
|
|
||||||
sortNow={sortNow}
|
|
||||||
mobile={panelProps.mobile}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</SortableProvider>
|
|
||||||
</div>
|
|
||||||
<DragOverlay>
|
|
||||||
<WorkspaceDragOverlay
|
|
||||||
sidebarProject={sidebarProject}
|
|
||||||
activeWorkspace={() => store.activeWorkspace}
|
|
||||||
workspaceLabel={workspaceLabel}
|
|
||||||
/>
|
|
||||||
</DragOverlay>
|
|
||||||
</DragDropProvider>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -2355,6 +2367,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
|
<div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
|
||||||
|
{autoselecting() ?? ""}
|
||||||
<Titlebar />
|
<Titlebar />
|
||||||
<div class="flex-1 min-h-0 min-w-0 flex">
|
<div class="flex-1 min-h-0 min-w-0 flex">
|
||||||
<div class="flex-1 min-h-0 relative">
|
<div class="flex-1 min-h-0 relative">
|
||||||
|
|||||||
@@ -14,10 +14,11 @@ import { Spinner } from "@opencode-ai/ui/spinner"
|
|||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { type Session } from "@opencode-ai/sdk/v2/client"
|
import { type Session } from "@opencode-ai/sdk/v2/client"
|
||||||
import { type LocalProject } from "@/context/layout"
|
import { type LocalProject } from "@/context/layout"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { loadSessionsQuery, useGlobalSync } from "@/context/global-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items"
|
import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items"
|
||||||
import { sortedRootSessions, workspaceKey } from "./helpers"
|
import { sortedRootSessions, workspaceKey } from "./helpers"
|
||||||
|
import { useQuery } from "@tanstack/solid-query"
|
||||||
|
|
||||||
type InlineEditorComponent = (props: {
|
type InlineEditorComponent = (props: {
|
||||||
id: string
|
id: string
|
||||||
@@ -316,12 +317,11 @@ export const SortableWorkspace = (props: {
|
|||||||
})
|
})
|
||||||
const open = createMemo(() => props.ctx.workspaceExpanded(props.directory, local()))
|
const open = createMemo(() => props.ctx.workspaceExpanded(props.directory, local()))
|
||||||
const boot = createMemo(() => open() || active())
|
const boot = createMemo(() => open() || active())
|
||||||
const booted = createMemo((prev) => prev || workspaceStore.status === "complete", false)
|
|
||||||
const count = createMemo(() => sessions()?.length ?? 0)
|
const count = createMemo(() => sessions()?.length ?? 0)
|
||||||
const hasMore = createMemo(() => workspaceStore.sessionTotal > count())
|
const hasMore = createMemo(() => workspaceStore.sessionTotal > count())
|
||||||
|
const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree) }))
|
||||||
const busy = createMemo(() => props.ctx.isBusy(props.directory))
|
const busy = createMemo(() => props.ctx.isBusy(props.directory))
|
||||||
const wasBusy = createMemo((prev) => prev || busy(), false)
|
const loading = () => query.isLoading
|
||||||
const loading = createMemo(() => open() && !booted() && count() === 0 && !wasBusy())
|
|
||||||
const touch = createMediaQuery("(hover: none)")
|
const touch = createMediaQuery("(hover: none)")
|
||||||
const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id)))
|
const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id)))
|
||||||
const loadMore = async () => {
|
const loadMore = async () => {
|
||||||
@@ -426,7 +426,7 @@ export const SortableWorkspace = (props: {
|
|||||||
mobile={props.mobile}
|
mobile={props.mobile}
|
||||||
ctx={props.ctx}
|
ctx={props.ctx}
|
||||||
showNew={showNew}
|
showNew={showNew}
|
||||||
loading={loading}
|
loading={() => query.isLoading && count() === 0}
|
||||||
sessions={sessions}
|
sessions={sessions}
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
loadMore={loadMore}
|
loadMore={loadMore}
|
||||||
@@ -452,10 +452,10 @@ export const LocalWorkspace = (props: {
|
|||||||
})
|
})
|
||||||
const slug = createMemo(() => base64Encode(props.project.worktree))
|
const slug = createMemo(() => base64Encode(props.project.worktree))
|
||||||
const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow()))
|
const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow()))
|
||||||
const booted = createMemo((prev) => prev || workspace().store.status === "complete", false)
|
|
||||||
const count = createMemo(() => sessions()?.length ?? 0)
|
const count = createMemo(() => sessions()?.length ?? 0)
|
||||||
const loading = createMemo(() => !booted() && count() === 0)
|
const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree) }))
|
||||||
const hasMore = createMemo(() => workspace().store.sessionTotal > count())
|
const hasMore = createMemo(() => workspace().store.sessionTotal > count())
|
||||||
|
const loading = () => query.isLoading && count() === 0
|
||||||
const loadMore = async () => {
|
const loadMore = async () => {
|
||||||
workspace().setStore("limit", (limit) => (limit ?? 0) + 5)
|
workspace().setStore("limit", (limit) => (limit ?? 0) + 5)
|
||||||
await globalSync.project.loadSessions(props.project.worktree)
|
await globalSync.project.loadSessions(props.project.worktree)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Project, UserMessage, VcsFileDiff } from "@opencode-ai/sdk/v2"
|
import type { Project, UserMessage } from "@opencode-ai/sdk/v2"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { useMutation } from "@tanstack/solid-query"
|
import { createQuery, skipToken, useMutation, useQueryClient } from "@tanstack/solid-query"
|
||||||
import {
|
import {
|
||||||
batch,
|
batch,
|
||||||
onCleanup,
|
onCleanup,
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
on,
|
on,
|
||||||
onMount,
|
onMount,
|
||||||
untrack,
|
untrack,
|
||||||
|
createResource,
|
||||||
} from "solid-js"
|
} from "solid-js"
|
||||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||||
import { createMediaQuery } from "@solid-primitives/media"
|
import { createMediaQuery } from "@solid-primitives/media"
|
||||||
@@ -323,6 +324,7 @@ export default function Page() {
|
|||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
const file = useFile()
|
const file = useFile()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
@@ -432,7 +434,6 @@ export default function Page() {
|
|||||||
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||||
const isChildSession = createMemo(() => !!info()?.parentID)
|
const isChildSession = createMemo(() => !!info()?.parentID)
|
||||||
const diffs = createMemo(() => (params.id ? list(sync.data.session_diff[params.id]) : []))
|
const diffs = createMemo(() => (params.id ? list(sync.data.session_diff[params.id]) : []))
|
||||||
const sessionCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
|
|
||||||
const canReview = createMemo(() => !!sync.project)
|
const canReview = createMemo(() => !!sync.project)
|
||||||
const reviewTab = createMemo(() => isDesktop())
|
const reviewTab = createMemo(() => isDesktop())
|
||||||
const tabState = createSessionTabs({
|
const tabState = createSessionTabs({
|
||||||
@@ -518,26 +519,6 @@ export default function Page() {
|
|||||||
deferRender: false,
|
deferRender: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const [vcs, setVcs] = createStore<{
|
|
||||||
diff: {
|
|
||||||
git: VcsFileDiff[]
|
|
||||||
branch: VcsFileDiff[]
|
|
||||||
}
|
|
||||||
ready: {
|
|
||||||
git: boolean
|
|
||||||
branch: boolean
|
|
||||||
}
|
|
||||||
}>({
|
|
||||||
diff: {
|
|
||||||
git: [] as VcsFileDiff[],
|
|
||||||
branch: [] as VcsFileDiff[],
|
|
||||||
},
|
|
||||||
ready: {
|
|
||||||
git: false,
|
|
||||||
branch: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const [followup, setFollowup] = persisted(
|
const [followup, setFollowup] = persisted(
|
||||||
Persist.workspace(sdk.directory, "followup", ["followup.v1"]),
|
Persist.workspace(sdk.directory, "followup", ["followup.v1"]),
|
||||||
createStore<{
|
createStore<{
|
||||||
@@ -571,68 +552,6 @@ export default function Page() {
|
|||||||
let todoTimer: number | undefined
|
let todoTimer: number | undefined
|
||||||
let diffFrame: number | undefined
|
let diffFrame: number | undefined
|
||||||
let diffTimer: number | undefined
|
let diffTimer: number | undefined
|
||||||
const vcsTask = new Map<VcsMode, Promise<void>>()
|
|
||||||
const vcsRun = new Map<VcsMode, number>()
|
|
||||||
|
|
||||||
const bumpVcs = (mode: VcsMode) => {
|
|
||||||
const next = (vcsRun.get(mode) ?? 0) + 1
|
|
||||||
vcsRun.set(mode, next)
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetVcs = (mode?: VcsMode) => {
|
|
||||||
const list = mode ? [mode] : (["git", "branch"] as const)
|
|
||||||
list.forEach((item) => {
|
|
||||||
bumpVcs(item)
|
|
||||||
vcsTask.delete(item)
|
|
||||||
setVcs("diff", item, [])
|
|
||||||
setVcs("ready", item, false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadVcs = (mode: VcsMode, force = false) => {
|
|
||||||
if (sync.project?.vcs !== "git") return Promise.resolve()
|
|
||||||
if (!force && vcs.ready[mode]) return Promise.resolve()
|
|
||||||
|
|
||||||
if (force) {
|
|
||||||
if (vcsTask.has(mode)) bumpVcs(mode)
|
|
||||||
vcsTask.delete(mode)
|
|
||||||
setVcs("ready", mode, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const current = vcsTask.get(mode)
|
|
||||||
if (current) return current
|
|
||||||
|
|
||||||
const run = bumpVcs(mode)
|
|
||||||
|
|
||||||
const task = sdk.client.vcs
|
|
||||||
.diff({ mode })
|
|
||||||
.then((result) => {
|
|
||||||
if (vcsRun.get(mode) !== run) return
|
|
||||||
setVcs("diff", mode, list(result.data))
|
|
||||||
setVcs("ready", mode, true)
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
if (vcsRun.get(mode) !== run) return
|
|
||||||
console.debug("[session-review] failed to load vcs diff", { mode, error })
|
|
||||||
setVcs("diff", mode, [])
|
|
||||||
setVcs("ready", mode, true)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
if (vcsTask.get(mode) === task) vcsTask.delete(mode)
|
|
||||||
})
|
|
||||||
|
|
||||||
vcsTask.set(mode, task)
|
|
||||||
return task
|
|
||||||
}
|
|
||||||
|
|
||||||
const refreshVcs = () => {
|
|
||||||
resetVcs()
|
|
||||||
const mode = untrack(vcsMode)
|
|
||||||
if (!mode) return
|
|
||||||
if (!untrack(wantsReview)) return
|
|
||||||
void loadVcs(mode, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
createComputed((prev) => {
|
createComputed((prev) => {
|
||||||
const open = desktopReviewOpen()
|
const open = desktopReviewOpen()
|
||||||
@@ -663,21 +582,52 @@ export default function Page() {
|
|||||||
list.push("turn")
|
list.push("turn")
|
||||||
return list
|
return list
|
||||||
})
|
})
|
||||||
|
const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes")
|
||||||
|
const wantsReview = createMemo(() =>
|
||||||
|
isDesktop()
|
||||||
|
? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
|
||||||
|
: store.mobileTab === "changes",
|
||||||
|
)
|
||||||
const vcsMode = createMemo<VcsMode | undefined>(() => {
|
const vcsMode = createMemo<VcsMode | undefined>(() => {
|
||||||
if (store.changes === "git" || store.changes === "branch") return store.changes
|
if (store.changes === "git" || store.changes === "branch") return store.changes
|
||||||
})
|
})
|
||||||
const reviewDiffs = createMemo(() => {
|
const vcsKey = createMemo(
|
||||||
if (store.changes === "git") return list(vcs.diff.git)
|
() => ["session-vcs", sdk.directory, sync.data.vcs?.branch ?? "", sync.data.vcs?.default_branch ?? ""] as const,
|
||||||
if (store.changes === "branch") return list(vcs.diff.branch)
|
)
|
||||||
|
const vcsQuery = createQuery(() => {
|
||||||
|
const mode = vcsMode()
|
||||||
|
const enabled = wantsReview() && sync.project?.vcs === "git"
|
||||||
|
|
||||||
|
return {
|
||||||
|
queryKey: [...vcsKey(), mode] as const,
|
||||||
|
enabled,
|
||||||
|
staleTime: Number.POSITIVE_INFINITY,
|
||||||
|
gcTime: 60 * 1000,
|
||||||
|
queryFn: mode
|
||||||
|
? () =>
|
||||||
|
sdk.client.vcs
|
||||||
|
.diff({ mode })
|
||||||
|
.then((result) => list(result.data))
|
||||||
|
.catch((error) => {
|
||||||
|
console.debug("[session-review] failed to load vcs diff", { mode, error })
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
: skipToken,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const refreshVcs = () => void queryClient.invalidateQueries({ queryKey: vcsKey() })
|
||||||
|
const reviewDiffs = () => {
|
||||||
|
if (store.changes === "git" || store.changes === "branch")
|
||||||
|
// avoids suspense
|
||||||
|
return vcsQuery.isFetched ? (vcsQuery.data ?? []) : []
|
||||||
return turnDiffs()
|
return turnDiffs()
|
||||||
})
|
}
|
||||||
const reviewCount = createMemo(() => reviewDiffs().length)
|
const reviewCount = () => reviewDiffs().length
|
||||||
const hasReview = createMemo(() => reviewCount() > 0)
|
const hasReview = () => reviewCount() > 0
|
||||||
const reviewReady = createMemo(() => {
|
const reviewReady = () => {
|
||||||
if (store.changes === "git") return vcs.ready.git
|
if (store.changes === "git" || store.changes === "branch") return !vcsQuery.isPending
|
||||||
if (store.changes === "branch") return vcs.ready.branch
|
|
||||||
return true
|
return true
|
||||||
})
|
}
|
||||||
|
|
||||||
const newSessionWorktree = createMemo(() => {
|
const newSessionWorktree = createMemo(() => {
|
||||||
if (store.newSessionWorktree === "create") return "create"
|
if (store.newSessionWorktree === "create") return "create"
|
||||||
@@ -805,8 +755,9 @@ export default function Page() {
|
|||||||
|
|
||||||
const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs
|
const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs
|
||||||
|
|
||||||
createEffect(
|
const [sessionSync] = createResource(
|
||||||
on([() => sdk.directory, () => params.id] as const, ([, id]) => {
|
() => [sdk.directory, params.id] as const,
|
||||||
|
([directory, id]) => {
|
||||||
if (refreshFrame !== undefined) cancelAnimationFrame(refreshFrame)
|
if (refreshFrame !== undefined) cancelAnimationFrame(refreshFrame)
|
||||||
if (refreshTimer !== undefined) window.clearTimeout(refreshTimer)
|
if (refreshTimer !== undefined) window.clearTimeout(refreshTimer)
|
||||||
refreshFrame = undefined
|
refreshFrame = undefined
|
||||||
@@ -817,13 +768,10 @@ export default function Page() {
|
|||||||
const stale = !cached
|
const stale = !cached
|
||||||
? false
|
? false
|
||||||
: (() => {
|
: (() => {
|
||||||
const info = getSessionPrefetch(sdk.directory, id)
|
const info = getSessionPrefetch(directory, id)
|
||||||
if (!info) return true
|
if (!info) return true
|
||||||
return Date.now() - info.at > SESSION_PREFETCH_TTL
|
return Date.now() - info.at > SESSION_PREFETCH_TTL
|
||||||
})()
|
})()
|
||||||
untrack(() => {
|
|
||||||
void sync.session.sync(id)
|
|
||||||
})
|
|
||||||
|
|
||||||
refreshFrame = requestAnimationFrame(() => {
|
refreshFrame = requestAnimationFrame(() => {
|
||||||
refreshFrame = undefined
|
refreshFrame = undefined
|
||||||
@@ -835,7 +783,9 @@ export default function Page() {
|
|||||||
})
|
})
|
||||||
}, 0)
|
}, 0)
|
||||||
})
|
})
|
||||||
}),
|
|
||||||
|
return sync.session.sync(id)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
@@ -897,27 +847,6 @@ export default function Page() {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
() => sdk.directory,
|
|
||||||
() => {
|
|
||||||
resetVcs()
|
|
||||||
},
|
|
||||||
{ defer: true },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
() => [sync.data.vcs?.branch, sync.data.vcs?.default_branch] as const,
|
|
||||||
(next, prev) => {
|
|
||||||
if (prev === undefined || same(next, prev)) return
|
|
||||||
refreshVcs()
|
|
||||||
},
|
|
||||||
{ defer: true },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const stopVcs = sdk.event.listen((evt) => {
|
const stopVcs = sdk.event.listen((evt) => {
|
||||||
if (evt.details.type !== "file.watcher.updated") return
|
if (evt.details.type !== "file.watcher.updated") return
|
||||||
const props =
|
const props =
|
||||||
@@ -1051,13 +980,6 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes")
|
|
||||||
const wantsReview = createMemo(() =>
|
|
||||||
isDesktop()
|
|
||||||
? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
|
|
||||||
: store.mobileTab === "changes",
|
|
||||||
)
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const list = changesOptions()
|
const list = changesOptions()
|
||||||
if (list.includes(store.changes)) return
|
if (list.includes(store.changes)) return
|
||||||
@@ -1066,22 +988,12 @@ export default function Page() {
|
|||||||
setStore("changes", next)
|
setStore("changes", next)
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const mode = vcsMode()
|
|
||||||
if (!mode) return
|
|
||||||
if (!wantsReview()) return
|
|
||||||
void loadVcs(mode)
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => sync.data.session_status[params.id ?? ""]?.type,
|
() => sync.data.session_status[params.id ?? ""]?.type,
|
||||||
(next, prev) => {
|
(next, prev) => {
|
||||||
const mode = vcsMode()
|
|
||||||
if (!mode) return
|
|
||||||
if (!wantsReview()) return
|
|
||||||
if (next !== "idle" || prev === undefined || prev === "idle") return
|
if (next !== "idle" || prev === undefined || prev === "idle") return
|
||||||
void loadVcs(mode, true)
|
refreshVcs()
|
||||||
},
|
},
|
||||||
{ defer: true },
|
{ defer: true },
|
||||||
),
|
),
|
||||||
@@ -1882,6 +1794,7 @@ export default function Page() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="relative bg-background-base size-full overflow-hidden flex flex-col">
|
<div class="relative bg-background-base size-full overflow-hidden flex flex-col">
|
||||||
|
{sessionSync() ?? ""}
|
||||||
<SessionHeader />
|
<SessionHeader />
|
||||||
<div class="flex-1 min-h-0 flex flex-col md:flex-row">
|
<div class="flex-1 min-h-0 flex flex-col md:flex-row">
|
||||||
<Show when={!isDesktop() && !!params.id}>
|
<Show when={!isDesktop() && !!params.id}>
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ import { useCommand } from "@/context/command"
|
|||||||
import { useFile, type SelectedLineRange } from "@/context/file"
|
import { useFile, type SelectedLineRange } from "@/context/file"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
|
import { usePlatform } from "@/context/platform"
|
||||||
|
import { useSettings } from "@/context/settings"
|
||||||
|
import { useSync } from "@/context/sync"
|
||||||
import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
|
import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
|
||||||
import { FileTabContent } from "@/pages/session/file-tabs"
|
import { FileTabContent } from "@/pages/session/file-tabs"
|
||||||
import { createOpenSessionFileTab, createSessionTabs, getTabReorderIndex, type Sizing } from "@/pages/session/helpers"
|
import { createOpenSessionFileTab, createSessionTabs, getTabReorderIndex, type Sizing } from "@/pages/session/helpers"
|
||||||
@@ -39,6 +42,9 @@ export function SessionSidePanel(props: {
|
|||||||
size: Sizing
|
size: Sizing
|
||||||
}) {
|
}) {
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
|
const platform = usePlatform()
|
||||||
|
const settings = useSettings()
|
||||||
|
const sync = useSync()
|
||||||
const file = useFile()
|
const file = useFile()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
@@ -46,9 +52,15 @@ export function SessionSidePanel(props: {
|
|||||||
const { sessionKey, tabs, view } = useSessionLayout()
|
const { sessionKey, tabs, view } = useSessionLayout()
|
||||||
|
|
||||||
const isDesktop = createMediaQuery("(min-width: 768px)")
|
const isDesktop = createMediaQuery("(min-width: 768px)")
|
||||||
|
const shown = createMemo(
|
||||||
|
() =>
|
||||||
|
platform.platform !== "desktop" ||
|
||||||
|
import.meta.env.VITE_OPENCODE_CHANNEL !== "beta" ||
|
||||||
|
settings.general.showFileTree(),
|
||||||
|
)
|
||||||
|
|
||||||
const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
|
const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
|
||||||
const fileOpen = createMemo(() => isDesktop() && layout.fileTree.opened())
|
const fileOpen = createMemo(() => isDesktop() && shown() && layout.fileTree.opened())
|
||||||
const open = createMemo(() => reviewOpen() || fileOpen())
|
const open = createMemo(() => reviewOpen() || fileOpen())
|
||||||
const reviewTab = createMemo(() => isDesktop())
|
const reviewTab = createMemo(() => isDesktop())
|
||||||
const panelWidth = createMemo(() => {
|
const panelWidth = createMemo(() => {
|
||||||
@@ -341,98 +353,99 @@ export function SessionSidePanel(props: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<Show when={shown()}>
|
||||||
id="file-tree-panel"
|
|
||||||
aria-hidden={!fileOpen()}
|
|
||||||
inert={!fileOpen()}
|
|
||||||
class="relative min-w-0 h-full shrink-0 overflow-hidden"
|
|
||||||
classList={{
|
|
||||||
"pointer-events-none": !fileOpen(),
|
|
||||||
"transition-[width] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
|
|
||||||
!props.size.active(),
|
|
||||||
}}
|
|
||||||
style={{ width: treeWidth() }}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="h-full flex flex-col overflow-hidden group/filetree"
|
id="file-tree-panel"
|
||||||
classList={{ "border-l border-border-weaker-base": reviewOpen() }}
|
aria-hidden={!fileOpen()}
|
||||||
|
inert={!fileOpen()}
|
||||||
|
class="relative min-w-0 h-full shrink-0 overflow-hidden"
|
||||||
|
classList={{
|
||||||
|
"pointer-events-none": !fileOpen(),
|
||||||
|
"transition-[width] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
|
||||||
|
!props.size.active(),
|
||||||
|
}}
|
||||||
|
style={{ width: treeWidth() }}
|
||||||
>
|
>
|
||||||
<Tabs
|
<div
|
||||||
variant="pill"
|
class="h-full flex flex-col overflow-hidden group/filetree"
|
||||||
value={fileTreeTab()}
|
classList={{ "border-l border-border-weaker-base": reviewOpen() }}
|
||||||
onChange={setFileTreeTabValue}
|
|
||||||
class="h-full"
|
|
||||||
data-scope="filetree"
|
|
||||||
>
|
>
|
||||||
<Tabs.List>
|
<Tabs
|
||||||
<Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
|
variant="pill"
|
||||||
{props.reviewCount()}{" "}
|
value={fileTreeTab()}
|
||||||
{language.t(
|
onChange={setFileTreeTabValue}
|
||||||
props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other",
|
class="h-full"
|
||||||
)}
|
data-scope="filetree"
|
||||||
</Tabs.Trigger>
|
>
|
||||||
<Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}>
|
<Tabs.List>
|
||||||
{language.t("session.files.all")}
|
<Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
|
||||||
</Tabs.Trigger>
|
{props.reviewCount()}{" "}
|
||||||
</Tabs.List>
|
{language.t(
|
||||||
<Tabs.Content value="changes" class="bg-background-stronger px-3 py-0">
|
props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other",
|
||||||
<Switch>
|
)}
|
||||||
<Match when={props.hasReview() || !props.diffsReady()}>
|
</Tabs.Trigger>
|
||||||
<Show
|
<Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}>
|
||||||
when={props.diffsReady()}
|
{language.t("session.files.all")}
|
||||||
fallback={
|
</Tabs.Trigger>
|
||||||
<div class="px-2 py-2 text-12-regular text-text-weak">
|
</Tabs.List>
|
||||||
{language.t("common.loading")}
|
<Tabs.Content value="changes" class="bg-background-stronger px-3 py-0">
|
||||||
{language.t("common.loading.ellipsis")}
|
<Switch>
|
||||||
</div>
|
<Match when={props.hasReview() || !props.diffsReady()}>
|
||||||
}
|
<Show
|
||||||
>
|
when={props.diffsReady()}
|
||||||
|
fallback={
|
||||||
|
<div class="px-2 py-2 text-12-regular text-text-weak">
|
||||||
|
{language.t("common.loading")}
|
||||||
|
{language.t("common.loading.ellipsis")}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FileTree
|
||||||
|
path=""
|
||||||
|
class="pt-3"
|
||||||
|
allowed={diffFiles()}
|
||||||
|
kinds={kinds()}
|
||||||
|
draggable={false}
|
||||||
|
active={props.activeDiff}
|
||||||
|
onFileClick={(node) => props.focusReviewDiff(node.path)}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</Tabs.Content>
|
||||||
|
<Tabs.Content value="all" class="bg-background-stronger px-3 py-0">
|
||||||
|
<Switch>
|
||||||
|
<Match when={nofiles()}>{empty(language.t("session.files.empty"))}</Match>
|
||||||
|
<Match when={true}>
|
||||||
<FileTree
|
<FileTree
|
||||||
path=""
|
path=""
|
||||||
class="pt-3"
|
class="pt-3"
|
||||||
allowed={diffFiles()}
|
modified={diffFiles()}
|
||||||
kinds={kinds()}
|
kinds={kinds()}
|
||||||
draggable={false}
|
onFileClick={(node) => openTab(file.tab(node.path))}
|
||||||
active={props.activeDiff}
|
|
||||||
onFileClick={(node) => props.focusReviewDiff(node.path)}
|
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Match>
|
||||||
</Match>
|
</Switch>
|
||||||
<Match when={true}>{empty(props.empty())}</Match>
|
</Tabs.Content>
|
||||||
</Switch>
|
</Tabs>
|
||||||
</Tabs.Content>
|
|
||||||
<Tabs.Content value="all" class="bg-background-stronger px-3 py-0">
|
|
||||||
<Switch>
|
|
||||||
<Match when={nofiles()}>{empty(language.t("session.files.empty"))}</Match>
|
|
||||||
<Match when={true}>
|
|
||||||
<FileTree
|
|
||||||
path=""
|
|
||||||
class="pt-3"
|
|
||||||
modified={diffFiles()}
|
|
||||||
kinds={kinds()}
|
|
||||||
onFileClick={(node) => openTab(file.tab(node.path))}
|
|
||||||
/>
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</Tabs.Content>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
<Show when={fileOpen()}>
|
|
||||||
<div onPointerDown={() => props.size.start()}>
|
|
||||||
<ResizeHandle
|
|
||||||
direction="horizontal"
|
|
||||||
edge="start"
|
|
||||||
size={layout.fileTree.width()}
|
|
||||||
min={200}
|
|
||||||
max={480}
|
|
||||||
onResize={(width) => {
|
|
||||||
props.size.touch()
|
|
||||||
layout.fileTree.resize(width)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
<Show when={fileOpen()}>
|
||||||
</div>
|
<div onPointerDown={() => props.size.start()}>
|
||||||
|
<ResizeHandle
|
||||||
|
direction="horizontal"
|
||||||
|
edge="start"
|
||||||
|
size={layout.fileTree.width()}
|
||||||
|
min={200}
|
||||||
|
max={480}
|
||||||
|
onResize={(width) => {
|
||||||
|
props.size.touch()
|
||||||
|
layout.fileTree.resize(width)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import { useLanguage } from "@/context/language"
|
|||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { useLocal } from "@/context/local"
|
import { useLocal } from "@/context/local"
|
||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
|
import { usePlatform } from "@/context/platform"
|
||||||
import { usePrompt } from "@/context/prompt"
|
import { usePrompt } from "@/context/prompt"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
|
import { useSettings } from "@/context/settings"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useTerminal } from "@/context/terminal"
|
import { useTerminal } from "@/context/terminal"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
@@ -39,8 +41,10 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
const permission = usePermission()
|
const permission = usePermission()
|
||||||
|
const platform = usePlatform()
|
||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
|
const settings = useSettings()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const terminal = useTerminal()
|
const terminal = useTerminal()
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
@@ -66,6 +70,10 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
})
|
})
|
||||||
const activeFileTab = tabState.activeFileTab
|
const activeFileTab = tabState.activeFileTab
|
||||||
const closableTab = tabState.closableTab
|
const closableTab = tabState.closableTab
|
||||||
|
const shown = () =>
|
||||||
|
platform.platform !== "desktop" ||
|
||||||
|
import.meta.env.VITE_OPENCODE_CHANNEL !== "beta" ||
|
||||||
|
settings.general.showFileTree()
|
||||||
|
|
||||||
const idle = { type: "idle" as const }
|
const idle = { type: "idle" as const }
|
||||||
const status = () => sync.data.session_status[params.id ?? ""] ?? idle
|
const status = () => sync.data.session_status[params.id ?? ""] ?? idle
|
||||||
@@ -457,12 +465,16 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
keybind: "mod+shift+r",
|
keybind: "mod+shift+r",
|
||||||
onSelect: () => view().reviewPanel.toggle(),
|
onSelect: () => view().reviewPanel.toggle(),
|
||||||
}),
|
}),
|
||||||
viewCommand({
|
...(shown()
|
||||||
id: "fileTree.toggle",
|
? [
|
||||||
title: language.t("command.fileTree.toggle"),
|
viewCommand({
|
||||||
keybind: "mod+\\",
|
id: "fileTree.toggle",
|
||||||
onSelect: () => layout.fileTree.toggle(),
|
title: language.t("command.fileTree.toggle"),
|
||||||
}),
|
keybind: "mod+\\",
|
||||||
|
onSelect: () => layout.fileTree.toggle(),
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
: []),
|
||||||
viewCommand({
|
viewCommand({
|
||||||
id: "input.focus",
|
id: "input.focus",
|
||||||
title: language.t("command.input.focus"),
|
title: language.t("command.input.focus"),
|
||||||
|
|||||||
@@ -469,7 +469,7 @@ export function persisted<T>(
|
|||||||
state,
|
state,
|
||||||
setState,
|
setState,
|
||||||
init,
|
init,
|
||||||
Object.assign(() => ready() === true, {
|
Object.assign(() => (ready.loading ? false : ready.latest === true), {
|
||||||
promise: init instanceof Promise ? init : undefined,
|
promise: init instanceof Promise ? init : undefined,
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ export function createSdkForServer({
|
|||||||
|
|
||||||
return createOpencodeClient({
|
return createOpencodeClient({
|
||||||
...config,
|
...config,
|
||||||
headers: { ...config.headers, ...auth },
|
headers: {
|
||||||
|
...(config.headers instanceof Headers ? Object.fromEntries(config.headers.entries()) : config.headers),
|
||||||
|
...auth,
|
||||||
|
},
|
||||||
baseUrl: server.url,
|
baseUrl: server.url,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.4.6",
|
"version": "1.14.18",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const dict = {
|
|||||||
"nav.enterprise": "المؤسسات",
|
"nav.enterprise": "المؤسسات",
|
||||||
"nav.zen": "Zen",
|
"nav.zen": "Zen",
|
||||||
"nav.login": "تسجيل الدخول",
|
"nav.login": "تسجيل الدخول",
|
||||||
"nav.free": "مجانا",
|
"nav.free": "تحميل",
|
||||||
"nav.home": "الرئيسية",
|
"nav.home": "الرئيسية",
|
||||||
"nav.openMenu": "فتح القائمة",
|
"nav.openMenu": "فتح القائمة",
|
||||||
"nav.getStartedFree": "ابدأ مجانا",
|
"nav.getStartedFree": "ابدأ مجانا",
|
||||||
@@ -558,6 +558,13 @@ export const dict = {
|
|||||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "الاستخدام الحالي لـ",
|
"workspace.monthlyLimit.currentUsage.beforeMonth": "الاستخدام الحالي لـ",
|
||||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "هو $",
|
"workspace.monthlyLimit.currentUsage.beforeAmount": "هو $",
|
||||||
|
|
||||||
|
"workspace.redeem.title": "استرداد قسيمة",
|
||||||
|
"workspace.redeem.subtitle": "استرد رمز القسيمة للحصول على رصيد أو مزايا.",
|
||||||
|
"workspace.redeem.placeholder": "أدخل رمز القسيمة",
|
||||||
|
"workspace.redeem.redeem": "استرداد",
|
||||||
|
"workspace.redeem.redeeming": "جارٍ الاسترداد...",
|
||||||
|
"workspace.redeem.success": "تم استرداد القسيمة بنجاح.",
|
||||||
|
|
||||||
"workspace.reload.title": "إعادة الشحن التلقائي",
|
"workspace.reload.title": "إعادة الشحن التلقائي",
|
||||||
"workspace.reload.disabled.before": "إعادة الشحن التلقائي",
|
"workspace.reload.disabled.before": "إعادة الشحن التلقائي",
|
||||||
"workspace.reload.disabled.state": "معطّل",
|
"workspace.reload.disabled.state": "معطّل",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const dict = {
|
|||||||
"nav.enterprise": "Enterprise",
|
"nav.enterprise": "Enterprise",
|
||||||
"nav.zen": "Zen",
|
"nav.zen": "Zen",
|
||||||
"nav.login": "Entrar",
|
"nav.login": "Entrar",
|
||||||
"nav.free": "Grátis",
|
"nav.free": "Download",
|
||||||
"nav.home": "Início",
|
"nav.home": "Início",
|
||||||
"nav.openMenu": "Abrir menu",
|
"nav.openMenu": "Abrir menu",
|
||||||
"nav.getStartedFree": "Começar grátis",
|
"nav.getStartedFree": "Começar grátis",
|
||||||
@@ -567,6 +567,13 @@ export const dict = {
|
|||||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Uso atual para",
|
"workspace.monthlyLimit.currentUsage.beforeMonth": "Uso atual para",
|
||||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "é $",
|
"workspace.monthlyLimit.currentUsage.beforeAmount": "é $",
|
||||||
|
|
||||||
|
"workspace.redeem.title": "Resgatar Cupom",
|
||||||
|
"workspace.redeem.subtitle": "Resgate um código de cupom para receber créditos ou vantagens.",
|
||||||
|
"workspace.redeem.placeholder": "Digite o código do cupom",
|
||||||
|
"workspace.redeem.redeem": "Resgatar",
|
||||||
|
"workspace.redeem.redeeming": "Resgatando...",
|
||||||
|
"workspace.redeem.success": "Cupom resgatado com sucesso.",
|
||||||
|
|
||||||
"workspace.reload.title": "Recarga Automática",
|
"workspace.reload.title": "Recarga Automática",
|
||||||
"workspace.reload.disabled.before": "A recarga automática está",
|
"workspace.reload.disabled.before": "A recarga automática está",
|
||||||
"workspace.reload.disabled.state": "desativada",
|
"workspace.reload.disabled.state": "desativada",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const dict = {
|
|||||||
"nav.enterprise": "Enterprise",
|
"nav.enterprise": "Enterprise",
|
||||||
"nav.zen": "Zen",
|
"nav.zen": "Zen",
|
||||||
"nav.login": "Log ind",
|
"nav.login": "Log ind",
|
||||||
"nav.free": "Gratis",
|
"nav.free": "Download",
|
||||||
"nav.home": "Hjem",
|
"nav.home": "Hjem",
|
||||||
"nav.openMenu": "Åbn menu",
|
"nav.openMenu": "Åbn menu",
|
||||||
"nav.getStartedFree": "Kom i gang gratis",
|
"nav.getStartedFree": "Kom i gang gratis",
|
||||||
@@ -563,6 +563,13 @@ export const dict = {
|
|||||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Nuværende brug for",
|
"workspace.monthlyLimit.currentUsage.beforeMonth": "Nuværende brug for",
|
||||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "er $",
|
"workspace.monthlyLimit.currentUsage.beforeAmount": "er $",
|
||||||
|
|
||||||
|
"workspace.redeem.title": "Indløs kupon",
|
||||||
|
"workspace.redeem.subtitle": "Indløs en kuponkode for at få kreditter eller fordele.",
|
||||||
|
"workspace.redeem.placeholder": "Indtast kuponkode",
|
||||||
|
"workspace.redeem.redeem": "Indløs",
|
||||||
|
"workspace.redeem.redeeming": "Indløser...",
|
||||||
|
"workspace.redeem.success": "Kuponen blev indløst.",
|
||||||
|
|
||||||
"workspace.reload.title": "Automatisk genopfyldning",
|
"workspace.reload.title": "Automatisk genopfyldning",
|
||||||
"workspace.reload.disabled.before": "Automatisk genopfyldning er",
|
"workspace.reload.disabled.before": "Automatisk genopfyldning er",
|
||||||
"workspace.reload.disabled.state": "deaktiveret",
|
"workspace.reload.disabled.state": "deaktiveret",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const dict = {
|
|||||||
"nav.enterprise": "Enterprise",
|
"nav.enterprise": "Enterprise",
|
||||||
"nav.zen": "Zen",
|
"nav.zen": "Zen",
|
||||||
"nav.login": "Anmelden",
|
"nav.login": "Anmelden",
|
||||||
"nav.free": "Kostenlos",
|
"nav.free": "Download",
|
||||||
"nav.home": "Startseite",
|
"nav.home": "Startseite",
|
||||||
"nav.openMenu": "Menü öffnen",
|
"nav.openMenu": "Menü öffnen",
|
||||||
"nav.getStartedFree": "Kostenlos starten",
|
"nav.getStartedFree": "Kostenlos starten",
|
||||||
@@ -566,6 +566,13 @@ export const dict = {
|
|||||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Aktuelle Nutzung für",
|
"workspace.monthlyLimit.currentUsage.beforeMonth": "Aktuelle Nutzung für",
|
||||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "ist $",
|
"workspace.monthlyLimit.currentUsage.beforeAmount": "ist $",
|
||||||
|
|
||||||
|
"workspace.redeem.title": "Gutschein einlösen",
|
||||||
|
"workspace.redeem.subtitle": "Löse einen Gutscheincode ein, um Guthaben oder Vorteile zu erhalten.",
|
||||||
|
"workspace.redeem.placeholder": "Gutscheincode eingeben",
|
||||||
|
"workspace.redeem.redeem": "Einlösen",
|
||||||
|
"workspace.redeem.redeeming": "Wird eingelöst...",
|
||||||
|
"workspace.redeem.success": "Gutschein erfolgreich eingelöst.",
|
||||||
|
|
||||||
"workspace.reload.title": "Auto-Reload",
|
"workspace.reload.title": "Auto-Reload",
|
||||||
"workspace.reload.disabled.before": "Auto-Reload ist",
|
"workspace.reload.disabled.before": "Auto-Reload ist",
|
||||||
"workspace.reload.disabled.state": "deaktiviert",
|
"workspace.reload.disabled.state": "deaktiviert",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const dict = {
|
|||||||
"nav.zen": "Zen",
|
"nav.zen": "Zen",
|
||||||
"nav.go": "Go",
|
"nav.go": "Go",
|
||||||
"nav.login": "Login",
|
"nav.login": "Login",
|
||||||
"nav.free": "Free",
|
"nav.free": "Download",
|
||||||
"nav.home": "Home",
|
"nav.home": "Home",
|
||||||
"nav.openMenu": "Open menu",
|
"nav.openMenu": "Open menu",
|
||||||
"nav.getStartedFree": "Get started for free",
|
"nav.getStartedFree": "Get started for free",
|
||||||
@@ -559,6 +559,13 @@ export const dict = {
|
|||||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Current usage for",
|
"workspace.monthlyLimit.currentUsage.beforeMonth": "Current usage for",
|
||||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "is $",
|
"workspace.monthlyLimit.currentUsage.beforeAmount": "is $",
|
||||||
|
|
||||||
|
"workspace.redeem.title": "Redeem Coupon",
|
||||||
|
"workspace.redeem.subtitle": "Redeem a coupon code to claim credits or perks.",
|
||||||
|
"workspace.redeem.placeholder": "Enter coupon code",
|
||||||
|
"workspace.redeem.redeem": "Redeem",
|
||||||
|
"workspace.redeem.redeeming": "Redeeming...",
|
||||||
|
"workspace.redeem.success": "Coupon redeemed successfully.",
|
||||||
|
|
||||||
"workspace.reload.title": "Auto Reload",
|
"workspace.reload.title": "Auto Reload",
|
||||||
"workspace.reload.disabled.before": "Auto reload is",
|
"workspace.reload.disabled.before": "Auto reload is",
|
||||||
"workspace.reload.disabled.state": "disabled",
|
"workspace.reload.disabled.state": "disabled",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const dict = {
|
|||||||
"nav.enterprise": "Enterprise",
|
"nav.enterprise": "Enterprise",
|
||||||
"nav.zen": "Zen",
|
"nav.zen": "Zen",
|
||||||
"nav.login": "Iniciar sesión",
|
"nav.login": "Iniciar sesión",
|
||||||
"nav.free": "Gratis",
|
"nav.free": "Descargar",
|
||||||
"nav.home": "Inicio",
|
"nav.home": "Inicio",
|
||||||
"nav.openMenu": "Abrir menú",
|
"nav.openMenu": "Abrir menú",
|
||||||
"nav.getStartedFree": "Empezar gratis",
|
"nav.getStartedFree": "Empezar gratis",
|
||||||
@@ -567,6 +567,13 @@ export const dict = {
|
|||||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Uso actual para",
|
"workspace.monthlyLimit.currentUsage.beforeMonth": "Uso actual para",
|
||||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "es $",
|
"workspace.monthlyLimit.currentUsage.beforeAmount": "es $",
|
||||||
|
|
||||||
|
"workspace.redeem.title": "Canjear cupón",
|
||||||
|
"workspace.redeem.subtitle": "Canjea un código de cupón para obtener crédito o beneficios.",
|
||||||
|
"workspace.redeem.placeholder": "Introduce el código del cupón",
|
||||||
|
"workspace.redeem.redeem": "Canjear",
|
||||||
|
"workspace.redeem.redeeming": "Canjeando...",
|
||||||
|
"workspace.redeem.success": "Cupón canjeado correctamente.",
|
||||||
|
|
||||||
"workspace.reload.title": "Auto Recarga",
|
"workspace.reload.title": "Auto Recarga",
|
||||||
"workspace.reload.disabled.before": "La auto recarga está",
|
"workspace.reload.disabled.before": "La auto recarga está",
|
||||||
"workspace.reload.disabled.state": "deshabilitada",
|
"workspace.reload.disabled.state": "deshabilitada",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const dict = {
|
|||||||
"nav.enterprise": "Entreprise",
|
"nav.enterprise": "Entreprise",
|
||||||
"nav.zen": "Zen",
|
"nav.zen": "Zen",
|
||||||
"nav.login": "Se connecter",
|
"nav.login": "Se connecter",
|
||||||
"nav.free": "Gratuit",
|
"nav.free": "Télécharger",
|
||||||
"nav.home": "Accueil",
|
"nav.home": "Accueil",
|
||||||
"nav.openMenu": "Ouvrir le menu",
|
"nav.openMenu": "Ouvrir le menu",
|
||||||
"nav.getStartedFree": "Commencer gratuitement",
|
"nav.getStartedFree": "Commencer gratuitement",
|
||||||
@@ -569,6 +569,13 @@ export const dict = {
|
|||||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "L'utilisation actuelle pour",
|
"workspace.monthlyLimit.currentUsage.beforeMonth": "L'utilisation actuelle pour",
|
||||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "est de",
|
"workspace.monthlyLimit.currentUsage.beforeAmount": "est de",
|
||||||
|
|
||||||
|
"workspace.redeem.title": "Utiliser un coupon",
|
||||||
|
"workspace.redeem.subtitle": "Utilisez un code promo pour obtenir du crédit ou des avantages.",
|
||||||
|
"workspace.redeem.placeholder": "Saisissez le code promo",
|
||||||
|
"workspace.redeem.redeem": "Utiliser",
|
||||||
|
"workspace.redeem.redeeming": "Utilisation...",
|
||||||
|
"workspace.redeem.success": "Coupon utilisé avec succès.",
|
||||||
|
|
||||||
"workspace.reload.title": "Rechargement automatique",
|
"workspace.reload.title": "Rechargement automatique",
|
||||||
"workspace.reload.disabled.before": "Le rechargement automatique est",
|
"workspace.reload.disabled.before": "Le rechargement automatique est",
|
||||||
"workspace.reload.disabled.state": "désactivé",
|
"workspace.reload.disabled.state": "désactivé",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const dict = {
|
|||||||
"nav.enterprise": "Enterprise",
|
"nav.enterprise": "Enterprise",
|
||||||
"nav.zen": "Zen",
|
"nav.zen": "Zen",
|
||||||
"nav.login": "Accedi",
|
"nav.login": "Accedi",
|
||||||
"nav.free": "Gratis",
|
"nav.free": "Scarica",
|
||||||
"nav.home": "Home",
|
"nav.home": "Home",
|
||||||
"nav.openMenu": "Apri menu",
|
"nav.openMenu": "Apri menu",
|
||||||
"nav.getStartedFree": "Inizia gratis",
|
"nav.getStartedFree": "Inizia gratis",
|
||||||
@@ -565,6 +565,13 @@ export const dict = {
|
|||||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Utilizzo attuale per",
|
"workspace.monthlyLimit.currentUsage.beforeMonth": "Utilizzo attuale per",
|
||||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "è $",
|
"workspace.monthlyLimit.currentUsage.beforeAmount": "è $",
|
||||||
|
|
||||||
|
"workspace.redeem.title": "Riscatta Coupon",
|
||||||
|
"workspace.redeem.subtitle": "Riscatta un codice coupon per ottenere credito o vantaggi.",
|
||||||
|
"workspace.redeem.placeholder": "Inserisci il codice coupon",
|
||||||
|
"workspace.redeem.redeem": "Riscatta",
|
||||||
|
"workspace.redeem.redeeming": "Riscatto in corso...",
|
||||||
|
"workspace.redeem.success": "Coupon riscattato con successo.",
|
||||||
|
|
||||||
"workspace.reload.title": "Ricarica Auto",
|
"workspace.reload.title": "Ricarica Auto",
|
||||||
"workspace.reload.disabled.before": "La ricarica auto è",
|
"workspace.reload.disabled.before": "La ricarica auto è",
|
||||||
"workspace.reload.disabled.state": "disabilitata",
|
"workspace.reload.disabled.state": "disabilitata",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const dict = {
|
|||||||
"nav.enterprise": "エンタープライズ",
|
"nav.enterprise": "エンタープライズ",
|
||||||
"nav.zen": "Zen",
|
"nav.zen": "Zen",
|
||||||
"nav.login": "ログイン",
|
"nav.login": "ログイン",
|
||||||
"nav.free": "無料",
|
"nav.free": "ダウンロード",
|
||||||
"nav.home": "ホーム",
|
"nav.home": "ホーム",
|
||||||
"nav.openMenu": "メニューを開く",
|
"nav.openMenu": "メニューを開く",
|
||||||
"nav.getStartedFree": "無料ではじめる",
|
"nav.getStartedFree": "無料ではじめる",
|
||||||
@@ -564,6 +564,13 @@ export const dict = {
|
|||||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "現在の使用状況(",
|
"workspace.monthlyLimit.currentUsage.beforeMonth": "現在の使用状況(",
|
||||||
"workspace.monthlyLimit.currentUsage.beforeAmount": ")は $",
|
"workspace.monthlyLimit.currentUsage.beforeAmount": ")は $",
|
||||||
|
|
||||||
|
"workspace.redeem.title": "クーポンを利用",
|
||||||
|
"workspace.redeem.subtitle": "クーポンコードを利用して、クレジットや特典を受け取ります。",
|
||||||
|
"workspace.redeem.placeholder": "クーポンコードを入力",
|
||||||
|
"workspace.redeem.redeem": "利用する",
|
||||||
|
"workspace.redeem.redeeming": "利用中...",
|
||||||
|
"workspace.redeem.success": "クーポンを利用しました。",
|
||||||
|
|
||||||
"workspace.reload.title": "自動チャージ",
|
"workspace.reload.title": "自動チャージ",
|
||||||
"workspace.reload.disabled.before": "自動チャージは",
|
"workspace.reload.disabled.before": "自動チャージは",
|
||||||
"workspace.reload.disabled.state": "無効",
|
"workspace.reload.disabled.state": "無効",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const dict = {
|
|||||||
"nav.enterprise": "엔터프라이즈",
|
"nav.enterprise": "엔터프라이즈",
|
||||||
"nav.zen": "Zen",
|
"nav.zen": "Zen",
|
||||||
"nav.login": "로그인",
|
"nav.login": "로그인",
|
||||||
"nav.free": "무료",
|
"nav.free": "다운로드",
|
||||||
"nav.home": "홈",
|
"nav.home": "홈",
|
||||||
"nav.openMenu": "메뉴 열기",
|
"nav.openMenu": "메뉴 열기",
|
||||||
"nav.getStartedFree": "무료로 시작하기",
|
"nav.getStartedFree": "무료로 시작하기",
|
||||||
@@ -558,6 +558,13 @@ export const dict = {
|
|||||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "현재",
|
"workspace.monthlyLimit.currentUsage.beforeMonth": "현재",
|
||||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "사용량: $",
|
"workspace.monthlyLimit.currentUsage.beforeAmount": "사용량: $",
|
||||||
|
|
||||||
|
"workspace.redeem.title": "쿠폰 사용",
|
||||||
|
"workspace.redeem.subtitle": "쿠폰 코드를 사용해 크레딧이나 혜택을 받으세요.",
|
||||||
|
"workspace.redeem.placeholder": "쿠폰 코드를 입력하세요",
|
||||||
|
"workspace.redeem.redeem": "사용",
|
||||||
|
"workspace.redeem.redeeming": "사용 중...",
|
||||||
|
"workspace.redeem.success": "쿠폰을 성공적으로 사용했습니다.",
|
||||||
|
|
||||||
"workspace.reload.title": "자동 충전",
|
"workspace.reload.title": "자동 충전",
|
||||||
"workspace.reload.disabled.before": "자동 충전이",
|
"workspace.reload.disabled.before": "자동 충전이",
|
||||||
"workspace.reload.disabled.state": "비활성화",
|
"workspace.reload.disabled.state": "비활성화",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const dict = {
|
|||||||
"nav.enterprise": "Enterprise",
|
"nav.enterprise": "Enterprise",
|
||||||
"nav.zen": "Zen",
|
"nav.zen": "Zen",
|
||||||
"nav.login": "Logg inn",
|
"nav.login": "Logg inn",
|
||||||
"nav.free": "Gratis",
|
"nav.free": "Last ned",
|
||||||
"nav.home": "Hjem",
|
"nav.home": "Hjem",
|
||||||
"nav.openMenu": "Åpne meny",
|
"nav.openMenu": "Åpne meny",
|
||||||
"nav.getStartedFree": "Kom i gang gratis",
|
"nav.getStartedFree": "Kom i gang gratis",
|
||||||
@@ -564,6 +564,13 @@ export const dict = {
|
|||||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Gjeldende forbruk for",
|
"workspace.monthlyLimit.currentUsage.beforeMonth": "Gjeldende forbruk for",
|
||||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "er $",
|
"workspace.monthlyLimit.currentUsage.beforeAmount": "er $",
|
||||||
|
|
||||||
|
"workspace.redeem.title": "Løs inn kupong",
|
||||||
|
"workspace.redeem.subtitle": "Løs inn en kupongkode for å få kreditt eller fordeler.",
|
||||||
|
"workspace.redeem.placeholder": "Skriv inn kupongkode",
|
||||||
|
"workspace.redeem.redeem": "Løs inn",
|
||||||
|
"workspace.redeem.redeeming": "Løser inn...",
|
||||||
|
"workspace.redeem.success": "Kupongen ble løst inn.",
|
||||||
|
|
||||||
"workspace.reload.title": "Auto-påfyll",
|
"workspace.reload.title": "Auto-påfyll",
|
||||||
"workspace.reload.disabled.before": "Auto-påfyll er",
|
"workspace.reload.disabled.before": "Auto-påfyll er",
|
||||||
"workspace.reload.disabled.state": "deaktivert",
|
"workspace.reload.disabled.state": "deaktivert",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const dict = {
|
|||||||
"nav.enterprise": "Enterprise",
|
"nav.enterprise": "Enterprise",
|
||||||
"nav.zen": "Zen",
|
"nav.zen": "Zen",
|
||||||
"nav.login": "Zaloguj się",
|
"nav.login": "Zaloguj się",
|
||||||
"nav.free": "Darmowe",
|
"nav.free": "Pobierz",
|
||||||
"nav.home": "Strona główna",
|
"nav.home": "Strona główna",
|
||||||
"nav.openMenu": "Otwórz menu",
|
"nav.openMenu": "Otwórz menu",
|
||||||
"nav.getStartedFree": "Zacznij za darmo",
|
"nav.getStartedFree": "Zacznij za darmo",
|
||||||
@@ -565,6 +565,13 @@ export const dict = {
|
|||||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Aktualne użycie za",
|
"workspace.monthlyLimit.currentUsage.beforeMonth": "Aktualne użycie za",
|
||||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "wynosi $",
|
"workspace.monthlyLimit.currentUsage.beforeAmount": "wynosi $",
|
||||||
|
|
||||||
|
"workspace.redeem.title": "Zrealizuj kupon",
|
||||||
|
"workspace.redeem.subtitle": "Zrealizuj kod kuponu, aby otrzymać środki lub korzyści.",
|
||||||
|
"workspace.redeem.placeholder": "Wpisz kod kuponu",
|
||||||
|
"workspace.redeem.redeem": "Zrealizuj",
|
||||||
|
"workspace.redeem.redeeming": "Realizowanie...",
|
||||||
|
"workspace.redeem.success": "Kupon został zrealizowany.",
|
||||||
|
|
||||||
"workspace.reload.title": "Automatyczne doładowanie",
|
"workspace.reload.title": "Automatyczne doładowanie",
|
||||||
"workspace.reload.disabled.before": "Automatyczne doładowanie jest",
|
"workspace.reload.disabled.before": "Automatyczne doładowanie jest",
|
||||||
"workspace.reload.disabled.state": "wyłączone",
|
"workspace.reload.disabled.state": "wyłączone",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const dict = {
|
|||||||
"nav.enterprise": "Enterprise",
|
"nav.enterprise": "Enterprise",
|
||||||
"nav.zen": "Zen",
|
"nav.zen": "Zen",
|
||||||
"nav.login": "Войти",
|
"nav.login": "Войти",
|
||||||
"nav.free": "Бесплатно",
|
"nav.free": "Скачать",
|
||||||
"nav.home": "Главная",
|
"nav.home": "Главная",
|
||||||
"nav.openMenu": "Открыть меню",
|
"nav.openMenu": "Открыть меню",
|
||||||
"nav.getStartedFree": "Начать бесплатно",
|
"nav.getStartedFree": "Начать бесплатно",
|
||||||
@@ -571,6 +571,13 @@ export const dict = {
|
|||||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Текущее использование за",
|
"workspace.monthlyLimit.currentUsage.beforeMonth": "Текущее использование за",
|
||||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "составляет $",
|
"workspace.monthlyLimit.currentUsage.beforeAmount": "составляет $",
|
||||||
|
|
||||||
|
"workspace.redeem.title": "Активировать купон",
|
||||||
|
"workspace.redeem.subtitle": "Активируйте код купона, чтобы получить кредит или бонусы.",
|
||||||
|
"workspace.redeem.placeholder": "Введите код купона",
|
||||||
|
"workspace.redeem.redeem": "Активировать",
|
||||||
|
"workspace.redeem.redeeming": "Активация...",
|
||||||
|
"workspace.redeem.success": "Купон успешно активирован.",
|
||||||
|
|
||||||
"workspace.reload.title": "Автопополнение",
|
"workspace.reload.title": "Автопополнение",
|
||||||
"workspace.reload.disabled.before": "Автопополнение",
|
"workspace.reload.disabled.before": "Автопополнение",
|
||||||
"workspace.reload.disabled.state": "отключено",
|
"workspace.reload.disabled.state": "отключено",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const dict = {
|
|||||||
"nav.enterprise": "องค์กร",
|
"nav.enterprise": "องค์กร",
|
||||||
"nav.zen": "Zen",
|
"nav.zen": "Zen",
|
||||||
"nav.login": "เข้าสู่ระบบ",
|
"nav.login": "เข้าสู่ระบบ",
|
||||||
"nav.free": "ฟรี",
|
"nav.free": "ดาวน์โหลด",
|
||||||
"nav.home": "หน้าหลัก",
|
"nav.home": "หน้าหลัก",
|
||||||
"nav.openMenu": "เปิดเมนู",
|
"nav.openMenu": "เปิดเมนู",
|
||||||
"nav.getStartedFree": "เริ่มต้นฟรี",
|
"nav.getStartedFree": "เริ่มต้นฟรี",
|
||||||
@@ -560,6 +560,13 @@ export const dict = {
|
|||||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "การใช้งานปัจจุบันสำหรับ",
|
"workspace.monthlyLimit.currentUsage.beforeMonth": "การใช้งานปัจจุบันสำหรับ",
|
||||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "คือ $",
|
"workspace.monthlyLimit.currentUsage.beforeAmount": "คือ $",
|
||||||
|
|
||||||
|
"workspace.redeem.title": "แลกคูปอง",
|
||||||
|
"workspace.redeem.subtitle": "แลกรหัสคูปองเพื่อรับเครดิตหรือสิทธิพิเศษ",
|
||||||
|
"workspace.redeem.placeholder": "กรอกรหัสคูปอง",
|
||||||
|
"workspace.redeem.redeem": "แลก",
|
||||||
|
"workspace.redeem.redeeming": "กำลังแลก...",
|
||||||
|
"workspace.redeem.success": "แลกคูปองสำเร็จ",
|
||||||
|
|
||||||
"workspace.reload.title": "โหลดซ้ำอัตโนมัติ",
|
"workspace.reload.title": "โหลดซ้ำอัตโนมัติ",
|
||||||
"workspace.reload.disabled.before": "การโหลดซ้ำอัตโนมัติ",
|
"workspace.reload.disabled.before": "การโหลดซ้ำอัตโนมัติ",
|
||||||
"workspace.reload.disabled.state": "ปิดใช้งานอยู่",
|
"workspace.reload.disabled.state": "ปิดใช้งานอยู่",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const dict = {
|
|||||||
"nav.enterprise": "Kurumsal",
|
"nav.enterprise": "Kurumsal",
|
||||||
"nav.zen": "Zen",
|
"nav.zen": "Zen",
|
||||||
"nav.login": "Giriş",
|
"nav.login": "Giriş",
|
||||||
"nav.free": "Ücretsiz",
|
"nav.free": "İndir",
|
||||||
"nav.home": "Ana sayfa",
|
"nav.home": "Ana sayfa",
|
||||||
"nav.openMenu": "Menüyü aç",
|
"nav.openMenu": "Menüyü aç",
|
||||||
"nav.getStartedFree": "Ücretsiz başla",
|
"nav.getStartedFree": "Ücretsiz başla",
|
||||||
@@ -567,6 +567,13 @@ export const dict = {
|
|||||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Şu anki kullanım",
|
"workspace.monthlyLimit.currentUsage.beforeMonth": "Şu anki kullanım",
|
||||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "$",
|
"workspace.monthlyLimit.currentUsage.beforeAmount": "$",
|
||||||
|
|
||||||
|
"workspace.redeem.title": "Kupon Kullan",
|
||||||
|
"workspace.redeem.subtitle": "Kredi veya avantajlardan yararlanmak için bir kupon kodu kullanın.",
|
||||||
|
"workspace.redeem.placeholder": "Kupon kodunu girin",
|
||||||
|
"workspace.redeem.redeem": "Kullan",
|
||||||
|
"workspace.redeem.redeeming": "Kullanılıyor...",
|
||||||
|
"workspace.redeem.success": "Kupon başarıyla kullanıldı.",
|
||||||
|
|
||||||
"workspace.reload.title": "Otomatik Yeniden Yükleme",
|
"workspace.reload.title": "Otomatik Yeniden Yükleme",
|
||||||
"workspace.reload.disabled.before": "Otomatik yeniden yükleme:",
|
"workspace.reload.disabled.before": "Otomatik yeniden yükleme:",
|
||||||
"workspace.reload.disabled.state": "devre dışı",
|
"workspace.reload.disabled.state": "devre dışı",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const dict = {
|
|||||||
"nav.enterprise": "企业版",
|
"nav.enterprise": "企业版",
|
||||||
"nav.zen": "Zen",
|
"nav.zen": "Zen",
|
||||||
"nav.login": "登录",
|
"nav.login": "登录",
|
||||||
"nav.free": "免费",
|
"nav.free": "下载",
|
||||||
"nav.home": "首页",
|
"nav.home": "首页",
|
||||||
"nav.openMenu": "打开菜单",
|
"nav.openMenu": "打开菜单",
|
||||||
"nav.getStartedFree": "免费开始",
|
"nav.getStartedFree": "免费开始",
|
||||||
@@ -542,6 +542,13 @@ export const dict = {
|
|||||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "当前",
|
"workspace.monthlyLimit.currentUsage.beforeMonth": "当前",
|
||||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "的使用量为 $",
|
"workspace.monthlyLimit.currentUsage.beforeAmount": "的使用量为 $",
|
||||||
|
|
||||||
|
"workspace.redeem.title": "兑换优惠券",
|
||||||
|
"workspace.redeem.subtitle": "兑换优惠码以领取充值额度或权益。",
|
||||||
|
"workspace.redeem.placeholder": "输入优惠码",
|
||||||
|
"workspace.redeem.redeem": "兑换",
|
||||||
|
"workspace.redeem.redeeming": "兑换中...",
|
||||||
|
"workspace.redeem.success": "优惠券兑换成功。",
|
||||||
|
|
||||||
"workspace.reload.title": "自动充值",
|
"workspace.reload.title": "自动充值",
|
||||||
"workspace.reload.disabled.before": "自动充值已",
|
"workspace.reload.disabled.before": "自动充值已",
|
||||||
"workspace.reload.disabled.state": "禁用",
|
"workspace.reload.disabled.state": "禁用",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const dict = {
|
|||||||
"nav.enterprise": "企業",
|
"nav.enterprise": "企業",
|
||||||
"nav.zen": "Zen",
|
"nav.zen": "Zen",
|
||||||
"nav.login": "登入",
|
"nav.login": "登入",
|
||||||
"nav.free": "免費",
|
"nav.free": "下載",
|
||||||
"nav.home": "首頁",
|
"nav.home": "首頁",
|
||||||
"nav.openMenu": "開啟選單",
|
"nav.openMenu": "開啟選單",
|
||||||
"nav.getStartedFree": "免費開始使用",
|
"nav.getStartedFree": "免費開始使用",
|
||||||
@@ -542,6 +542,13 @@ export const dict = {
|
|||||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "目前",
|
"workspace.monthlyLimit.currentUsage.beforeMonth": "目前",
|
||||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "的使用量為 $",
|
"workspace.monthlyLimit.currentUsage.beforeAmount": "的使用量為 $",
|
||||||
|
|
||||||
|
"workspace.redeem.title": "兌換優惠券",
|
||||||
|
"workspace.redeem.subtitle": "兌換優惠碼以領取儲值額度或權益。",
|
||||||
|
"workspace.redeem.placeholder": "輸入優惠碼",
|
||||||
|
"workspace.redeem.redeem": "兌換",
|
||||||
|
"workspace.redeem.redeeming": "兌換中...",
|
||||||
|
"workspace.redeem.success": "優惠券兌換成功。",
|
||||||
|
|
||||||
"workspace.reload.title": "自動儲值",
|
"workspace.reload.title": "自動儲值",
|
||||||
"workspace.reload.disabled.before": "自動儲值已",
|
"workspace.reload.disabled.before": "自動儲值已",
|
||||||
"workspace.reload.disabled.state": "停用",
|
"workspace.reload.disabled.state": "停用",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { APIEvent } from "@solidjs/start/server"
|
import type { APIEvent } from "@solidjs/start/server"
|
||||||
import { AWS } from "@opencode-ai/console-core/aws.js"
|
import { AWS } from "@opencode-ai/console-core/aws.js"
|
||||||
|
import { Resource } from "@opencode-ai/console-resource"
|
||||||
import { i18n } from "~/i18n"
|
import { i18n } from "~/i18n"
|
||||||
import { localeFromRequest } from "~/lib/language"
|
import { localeFromRequest } from "~/lib/language"
|
||||||
import { createLead } from "~/lib/salesforce"
|
import { createLead } from "~/lib/salesforce"
|
||||||
@@ -14,6 +15,64 @@ interface EnterpriseFormData {
|
|||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EMAIL_OCTOPUS_LIST_ID = "1b381e5e-39bd-11f1-ba4a-cdd4791f0c43"
|
||||||
|
|
||||||
|
function splitFullName(fullName: string) {
|
||||||
|
const parts = fullName
|
||||||
|
.trim()
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter((p) => p.length > 0)
|
||||||
|
if (parts.length === 0) return { firstName: "", lastName: "" }
|
||||||
|
if (parts.length === 1) return { firstName: parts[0], lastName: "" }
|
||||||
|
return { firstName: parts[0], lastName: parts.slice(1).join(" ") }
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEmailOctopusApiKey() {
|
||||||
|
if (process.env.EMAILOCTOPUS_API_KEY) return process.env.EMAILOCTOPUS_API_KEY
|
||||||
|
try {
|
||||||
|
return Resource.EMAILOCTOPUS_API_KEY.value
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function subscribe(email: string, fullName: string) {
|
||||||
|
const apiKey = getEmailOctopusApiKey()
|
||||||
|
if (!apiKey) {
|
||||||
|
console.warn("Skipping EmailOctopus subscribe: missing API key")
|
||||||
|
return Promise.resolve(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = splitFullName(fullName)
|
||||||
|
const fields: Record<string, string> = {}
|
||||||
|
if (name.firstName) fields.FirstName = name.firstName
|
||||||
|
if (name.lastName) fields.LastName = name.lastName
|
||||||
|
|
||||||
|
const payload: { email_address: string; fields?: Record<string, string> } = { email_address: email }
|
||||||
|
if (Object.keys(fields).length) payload.fields = fields
|
||||||
|
|
||||||
|
return fetch(`https://api.emailoctopus.com/lists/${EMAIL_OCTOPUS_LIST_ID}/contacts`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
}).then(
|
||||||
|
(res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error("EmailOctopus subscribe failed:", res.status, res.statusText)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
console.error("Failed to subscribe enterprise email:", err)
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export async function POST(event: APIEvent) {
|
export async function POST(event: APIEvent) {
|
||||||
const dict = i18n(localeFromRequest(event.request))
|
const dict = i18n(localeFromRequest(event.request))
|
||||||
try {
|
try {
|
||||||
@@ -41,7 +100,7 @@ ${body.role}<br>
|
|||||||
${body.company ? `${body.company}<br>` : ""}${body.email}<br>
|
${body.company ? `${body.company}<br>` : ""}${body.email}<br>
|
||||||
${body.phone ? `${body.phone}<br>` : ""}`.trim()
|
${body.phone ? `${body.phone}<br>` : ""}`.trim()
|
||||||
|
|
||||||
const [lead, mail] = await Promise.all([
|
const [lead, mail, octopus] = await Promise.all([
|
||||||
createLead({
|
createLead({
|
||||||
name: body.name,
|
name: body.name,
|
||||||
role: body.role,
|
role: body.role,
|
||||||
@@ -49,6 +108,9 @@ ${body.phone ? `${body.phone}<br>` : ""}`.trim()
|
|||||||
email: body.email,
|
email: body.email,
|
||||||
phone: body.phone,
|
phone: body.phone,
|
||||||
message: body.message,
|
message: body.message,
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error("Failed to create Salesforce lead:", err)
|
||||||
|
return false
|
||||||
}),
|
}),
|
||||||
AWS.sendEmail({
|
AWS.sendEmail({
|
||||||
to: "contact@anoma.ly",
|
to: "contact@anoma.ly",
|
||||||
@@ -62,9 +124,14 @@ ${body.phone ? `${body.phone}<br>` : ""}`.trim()
|
|||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
subscribe(body.email, body.name),
|
||||||
])
|
])
|
||||||
|
|
||||||
if (!lead && !mail) {
|
if (!lead && !mail && !octopus) {
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.warn("Enterprise inquiry accepted in dev mode without integrations", { email: body.email })
|
||||||
|
return Response.json({ success: true, message: dict["enterprise.form.success.submitted"] }, { status: 200 })
|
||||||
|
}
|
||||||
console.error("Enterprise inquiry delivery failed", { email: body.email })
|
console.error("Enterprise inquiry delivery failed", { email: body.email })
|
||||||
return Response.json({ error: dict["enterprise.form.error.internalServer"] }, { status: 500 })
|
return Response.json({ error: dict["enterprise.form.error.internalServer"] }, { status: 500 })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { APIEvent } from "@solidjs/start"
|
import type { APIEvent } from "@solidjs/start"
|
||||||
import type { DownloadPlatform } from "../types"
|
import type { DownloadPlatform } from "../types"
|
||||||
|
|
||||||
const assetNames: Record<string, string> = {
|
const prodAssetNames: Record<string, string> = {
|
||||||
"darwin-aarch64-dmg": "opencode-desktop-darwin-aarch64.dmg",
|
"darwin-aarch64-dmg": "opencode-desktop-darwin-aarch64.dmg",
|
||||||
"darwin-x64-dmg": "opencode-desktop-darwin-x64.dmg",
|
"darwin-x64-dmg": "opencode-desktop-darwin-x64.dmg",
|
||||||
"windows-x64-nsis": "opencode-desktop-windows-x64.exe",
|
"windows-x64-nsis": "opencode-desktop-windows-x64.exe",
|
||||||
@@ -10,6 +10,15 @@ const assetNames: Record<string, string> = {
|
|||||||
"linux-x64-rpm": "opencode-desktop-linux-x86_64.rpm",
|
"linux-x64-rpm": "opencode-desktop-linux-x86_64.rpm",
|
||||||
} satisfies Record<DownloadPlatform, string>
|
} satisfies Record<DownloadPlatform, string>
|
||||||
|
|
||||||
|
const betaAssetNames: Record<string, string> = {
|
||||||
|
"darwin-aarch64-dmg": "opencode-electron-mac-arm64.dmg",
|
||||||
|
"darwin-x64-dmg": "opencode-electron-mac-x64.dmg",
|
||||||
|
"windows-x64-nsis": "opencode-electron-win-x64.exe",
|
||||||
|
"linux-x64-deb": "opencode-electron-linux-amd64.deb",
|
||||||
|
"linux-x64-appimage": "opencode-electron-linux-x86_64.AppImage",
|
||||||
|
"linux-x64-rpm": "opencode-electron-linux-x86_64.rpm",
|
||||||
|
} satisfies Record<DownloadPlatform, string>
|
||||||
|
|
||||||
// Doing this on the server lets us preserve the original name for platforms we don't care to rename for
|
// Doing this on the server lets us preserve the original name for platforms we don't care to rename for
|
||||||
const downloadNames: Record<string, string> = {
|
const downloadNames: Record<string, string> = {
|
||||||
"darwin-aarch64-dmg": "OpenCode Desktop.dmg",
|
"darwin-aarch64-dmg": "OpenCode Desktop.dmg",
|
||||||
@@ -18,7 +27,7 @@ const downloadNames: Record<string, string> = {
|
|||||||
} satisfies { [K in DownloadPlatform]?: string }
|
} satisfies { [K in DownloadPlatform]?: string }
|
||||||
|
|
||||||
export async function GET({ params: { platform, channel } }: APIEvent) {
|
export async function GET({ params: { platform, channel } }: APIEvent) {
|
||||||
const assetName = assetNames[platform]
|
const assetName = channel === "stable" ? prodAssetNames[platform] : betaAssetNames[platform]
|
||||||
if (!assetName) return new Response(null, { status: 404 })
|
if (!assetName) return new Response(null, { status: 404 })
|
||||||
|
|
||||||
const resp = await fetch(
|
const resp = await fetch(
|
||||||
@@ -37,5 +46,5 @@ export async function GET({ params: { platform, channel } }: APIEvent) {
|
|||||||
const headers = new Headers(resp.headers)
|
const headers = new Headers(resp.headers)
|
||||||
if (downloadName) headers.set("content-disposition", `attachment; filename="${downloadName}"`)
|
if (downloadName) headers.set("content-disposition", `attachment; filename="${downloadName}"`)
|
||||||
|
|
||||||
return new Response(resp.body, { ...resp, headers })
|
return new Response(resp.body, { status: resp.status, statusText: resp.statusText, headers })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { Header } from "~/component/header"
|
|||||||
import { Footer } from "~/component/footer"
|
import { Footer } from "~/component/footer"
|
||||||
import { Legal } from "~/component/legal"
|
import { Legal } from "~/component/legal"
|
||||||
import { github } from "~/lib/github"
|
import { github } from "~/lib/github"
|
||||||
import { createMemo } from "solid-js"
|
|
||||||
import { config } from "~/config"
|
import { config } from "~/config"
|
||||||
import { useI18n } from "~/context/i18n"
|
import { useI18n } from "~/context/i18n"
|
||||||
import { useLanguage } from "~/context/language"
|
import { useLanguage } from "~/context/language"
|
||||||
@@ -30,7 +29,7 @@ function CopyStatus() {
|
|||||||
export default function Home() {
|
export default function Home() {
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const githubData = createAsync(() => github())
|
const _githubData = createAsync(() => github())
|
||||||
const handleCopyClick = (event: Event) => {
|
const handleCopyClick = (event: Event) => {
|
||||||
const button = event.currentTarget as HTMLButtonElement
|
const button = event.currentTarget as HTMLButtonElement
|
||||||
const text = button.textContent
|
const text = button.textContent
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Actor } from "@opencode-ai/console-core/actor.js"
|
|||||||
import { Resource } from "@opencode-ai/console-resource"
|
import { Resource } from "@opencode-ai/console-resource"
|
||||||
import { LiteData } from "@opencode-ai/console-core/lite.js"
|
import { LiteData } from "@opencode-ai/console-core/lite.js"
|
||||||
import { BlackData } from "@opencode-ai/console-core/black.js"
|
import { BlackData } from "@opencode-ai/console-core/black.js"
|
||||||
|
import { User } from "@opencode-ai/console-core/user.js"
|
||||||
|
|
||||||
export async function POST(input: APIEvent) {
|
export async function POST(input: APIEvent) {
|
||||||
const body = await Billing.stripe().webhooks.constructEventAsync(
|
const body = await Billing.stripe().webhooks.constructEventAsync(
|
||||||
@@ -109,6 +110,8 @@ export async function POST(input: APIEvent) {
|
|||||||
if (type === "lite") {
|
if (type === "lite") {
|
||||||
const workspaceID = body.data.object.metadata?.workspaceID
|
const workspaceID = body.data.object.metadata?.workspaceID
|
||||||
const userID = body.data.object.metadata?.userID
|
const userID = body.data.object.metadata?.userID
|
||||||
|
const userEmail = body.data.object.metadata?.userEmail
|
||||||
|
const coupon = body.data.object.metadata?.coupon
|
||||||
const customerID = body.data.object.customer as string
|
const customerID = body.data.object.customer as string
|
||||||
const invoiceID = body.data.object.latest_invoice as string
|
const invoiceID = body.data.object.latest_invoice as string
|
||||||
const subscriptionID = body.data.object.id as string
|
const subscriptionID = body.data.object.id as string
|
||||||
@@ -156,6 +159,10 @@ export async function POST(input: APIEvent) {
|
|||||||
id: Identifier.create("lite"),
|
id: Identifier.create("lite"),
|
||||||
userID: userID,
|
userID: userID,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (userEmail && coupon === LiteData.firstMonth100Coupon) {
|
||||||
|
await Billing.redeemCoupon(userEmail, "GOFREEMONTH")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,9 +116,9 @@ const createSessionUrl = action(async (workspaceID: string, returnUrl: string) =
|
|||||||
|
|
||||||
const setUseBalance = action(async (form: FormData) => {
|
const setUseBalance = action(async (form: FormData) => {
|
||||||
"use server"
|
"use server"
|
||||||
const workspaceID = form.get("workspaceID")?.toString()
|
const workspaceID = form.get("workspaceID") as string | null
|
||||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||||
const useBalance = form.get("useBalance")?.toString() === "true"
|
const useBalance = (form.get("useBalance") as string | null) === "true"
|
||||||
|
|
||||||
return json(
|
return json(
|
||||||
await withActor(async () => {
|
await withActor(async () => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { BillingSection } from "./billing-section"
|
|||||||
import { ReloadSection } from "./reload-section"
|
import { ReloadSection } from "./reload-section"
|
||||||
import { PaymentSection } from "./payment-section"
|
import { PaymentSection } from "./payment-section"
|
||||||
import { BlackSection } from "./black-section"
|
import { BlackSection } from "./black-section"
|
||||||
|
import { RedeemSection } from "./redeem-section"
|
||||||
import { createMemo, Show } from "solid-js"
|
import { createMemo, Show } from "solid-js"
|
||||||
import { createAsync, useParams } from "@solidjs/router"
|
import { createAsync, useParams } from "@solidjs/router"
|
||||||
import { queryBillingInfo, querySessionInfo } from "../../common"
|
import { queryBillingInfo, querySessionInfo } from "../../common"
|
||||||
@@ -21,6 +22,7 @@ export default function () {
|
|||||||
<BlackSection />
|
<BlackSection />
|
||||||
</Show>
|
</Show>
|
||||||
<BillingSection />
|
<BillingSection />
|
||||||
|
<RedeemSection />
|
||||||
<Show when={billingInfo()?.customerID}>
|
<Show when={billingInfo()?.customerID}>
|
||||||
<ReloadSection />
|
<ReloadSection />
|
||||||
<MonthlyLimitSection />
|
<MonthlyLimitSection />
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import { formError, localizeError } from "~/lib/form-error"
|
|||||||
|
|
||||||
const setMonthlyLimit = action(async (form: FormData) => {
|
const setMonthlyLimit = action(async (form: FormData) => {
|
||||||
"use server"
|
"use server"
|
||||||
const limit = form.get("limit")?.toString()
|
const limit = form.get("limit") as string | null
|
||||||
if (!limit) return { error: formError.limitRequired }
|
if (!limit) return { error: formError.limitRequired }
|
||||||
const numericLimit = parseInt(limit)
|
const numericLimit = parseInt(limit)
|
||||||
if (numericLimit < 0) return { error: formError.monthlyLimitInvalid }
|
if (numericLimit < 0) return { error: formError.monthlyLimitInvalid }
|
||||||
const workspaceID = form.get("workspaceID")?.toString()
|
const workspaceID = form.get("workspaceID") as string | null
|
||||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||||
return json(
|
return json(
|
||||||
await withActor(
|
await withActor(
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
.root {
|
||||||
|
[data-slot="redeem-container"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
min-width: 20rem;
|
||||||
|
width: fit-content;
|
||||||
|
|
||||||
|
@media (max-width: 30rem) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="redeem-form"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
|
||||||
|
[data-slot="input-row"] {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
@media (max-width: 30rem) {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
flex: 1;
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--color-text-disabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="form-error"] {
|
||||||
|
color: var(--color-danger);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="form-success"] {
|
||||||
|
color: var(--color-success, var(--color-accent));
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { json, action, useParams, useSubmission } from "@solidjs/router"
|
||||||
|
import { Show } from "solid-js"
|
||||||
|
import { withActor } from "~/context/auth.withActor"
|
||||||
|
import { Billing } from "@opencode-ai/console-core/billing.js"
|
||||||
|
import { User } from "@opencode-ai/console-core/user.js"
|
||||||
|
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||||
|
import { CouponType } from "@opencode-ai/console-core/schema/billing.sql.js"
|
||||||
|
import styles from "./redeem-section.module.css"
|
||||||
|
import { queryBillingInfo } from "../../common"
|
||||||
|
import { useI18n } from "~/context/i18n"
|
||||||
|
import { formError, localizeError } from "~/lib/form-error"
|
||||||
|
|
||||||
|
const redeem = action(async (form: FormData) => {
|
||||||
|
"use server"
|
||||||
|
const workspaceID = form.get("workspaceID") as string | null
|
||||||
|
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||||
|
const code = (form.get("code") as string | null)?.trim().toUpperCase()
|
||||||
|
if (!code) return { error: "Coupon code is required." }
|
||||||
|
if (!(CouponType as readonly string[]).includes(code)) return { error: "Invalid coupon code." }
|
||||||
|
|
||||||
|
return json(
|
||||||
|
await withActor(async () => {
|
||||||
|
const actor = Actor.assert("user")
|
||||||
|
const email = await User.getAuthEmail(actor.properties.userID)
|
||||||
|
if (!email) return { error: "No email on account." }
|
||||||
|
return Billing.redeemCoupon(email, code as (typeof CouponType)[number])
|
||||||
|
.then(() => ({ error: undefined, data: true }))
|
||||||
|
.catch((e) => ({ error: e.message as string }))
|
||||||
|
}, workspaceID),
|
||||||
|
{ revalidate: queryBillingInfo.key },
|
||||||
|
)
|
||||||
|
}, "billing.redeemCoupon")
|
||||||
|
|
||||||
|
export function RedeemSection() {
|
||||||
|
const params = useParams()
|
||||||
|
const i18n = useI18n()
|
||||||
|
const submission = useSubmission(redeem)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section class={styles.root}>
|
||||||
|
<div data-slot="section-title">
|
||||||
|
<h2>{i18n.t("workspace.redeem.title")}</h2>
|
||||||
|
<p>{i18n.t("workspace.redeem.subtitle")}</p>
|
||||||
|
</div>
|
||||||
|
<div data-slot="redeem-container">
|
||||||
|
<form action={redeem} method="post" data-slot="redeem-form">
|
||||||
|
<div data-slot="input-row">
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
data-component="input"
|
||||||
|
name="code"
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder={i18n.t("workspace.redeem.placeholder")}
|
||||||
|
/>
|
||||||
|
<button type="submit" data-color="primary" disabled={submission.pending}>
|
||||||
|
{submission.pending ? i18n.t("workspace.redeem.redeeming") : i18n.t("workspace.redeem.redeem")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Show when={submission.result && (submission.result as any).error}>
|
||||||
|
{(err: any) => <div data-slot="form-error">{localizeError(i18n.t, err())}</div>}
|
||||||
|
</Show>
|
||||||
|
<Show when={submission.result && !(submission.result as any).error && (submission.result as any).data}>
|
||||||
|
<div data-slot="form-success">{i18n.t("workspace.redeem.success")}</div>
|
||||||
|
</Show>
|
||||||
|
<input type="hidden" name="workspaceID" value={params.id} />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ import { formError, formErrorReloadAmountMin, formErrorReloadTriggerMin, localiz
|
|||||||
|
|
||||||
const reload = action(async (form: FormData) => {
|
const reload = action(async (form: FormData) => {
|
||||||
"use server"
|
"use server"
|
||||||
const workspaceID = form.get("workspaceID")?.toString()
|
const workspaceID = form.get("workspaceID") as string | null
|
||||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||||
return json(await withActor(() => Billing.reload(), workspaceID), {
|
return json(await withActor(() => Billing.reload(), workspaceID), {
|
||||||
revalidate: queryBillingInfo.key,
|
revalidate: queryBillingInfo.key,
|
||||||
@@ -21,11 +21,11 @@ const reload = action(async (form: FormData) => {
|
|||||||
|
|
||||||
const setReload = action(async (form: FormData) => {
|
const setReload = action(async (form: FormData) => {
|
||||||
"use server"
|
"use server"
|
||||||
const workspaceID = form.get("workspaceID")?.toString()
|
const workspaceID = form.get("workspaceID") as string | null
|
||||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||||
const reloadValue = form.get("reload")?.toString() === "true"
|
const reloadValue = (form.get("reload") as string | null) === "true"
|
||||||
const amountStr = form.get("reloadAmount")?.toString()
|
const amountStr = form.get("reloadAmount") as string | null
|
||||||
const triggerStr = form.get("reloadTrigger")?.toString()
|
const triggerStr = form.get("reloadTrigger") as string | null
|
||||||
|
|
||||||
const reloadAmount = amountStr && amountStr.trim() !== "" ? parseInt(amountStr) : null
|
const reloadAmount = amountStr && amountStr.trim() !== "" ? parseInt(amountStr) : null
|
||||||
const reloadTrigger = triggerStr && triggerStr.trim() !== "" ? parseInt(triggerStr) : null
|
const reloadTrigger = triggerStr && triggerStr.trim() !== "" ? parseInt(triggerStr) : null
|
||||||
@@ -91,8 +91,8 @@ export function ReloadSection() {
|
|||||||
const info = billingInfo()!
|
const info = billingInfo()!
|
||||||
setStore("show", true)
|
setStore("show", true)
|
||||||
setStore("reload", true)
|
setStore("reload", true)
|
||||||
setStore("reloadAmount", info.reloadAmount.toString())
|
setStore("reloadAmount", String(info.reloadAmount))
|
||||||
setStore("reloadTrigger", info.reloadTrigger.toString())
|
setStore("reloadTrigger", String(info.reloadTrigger))
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
@@ -152,11 +152,11 @@ export function ReloadSection() {
|
|||||||
data-component="input"
|
data-component="input"
|
||||||
name="reloadAmount"
|
name="reloadAmount"
|
||||||
type="number"
|
type="number"
|
||||||
min={billingInfo()?.reloadAmountMin.toString()}
|
min={String(billingInfo()?.reloadAmountMin ?? "")}
|
||||||
step="1"
|
step="1"
|
||||||
value={store.reloadAmount}
|
value={store.reloadAmount}
|
||||||
onInput={(e) => setStore("reloadAmount", e.currentTarget.value)}
|
onInput={(e) => setStore("reloadAmount", e.currentTarget.value)}
|
||||||
placeholder={billingInfo()?.reloadAmount.toString()}
|
placeholder={String(billingInfo()?.reloadAmount ?? "")}
|
||||||
disabled={!store.reload}
|
disabled={!store.reload}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -166,11 +166,11 @@ export function ReloadSection() {
|
|||||||
data-component="input"
|
data-component="input"
|
||||||
name="reloadTrigger"
|
name="reloadTrigger"
|
||||||
type="number"
|
type="number"
|
||||||
min={billingInfo()?.reloadTriggerMin.toString()}
|
min={String(billingInfo()?.reloadTriggerMin ?? "")}
|
||||||
step="1"
|
step="1"
|
||||||
value={store.reloadTrigger}
|
value={store.reloadTrigger}
|
||||||
onInput={(e) => setStore("reloadTrigger", e.currentTarget.value)}
|
onInput={(e) => setStore("reloadTrigger", e.currentTarget.value)}
|
||||||
placeholder={billingInfo()?.reloadTrigger.toString()}
|
placeholder={String(billingInfo()?.reloadTrigger ?? "")}
|
||||||
disabled={!store.reload}
|
disabled={!store.reload}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -120,9 +120,9 @@ const createSessionUrl = action(async (workspaceID: string, returnUrl: string) =
|
|||||||
|
|
||||||
const setLiteUseBalance = action(async (form: FormData) => {
|
const setLiteUseBalance = action(async (form: FormData) => {
|
||||||
"use server"
|
"use server"
|
||||||
const workspaceID = form.get("workspaceID")?.toString()
|
const workspaceID = form.get("workspaceID") as string | null
|
||||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||||
const useBalance = form.get("useBalance")?.toString() === "true"
|
const useBalance = (form.get("useBalance") as string | null) === "true"
|
||||||
|
|
||||||
return json(
|
return json(
|
||||||
await withActor(async () => {
|
await withActor(async () => {
|
||||||
|
|||||||
@@ -12,18 +12,18 @@ import { formError, localizeError } from "~/lib/form-error"
|
|||||||
|
|
||||||
const removeKey = action(async (form: FormData) => {
|
const removeKey = action(async (form: FormData) => {
|
||||||
"use server"
|
"use server"
|
||||||
const id = form.get("id")?.toString()
|
const id = form.get("id") as string | null
|
||||||
if (!id) return { error: formError.idRequired }
|
if (!id) return { error: formError.idRequired }
|
||||||
const workspaceID = form.get("workspaceID")?.toString()
|
const workspaceID = form.get("workspaceID") as string | null
|
||||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||||
return json(await withActor(() => Key.remove({ id }), workspaceID), { revalidate: listKeys.key })
|
return json(await withActor(() => Key.remove({ id }), workspaceID), { revalidate: listKeys.key })
|
||||||
}, "key.remove")
|
}, "key.remove")
|
||||||
|
|
||||||
const createKey = action(async (form: FormData) => {
|
const createKey = action(async (form: FormData) => {
|
||||||
"use server"
|
"use server"
|
||||||
const name = form.get("name")?.toString().trim()
|
const name = (form.get("name") as string | null)?.trim()
|
||||||
if (!name) return { error: formError.nameRequired }
|
if (!name) return { error: formError.nameRequired }
|
||||||
const workspaceID = form.get("workspaceID")?.toString()
|
const workspaceID = form.get("workspaceID") as string | null
|
||||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||||
return json(
|
return json(
|
||||||
await withActor(
|
await withActor(
|
||||||
|
|||||||
@@ -24,13 +24,13 @@ const listMembers = query(async (workspaceID: string) => {
|
|||||||
|
|
||||||
const inviteMember = action(async (form: FormData) => {
|
const inviteMember = action(async (form: FormData) => {
|
||||||
"use server"
|
"use server"
|
||||||
const email = form.get("email")?.toString().trim()
|
const email = (form.get("email") as string | null)?.trim()
|
||||||
if (!email) return { error: formError.emailRequired }
|
if (!email) return { error: formError.emailRequired }
|
||||||
const workspaceID = form.get("workspaceID")?.toString()
|
const workspaceID = form.get("workspaceID") as string | null
|
||||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||||
const role = form.get("role")?.toString() as (typeof UserRole)[number]
|
const role = form.get("role") as (typeof UserRole)[number] | null
|
||||||
if (!role) return { error: formError.roleRequired }
|
if (!role) return { error: formError.roleRequired }
|
||||||
const limit = form.get("limit")?.toString()
|
const limit = form.get("limit") as string | null
|
||||||
const monthlyLimit = limit && limit.trim() !== "" ? parseInt(limit) : null
|
const monthlyLimit = limit && limit.trim() !== "" ? parseInt(limit) : null
|
||||||
if (monthlyLimit !== null && monthlyLimit < 0) return { error: formError.monthlyLimitInvalid }
|
if (monthlyLimit !== null && monthlyLimit < 0) return { error: formError.monthlyLimitInvalid }
|
||||||
return json(
|
return json(
|
||||||
@@ -47,9 +47,9 @@ const inviteMember = action(async (form: FormData) => {
|
|||||||
|
|
||||||
const removeMember = action(async (form: FormData) => {
|
const removeMember = action(async (form: FormData) => {
|
||||||
"use server"
|
"use server"
|
||||||
const id = form.get("id")?.toString()
|
const id = form.get("id") as string | null
|
||||||
if (!id) return { error: formError.idRequired }
|
if (!id) return { error: formError.idRequired }
|
||||||
const workspaceID = form.get("workspaceID")?.toString()
|
const workspaceID = form.get("workspaceID") as string | null
|
||||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||||
return json(
|
return json(
|
||||||
await withActor(
|
await withActor(
|
||||||
@@ -66,13 +66,13 @@ const removeMember = action(async (form: FormData) => {
|
|||||||
const updateMember = action(async (form: FormData) => {
|
const updateMember = action(async (form: FormData) => {
|
||||||
"use server"
|
"use server"
|
||||||
|
|
||||||
const id = form.get("id")?.toString()
|
const id = form.get("id") as string | null
|
||||||
if (!id) return { error: formError.idRequired }
|
if (!id) return { error: formError.idRequired }
|
||||||
const workspaceID = form.get("workspaceID")?.toString()
|
const workspaceID = form.get("workspaceID") as string | null
|
||||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||||
const role = form.get("role")?.toString() as (typeof UserRole)[number]
|
const role = form.get("role") as (typeof UserRole)[number] | null
|
||||||
if (!role) return { error: formError.roleRequired }
|
if (!role) return { error: formError.roleRequired }
|
||||||
const limit = form.get("limit")?.toString()
|
const limit = form.get("limit") as string | null
|
||||||
const monthlyLimit = limit && limit.trim() !== "" ? parseInt(limit) : null
|
const monthlyLimit = limit && limit.trim() !== "" ? parseInt(limit) : null
|
||||||
if (monthlyLimit !== null && monthlyLimit < 0) return { error: formError.monthlyLimitInvalid }
|
if (monthlyLimit !== null && monthlyLimit < 0) return { error: formError.monthlyLimitInvalid }
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ function MemberRow(props: {
|
|||||||
}
|
}
|
||||||
setStore("editing", true)
|
setStore("editing", true)
|
||||||
setStore("selectedRole", props.member.role)
|
setStore("selectedRole", props.member.role)
|
||||||
setStore("limit", props.member.monthlyLimit?.toString() ?? "")
|
setStore("limit", props.member.monthlyLimit != null ? String(props.member.monthlyLimit) : "")
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
|
|||||||
@@ -67,11 +67,11 @@ const getModelsInfo = query(async (workspaceID: string) => {
|
|||||||
|
|
||||||
const updateModel = action(async (form: FormData) => {
|
const updateModel = action(async (form: FormData) => {
|
||||||
"use server"
|
"use server"
|
||||||
const model = form.get("model")?.toString()
|
const model = form.get("model") as string | null
|
||||||
if (!model) return { error: formError.modelRequired }
|
if (!model) return { error: formError.modelRequired }
|
||||||
const workspaceID = form.get("workspaceID")?.toString()
|
const workspaceID = form.get("workspaceID") as string | null
|
||||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||||
const enabled = form.get("enabled")?.toString() === "true"
|
const enabled = (form.get("enabled") as string | null) === "true"
|
||||||
return json(
|
return json(
|
||||||
withActor(async () => {
|
withActor(async () => {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
@@ -163,7 +163,7 @@ export function ModelSection() {
|
|||||||
<form action={updateModel} method="post">
|
<form action={updateModel} method="post">
|
||||||
<input type="hidden" name="model" value={id} />
|
<input type="hidden" name="model" value={id} />
|
||||||
<input type="hidden" name="workspaceID" value={params.id} />
|
<input type="hidden" name="workspaceID" value={params.id} />
|
||||||
<input type="hidden" name="enabled" value={isEnabled().toString()} />
|
<input type="hidden" name="enabled" value={String(isEnabled())} />
|
||||||
<label data-slot="model-toggle-label">
|
<label data-slot="model-toggle-label">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user