mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-15 17:08:21 -04:00
Compare commits
195 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 26ad7d7561 | |||
| 0fb44051ee | |||
| 1b096b4148 | |||
| be227503af | |||
| 4ddfa7c6fa | |||
| 2415434ad9 | |||
| bf4c647653 | |||
| f35bb5184c | |||
| 2c5335d84c | |||
| 7793db3ac8 | |||
| 5f77482a29 | |||
| 30aec297d8 | |||
| a9c810cbbc | |||
| fe2e4e21d0 | |||
| 03a2504d3d | |||
| bace18ccd3 | |||
| cd2e6f06ee | |||
| 7e7ad37736 | |||
| ca8db315a9 | |||
| 9eb07ab55a | |||
| e07e420637 | |||
| 84f94e6143 | |||
| a150424c16 | |||
| 2e71292f2f | |||
| e2527db3c7 | |||
| a1dee8b273 | |||
| 04e5ca9d1b | |||
| 6dd4d1473c | |||
| 31c5454ee6 | |||
| 31b233e9db | |||
| 567d6ed1a3 | |||
| 2bde20c2ba | |||
| 92c70c9c3d | |||
| 318dbe93ba | |||
| 8bd249d8cf | |||
| 710e408709 | |||
| dac0dd5309 | |||
| bf05e8a122 | |||
| 51b10b128e | |||
| 47a45601fd | |||
| 69623c2b79 | |||
| 38536cf733 | |||
| 8bf0675997 | |||
| ff967e582c | |||
| 20bf18ffb4 | |||
| 51891d56e7 | |||
| cc22646467 | |||
| eb70b6137b | |||
| 6e2bcafd34 | |||
| 07b983e82f | |||
| c51a1588c7 | |||
| f43b0d3afd | |||
| 722f4dd416 | |||
| bb82aab5c8 | |||
| 936363e7a8 | |||
| 8688ed7e3e | |||
| 5b54203168 | |||
| bed780fac9 | |||
| 14ec7ed5b3 | |||
| 2c65273204 | |||
| 2e0f88d0e3 | |||
| e1073e5d18 | |||
| 649618c50a | |||
| 02608a4e97 | |||
| 3ad6923c61 | |||
| e4300e9b74 | |||
| 538cfaff0d | |||
| 1dad38d1b5 | |||
| c9e2a38bf4 | |||
| 5863e1254c | |||
| 4c9abff445 | |||
| 2cf68f32b5 | |||
| 90fb32be30 | |||
| 97e713e8aa | |||
| 826419127a | |||
| e0449c0b96 | |||
| 5e342f7114 | |||
| 174ab58343 | |||
| 954d618790 | |||
| 91073360c6 | |||
| 8a2cfc00c9 | |||
| 0fc33e2a06 | |||
| 6c6ed68b5a | |||
| e9106efbdc | |||
| e9e2612706 | |||
| 4ae468ff06 | |||
| 785918262a | |||
| be8fee5c2b | |||
| 4597c687fa | |||
| c4aa049042 | |||
| 1d46b5c33c | |||
| 6ae6f0fe8f | |||
| a0409e64d8 | |||
| ce4e658e3f | |||
| 215fb39fc5 | |||
| 1cc94bb23e | |||
| bc42187c58 | |||
| 80c0b06980 | |||
| 381eabb970 | |||
| c939aa04fe | |||
| 9b8e356fca | |||
| 07e5ea9367 | |||
| 7a54a2c49c | |||
| 600e405ba0 | |||
| 1daf535e6b | |||
| cc52dc396c | |||
| 132ef57272 | |||
| 0777cf1ccf | |||
| c4bc902958 | |||
| d68397b42f | |||
| 531eebb88e | |||
| db9391e8a6 | |||
| f1c31f4eba | |||
| ada5b31bb0 | |||
| ffcb45d7c9 | |||
| 6566ede935 | |||
| 0bb677cef9 | |||
| 8415ff50cb | |||
| 37522185d3 | |||
| 960eacebcf | |||
| b4a6419215 | |||
| 7c1e61af1e | |||
| 671d193786 | |||
| 2ababc87da | |||
| 5372c63c7c | |||
| ab701d20eb | |||
| 384a8f58c6 | |||
| 50c9d5221f | |||
| 1a08ee77c7 | |||
| 79cff288a6 | |||
| 0efc334ff9 | |||
| 277ecc505c | |||
| f565ff3c09 | |||
| 161247c70d | |||
| a86ecf3bba | |||
| 6e84142b59 | |||
| fc52c5ac87 | |||
| 537666149b | |||
| c06ad7c881 | |||
| 0a7cb20e66 | |||
| f43209bb8c | |||
| b34d9242d1 | |||
| 9654412021 | |||
| 31d2fecf63 | |||
| f3f59db3ae | |||
| 89e2a23b5b | |||
| ea5f97609a | |||
| 4119051077 | |||
| f116a55e4a | |||
| 89e371c94b | |||
| b1a6c40ad0 | |||
| 1772e8ee6e | |||
| 0050134d9e | |||
| d46af9cf1e | |||
| bea56feb63 | |||
| 685a894a6f | |||
| 79ea379df2 | |||
| 4863aedf3d | |||
| b5cb9aae7f | |||
| 4d09a71ef4 | |||
| 65a3f7f749 | |||
| b1d14acc35 | |||
| 3867fa2bad | |||
| 07808bea12 | |||
| 914a643ab2 | |||
| c495635f05 | |||
| 8ff40133b4 | |||
| aacdb34e3f | |||
| 233427f08e | |||
| e82542b802 | |||
| 2006259a02 | |||
| 472b33113e | |||
| 218147271c | |||
| a29deb1ee9 | |||
| f24049736e | |||
| 21a644fef5 | |||
| 106f8e94d6 | |||
| 7a2c49e762 | |||
| 9b4d5b0395 | |||
| f20655bef4 | |||
| 31c099be43 | |||
| 155e1f20d6 | |||
| fe0c4f8c74 | |||
| 53ff1b57c9 | |||
| 1025540fcc | |||
| eb9a683b40 | |||
| 48c26fa039 | |||
| 10d1e04e9b | |||
| 12acb9a59a | |||
| 807c804c24 | |||
| 660a00d317 | |||
| effd27b239 | |||
| 06d7840d1d | |||
| 0875203a6c | |||
| b9131aa69c |
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
"@opencode-ai/http-recorder": minor
|
|
||||||
---
|
|
||||||
|
|
||||||
Publish the initial beta of the Effect HTTP and WebSocket record/replay library.
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
|
|
||||||
"changelog": "@changesets/cli/changelog",
|
|
||||||
"commit": false,
|
|
||||||
"fixed": [],
|
|
||||||
"linked": [],
|
|
||||||
"access": "public",
|
|
||||||
"baseBranch": "dev",
|
|
||||||
"updateInternalDependencies": "patch",
|
|
||||||
"ignore": []
|
|
||||||
}
|
|
||||||
+2
-4
@@ -1,5 +1,3 @@
|
|||||||
# web + desktop packages
|
# web + desktop packages
|
||||||
packages/app/ @adamdotdevin
|
packages/app/ @Hona @Brendonovich
|
||||||
packages/tauri/ @adamdotdevin
|
packages/desktop/ @Hona @Brendonovich
|
||||||
packages/desktop/src-tauri/ @brendonovich
|
|
||||||
packages/desktop/ @adamdotdevin
|
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
name: http-recorder release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
paths:
|
|
||||||
- ".changeset/**"
|
|
||||||
- "packages/http-recorder/**"
|
|
||||||
- ".github/workflows/http-recorder-release.yml"
|
|
||||||
|
|
||||||
concurrency: http-recorder-release
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
id-token: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
if: github.repository == 'anomalyco/opencode'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- uses: ./.github/actions/setup-bun
|
|
||||||
|
|
||||||
- name: Setup git committer
|
|
||||||
id: committer
|
|
||||||
uses: ./.github/actions/setup-git-committer
|
|
||||||
with:
|
|
||||||
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
|
||||||
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
|
||||||
|
|
||||||
- name: Verify package
|
|
||||||
run: |
|
|
||||||
bun run --cwd packages/http-recorder build
|
|
||||||
bun run --cwd packages/http-recorder test
|
|
||||||
bun run --cwd packages/http-recorder typecheck
|
|
||||||
bun run --cwd packages/http-recorder verify:package
|
|
||||||
|
|
||||||
- name: Version or publish beta
|
|
||||||
uses: changesets/action@3841a0683d3cfa6dae0f9bb335290003010fe3f0 # v1.9.0
|
|
||||||
with:
|
|
||||||
version: bun run version:http-recorder
|
|
||||||
publish: bun run release:http-recorder
|
|
||||||
commit: "chore(http-recorder): release beta"
|
|
||||||
title: "chore(http-recorder): release beta"
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
|
|
||||||
NPM_CONFIG_PROVENANCE: true
|
|
||||||
@@ -65,7 +65,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
run: bun turbo test:ci --log-order=stream --log-prefix=task
|
run: bun turbo test --output-logs=errors-only --log-order=grouped --log-prefix=task
|
||||||
env:
|
env:
|
||||||
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }}
|
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }}
|
||||||
|
|
||||||
@@ -74,26 +74,6 @@ jobs:
|
|||||||
working-directory: packages/opencode
|
working-directory: packages/opencode
|
||||||
run: bun run test:httpapi
|
run: bun run test:httpapi
|
||||||
|
|
||||||
- name: Publish unit reports
|
|
||||||
if: always()
|
|
||||||
uses: mikepenz/action-junit-report@bccf2e31636835cf0874589931c4116687171386 # v6.4.0
|
|
||||||
with:
|
|
||||||
report_paths: packages/*/.artifacts/unit/junit.xml
|
|
||||||
check_name: "unit results (${{ matrix.settings.name }})"
|
|
||||||
detailed_summary: true
|
|
||||||
include_time_in_summary: true
|
|
||||||
fail_on_failure: false
|
|
||||||
|
|
||||||
- name: Upload unit artifacts
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
||||||
with:
|
|
||||||
name: unit-${{ matrix.settings.name }}-${{ github.run_attempt }}
|
|
||||||
include-hidden-files: true
|
|
||||||
if-no-files-found: ignore
|
|
||||||
retention-days: 7
|
|
||||||
path: packages/*/.artifacts/unit/junit.xml
|
|
||||||
|
|
||||||
e2e:
|
e2e:
|
||||||
name: e2e (${{ matrix.settings.name }})
|
name: e2e (${{ matrix.settings.name }})
|
||||||
strategy:
|
strategy:
|
||||||
@@ -151,7 +131,6 @@ jobs:
|
|||||||
run: bun --cwd packages/app test:e2e:local
|
run: bun --cwd packages/app test:e2e:local
|
||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
PLAYWRIGHT_JUNIT_OUTPUT: e2e/junit-${{ matrix.settings.name }}.xml
|
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
|
|
||||||
- name: Upload Playwright artifacts
|
- name: Upload Playwright artifacts
|
||||||
@@ -162,6 +141,5 @@ jobs:
|
|||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
path: |
|
path: |
|
||||||
packages/app/e2e/junit-*.xml
|
|
||||||
packages/app/e2e/test-results
|
packages/app/e2e/test-results
|
||||||
packages/app/e2e/playwright-report
|
packages/app/e2e/playwright-report
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
mode: primary
|
mode: primary
|
||||||
hidden: true
|
hidden: true
|
||||||
model: opencode/gpt-5.4-nano
|
model: opencode/gpt-5.4-mini
|
||||||
color: "#44BA81"
|
color: "#44BA81"
|
||||||
tools:
|
tools:
|
||||||
"*": false
|
"*": false
|
||||||
|
|||||||
@@ -2,8 +2,15 @@
|
|||||||
"$schema": "https://opencode.ai/config.json",
|
"$schema": "https://opencode.ai/config.json",
|
||||||
"provider": {},
|
"provider": {},
|
||||||
"permission": {},
|
"permission": {},
|
||||||
"reference": {
|
"references": {
|
||||||
"effect": "github.com/Effect-TS/effect-smol",
|
"effect": {
|
||||||
|
"repository": "github.com/Effect-TS/effect-smol",
|
||||||
|
"description": "Use for Effect v4 and effect-smol implementation details",
|
||||||
|
},
|
||||||
|
"opencode-local": {
|
||||||
|
"path": "~/.local/share/opencode",
|
||||||
|
"description": "Contains opencode logs and data",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"mcp": {},
|
"mcp": {},
|
||||||
"tools": {
|
"tools": {
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
- The default branch in this repo is `dev`.
|
- The default branch in this repo is `dev`.
|
||||||
- Local `main` ref may not exist; use `dev` or `origin/dev` for diffs.
|
- Local `main` ref may not exist; use `dev` or `origin/dev` for diffs.
|
||||||
|
|
||||||
|
## Branch Names
|
||||||
|
|
||||||
|
Use a short branch name of at most three words, separated by hyphens. Do not use slashes or type prefixes such as `feat/` or `fix/`.
|
||||||
|
|
||||||
|
Examples: `session-recovery`, `fix-scroll-state`, `regenerate-sdk`.
|
||||||
|
|
||||||
## Commits and PR Titles
|
## Commits and PR Titles
|
||||||
|
|
||||||
Use conventional commit-style messages and PR titles: `type(scope): summary`.
|
Use conventional commit-style messages and PR titles: `type(scope): summary`.
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
exact = true
|
exact = true
|
||||||
# Only install newly resolved package versions published at least 3 days ago.
|
# Only install newly resolved package versions published at least 3 days ago.
|
||||||
minimumReleaseAge = 259200
|
minimumReleaseAge = 259200
|
||||||
minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "gitlab-ai-provider", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64"]
|
minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@ai-sdk/anthropic", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "opentui-spinner", "gitlab-ai-provider", "opencode-gitlab-auth", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64", "app-builder-lib", "dmg-builder", "electron-builder", "electron-publish"]
|
||||||
|
|
||||||
[test]
|
[test]
|
||||||
root = "./do-not-run-tests-from-root"
|
root = "./do-not-run-tests-from-root"
|
||||||
|
|||||||
+1
-1
@@ -165,7 +165,7 @@ export const app = new sst.cloudflare.x.SolidStart("Stats", {
|
|||||||
domain: `stats.${domain}`,
|
domain: `stats.${domain}`,
|
||||||
link: [database, EMAILOCTOPUS_API_KEY],
|
link: [database, EMAILOCTOPUS_API_KEY],
|
||||||
environment: {
|
environment: {
|
||||||
PUBLIC_URL: `https://${domain}/stats`,
|
PUBLIC_URL: `https://${domain}/data`,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-5DhbOm/gs2mfjmNYdZHkr0ZopgSC2HcGN9/r1noGqhc=",
|
"x86_64-linux": "sha256-u3CBUflGrE1unVRTvvLKaDkvx5ZHXyRLd/SJOUUsRdc=",
|
||||||
"aarch64-linux": "sha256-0dIKcqKmhrPhRpabnfM20wnqt/AkoCWDMgG9cQZ8P3o=",
|
"aarch64-linux": "sha256-hUu70IlOXXQ9apQe3IPOdQGf3ZSVCpvRuAWSKxipk/k=",
|
||||||
"aarch64-darwin": "sha256-Sx3G63vORj69u2AOMDfpk6NU7fMgxsbYBh3jnGohNXI=",
|
"aarch64-darwin": "sha256-N6Mqmo94q0rsskLcXoUGWCMJjQ9wCuuo0Lby3j6+Dps=",
|
||||||
"x86_64-darwin": "sha256-4g2ydNayqNqWlBeQt90rUI6bY5dMvXP4OmQ6QWaJbuI="
|
"x86_64-darwin": "sha256-5691VKK2zWqAwe94CCjZuRjXEaswedevFtHxensVLfg="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-9
@@ -13,9 +13,6 @@
|
|||||||
"dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev",
|
"dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev",
|
||||||
"dev:storybook": "bun --cwd packages/storybook storybook",
|
"dev:storybook": "bun --cwd packages/storybook storybook",
|
||||||
"lint": "oxlint",
|
"lint": "oxlint",
|
||||||
"changeset": "changeset",
|
|
||||||
"version:http-recorder": "changeset version",
|
|
||||||
"release:http-recorder": "bun ./packages/http-recorder/script/publish.ts",
|
|
||||||
"typecheck": "bun turbo typecheck",
|
"typecheck": "bun turbo typecheck",
|
||||||
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
|
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
|
||||||
"postinstall": "bun run --cwd packages/core fix-node-pty",
|
"postinstall": "bun run --cwd packages/core fix-node-pty",
|
||||||
@@ -42,9 +39,9 @@
|
|||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@hono/standard-validator": "0.2.0",
|
"@hono/standard-validator": "0.2.0",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
"@opentui/core": "0.3.2",
|
"@opentui/core": "0.3.4",
|
||||||
"@opentui/keymap": "0.3.2",
|
"@opentui/keymap": "0.3.4",
|
||||||
"@opentui/solid": "0.3.2",
|
"@opentui/solid": "0.3.4",
|
||||||
"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",
|
||||||
@@ -55,7 +52,7 @@
|
|||||||
"@cloudflare/workers-types": "4.20251008.0",
|
"@cloudflare/workers-types": "4.20251008.0",
|
||||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||||
"@pierre/diffs": "1.1.0-beta.18",
|
"@pierre/diffs": "1.1.0-beta.18",
|
||||||
"opentui-spinner": "0.0.6",
|
"opentui-spinner": "0.0.7",
|
||||||
"@solid-primitives/storage": "4.3.3",
|
"@solid-primitives/storage": "4.3.3",
|
||||||
"@tailwindcss/vite": "4.1.11",
|
"@tailwindcss/vite": "4.1.11",
|
||||||
"diff": "8.0.2",
|
"diff": "8.0.2",
|
||||||
@@ -96,7 +93,6 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/artifact": "5.0.1",
|
"@actions/artifact": "5.0.1",
|
||||||
"@changesets/cli": "2.31.0",
|
|
||||||
"@tsconfig/bun": "catalog:",
|
"@tsconfig/bun": "catalog:",
|
||||||
"@types/mime-types": "3.0.1",
|
"@types/mime-types": "3.0.1",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
@@ -144,7 +140,8 @@
|
|||||||
"@types/node": "catalog:"
|
"@types/node": "catalog:"
|
||||||
},
|
},
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch",
|
"@ff-labs/fff-bun@0.9.3": "patches/@ff-labs%2Ffff-bun@0.9.3.patch",
|
||||||
|
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
|
||||||
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
||||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.16.2",
|
"version": "1.17.4",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts",
|
".": "./src/index.ts",
|
||||||
"./desktop-menu": "./src/desktop-menu.ts",
|
"./desktop-menu": "./src/desktop-menu.ts",
|
||||||
|
"./updater": "./src/updater.ts",
|
||||||
"./wsl/types": "./src/wsl/types.ts",
|
"./wsl/types": "./src/wsl/types.ts",
|
||||||
"./vite": "./vite.js",
|
"./vite": "./vite.js",
|
||||||
"./index.css": "./src/index.css"
|
"./index.css": "./src/index.css"
|
||||||
@@ -17,8 +18,7 @@
|
|||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"serve": "vite preview",
|
"serve": "vite preview",
|
||||||
"test": "bun run test:unit",
|
"test": "bun run test:unit",
|
||||||
"test:ci": "mkdir -p .artifacts/unit && bun test --preload ./happydom.ts ./src --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml",
|
"test:unit": "bun test --only-failures --preload ./happydom.ts ./src",
|
||||||
"test:unit": "bun test --preload ./happydom.ts ./src",
|
|
||||||
"test:unit:watch": "bun test --watch --preload ./happydom.ts ./src",
|
"test:unit:watch": "bun test --watch --preload ./happydom.ts ./src",
|
||||||
"test:e2e": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
"test:e2e:local": "playwright test",
|
"test:e2e:local": "playwright test",
|
||||||
|
|||||||
@@ -7,12 +7,6 @@ const serverPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"
|
|||||||
const command = `bun run dev -- --host 0.0.0.0 --port ${port}`
|
const command = `bun run dev -- --host 0.0.0.0 --port ${port}`
|
||||||
const reuse = !process.env.CI
|
const reuse = !process.env.CI
|
||||||
const workers = Number(process.env.PLAYWRIGHT_WORKERS ?? (process.env.CI ? 5 : 0)) || undefined
|
const workers = Number(process.env.PLAYWRIGHT_WORKERS ?? (process.env.CI ? 5 : 0)) || undefined
|
||||||
const reporter = [["html", { outputFolder: "e2e/playwright-report", open: "never" }], ["line"]] as const
|
|
||||||
|
|
||||||
if (process.env.PLAYWRIGHT_JUNIT_OUTPUT) {
|
|
||||||
reporter.push(["junit", { outputFile: process.env.PLAYWRIGHT_JUNIT_OUTPUT }])
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: "./e2e",
|
testDir: "./e2e",
|
||||||
outputDir: "./e2e/test-results",
|
outputDir: "./e2e/test-results",
|
||||||
@@ -24,7 +18,7 @@ export default defineConfig({
|
|||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
retries: process.env.CI ? 2 : 0,
|
retries: process.env.CI ? 2 : 0,
|
||||||
workers,
|
workers,
|
||||||
reporter,
|
reporter: [["html", { outputFolder: "e2e/playwright-report", open: "never" }], ["line"]],
|
||||||
webServer: {
|
webServer: {
|
||||||
command,
|
command,
|
||||||
url: baseURL,
|
url: baseURL,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Font } from "@opencode-ai/ui/font"
|
|||||||
import { Splash } from "@opencode-ai/ui/logo"
|
import { Splash } from "@opencode-ai/ui/logo"
|
||||||
import { ThemeProvider } from "@opencode-ai/ui/theme/context"
|
import { ThemeProvider } from "@opencode-ai/ui/theme/context"
|
||||||
import { MetaProvider } from "@solidjs/meta"
|
import { MetaProvider } from "@solidjs/meta"
|
||||||
import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router"
|
import { type BaseRouterProps, Navigate, Route, Router, useParams, useSearchParams } from "@solidjs/router"
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
|
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import {
|
import {
|
||||||
@@ -43,25 +43,88 @@ import { PromptProvider } from "@/context/prompt"
|
|||||||
import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
|
import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
|
||||||
import { SettingsProvider, useSettings } from "@/context/settings"
|
import { SettingsProvider, useSettings } from "@/context/settings"
|
||||||
import { TerminalProvider } from "@/context/terminal"
|
import { TerminalProvider } from "@/context/terminal"
|
||||||
import { TabsProvider } from "@/context/tabs"
|
import { TabsProvider, useTabs, type DraftTab } from "@/context/tabs"
|
||||||
|
import { SDKProvider, useSDK } from "@/context/sdk"
|
||||||
import { WslServersProvider } from "@/wsl/context"
|
import { WslServersProvider } from "@/wsl/context"
|
||||||
import DirectoryLayout from "@/pages/directory-layout"
|
import DirectoryLayout, { DirectoryDataProvider } from "@/pages/directory-layout"
|
||||||
import Layout from "@/pages/layout"
|
import Layout from "@/pages/layout"
|
||||||
import { ErrorPage } from "./pages/error"
|
import { ErrorPage } from "./pages/error"
|
||||||
import { useCheckServerHealth } from "./utils/server-health"
|
import { useCheckServerHealth } from "./utils/server-health"
|
||||||
|
|
||||||
const HomeRoute = lazy(() => import("@/pages/home"))
|
const HomeRoute = lazy(() => import("@/pages/home"))
|
||||||
const Session = lazy(() => import("@/pages/session"))
|
const Session = lazy(() => import("@/pages/session"))
|
||||||
|
const NewSession = lazy(() => import("@/pages/new-session"))
|
||||||
|
|
||||||
const SessionRoute = Object.assign(
|
const SessionRoute = Object.assign(
|
||||||
() => (
|
() => {
|
||||||
<SessionProviders>
|
const settings = useSettings()
|
||||||
<Session />
|
const params = useParams()
|
||||||
</SessionProviders>
|
const [search] = useSearchParams<{ draftId?: string; prompt?: string }>()
|
||||||
),
|
const sdk = useSDK()
|
||||||
|
const server = useServer()
|
||||||
|
const tabs = useTabs()
|
||||||
|
|
||||||
|
// When the new layout is enabled, the legacy new-session route (/:dir/session with no id)
|
||||||
|
// is replaced by a draft at /new-session?draftId=…
|
||||||
|
createEffect(() => {
|
||||||
|
if (!settings.general.newLayoutDesigns()) return
|
||||||
|
if (params.id || search.draftId) return
|
||||||
|
if (!tabs.ready() || !sdk.directory) return
|
||||||
|
tabs.newDraft({ server: server.key, directory: sdk.directory }, search.prompt)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SessionProviders>
|
||||||
|
<Session />
|
||||||
|
</SessionProviders>
|
||||||
|
)
|
||||||
|
},
|
||||||
{ preload: Session.preload },
|
{ preload: Session.preload },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function DraftRoute() {
|
||||||
|
const [search] = useSearchParams<{ draftId?: string }>()
|
||||||
|
const tabs = useTabs()
|
||||||
|
return (
|
||||||
|
<Show when={tabs.ready()}>
|
||||||
|
<Show when={search.draftId} keyed fallback={<Navigate href="/" />}>
|
||||||
|
{(draftID) => <ResolvedDraftRoute draftID={draftID} />}
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ResolvedDraftRoute(props: { draftID: string }) {
|
||||||
|
const server = useServer()
|
||||||
|
const tabs = useTabs()
|
||||||
|
const draft = createMemo(() =>
|
||||||
|
tabs.store.find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === props.draftID),
|
||||||
|
)
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const current = draft()
|
||||||
|
if (current && current.server !== server.key) server.setActive(current.server)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Key on the directory so retargeting the draft's project re-instantiates the
|
||||||
|
// SDK/data providers for the new directory while keeping the same draft id.
|
||||||
|
const directory = () => draft()?.directory
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={directory()} keyed>
|
||||||
|
{(dir) => (
|
||||||
|
<SDKProvider directory={dir}>
|
||||||
|
<DirectoryDataProvider directory={dir} draftID={props.draftID}>
|
||||||
|
<DraftProviders>
|
||||||
|
<NewSession />
|
||||||
|
</DraftProviders>
|
||||||
|
</DirectoryDataProvider>
|
||||||
|
</SDKProvider>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function UiI18nBridge(props: ParentProps) {
|
function UiI18nBridge(props: ParentProps) {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
return <I18nProvider value={{ locale: language.intl, t: language.t }}>{props.children}</I18nProvider>
|
return <I18nProvider value={{ locale: language.intl, t: language.t }}>{props.children}</I18nProvider>
|
||||||
@@ -70,7 +133,6 @@ function UiI18nBridge(props: ParentProps) {
|
|||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
__OPENCODE__?: {
|
__OPENCODE__?: {
|
||||||
updaterEnabled?: boolean
|
|
||||||
deepLinks?: string[]
|
deepLinks?: string[]
|
||||||
}
|
}
|
||||||
api?: {
|
api?: {
|
||||||
@@ -142,6 +204,18 @@ function SessionProviders(props: ParentProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The draft page only renders the prompt composer, so it drops TerminalProvider.
|
||||||
|
// FileProvider and CommentsProvider stay because PromptInput uses file search and comment context.
|
||||||
|
function DraftProviders(props: ParentProps) {
|
||||||
|
return (
|
||||||
|
<FileProvider>
|
||||||
|
<PromptProvider>
|
||||||
|
<CommentsProvider>{props.children}</CommentsProvider>
|
||||||
|
</PromptProvider>
|
||||||
|
</FileProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
|
function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
|
||||||
return (
|
return (
|
||||||
<AppShellProviders>
|
<AppShellProviders>
|
||||||
@@ -336,6 +410,7 @@ export function AppInterface(props: {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Route path="/" component={HomeRoute} />
|
<Route path="/" component={HomeRoute} />
|
||||||
|
<Route path="/new-session" component={DraftRoute} />
|
||||||
<Route path="/:dir" component={DirectoryLayout}>
|
<Route path="/:dir" component={DirectoryLayout}>
|
||||||
<Route path="/" component={() => <Navigate href="session" />} />
|
<Route path="/" component={() => <Navigate href="session" />} />
|
||||||
<Route path="/session/:id?" component={SessionRoute} />
|
<Route path="/session/:id?" component={SessionRoute} />
|
||||||
|
|||||||
@@ -261,11 +261,7 @@ function createSessionEntries(props: {
|
|||||||
return { sessions }
|
return { sessions }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DialogSelectFile(props: {
|
export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFile?: (path: string) => void }) {
|
||||||
mode?: DialogSelectFileMode
|
|
||||||
onOpenFile?: (path: string) => void
|
|
||||||
onSelectFile?: (path: string) => void
|
|
||||||
}) {
|
|
||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
@@ -379,10 +375,6 @@ export function DialogSelectFile(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!item.path) return
|
if (!item.path) return
|
||||||
if (props.onSelectFile) {
|
|
||||||
props.onSelectFile(item.path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
open(item.path)
|
open(item.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,6 +399,7 @@ export function DialogSelectFile(props: {
|
|||||||
items={items}
|
items={items}
|
||||||
key={(item) => item.id}
|
key={(item) => item.id}
|
||||||
filterKeys={["title", "description", "category"]}
|
filterKeys={["title", "description", "category"]}
|
||||||
|
skipFilter={(item) => item.type === "file"}
|
||||||
groupBy={grouped() ? (item) => item.category : () => ""}
|
groupBy={grouped() ? (item) => item.category : () => ""}
|
||||||
onMove={handleMove}
|
onMove={handleMove}
|
||||||
onSelect={handleSelect}
|
onSelect={handleSelect}
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import { useMutation, useQueryClient } from "@tanstack/solid-query"
|
|
||||||
import { Component, createMemo, Show } from "solid-js"
|
import { Component, createMemo, Show } from "solid-js"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useSDK } from "@/context/sdk"
|
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { Switch } from "@opencode-ai/ui/switch"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useQueryOptions } from "@/context/server-sync"
|
import { useMcpToggle } from "@/context/mcp"
|
||||||
import { pathKey } from "@/utils/path-key"
|
|
||||||
|
|
||||||
const statusLabels = {
|
const statusLabels = {
|
||||||
connected: "mcp.status.connected",
|
connected: "mcp.status.connected",
|
||||||
@@ -19,10 +16,7 @@ const statusLabels = {
|
|||||||
|
|
||||||
export const DialogSelectMcp: Component = () => {
|
export const DialogSelectMcp: Component = () => {
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const sdk = useSDK()
|
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const queryClient = useQueryClient()
|
|
||||||
const queryOptions = useQueryOptions()
|
|
||||||
|
|
||||||
const items = createMemo(() =>
|
const items = createMemo(() =>
|
||||||
Object.entries(sync.data.mcp ?? {})
|
Object.entries(sync.data.mcp ?? {})
|
||||||
@@ -30,21 +24,7 @@ export const DialogSelectMcp: Component = () => {
|
|||||||
.sort((a, b) => a.name.localeCompare(b.name)),
|
.sort((a, b) => a.name.localeCompare(b.name)),
|
||||||
)
|
)
|
||||||
|
|
||||||
const toggle = useMutation(() => ({
|
const toggle = useMcpToggle()
|
||||||
mutationFn: async (name: string) => {
|
|
||||||
const status = sync.data.mcp[name]
|
|
||||||
if (status?.status === "connected") {
|
|
||||||
await sdk.client.mcp.disconnect({ name })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (status?.status === "needs_auth") {
|
|
||||||
await sdk.client.mcp.auth.authenticate({ name })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await sdk.client.mcp.connect({ name })
|
|
||||||
},
|
|
||||||
onSuccess: () => queryClient.refetchQueries(queryOptions.mcp(pathKey(sync.directory))),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length)
|
const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length)
|
||||||
const totalCount = createMemo(() => items().length)
|
const totalCount = createMemo(() => items().length)
|
||||||
|
|||||||
@@ -509,11 +509,16 @@ export function useServerManagementController(options: { onSelect?: () => void;
|
|||||||
resetEdit()
|
resetEdit()
|
||||||
})
|
})
|
||||||
|
|
||||||
async function handleRemove(url: ServerConnection.Key) {
|
async function handleRemove(key: ServerConnection.Key) {
|
||||||
tabs.removeServer(url)
|
try {
|
||||||
server.remove(url)
|
if (key.startsWith("wsl:")) await platform.wslServers?.removeServer(key)
|
||||||
if ((await platform.getDefaultServer?.()) === url) {
|
tabs.removeServer(key)
|
||||||
void platform.setDefaultServer?.(null)
|
server.remove(key)
|
||||||
|
if ((await platform.getDefaultServer?.()) === key) {
|
||||||
|
await setDefault(null)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showRequestError(language, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { ServerConnection } from "@/context/server"
|
||||||
|
import type { Platform } from "@/context/platform"
|
||||||
|
|
||||||
|
export function directoryPickerKind(platform: Platform["platform"], server: ServerConnection.Any) {
|
||||||
|
if (platform === "desktop" && ServerConnection.local(server)) return "native" as const
|
||||||
|
return "server" as const
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { directoryPickerKind } from "./directory-picker-policy"
|
||||||
|
|
||||||
|
const local = {
|
||||||
|
type: "sidecar",
|
||||||
|
variant: "base",
|
||||||
|
http: { url: "http://localhost:4096" },
|
||||||
|
} as const
|
||||||
|
const remote = {
|
||||||
|
type: "ssh",
|
||||||
|
host: "example.test",
|
||||||
|
http: { url: "http://localhost:4096" },
|
||||||
|
} as const
|
||||||
|
|
||||||
|
describe("directoryPickerKind", () => {
|
||||||
|
test("uses the native picker only for local desktop projects", () => {
|
||||||
|
expect(directoryPickerKind("desktop", local)).toBe("native")
|
||||||
|
expect(directoryPickerKind("desktop", remote)).toBe("server")
|
||||||
|
expect(directoryPickerKind("web", local)).toBe("server")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
|
import { ServerConnection } from "@/context/server"
|
||||||
|
import { usePlatform } from "@/context/platform"
|
||||||
|
import { DialogSelectDirectory } from "./dialog-select-directory"
|
||||||
|
import { directoryPickerKind } from "./directory-picker-policy"
|
||||||
|
|
||||||
|
type DirectoryPickerInput = {
|
||||||
|
server: ServerConnection.Any
|
||||||
|
title?: string
|
||||||
|
multiple?: boolean
|
||||||
|
onSelect: (result: string | string[] | null) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDirectoryPicker() {
|
||||||
|
const platform = usePlatform()
|
||||||
|
const dialog = useDialog()
|
||||||
|
|
||||||
|
return (input: DirectoryPickerInput) => {
|
||||||
|
if (directoryPickerKind(platform.platform, input.server) === "native" && platform.platform === "desktop") {
|
||||||
|
void platform.openDirectoryPickerDialog({ title: input.title, multiple: input.multiple }).then(input.onSelect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.show(
|
||||||
|
() => <DialogSelectDirectory {...input} />,
|
||||||
|
() => input.onSelect(null),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ beforeAll(async () => {
|
|||||||
mock.module("@solidjs/router", () => ({
|
mock.module("@solidjs/router", () => ({
|
||||||
useNavigate: () => () => undefined,
|
useNavigate: () => () => undefined,
|
||||||
useParams: () => ({}),
|
useParams: () => ({}),
|
||||||
|
useLocation: () => ({}),
|
||||||
|
useSearchParams: () => [{}, () => undefined],
|
||||||
}))
|
}))
|
||||||
mock.module("@/context/file", () => ({
|
mock.module("@/context/file", () => ({
|
||||||
useFile: () => ({
|
useFile: () => ({
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { Icon } from "@opencode-ai/ui/v2/icon"
|
||||||
|
import { Popover } from "@opencode-ai/ui/popover"
|
||||||
|
import { createSignal, Show } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
|
||||||
|
export function HelpButton() {
|
||||||
|
if (import.meta.env.VITE_OPENCODE_CHANNEL !== "dev") return null
|
||||||
|
|
||||||
|
const [state, setState] = /* persisted(Persist.global("help-button"), */ createStore({ dismissed: false }) /* ) */
|
||||||
|
const [shown, setShown] = createSignal(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={!state.dismissed}>
|
||||||
|
<div class="fixed bottom-4 right-4 z-50">
|
||||||
|
<Popover
|
||||||
|
open={shown()}
|
||||||
|
onOpenChange={setShown}
|
||||||
|
triggerAs="button"
|
||||||
|
triggerProps={{
|
||||||
|
type: "button",
|
||||||
|
"aria-label": "Help",
|
||||||
|
class:
|
||||||
|
"size-7 rounded-full bg-background-base shadow-[var(--shadow-lg-border-base)] flex items-center justify-center text-text-base hover:text-text-strong transition-colors",
|
||||||
|
}}
|
||||||
|
trigger={<span aria-hidden="true">?</span>}
|
||||||
|
class="[&_[data-slot=popover-body]]:p-0 w-[320px] max-w-[calc(100vw-40px)] bg-transparent border-0 shadow-none rounded-xl"
|
||||||
|
gutter={8}
|
||||||
|
placement="top-end"
|
||||||
|
>
|
||||||
|
<Show when={shown()}>
|
||||||
|
<div class="relative flex flex-col gap-1 w-[320px] p-4 rounded-xl bg-background-strong shadow-[var(--shadow-lg-border-base)]">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="Close"
|
||||||
|
class="absolute top-3.5 right-3.5 size-6 rounded-md flex items-center justify-center text-text-base hover:text-text-strong hover:bg-surface-raised-base-hover transition-colors"
|
||||||
|
onClick={() => {
|
||||||
|
setShown(false)
|
||||||
|
setState("dismissed", true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name="xmark-small" />
|
||||||
|
</button>
|
||||||
|
<span class="text-14-regular text-text-strong">Lorem ipsum dolor sit amet</span>
|
||||||
|
<p class="text-12-regular text-text-weak">
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et
|
||||||
|
dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -31,9 +31,10 @@ import {
|
|||||||
FileAttachmentPart,
|
FileAttachmentPart,
|
||||||
} from "@/context/prompt"
|
} from "@/context/prompt"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate, useSearchParams } from "@solidjs/router"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { useServer } from "@/context/server"
|
import { useServer } from "@/context/server"
|
||||||
|
import { useTabs } from "@/context/tabs"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useComments } from "@/context/comments"
|
import { useComments } from "@/context/comments"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
@@ -52,12 +53,11 @@ import { usePermission } from "@/context/permission"
|
|||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { serverAttachmentFile } from "./prompt-input/server-attachment"
|
|
||||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
import { createSessionTabs } from "@/pages/session/helpers"
|
import { createSessionTabs } from "@/pages/session/helpers"
|
||||||
import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom"
|
import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom"
|
||||||
import { createPromptAttachments } from "./prompt-input/attachments"
|
import { createPromptAttachments } from "./prompt-input/attachments"
|
||||||
import { ACCEPTED_FILE_TYPES } from "./prompt-input/files"
|
import { ACCEPTED_FILE_TYPES, pickAttachmentFiles } from "./prompt-input/files"
|
||||||
import {
|
import {
|
||||||
canNavigateHistoryAtCursor,
|
canNavigateHistoryAtCursor,
|
||||||
navigatePromptHistory,
|
navigatePromptHistory,
|
||||||
@@ -73,6 +73,8 @@ import { PromptContextItems } from "./prompt-input/context-items"
|
|||||||
import { PromptImageAttachments } from "./prompt-input/image-attachments"
|
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 { useDirectoryPicker } from "./directory-picker"
|
||||||
|
import { showToast } from "@/utils/toast"
|
||||||
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
||||||
import { useQueries } from "@tanstack/solid-query"
|
import { useQueries } from "@tanstack/solid-query"
|
||||||
import { useQueryOptions } from "@/context/server-sync"
|
import { useQueryOptions } from "@/context/server-sync"
|
||||||
@@ -140,7 +142,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const permission = usePermission()
|
const permission = usePermission()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
|
const pickDirectory = useDirectoryPicker()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
|
const tabsStore = useTabs()
|
||||||
|
const [search] = useSearchParams<{ draftId?: string }>()
|
||||||
const { params, tabs, view } = useSessionLayout()
|
const { params, tabs, view } = useSessionLayout()
|
||||||
let editorRef!: HTMLDivElement
|
let editorRef!: HTMLDivElement
|
||||||
let fileInputRef: HTMLInputElement | undefined
|
let fileInputRef: HTMLInputElement | undefined
|
||||||
@@ -467,23 +472,18 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const escBlur = () => platform.platform === "desktop" && platform.os === "macos"
|
const escBlur = () => platform.platform === "desktop" && platform.os === "macos"
|
||||||
|
|
||||||
const pick = () => {
|
const pick = () => {
|
||||||
if (server.isLocal()) {
|
pickAttachmentFiles({
|
||||||
fileInputRef?.click()
|
picker: platform.openAttachmentPickerDialog,
|
||||||
return
|
directory: () => sdk.directory,
|
||||||
}
|
fallback: () => fileInputRef?.click(),
|
||||||
void import("@/components/dialog-select-file").then((module) =>
|
onFile: addAttachment,
|
||||||
dialog.show(() => (
|
onError: (error) =>
|
||||||
<module.DialogSelectFile
|
showToast({
|
||||||
mode="files"
|
variant: "error",
|
||||||
onSelectFile={(path) => {
|
title: language.t("common.requestFailed"),
|
||||||
void sdk.client.v2.fs
|
description: error instanceof Error ? error.message : String(error),
|
||||||
.read({ path })
|
}),
|
||||||
.then((response) => response.data?.data)
|
})
|
||||||
.then((data) => data && addAttachments([serverAttachmentFile(path, data)]))
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const setMode = (mode: "normal" | "shell") => {
|
const setMode = (mode: "normal" | "shell") => {
|
||||||
@@ -644,6 +644,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
},
|
},
|
||||||
key: atKey,
|
key: atKey,
|
||||||
filterKeys: ["display"],
|
filterKeys: ["display"],
|
||||||
|
skipFilter: (item) => item.type === "file" && !item.recent,
|
||||||
groupBy: (item) => {
|
groupBy: (item) => {
|
||||||
if (item.type === "agent") return "agent"
|
if (item.type === "agent") return "agent"
|
||||||
if (item.recent) return "recent"
|
if (item.recent) return "recent"
|
||||||
@@ -1094,7 +1095,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const { addAttachments, removeAttachment, handlePaste } = createPromptAttachments({
|
const { addAttachment, addAttachments, removeAttachment, handlePaste } = createPromptAttachments({
|
||||||
editor: () => editorRef,
|
editor: () => editorRef,
|
||||||
isDialogActive: () => !!dialog.active,
|
isDialogActive: () => !!dialog.active,
|
||||||
setDraggingType: (type) => setStore("draggingType", type),
|
setDraggingType: (type) => setStore("draggingType", type),
|
||||||
@@ -1383,9 +1384,19 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
}
|
}
|
||||||
layout.projects.open(worktree)
|
layout.projects.open(worktree)
|
||||||
server.projects.touch(worktree)
|
server.projects.touch(worktree)
|
||||||
|
|
||||||
|
// On the draft route, retarget the existing draft in place so we keep the same
|
||||||
|
// draft id (and its tab/prompt) instead of spawning a new draft for the new directory.
|
||||||
|
const draftID = search.draftId
|
||||||
|
if (draftID) {
|
||||||
|
tabsStore.updateDraft(draftID, { server: server.key, directory: worktree })
|
||||||
|
restoreFocus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
navigate(`/${base64Encode(worktree)}/session`)
|
navigate(`/${base64Encode(worktree)}/session`)
|
||||||
}
|
}
|
||||||
const addProject = async () => {
|
const addProject = () => {
|
||||||
const conn = server.current
|
const conn = server.current
|
||||||
if (!conn) return
|
if (!conn) return
|
||||||
const select = (result: string | string[] | null) => {
|
const select = (result: string | string[] | null) => {
|
||||||
@@ -1393,15 +1404,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
if (!directory) return
|
if (!directory) return
|
||||||
selectProject(directory)
|
selectProject(directory)
|
||||||
}
|
}
|
||||||
if (platform.openDirectoryPickerDialog && server.isLocal()) {
|
pickDirectory({
|
||||||
select(await platform.openDirectoryPickerDialog({ title: language.t("command.project.open") }))
|
server: conn,
|
||||||
return
|
title: language.t("command.project.open"),
|
||||||
}
|
onSelect: select,
|
||||||
void import("@/components/dialog-select-directory").then((x) => {
|
|
||||||
dialog.show(
|
|
||||||
() => <x.DialogSelectDirectory onSelect={select} server={conn} />,
|
|
||||||
() => select(null),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { attachmentMime } from "./files"
|
import { attachmentMime, pickAttachmentFiles } from "./files"
|
||||||
import { pasteMode } from "./paste"
|
import { pasteMode } from "./paste"
|
||||||
|
|
||||||
describe("attachmentMime", () => {
|
describe("attachmentMime", () => {
|
||||||
@@ -24,6 +24,70 @@ describe("attachmentMime", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("pickAttachmentFiles", () => {
|
||||||
|
test("reads the current project directory for every native picker invocation", async () => {
|
||||||
|
const paths: string[] = []
|
||||||
|
const files: File[] = []
|
||||||
|
const file = new File(["hello"], "hello.txt", { type: "text/plain" })
|
||||||
|
let directory = "C:\\Projects\\LoremIpsum"
|
||||||
|
const picker = async (options?: { defaultPath?: string }, onFile?: (file: File) => Promise<unknown>) => {
|
||||||
|
paths.push(options?.defaultPath ?? "")
|
||||||
|
await onFile?.(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
pickAttachmentFiles({
|
||||||
|
picker,
|
||||||
|
directory: () => directory,
|
||||||
|
fallback: () => undefined,
|
||||||
|
onFile: async (selected) => files.push(selected),
|
||||||
|
onError: () => undefined,
|
||||||
|
})
|
||||||
|
await Promise.resolve()
|
||||||
|
directory = "C:\\Projects\\DolorSit"
|
||||||
|
pickAttachmentFiles({
|
||||||
|
picker,
|
||||||
|
directory: () => directory,
|
||||||
|
fallback: () => undefined,
|
||||||
|
onFile: async (selected) => files.push(selected),
|
||||||
|
onError: () => undefined,
|
||||||
|
})
|
||||||
|
await Promise.resolve()
|
||||||
|
expect(files).toEqual([file, file])
|
||||||
|
expect(paths).toEqual(["C:\\Projects\\LoremIpsum", "C:\\Projects\\DolorSit"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("uses the browser file input when no native picker exists", async () => {
|
||||||
|
let fallback = 0
|
||||||
|
pickAttachmentFiles({
|
||||||
|
directory: () => "/projects/consectetur-adipiscing",
|
||||||
|
fallback: () => {
|
||||||
|
fallback += 1
|
||||||
|
},
|
||||||
|
onFile: async () => undefined,
|
||||||
|
onError: () => undefined,
|
||||||
|
})
|
||||||
|
expect(fallback).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("reports native picker failures without rejecting", async () => {
|
||||||
|
const error = new Error("picker unavailable")
|
||||||
|
const errors: unknown[] = []
|
||||||
|
const handled = Promise.withResolvers<void>()
|
||||||
|
pickAttachmentFiles({
|
||||||
|
picker: async () => Promise.reject(error),
|
||||||
|
directory: () => "C:\\Projects\\LoremIpsum",
|
||||||
|
fallback: () => undefined,
|
||||||
|
onFile: async () => undefined,
|
||||||
|
onError: (cause) => {
|
||||||
|
errors.push(cause)
|
||||||
|
handled.resolve()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await handled.promise
|
||||||
|
expect(errors).toEqual([error])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("pasteMode", () => {
|
describe("pasteMode", () => {
|
||||||
test("uses native paste for short single-line text", () => {
|
test("uses native paste for short single-line text", () => {
|
||||||
expect(pasteMode("hello world")).toBe("native")
|
expect(pasteMode("hello world")).toBe("native")
|
||||||
|
|||||||
@@ -2,6 +2,38 @@ import { ACCEPTED_FILE_TYPES, ACCEPTED_IMAGE_TYPES } from "@/constants/file-pick
|
|||||||
|
|
||||||
export { ACCEPTED_FILE_TYPES }
|
export { ACCEPTED_FILE_TYPES }
|
||||||
|
|
||||||
|
type AttachmentPicker = (
|
||||||
|
options: {
|
||||||
|
defaultPath?: string
|
||||||
|
multiple?: boolean
|
||||||
|
accept?: string[]
|
||||||
|
},
|
||||||
|
onFile: (file: File) => Promise<unknown>,
|
||||||
|
) => Promise<void>
|
||||||
|
|
||||||
|
export function pickAttachmentFiles(input: {
|
||||||
|
picker?: AttachmentPicker
|
||||||
|
directory: () => string
|
||||||
|
fallback: () => void
|
||||||
|
onFile: (file: File) => Promise<unknown>
|
||||||
|
onError: (error: unknown) => void
|
||||||
|
}) {
|
||||||
|
if (!input.picker) {
|
||||||
|
input.fallback()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
void input
|
||||||
|
.picker(
|
||||||
|
{
|
||||||
|
defaultPath: input.directory(),
|
||||||
|
multiple: true,
|
||||||
|
accept: ACCEPTED_FILE_TYPES,
|
||||||
|
},
|
||||||
|
input.onFile,
|
||||||
|
)
|
||||||
|
.catch(input.onError)
|
||||||
|
}
|
||||||
|
|
||||||
const IMAGE_MIMES = new Set(ACCEPTED_IMAGE_TYPES)
|
const IMAGE_MIMES = new Set(ACCEPTED_IMAGE_TYPES)
|
||||||
const IMAGE_EXTS = new Map([
|
const IMAGE_EXTS = new Map([
|
||||||
["gif", "image/gif"],
|
["gif", "image/gif"],
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
|
||||||
import { serverAttachmentFile } from "./server-attachment"
|
|
||||||
|
|
||||||
describe("serverAttachmentFile", () => {
|
|
||||||
test("creates a file from server text content", async () => {
|
|
||||||
const file = serverAttachmentFile("docs/readme.txt", { type: "text", content: "hello", mime: "text/plain" })
|
|
||||||
|
|
||||||
expect(file.name).toBe("readme.txt")
|
|
||||||
expect(file.type).toBe("text/plain")
|
|
||||||
expect(await file.text()).toBe("hello")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("creates a file from server base64 content", async () => {
|
|
||||||
const file = serverAttachmentFile("images/pixel.png", {
|
|
||||||
type: "binary",
|
|
||||||
content: "aGVsbG8=",
|
|
||||||
encoding: "base64",
|
|
||||||
mime: "image/png",
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(file.name).toBe("pixel.png")
|
|
||||||
expect(file.type).toBe("image/png")
|
|
||||||
expect(await file.text()).toBe("hello")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
|
||||||
import type { FileSystemBinaryContent, FileSystemTextContent } from "@opencode-ai/sdk/v2"
|
|
||||||
|
|
||||||
export function serverAttachmentFile(path: string, data: FileSystemTextContent | FileSystemBinaryContent) {
|
|
||||||
const content =
|
|
||||||
data.type === "text" ? data.content : Uint8Array.from(atob(data.content), (char) => char.charCodeAt(0))
|
|
||||||
return new File([content], getFilename(path), { type: data.mime })
|
|
||||||
}
|
|
||||||
@@ -61,6 +61,8 @@ beforeAll(async () => {
|
|||||||
mock.module("@solidjs/router", () => ({
|
mock.module("@solidjs/router", () => ({
|
||||||
useNavigate: () => () => undefined,
|
useNavigate: () => () => undefined,
|
||||||
useParams: () => params,
|
useParams: () => params,
|
||||||
|
useLocation: () => ({}),
|
||||||
|
useSearchParams: () => [{}, () => undefined],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
mock.module("@opencode-ai/sdk/v2/client", () => ({
|
mock.module("@opencode-ai/sdk/v2/client", () => ({
|
||||||
@@ -103,6 +105,16 @@ beforeAll(async () => {
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
mock.module("@/context/server", () => ({
|
||||||
|
useServer: () => ({ key: "server-key" }),
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module("@/context/tabs", () => ({
|
||||||
|
useTabs: () => ({
|
||||||
|
promoteDraft: () => undefined,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
mock.module("@/context/prompt", () => ({
|
mock.module("@/context/prompt", () => ({
|
||||||
usePrompt: () => ({
|
usePrompt: () => ({
|
||||||
current: () => promptValue,
|
current: () => promptValue,
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import type { Message, Session } from "@opencode-ai/sdk/v2/client"
|
|||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
import { Binary } from "@opencode-ai/core/util/binary"
|
import { Binary } from "@opencode-ai/core/util/binary"
|
||||||
import { useNavigate, useParams } from "@solidjs/router"
|
import { useNavigate, useParams, useSearchParams } from "@solidjs/router"
|
||||||
import { batch, type Accessor } from "solid-js"
|
import { batch, type Accessor } from "solid-js"
|
||||||
import type { FileSelection } from "@/context/file"
|
import type { FileSelection } from "@/context/file"
|
||||||
|
import { useServer } from "@/context/server"
|
||||||
|
import { useTabs } from "@/context/tabs"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useServerSync } from "@/context/server-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
@@ -213,6 +215,9 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
|
const [search] = useSearchParams<{ draftId?: string }>()
|
||||||
|
const server = useServer()
|
||||||
|
const tabs = useTabs()
|
||||||
const pendingKey = (sessionID: string) => ScopedKey.from(sdk.scope, sessionID)
|
const pendingKey = (sessionID: string) => ScopedKey.from(sdk.scope, sessionID)
|
||||||
|
|
||||||
const errorMessage = (err: unknown) => {
|
const errorMessage = (err: unknown) => {
|
||||||
@@ -381,7 +386,14 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
if (shouldAutoAccept) permission.enableAutoAccept(session.id, sessionDirectory)
|
if (shouldAutoAccept) permission.enableAutoAccept(session.id, sessionDirectory)
|
||||||
local.session.promote(sessionDirectory, session.id)
|
local.session.promote(sessionDirectory, session.id)
|
||||||
layout.handoff.setTabs(base64Encode(sessionDirectory), session.id)
|
layout.handoff.setTabs(base64Encode(sessionDirectory), session.id)
|
||||||
navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`)
|
const draftID = search.draftId
|
||||||
|
if (draftID)
|
||||||
|
tabs.promoteDraft(draftID, {
|
||||||
|
server: server.key,
|
||||||
|
dirBase64: base64Encode(sessionDirectory),
|
||||||
|
sessionId: session.id,
|
||||||
|
})
|
||||||
|
else navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!session) {
|
if (!session) {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Component, Show, createMemo, createResource, onMount, type JSX } from "solid-js"
|
import { Component, Show, createMemo, createResource, onMount, type JSX } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Select } from "@opencode-ai/ui/select"
|
import { Select } from "@opencode-ai/ui/select"
|
||||||
@@ -8,13 +7,13 @@ import { TextField } from "@opencode-ai/ui/text-field"
|
|||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context"
|
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { showToast } from "@/utils/toast"
|
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
import { usePlatform, type DisplayBackend } from "@/context/platform"
|
import { usePlatform, type DisplayBackend } from "@/context/platform"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useServerSync } from "@/context/server-sync"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { useServerSDK } from "@/context/server-sdk"
|
||||||
|
import { useUpdaterAction } from "./updater-action"
|
||||||
import {
|
import {
|
||||||
monoDefault,
|
monoDefault,
|
||||||
monoFontFamily,
|
monoFontFamily,
|
||||||
@@ -91,9 +90,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
const params = useParams()
|
const params = useParams()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
|
|
||||||
const [store, setStore] = createStore({
|
const updater = useUpdaterAction()
|
||||||
checking: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux")
|
const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux")
|
||||||
const dir = createMemo(() => decode64(params.dir))
|
const dir = createMemo(() => decode64(params.dir))
|
||||||
@@ -123,58 +120,6 @@ export const SettingsGeneral: Component = () => {
|
|||||||
}
|
}
|
||||||
const desktop = createMemo(() => platform.platform === "desktop")
|
const desktop = createMemo(() => platform.platform === "desktop")
|
||||||
|
|
||||||
const check = () => {
|
|
||||||
if (!platform.checkUpdate) return
|
|
||||||
setStore("checking", true)
|
|
||||||
|
|
||||||
void platform
|
|
||||||
.checkUpdate()
|
|
||||||
.then((result) => {
|
|
||||||
if (!result.updateAvailable) {
|
|
||||||
showToast({
|
|
||||||
variant: "success",
|
|
||||||
icon: "circle-check",
|
|
||||||
title: language.t("settings.updates.toast.latest.title"),
|
|
||||||
description: language.t("settings.updates.toast.latest.description", { version: platform.version ?? "" }),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const actions = platform.updateAndRestart
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
label: language.t("toast.update.action.installRestart"),
|
|
||||||
onClick: async () => {
|
|
||||||
await platform.updateAndRestart!()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: language.t("toast.update.action.notYet"),
|
|
||||||
onClick: "dismiss" as const,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
label: language.t("toast.update.action.notYet"),
|
|
||||||
onClick: "dismiss" as const,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
showToast({
|
|
||||||
persistent: true,
|
|
||||||
icon: "download",
|
|
||||||
title: language.t("toast.update.title"),
|
|
||||||
description: language.t("toast.update.description", { version: result.version ?? "" }),
|
|
||||||
actions,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch((err: unknown) => {
|
|
||||||
const message = err instanceof Error ? err.message : String(err)
|
|
||||||
showToast({ title: language.t("common.requestFailed"), description: message })
|
|
||||||
})
|
|
||||||
.finally(() => setStore("checking", false))
|
|
||||||
}
|
|
||||||
|
|
||||||
const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
|
const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
|
||||||
|
|
||||||
const serverSync = useServerSync()
|
const serverSync = useServerSync()
|
||||||
@@ -723,19 +668,6 @@ export const SettingsGeneral: Component = () => {
|
|||||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.updates")}</h3>
|
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.updates")}</h3>
|
||||||
|
|
||||||
<SettingsList>
|
<SettingsList>
|
||||||
<SettingsRow
|
|
||||||
title={language.t("settings.updates.row.startup.title")}
|
|
||||||
description={language.t("settings.updates.row.startup.description")}
|
|
||||||
>
|
|
||||||
<div data-action="settings-updates-startup">
|
|
||||||
<Switch
|
|
||||||
checked={settings.updates.startup()}
|
|
||||||
disabled={!platform.checkUpdate}
|
|
||||||
onChange={(checked) => settings.updates.setStartup(checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SettingsRow>
|
|
||||||
|
|
||||||
<SettingsRow
|
<SettingsRow
|
||||||
title={language.t("settings.general.row.releaseNotes.title")}
|
title={language.t("settings.general.row.releaseNotes.title")}
|
||||||
description={language.t("settings.general.row.releaseNotes.description")}
|
description={language.t("settings.general.row.releaseNotes.description")}
|
||||||
@@ -752,10 +684,8 @@ export const SettingsGeneral: Component = () => {
|
|||||||
title={language.t("settings.updates.row.check.title")}
|
title={language.t("settings.updates.row.check.title")}
|
||||||
description={language.t("settings.updates.row.check.description")}
|
description={language.t("settings.updates.row.check.description")}
|
||||||
>
|
>
|
||||||
<Button size="small" variant="secondary" disabled={store.checking || !platform.checkUpdate} onClick={check}>
|
<Button size="small" variant="secondary" disabled={!updater.action().run} onClick={updater.run}>
|
||||||
{store.checking
|
{language.t(updater.action().label)}
|
||||||
? language.t("settings.updates.action.checking")
|
|
||||||
: language.t("settings.updates.action.checkNow")}
|
|
||||||
</Button>
|
</Button>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
</SettingsList>
|
</SettingsList>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Component, Show, createMemo, createResource, onMount } from "solid-js"
|
import { Component, Show, createMemo, createResource, onMount } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
|
||||||
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
|
import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
|
||||||
@@ -8,13 +7,13 @@ import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
|||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context"
|
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { showToast } from "@/utils/toast"
|
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
import { usePlatform, type DisplayBackend } from "@/context/platform"
|
import { usePlatform, type DisplayBackend } from "@/context/platform"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useServerSync } from "@/context/server-sync"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { useServerSDK } from "@/context/server-sdk"
|
||||||
|
import { useUpdaterAction } from "../updater-action"
|
||||||
import {
|
import {
|
||||||
monoDefault,
|
monoDefault,
|
||||||
monoFontFamily,
|
monoFontFamily,
|
||||||
@@ -93,9 +92,7 @@ export const SettingsGeneralV2: Component = () => {
|
|||||||
const params = useParams()
|
const params = useParams()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
|
|
||||||
const [store, setStore] = createStore({
|
const updater = useUpdaterAction()
|
||||||
checking: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux")
|
const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux")
|
||||||
const dir = createMemo(() => decode64(params.dir))
|
const dir = createMemo(() => decode64(params.dir))
|
||||||
@@ -125,58 +122,6 @@ export const SettingsGeneralV2: Component = () => {
|
|||||||
}
|
}
|
||||||
const desktop = createMemo(() => platform.platform === "desktop")
|
const desktop = createMemo(() => platform.platform === "desktop")
|
||||||
|
|
||||||
const check = () => {
|
|
||||||
if (!platform.checkUpdate) return
|
|
||||||
setStore("checking", true)
|
|
||||||
|
|
||||||
void platform
|
|
||||||
.checkUpdate()
|
|
||||||
.then((result) => {
|
|
||||||
if (!result.updateAvailable) {
|
|
||||||
showToast({
|
|
||||||
variant: "success",
|
|
||||||
icon: "circle-check",
|
|
||||||
title: language.t("settings.updates.toast.latest.title"),
|
|
||||||
description: language.t("settings.updates.toast.latest.description", { version: platform.version ?? "" }),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const actions = platform.updateAndRestart
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
label: language.t("toast.update.action.installRestart"),
|
|
||||||
onClick: async () => {
|
|
||||||
await platform.updateAndRestart!()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: language.t("toast.update.action.notYet"),
|
|
||||||
onClick: "dismiss" as const,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
label: language.t("toast.update.action.notYet"),
|
|
||||||
onClick: "dismiss" as const,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
showToast({
|
|
||||||
persistent: true,
|
|
||||||
icon: "download",
|
|
||||||
title: language.t("toast.update.title"),
|
|
||||||
description: language.t("toast.update.description", { version: result.version ?? "" }),
|
|
||||||
actions,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch((err: unknown) => {
|
|
||||||
const message = err instanceof Error ? err.message : String(err)
|
|
||||||
showToast({ title: language.t("common.requestFailed"), description: message })
|
|
||||||
})
|
|
||||||
.finally(() => setStore("checking", false))
|
|
||||||
}
|
|
||||||
|
|
||||||
const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
|
const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
|
||||||
|
|
||||||
const serverSync = useServerSync()
|
const serverSync = useServerSync()
|
||||||
@@ -740,19 +685,6 @@ export const SettingsGeneralV2: Component = () => {
|
|||||||
<h3 class="settings-v2-section-title">{language.t("settings.general.section.updates")}</h3>
|
<h3 class="settings-v2-section-title">{language.t("settings.general.section.updates")}</h3>
|
||||||
|
|
||||||
<SettingsListV2>
|
<SettingsListV2>
|
||||||
<SettingsRowV2
|
|
||||||
title={language.t("settings.updates.row.startup.title")}
|
|
||||||
description={language.t("settings.updates.row.startup.description")}
|
|
||||||
>
|
|
||||||
<div data-action="settings-updates-startup">
|
|
||||||
<Switch
|
|
||||||
checked={settings.updates.startup()}
|
|
||||||
disabled={!platform.checkUpdate}
|
|
||||||
onChange={(checked) => settings.updates.setStartup(checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SettingsRowV2>
|
|
||||||
|
|
||||||
<SettingsRowV2
|
<SettingsRowV2
|
||||||
title={language.t("settings.general.row.releaseNotes.title")}
|
title={language.t("settings.general.row.releaseNotes.title")}
|
||||||
description={language.t("settings.general.row.releaseNotes.description")}
|
description={language.t("settings.general.row.releaseNotes.description")}
|
||||||
@@ -769,10 +701,8 @@ export const SettingsGeneralV2: Component = () => {
|
|||||||
title={language.t("settings.updates.row.check.title")}
|
title={language.t("settings.updates.row.check.title")}
|
||||||
description={language.t("settings.updates.row.check.description")}
|
description={language.t("settings.updates.row.check.description")}
|
||||||
>
|
>
|
||||||
<ButtonV2 size="normal" variant="neutral" disabled={store.checking || !platform.checkUpdate} onClick={check}>
|
<ButtonV2 size="normal" variant="neutral" disabled={!updater.action().run} onClick={updater.run}>
|
||||||
{store.checking
|
{language.t(updater.action().label)}
|
||||||
? language.t("settings.updates.action.checking")
|
|
||||||
: language.t("settings.updates.action.checkNow")}
|
|
||||||
</ButtonV2>
|
</ButtonV2>
|
||||||
</SettingsRowV2>
|
</SettingsRowV2>
|
||||||
</SettingsListV2>
|
</SettingsListV2>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { Switch } from "@opencode-ai/ui/switch"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { useMutation, useQueryClient } from "@tanstack/solid-query"
|
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import { type Accessor, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js"
|
import { type Accessor, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js"
|
||||||
@@ -11,14 +10,12 @@ import { createStore } from "solid-js/store"
|
|||||||
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
|
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useSDK } from "@/context/sdk"
|
|
||||||
import { ServerConnection, useServer } from "@/context/server"
|
import { ServerConnection, useServer } from "@/context/server"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { type ServerHealth } from "@/utils/server-health"
|
import { type ServerHealth } from "@/utils/server-health"
|
||||||
import { useQueryOptions } from "@/context/server-sync"
|
|
||||||
import { pathKey } from "@/utils/path-key"
|
|
||||||
import { useGlobal } from "@/context/global"
|
import { useGlobal } from "@/context/global"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
|
import { useMcpToggle } from "@/context/mcp"
|
||||||
|
|
||||||
const pluginEmptyMessage = (value: string, file: string): JSXElement => {
|
const pluginEmptyMessage = (value: string, file: string): JSXElement => {
|
||||||
const parts = value.split(file)
|
const parts = value.split(file)
|
||||||
@@ -99,37 +96,6 @@ const useDefaultServerKey = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const useMcpToggleMutation = () => {
|
|
||||||
const sync = useSync()
|
|
||||||
const sdk = useSDK()
|
|
||||||
const language = useLanguage()
|
|
||||||
const queryClient = useQueryClient()
|
|
||||||
const queryOptions = useQueryOptions()
|
|
||||||
|
|
||||||
return useMutation(() => ({
|
|
||||||
mutationFn: async (name: string) => {
|
|
||||||
const status = sync.data.mcp[name]
|
|
||||||
if (status?.status === "connected") {
|
|
||||||
await sdk.client.mcp.disconnect({ name })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (status?.status === "needs_auth") {
|
|
||||||
await sdk.client.mcp.auth.authenticate({ name })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await sdk.client.mcp.connect({ name })
|
|
||||||
},
|
|
||||||
onSuccess: () => queryClient.refetchQueries(queryOptions.mcp(pathKey(sync.directory))),
|
|
||||||
onError: (err) => {
|
|
||||||
showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: language.t("common.requestFailed"),
|
|
||||||
description: err instanceof Error ? err.message : String(err),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
type ServerStatusState = {
|
type ServerStatusState = {
|
||||||
servers: () => ServerStatusItem[]
|
servers: () => ServerStatusItem[]
|
||||||
defaultKey: () => ServerConnection.Key | undefined
|
defaultKey: () => ServerConnection.Key | undefined
|
||||||
@@ -311,7 +277,7 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
|||||||
dialogRun += 1
|
dialogRun += 1
|
||||||
})
|
})
|
||||||
const sortedServers = createMemo(() => listServersByHealth(global.servers.list(), server.key, global.servers.health))
|
const sortedServers = createMemo(() => listServersByHealth(global.servers.list(), server.key, global.servers.health))
|
||||||
const toggleMcp = useMcpToggleMutation()
|
const toggleMcp = useMcpToggle()
|
||||||
const defaultServer = useDefaultServerKey(platform.getDefaultServer)
|
const defaultServer = useDefaultServerKey(platform.getDefaultServer)
|
||||||
const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b)))
|
const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b)))
|
||||||
const mcpStatus = (name: string) => sync.data.mcp?.[name]?.status
|
const mcpStatus = (name: string) => sync.data.mcp?.[name]?.status
|
||||||
|
|||||||
@@ -280,7 +280,8 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
|
|
||||||
const matchRoute = (route: LayoutRoute) => {
|
const matchRoute = (route: LayoutRoute) => {
|
||||||
if (route.type === "home") return
|
if (route.type === "home") return
|
||||||
if (route.type === "dir-new-sesssion") {
|
if (route.type === "draft") {
|
||||||
|
return tabsStore.find((item) => item.type === "draft" && item.draftID === route.draftID)
|
||||||
}
|
}
|
||||||
if (route.type === "session") {
|
if (route.type === "session") {
|
||||||
const main = tabsStore.find(
|
const main = tabsStore.find(
|
||||||
@@ -447,11 +448,33 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
refreshTabsAreOverflowing()
|
refreshTabsAreOverflowing()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const divider = () =>
|
||||||
|
i() !== 0 && (
|
||||||
|
<div class="w-[1.5px] h-3 shrink-0 rounded-full bg-[var(--v2-background-bg-layer-02)]" />
|
||||||
|
)
|
||||||
|
|
||||||
|
if (tab.type === "draft") {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{divider()}
|
||||||
|
<DraftTabItem
|
||||||
|
ref={ref}
|
||||||
|
href={tabHref(tab)}
|
||||||
|
title={language.t("command.session.new")}
|
||||||
|
active={currentTab() === tab}
|
||||||
|
onNavigate={() => {
|
||||||
|
navigateTab(tab)
|
||||||
|
ref.scrollIntoView({ behavior: "instant" })
|
||||||
|
}}
|
||||||
|
onClose={() => tabsStoreActions.removeTab(i())}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{i() !== 0 && (
|
{divider()}
|
||||||
<div class="w-[1.5px] h-3 shrink-0 rounded-full bg-[var(--v2-background-bg-layer-02)]" />
|
|
||||||
)}
|
|
||||||
<TabNavItem
|
<TabNavItem
|
||||||
ref={ref}
|
ref={ref}
|
||||||
href={tabHref(tab)}
|
href={tabHref(tab)}
|
||||||
@@ -782,7 +805,6 @@ function TabNavItem(props: {
|
|||||||
>
|
>
|
||||||
<Show when={session.latest}>
|
<Show when={session.latest}>
|
||||||
{(session) => {
|
{(session) => {
|
||||||
console.log({ session: session() })
|
|
||||||
const project = createMemo(() => projectForSession(session(), serverCtx()?.projects.list() ?? []))
|
const project = createMemo(() => projectForSession(session(), serverCtx()?.projects.list() ?? []))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -851,6 +873,59 @@ function ProjectTabAvatar(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function DraftTabItem(props: {
|
||||||
|
ref?: HTMLDivElement
|
||||||
|
href: string
|
||||||
|
title: string
|
||||||
|
active?: boolean
|
||||||
|
onNavigate: () => void
|
||||||
|
onClose: () => void
|
||||||
|
}) {
|
||||||
|
const closeTab = (event: MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
props.onClose()
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={props.ref}
|
||||||
|
data-active={props.active}
|
||||||
|
class="group relative shrink-0 flex h-7 max-w-60 flex-row items-center gap-1.5 overflow-hidden rounded-[6px] bg-[var(--tab-bg)] pl-1.5 pr-8 whitespace-nowrap [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-overlay-simple-overlay-pressed)] focus-within:outline focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-[var(--v2-border-border-focus)]"
|
||||||
|
onMouseDown={(event) => {
|
||||||
|
if (event.button !== 1) return
|
||||||
|
closeTab(event)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={props.href}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
props.onNavigate()
|
||||||
|
}}
|
||||||
|
class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 overflow-hidden text-[13px] font-medium leading-5 text-v2-text-text-faint group-data-[active='true']:text-[var(--v2-text-text-base)]"
|
||||||
|
>
|
||||||
|
<span class="flex size-4 shrink-0 rotate-90 items-center justify-center">
|
||||||
|
<IconV2 name="edit" />
|
||||||
|
</span>
|
||||||
|
<span class="truncate leading-5">{props.title}</span>
|
||||||
|
</a>
|
||||||
|
<div class="absolute right-0 inset-y-0 flex w-7 items-center justify-center">
|
||||||
|
<IconButtonV2
|
||||||
|
size="small"
|
||||||
|
variant="ghost-muted"
|
||||||
|
onMouseDown={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
}}
|
||||||
|
onClick={closeTab}
|
||||||
|
icon={<IconV2 name="xmark-small" />}
|
||||||
|
aria-label="Close tab"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function NewSessionTabItem(props: { ref?: HTMLDivElement; href: string; title: string; onClose: () => void }) {
|
function NewSessionTabItem(props: { ref?: HTMLDivElement; href: string; title: string; onClose: () => void }) {
|
||||||
const closeTab = (event: MouseEvent) => {
|
const closeTab = (event: MouseEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { updaterAction } from "./updater-action"
|
||||||
|
|
||||||
|
describe("updaterAction", () => {
|
||||||
|
test("disables update actions when the platform has no updater", () => {
|
||||||
|
expect(updaterAction(undefined)).toEqual({ label: "settings.updates.action.checkNow" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("projects updater transitions into one settings action", () => {
|
||||||
|
expect(updaterAction({ status: "idle" })).toEqual({
|
||||||
|
label: "settings.updates.action.checkNow",
|
||||||
|
run: "check",
|
||||||
|
})
|
||||||
|
expect(updaterAction({ status: "checking" })).toEqual({ label: "settings.updates.action.checking" })
|
||||||
|
expect(updaterAction({ status: "downloading", version: "2.0.0" })).toEqual({
|
||||||
|
label: "settings.updates.action.downloading",
|
||||||
|
})
|
||||||
|
expect(updaterAction({ status: "ready", version: "2.0.0" })).toEqual({
|
||||||
|
label: "toast.update.action.installRestart",
|
||||||
|
run: "install",
|
||||||
|
})
|
||||||
|
expect(updaterAction({ status: "installing", version: "2.0.0" })).toEqual({
|
||||||
|
label: "settings.updates.action.installing",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { createMemo } from "solid-js"
|
||||||
|
import type { UpdaterState } from "@/updater"
|
||||||
|
import { usePlatform } from "@/context/platform"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { showToast } from "@/utils/toast"
|
||||||
|
|
||||||
|
export function updaterAction(state: UpdaterState | undefined) {
|
||||||
|
if (!state) return { label: "settings.updates.action.checkNow" as const }
|
||||||
|
switch (state.status) {
|
||||||
|
case "checking":
|
||||||
|
return { label: "settings.updates.action.checking" as const }
|
||||||
|
case "downloading":
|
||||||
|
return { label: "settings.updates.action.downloading" as const }
|
||||||
|
case "ready":
|
||||||
|
return { label: "toast.update.action.installRestart" as const, run: "install" as const }
|
||||||
|
case "installing":
|
||||||
|
return { label: "settings.updates.action.installing" as const }
|
||||||
|
case "disabled":
|
||||||
|
return { label: "settings.updates.action.checkNow" as const }
|
||||||
|
default:
|
||||||
|
return { label: "settings.updates.action.checkNow" as const, run: "check" as const }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdaterAction() {
|
||||||
|
const platform = usePlatform()
|
||||||
|
const language = useLanguage()
|
||||||
|
const action = createMemo(() => updaterAction(platform.updater?.state()))
|
||||||
|
|
||||||
|
return {
|
||||||
|
action,
|
||||||
|
async run() {
|
||||||
|
const run = action().run
|
||||||
|
if (run === "install") return platform.updater?.install()
|
||||||
|
if (run !== "check") return
|
||||||
|
|
||||||
|
const state = await platform.updater?.check()
|
||||||
|
if (state?.status === "up-to-date") {
|
||||||
|
showToast({
|
||||||
|
variant: "success",
|
||||||
|
icon: "circle-check",
|
||||||
|
title: language.t("settings.updates.toast.latest.title"),
|
||||||
|
description: language.t("settings.updates.toast.latest.description", { version: platform.version ?? "" }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (state?.status === "error") {
|
||||||
|
showToast({ title: language.t("common.requestFailed"), description: state.message })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ beforeAll(async () => {
|
|||||||
mock.module("@solidjs/router", () => ({
|
mock.module("@solidjs/router", () => ({
|
||||||
useNavigate: () => () => undefined,
|
useNavigate: () => () => undefined,
|
||||||
useParams: () => ({}),
|
useParams: () => ({}),
|
||||||
|
useLocation: () => ({}),
|
||||||
|
useSearchParams: () => [{}, () => undefined],
|
||||||
}))
|
}))
|
||||||
mock.module("@opencode-ai/ui/context", () => ({
|
mock.module("@opencode-ai/ui/context", () => ({
|
||||||
createSimpleContext: () => ({
|
createSimpleContext: () => ({
|
||||||
|
|||||||
@@ -609,6 +609,9 @@ export const createDirSyncContext = (
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
mcp: {
|
||||||
|
toggle: (name: string) => serverSync.mcp.toggle(directory, name),
|
||||||
|
},
|
||||||
absolute,
|
absolute,
|
||||||
get directory() {
|
get directory() {
|
||||||
return current()[0].path.directory
|
return current()[0].path.directory
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { toggleMcp } from "./mcp"
|
||||||
|
|
||||||
|
describe("toggleMcp", () => {
|
||||||
|
test("runs the status action before refreshing the owning query", async () => {
|
||||||
|
const calls: string[] = []
|
||||||
|
const input = (status: "connected" | "needs_auth" | "disabled") => ({
|
||||||
|
status,
|
||||||
|
connect: async () => {
|
||||||
|
calls.push("connect")
|
||||||
|
},
|
||||||
|
disconnect: async () => {
|
||||||
|
calls.push("disconnect")
|
||||||
|
},
|
||||||
|
authenticate: async () => {
|
||||||
|
calls.push("authenticate")
|
||||||
|
},
|
||||||
|
refresh: async () => {
|
||||||
|
calls.push("refresh")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await toggleMcp(input("connected"))
|
||||||
|
expect(calls).toEqual(["disconnect", "refresh"])
|
||||||
|
|
||||||
|
calls.length = 0
|
||||||
|
await toggleMcp(input("needs_auth"))
|
||||||
|
expect(calls).toEqual(["authenticate", "refresh"])
|
||||||
|
|
||||||
|
calls.length = 0
|
||||||
|
await toggleMcp(input("disabled"))
|
||||||
|
expect(calls).toEqual(["connect", "refresh"])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import type { McpStatus } from "@opencode-ai/sdk/v2/client"
|
||||||
|
|
||||||
|
export async function toggleMcp(input: {
|
||||||
|
status: McpStatus["status"]
|
||||||
|
connect: () => Promise<void>
|
||||||
|
disconnect: () => Promise<void>
|
||||||
|
authenticate: () => Promise<void>
|
||||||
|
refresh: () => Promise<void>
|
||||||
|
}) {
|
||||||
|
await {
|
||||||
|
connected: input.disconnect,
|
||||||
|
needs_auth: input.authenticate,
|
||||||
|
disabled: input.connect,
|
||||||
|
failed: input.connect,
|
||||||
|
needs_client_registration: input.connect,
|
||||||
|
}[input.status]()
|
||||||
|
await input.refresh()
|
||||||
|
}
|
||||||
@@ -77,6 +77,7 @@ export type ReviewDiffStyle = "unified" | "split"
|
|||||||
|
|
||||||
export type LayoutRoute =
|
export type LayoutRoute =
|
||||||
| { type: "home" }
|
| { type: "home" }
|
||||||
|
| { type: "draft"; draftID: string; server?: ServerConnection.Key }
|
||||||
| { type: "dir-new-sesssion"; dir: string; dirBase64: string; server?: ServerConnection.Key }
|
| { type: "dir-new-sesssion"; dir: string; dirBase64: string; server?: ServerConnection.Key }
|
||||||
| { type: "session"; dir: string; dirBase64: string; sessionId: string; server?: ServerConnection.Key }
|
| { type: "session"; dir: string; dirBase64: string; sessionId: string; server?: ServerConnection.Key }
|
||||||
|
|
||||||
@@ -120,10 +121,16 @@ const normalizeStoredSessionTabs = (key: string, tabs: SessionTabs) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentRoute = (pathname: string): LayoutRoute => {
|
const currentRoute = (pathname: string, search: string): LayoutRoute => {
|
||||||
const parts = pathname.split("/").filter(Boolean)
|
const parts = pathname.split("/").filter(Boolean)
|
||||||
if (parts.length === 0) return { type: "home" }
|
if (parts.length === 0) return { type: "home" }
|
||||||
|
|
||||||
|
if (parts[0] === "new-session") {
|
||||||
|
const draftID = new URLSearchParams(search).get("draftId")
|
||||||
|
if (!draftID) return { type: "home" }
|
||||||
|
return { type: "draft", draftID }
|
||||||
|
}
|
||||||
|
|
||||||
const dirBase64 = parts[0]
|
const dirBase64 = parts[0]
|
||||||
const dir = decode64(dirBase64)
|
const dir = decode64(dirBase64)
|
||||||
if (!dir) return { type: "home" }
|
if (!dir) return { type: "home" }
|
||||||
@@ -145,7 +152,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const route = createMemo(() => {
|
const route = createMemo(() => {
|
||||||
const value = currentRoute(location.pathname)
|
const value = currentRoute(location.pathname, location.search)
|
||||||
if (value.type === "home") return value
|
if (value.type === "home") return value
|
||||||
return { ...value, server: server.key }
|
return { ...value, server: server.key }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { useMutation } from "@tanstack/solid-query"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { useSync } from "@/context/sync"
|
||||||
|
import { showToast } from "@/utils/toast"
|
||||||
|
|
||||||
|
export function useMcpToggle() {
|
||||||
|
const sync = useSync()
|
||||||
|
const language = useLanguage()
|
||||||
|
|
||||||
|
return useMutation(() => ({
|
||||||
|
mutationFn: sync.mcp.toggle,
|
||||||
|
onError: (error) =>
|
||||||
|
showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: language.t("common.requestFailed"),
|
||||||
|
description: error instanceof Error ? error.message : String(error),
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -4,12 +4,18 @@ import type { Accessor } from "solid-js"
|
|||||||
import type { DesktopMenuAction } from "../desktop-menu"
|
import type { DesktopMenuAction } from "../desktop-menu"
|
||||||
import { ServerConnection } from "./server"
|
import { ServerConnection } from "./server"
|
||||||
import type { WslServersPlatform } from "../wsl/types"
|
import type { WslServersPlatform } from "../wsl/types"
|
||||||
|
import type { UpdaterPlatform } from "../updater"
|
||||||
|
|
||||||
type PickerPaths = string | string[] | null
|
type PickerPaths = string | string[] | null
|
||||||
type OpenDirectoryPickerOptions = { title?: string; multiple?: boolean }
|
type OpenDirectoryPickerOptions = { title?: string; multiple?: boolean }
|
||||||
type OpenFilePickerOptions = { title?: string; multiple?: boolean; accept?: string[]; extensions?: string[] }
|
type OpenAttachmentPickerOptions = {
|
||||||
|
title?: string
|
||||||
|
multiple?: boolean
|
||||||
|
accept?: string[]
|
||||||
|
extensions?: string[]
|
||||||
|
defaultPath?: string
|
||||||
|
}
|
||||||
type SaveFilePickerOptions = { title?: string; defaultPath?: string }
|
type SaveFilePickerOptions = { title?: string; defaultPath?: string }
|
||||||
type UpdateInfo = { updateAvailable: boolean; version?: string }
|
|
||||||
type PlatformName = "web" | "desktop"
|
type PlatformName = "web" | "desktop"
|
||||||
type DesktopOS = "macos" | "windows" | "linux"
|
type DesktopOS = "macos" | "windows" | "linux"
|
||||||
|
|
||||||
@@ -21,13 +27,7 @@ export type FatalRendererErrorLog = {
|
|||||||
os?: DesktopOS
|
os?: DesktopOS
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Platform = {
|
type PlatformBase = {
|
||||||
/** Platform discriminator */
|
|
||||||
platform: PlatformName
|
|
||||||
|
|
||||||
/** Desktop OS (Tauri only) */
|
|
||||||
os?: DesktopOS
|
|
||||||
|
|
||||||
/** App version */
|
/** App version */
|
||||||
version?: string
|
version?: string
|
||||||
|
|
||||||
@@ -49,23 +49,20 @@ export type Platform = {
|
|||||||
/** Send a system notification (optional deep link) */
|
/** Send a system notification (optional deep link) */
|
||||||
notify(title: string, description?: string, href?: string): Promise<void>
|
notify(title: string, description?: string, href?: string): Promise<void>
|
||||||
|
|
||||||
/** Open directory picker dialog (native on Tauri, server-backed on web) */
|
/** Open a native attachment picker and read selected files sequentially (desktop only) */
|
||||||
openDirectoryPickerDialog?(opts?: OpenDirectoryPickerOptions): Promise<PickerPaths>
|
openAttachmentPickerDialog?(
|
||||||
|
opts: OpenAttachmentPickerOptions,
|
||||||
|
onFile: (file: File) => Promise<unknown>,
|
||||||
|
): Promise<void>
|
||||||
|
|
||||||
/** Open native file picker dialog (Tauri only) */
|
/** Open a native save file picker dialog (desktop only) */
|
||||||
openFilePickerDialog?(opts?: OpenFilePickerOptions): Promise<PickerPaths>
|
|
||||||
|
|
||||||
/** Save file picker dialog (Tauri only) */
|
|
||||||
saveFilePickerDialog?(opts?: SaveFilePickerOptions): Promise<string | null>
|
saveFilePickerDialog?(opts?: SaveFilePickerOptions): Promise<string | null>
|
||||||
|
|
||||||
/** Storage mechanism, defaults to localStorage */
|
/** Storage mechanism, defaults to localStorage */
|
||||||
storage?: (name?: string) => SyncStorage | AsyncStorage
|
storage?: (name?: string) => SyncStorage | AsyncStorage
|
||||||
|
|
||||||
/** Check for a downloadable desktop update */
|
/** Application-global desktop updater */
|
||||||
checkUpdate?(): Promise<UpdateInfo>
|
updater?: UpdaterPlatform
|
||||||
|
|
||||||
/** Install the downloaded update using the platform restart flow */
|
|
||||||
updateAndRestart?(): Promise<void>
|
|
||||||
|
|
||||||
/** Fetch override */
|
/** Fetch override */
|
||||||
fetch?: typeof fetch
|
fetch?: typeof fetch
|
||||||
@@ -113,6 +110,16 @@ export type Platform = {
|
|||||||
recordFatalRendererError?(error: FatalRendererErrorLog): Promise<void>
|
recordFatalRendererError?(error: FatalRendererErrorLog): Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Platform = PlatformBase &
|
||||||
|
(
|
||||||
|
| { platform: "web"; os?: never }
|
||||||
|
| {
|
||||||
|
platform: "desktop"
|
||||||
|
os?: DesktopOS
|
||||||
|
openDirectoryPickerDialog(opts?: OpenDirectoryPickerOptions): Promise<PickerPaths>
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
export type DisplayBackend = "auto" | "wayland"
|
export type DisplayBackend = "auto" | "wayland"
|
||||||
|
|
||||||
export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({
|
export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { checksum } from "@opencode-ai/core/util/encode"
|
import { checksum } from "@opencode-ai/core/util/encode"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams, useSearchParams } from "@solidjs/router"
|
||||||
import { batch, createMemo, createRoot, getOwner, onCleanup } from "solid-js"
|
import { batch, createMemo, createRoot, getOwner, onCleanup } from "solid-js"
|
||||||
import { createStore, type SetStoreFunction } from "solid-js/store"
|
import { createStore, type SetStoreFunction } from "solid-js/store"
|
||||||
import type { FileSelection } from "@/context/file"
|
import type { FileSelection } from "@/context/file"
|
||||||
@@ -153,9 +153,11 @@ const MAX_PROMPT_SESSIONS = 20
|
|||||||
|
|
||||||
type PromptSession = ReturnType<typeof createPromptSession>
|
type PromptSession = ReturnType<typeof createPromptSession>
|
||||||
|
|
||||||
type Scope = {
|
type Scope = { draftID: string } | { dir: string; id?: string }
|
||||||
dir: string
|
|
||||||
id?: string
|
function scopeKey(scope: Scope) {
|
||||||
|
if ("draftID" in scope) return `draft:${scope.draftID}`
|
||||||
|
return `${scope.dir}:${scope.id ?? WORKSPACE_KEY}`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PromptCacheEntry = {
|
type PromptCacheEntry = {
|
||||||
@@ -163,11 +165,15 @@ type PromptCacheEntry = {
|
|||||||
dispose: VoidFunction
|
dispose: VoidFunction
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPromptSession(scope: ServerScope, dir: string, id: string | undefined) {
|
function promptTarget(serverScope: ServerScope, scope: Scope) {
|
||||||
const legacy = `${dir}/prompt${id ? "/" + id : ""}.v2`
|
if ("draftID" in scope) return Persist.draft(scope.draftID, "prompt")
|
||||||
|
const legacy = `${scope.dir}/prompt${scope.id ? "/" + scope.id : ""}.v2`
|
||||||
|
return Persist.serverScoped(serverScope, scope.dir, scope.id, "prompt", [legacy])
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPromptSession(serverScope: ServerScope, scope: Scope) {
|
||||||
const [store, setStore, _, ready] = persisted(
|
const [store, setStore, _, ready] = persisted(
|
||||||
Persist.serverScoped(scope, dir, id, "prompt", [legacy]),
|
promptTarget(serverScope, scope),
|
||||||
createStore<{
|
createStore<{
|
||||||
prompt: Prompt
|
prompt: Prompt
|
||||||
cursor?: number
|
cursor?: number
|
||||||
@@ -231,6 +237,7 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
|
|||||||
gate: false,
|
gate: false,
|
||||||
init: () => {
|
init: () => {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
|
const [search] = useSearchParams<{ draftId?: string }>()
|
||||||
const serverSDK = useServerSDK()
|
const serverSDK = useServerSDK()
|
||||||
const cache = new Map<string, PromptCacheEntry>()
|
const cache = new Map<string, PromptCacheEntry>()
|
||||||
|
|
||||||
@@ -254,8 +261,8 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const owner = getOwner()
|
const owner = getOwner()
|
||||||
const load = (dir: string, id: string | undefined) => {
|
const load = (scope: Scope) => {
|
||||||
const key = `${dir}:${id ?? WORKSPACE_KEY}`
|
const key = scopeKey(scope)
|
||||||
const existing = cache.get(key)
|
const existing = cache.get(key)
|
||||||
if (existing) {
|
if (existing) {
|
||||||
cache.delete(key)
|
cache.delete(key)
|
||||||
@@ -265,7 +272,7 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
|
|||||||
|
|
||||||
const entry = createRoot(
|
const entry = createRoot(
|
||||||
(dispose) => ({
|
(dispose) => ({
|
||||||
value: createPromptSession(serverSDK.scope, dir, id),
|
value: createPromptSession(serverSDK.scope, scope),
|
||||||
dispose,
|
dispose,
|
||||||
}),
|
}),
|
||||||
owner,
|
owner,
|
||||||
@@ -276,8 +283,10 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
|
|||||||
return entry.value
|
return entry.value
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = createMemo(() => load(params.dir!, params.id))
|
const session = createMemo(() =>
|
||||||
const pick = (scope?: Scope) => (scope ? load(scope.dir, scope.id) : session())
|
load(search.draftId ? { draftID: search.draftId } : { dir: params.dir!, id: params.id }),
|
||||||
|
)
|
||||||
|
const pick = (scope?: Scope) => (scope ? load(scope) : session())
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ready: () => session().ready,
|
ready: () => session().ready,
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import { ServerConnection, useServer } from "./server"
|
|||||||
import { retry } from "@opencode-ai/core/util/retry"
|
import { retry } from "@opencode-ai/core/util/retry"
|
||||||
import type { ServerScope } from "@/utils/server-scope"
|
import type { ServerScope } from "@/utils/server-scope"
|
||||||
import { persisted } from "@/utils/persist"
|
import { persisted } from "@/utils/persist"
|
||||||
|
import { toggleMcp } from "./global-sync/mcp"
|
||||||
|
|
||||||
type GlobalStore = {
|
type GlobalStore = {
|
||||||
ready: boolean
|
ready: boolean
|
||||||
@@ -481,6 +482,28 @@ export function createServerSyncContextInner(_serverSDK?: ServerSDK) {
|
|||||||
todo: {
|
todo: {
|
||||||
set: setSessionTodo,
|
set: setSessionTodo,
|
||||||
},
|
},
|
||||||
|
mcp: {
|
||||||
|
toggle: async (directory: string, name: string) => {
|
||||||
|
const key = directoryKey(directory)
|
||||||
|
const sdk = sdkFor(key)
|
||||||
|
const status = children.child(key, { bootstrap: false })[0].mcp[name].status
|
||||||
|
await toggleMcp({
|
||||||
|
status,
|
||||||
|
connect: async () => {
|
||||||
|
await sdk.mcp.connect({ name })
|
||||||
|
},
|
||||||
|
disconnect: async () => {
|
||||||
|
await sdk.mcp.disconnect({ name })
|
||||||
|
},
|
||||||
|
authenticate: async () => {
|
||||||
|
await sdk.mcp.auth.authenticate({ name })
|
||||||
|
},
|
||||||
|
refresh: async () => {
|
||||||
|
await queryClient.refetchQueries(queryOptionsApi.mcp(key))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,9 +35,6 @@ export interface Settings {
|
|||||||
showCustomAgents: boolean
|
showCustomAgents: boolean
|
||||||
newLayoutDesigns?: boolean
|
newLayoutDesigns?: boolean
|
||||||
}
|
}
|
||||||
updates: {
|
|
||||||
startup: boolean
|
|
||||||
}
|
|
||||||
appearance: {
|
appearance: {
|
||||||
fontSize: number
|
fontSize: number
|
||||||
mono: string
|
mono: string
|
||||||
@@ -122,9 +119,6 @@ const defaultSettings: Settings = {
|
|||||||
showSessionProgressBar: true,
|
showSessionProgressBar: true,
|
||||||
showCustomAgents: false,
|
showCustomAgents: false,
|
||||||
},
|
},
|
||||||
updates: {
|
|
||||||
startup: true,
|
|
||||||
},
|
|
||||||
appearance: {
|
appearance: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
mono: "",
|
mono: "",
|
||||||
@@ -250,12 +244,6 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
|||||||
setStore("general", "newLayoutDesigns", value)
|
setStore("general", "newLayoutDesigns", value)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
updates: {
|
|
||||||
startup: withFallback(() => store.updates?.startup, defaultSettings.updates.startup),
|
|
||||||
setStartup(value: boolean) {
|
|
||||||
setStore("updates", "startup", value)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
appearance: {
|
appearance: {
|
||||||
fontSize: withFallback(() => store.appearance?.fontSize, defaultSettings.appearance.fontSize),
|
fontSize: withFallback(() => store.appearance?.fontSize, defaultSettings.appearance.fontSize),
|
||||||
setFontSize(value: number) {
|
setFontSize(value: number) {
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import type { Session } from "@opencode-ai/sdk/v2/client"
|
|||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
import { createStore, produce } from "solid-js/store"
|
import { createStore, produce } from "solid-js/store"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted, removePersisted, draftPersistedKeys } from "@/utils/persist"
|
||||||
import { ServerConnection, useServer } from "./server"
|
import { ServerConnection, useServer } from "./server"
|
||||||
import { createEffect, startTransition } from "solid-js"
|
import { createEffect, startTransition } from "solid-js"
|
||||||
import { useNavigate, useParams } from "@solidjs/router"
|
import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
||||||
|
import { usePlatform } from "./platform"
|
||||||
|
import { uuid } from "@/utils/uuid"
|
||||||
import { SessionTabsRemovedDetail } from "@/components/titlebar-session-events"
|
import { SessionTabsRemovedDetail } from "@/components/titlebar-session-events"
|
||||||
|
|
||||||
export type SessionTab = {
|
export type SessionTab = {
|
||||||
@@ -15,10 +17,22 @@ export type SessionTab = {
|
|||||||
sessionId: string
|
sessionId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Tab = SessionTab
|
export type DraftTab = {
|
||||||
|
type: "draft"
|
||||||
|
draftID: string
|
||||||
|
server: ServerConnection.Key
|
||||||
|
directory: string
|
||||||
|
worktree?: string
|
||||||
|
}
|
||||||
|
|
||||||
export const tabHref = (tab: Tab) => `/${tab.dirBase64}/session/${tab.sessionId}`
|
export type Tab = SessionTab | DraftTab
|
||||||
export const tabKey = (tab: Tab) => `${tab.server}\n${tabHref(tab)}`
|
|
||||||
|
export const draftHref = (draftID: string) => `/new-session?draftId=${encodeURIComponent(draftID)}`
|
||||||
|
|
||||||
|
export const tabHref = (tab: Tab) =>
|
||||||
|
tab.type === "draft" ? draftHref(tab.draftID) : `/${tab.dirBase64}/session/${tab.sessionId}`
|
||||||
|
|
||||||
|
export const tabKey = (tab: Tab) => (tab.type === "draft" ? `draft:${tab.draftID}` : `${tab.server}\n${tabHref(tab)}`)
|
||||||
|
|
||||||
export function sessionHasOpenTab(tabs: Tab[], server: ServerConnection.Key, session: Session) {
|
export function sessionHasOpenTab(tabs: Tab[], server: ServerConnection.Key, session: Session) {
|
||||||
const dirBase64 = base64Encode(session.directory)
|
const dirBase64 = base64Encode(session.directory)
|
||||||
@@ -33,6 +47,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
|||||||
gate: false,
|
gate: false,
|
||||||
init: () => {
|
init: () => {
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
|
const platform = usePlatform()
|
||||||
const fallback = server.key
|
const fallback = server.key
|
||||||
const [store, setStore, _, ready] = persisted(
|
const [store, setStore, _, ready] = persisted(
|
||||||
{
|
{
|
||||||
@@ -50,9 +65,14 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
|||||||
|
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
const closing = new Set<string>()
|
const closing = new Set<string>()
|
||||||
|
|
||||||
|
const removeDraftPersisted = (draftID: string) => {
|
||||||
|
for (const key of draftPersistedKeys()) removePersisted(Persist.draft(draftID, key), platform)
|
||||||
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!ready()) return
|
if (!ready()) return
|
||||||
const servers = new Set(server.list.map(ServerConnection.key))
|
const servers = new Set(server.list.map(ServerConnection.key))
|
||||||
@@ -83,10 +103,48 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
draft(draftID: string) {
|
||||||
|
const tab = store.find((item) => item.type === "draft" && item.draftID === draftID)
|
||||||
|
if (!tab || tab.type !== "draft") throw new Error(`Draft not found: ${draftID}`)
|
||||||
|
return tab
|
||||||
|
},
|
||||||
|
newDraft(draft: Omit<DraftTab, "type" | "draftID">, prompt?: string) {
|
||||||
|
const draftID = uuid()
|
||||||
|
setStore(
|
||||||
|
produce((tabs) => {
|
||||||
|
tabs.push({ type: "draft", draftID, ...draft })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
navigate(prompt ? `${draftHref(draftID)}&prompt=${encodeURIComponent(prompt)}` : draftHref(draftID))
|
||||||
|
},
|
||||||
|
updateDraft(draftID: string, draft: Partial<Omit<DraftTab, "type" | "draftID">>) {
|
||||||
|
setStore(
|
||||||
|
(tab) => tab.type === "draft" && tab.draftID === draftID,
|
||||||
|
produce((tab) => Object.assign(tab, draft)),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
promoteDraft(draftID: string, session: Omit<SessionTab, "type">) {
|
||||||
|
// We're viewing this draft when /new-session?draftId=… points at it. Promoting
|
||||||
|
// replaces the draft tab with a session tab, so the draft route would stop resolving
|
||||||
|
// and fall back home. Navigate to the new session first so we leave /new-session
|
||||||
|
// before the draft is removed from the store.
|
||||||
|
const active = location.pathname === "/new-session" && location.query.draftId === draftID
|
||||||
|
startTransition(() => {
|
||||||
|
setStore(
|
||||||
|
produce((tabs) => {
|
||||||
|
const index = tabs.findIndex((tab) => tab.type === "draft" && tab.draftID === draftID)
|
||||||
|
if (index !== -1) tabs[index] = { type: "session", ...session }
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
if (active) navigateTab({ type: "session", ...session })
|
||||||
|
})
|
||||||
|
removeDraftPersisted(draftID)
|
||||||
|
},
|
||||||
removeTab: (index: number) => {
|
removeTab: (index: number) => {
|
||||||
const tab = store[index]
|
const tab = store[index]
|
||||||
if (!tab) return
|
if (!tab) return
|
||||||
const key = tabKey(tab)
|
const key = tabKey(tab)
|
||||||
|
const draftID = tab.type === "draft" ? tab.draftID : undefined
|
||||||
const nextTab = store[index + 1] ?? store[index - 1]
|
const nextTab = store[index + 1] ?? store[index - 1]
|
||||||
closing.add(key)
|
closing.add(key)
|
||||||
void startTransition(() => {
|
void startTransition(() => {
|
||||||
@@ -98,9 +156,12 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
|||||||
if (nextTab) navigateTab(nextTab)
|
if (nextTab) navigateTab(nextTab)
|
||||||
else navigate("/")
|
else navigate("/")
|
||||||
}).finally(() => closing.delete(key))
|
}).finally(() => closing.delete(key))
|
||||||
|
if (draftID) removeDraftPersisted(draftID)
|
||||||
},
|
},
|
||||||
removeServer(key: ServerConnection.Key) {
|
removeServer(key: ServerConnection.Key) {
|
||||||
|
const drafts = store.flatMap((tab) => (tab.type === "draft" && tab.server === key ? [tab.draftID] : []))
|
||||||
setStore((tabs) => tabs.filter((tab) => tab.server !== key))
|
setStore((tabs) => tabs.filter((tab) => tab.server !== key))
|
||||||
|
for (const draftID of drafts) removeDraftPersisted(draftID)
|
||||||
if (server.key === key) navigate("/")
|
if (server.key === key) navigate("/")
|
||||||
},
|
},
|
||||||
removeSessions: (input: SessionTabsRemovedDetail) => {
|
removeSessions: (input: SessionTabsRemovedDetail) => {
|
||||||
@@ -110,7 +171,12 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
|||||||
const sessionIDs = new Set(input.sessionIDs)
|
const sessionIDs = new Set(input.sessionIDs)
|
||||||
const currentHref =
|
const currentHref =
|
||||||
params.dir && params.id
|
params.dir && params.id
|
||||||
? tabHref({ type: "session", server: server.key, dirBase64: params.dir, sessionId: params.id })
|
? tabHref({
|
||||||
|
type: "session",
|
||||||
|
server: server.key,
|
||||||
|
dirBase64: params.dir,
|
||||||
|
sessionId: params.id,
|
||||||
|
})
|
||||||
: undefined
|
: undefined
|
||||||
const currentIndex = currentHref
|
const currentIndex = currentHref
|
||||||
? tabs.findIndex(
|
? tabs.findIndex(
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ beforeAll(async () => {
|
|||||||
mock.module("@solidjs/router", () => ({
|
mock.module("@solidjs/router", () => ({
|
||||||
useNavigate: () => () => undefined,
|
useNavigate: () => () => undefined,
|
||||||
useParams: () => ({}),
|
useParams: () => ({}),
|
||||||
|
useLocation: () => ({}),
|
||||||
|
useSearchParams: () => [{}, () => undefined],
|
||||||
}))
|
}))
|
||||||
mock.module("@opencode-ai/ui/context", () => ({
|
mock.module("@opencode-ai/ui/context", () => ({
|
||||||
createSimpleContext: () => ({
|
createSimpleContext: () => ({
|
||||||
|
|||||||
@@ -866,6 +866,8 @@ export const dict = {
|
|||||||
"settings.updates.row.check.description": "Manually check for updates and install if available",
|
"settings.updates.row.check.description": "Manually check for updates and install if available",
|
||||||
"settings.updates.action.checkNow": "Check now",
|
"settings.updates.action.checkNow": "Check now",
|
||||||
"settings.updates.action.checking": "Checking...",
|
"settings.updates.action.checking": "Checking...",
|
||||||
|
"settings.updates.action.downloading": "Downloading...",
|
||||||
|
"settings.updates.action.installing": "Installing...",
|
||||||
"settings.updates.toast.latest.title": "You're up to date",
|
"settings.updates.toast.latest.title": "You're up to date",
|
||||||
"settings.updates.toast.latest.description": "You're running the latest version of OpenCode.",
|
"settings.updates.toast.latest.description": "You're running the latest version of OpenCode.",
|
||||||
"sound.option.none": "None",
|
"sound.option.none": "None",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export { useCommand } from "./context/command"
|
|||||||
export { loadLocaleDict, normalizeLocale, type Locale } from "./context/language"
|
export { loadLocaleDict, normalizeLocale, type Locale } from "./context/language"
|
||||||
export { useWslServers } from "./wsl/context"
|
export { useWslServers } from "./wsl/context"
|
||||||
export { type DisplayBackend, type FatalRendererErrorLog, type Platform, PlatformProvider } from "./context/platform"
|
export { type DisplayBackend, type FatalRendererErrorLog, type Platform, PlatformProvider } from "./context/platform"
|
||||||
|
export { type UpdaterPlatform, type UpdaterState } from "./updater"
|
||||||
export {
|
export {
|
||||||
type WslDistroProbe,
|
type WslDistroProbe,
|
||||||
type WslInstalledDistro,
|
type WslInstalledDistro,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { useSync } from "@/context/sync"
|
|||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
|
|
||||||
function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
|
export function DirectoryDataProvider(props: ParentProps<{ directory: string; draftID?: string }>) {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
@@ -18,6 +18,8 @@ function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
|
|||||||
const slug = createMemo(() => base64Encode(props.directory))
|
const slug = createMemo(() => base64Encode(props.directory))
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
// A draft lives at /new-session?draftId=… and has no directory segment to normalize.
|
||||||
|
if (props.draftID) return
|
||||||
const next = sync.data.path.directory
|
const next = sync.data.path.directory
|
||||||
if (!next || next === props.directory) return
|
if (!next || next === props.directory) return
|
||||||
const path = location.pathname.slice(slug().length + 1)
|
const path = location.pathname.slice(slug().length + 1)
|
||||||
|
|||||||
@@ -225,8 +225,6 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
|
|||||||
const formattedError = () => formatError(props.error, language.t)
|
const formattedError = () => formatError(props.error, language.t)
|
||||||
let recordedFatalError: Promise<void> | undefined
|
let recordedFatalError: Promise<void> | undefined
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
checking: false,
|
|
||||||
version: undefined as string | undefined,
|
|
||||||
actionError: undefined as string | undefined,
|
actionError: undefined as string | undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -247,32 +245,25 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function checkForUpdates() {
|
async function checkForUpdates() {
|
||||||
if (!platform.checkUpdate) return
|
const state = await platform.updater?.check()
|
||||||
setStore("checking", true)
|
setStore("actionError", state?.status === "error" ? state.message : undefined)
|
||||||
await platform
|
|
||||||
.checkUpdate()
|
|
||||||
.then((result) => {
|
|
||||||
setStore("actionError", undefined)
|
|
||||||
if (result.updateAvailable && result.version) setStore("version", result.version)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setStore("actionError", formatError(err, language.t))
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setStore("checking", false)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function installUpdate() {
|
async function installUpdate() {
|
||||||
if (!platform.updateAndRestart) return
|
await platform.updater
|
||||||
await platform
|
?.install()
|
||||||
.updateAndRestart()
|
|
||||||
.then(() => setStore("actionError", undefined))
|
.then(() => setStore("actionError", undefined))
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
setStore("actionError", formatError(err, language.t))
|
setStore("actionError", formatError(err, language.t))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateVersion = () => {
|
||||||
|
const state = platform.updater?.state()
|
||||||
|
if (state?.status !== "ready") return
|
||||||
|
return state.version
|
||||||
|
}
|
||||||
|
|
||||||
async function exportDebugLogs() {
|
async function exportDebugLogs() {
|
||||||
const exportLogs = platform.exportDebugLogs
|
const exportLogs = platform.exportDebugLogs
|
||||||
if (!exportLogs) return
|
if (!exportLogs) return
|
||||||
@@ -327,20 +318,27 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={platform.checkUpdate}>
|
<Show when={platform.updater}>
|
||||||
<Show
|
<Show
|
||||||
when={store.version}
|
when={updateVersion()}
|
||||||
fallback={
|
fallback={
|
||||||
<Button size="large" variant="ghost" onClick={checkForUpdates} disabled={store.checking}>
|
<Button
|
||||||
{store.checking
|
size="large"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={checkForUpdates}
|
||||||
|
disabled={["checking", "downloading", "installing"].includes(platform.updater?.state().status ?? "")}
|
||||||
|
>
|
||||||
|
{platform.updater?.state().status === "checking"
|
||||||
? language.t("error.page.action.checking")
|
? language.t("error.page.action.checking")
|
||||||
: language.t("error.page.action.checkUpdates")}
|
: language.t("error.page.action.checkUpdates")}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Button size="large" onClick={installUpdate}>
|
{(version) => (
|
||||||
{language.t("error.page.action.updateTo", { version: store.version ?? "" })}
|
<Button size="large" onClick={installUpdate}>
|
||||||
</Button>
|
{language.t("error.page.action.updateTo", { version: version() })}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useQuery } from "@tanstack/solid-query"
|
|||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { Logo } from "@opencode-ai/ui/logo"
|
import { Logo } from "@opencode-ai/ui/logo"
|
||||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||||
|
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||||
import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
|
import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
|
||||||
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
@@ -18,7 +19,7 @@ import { Icon } from "@opencode-ai/ui/icon"
|
|||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { DateTime } from "luxon"
|
import { DateTime } from "luxon"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
|
import { useDirectoryPicker } from "@/components/directory-picker"
|
||||||
import { DialogSelectServer, useServerManagementController } from "@/components/dialog-select-server"
|
import { DialogSelectServer, useServerManagementController } from "@/components/dialog-select-server"
|
||||||
import { DialogServerV2 } from "@/components/settings-v2/dialog-server-v2"
|
import { DialogServerV2 } from "@/components/settings-v2/dialog-server-v2"
|
||||||
import { ServerConnection, useServer } from "@/context/server"
|
import { ServerConnection, useServer } from "@/context/server"
|
||||||
@@ -53,7 +54,7 @@ const HOME_ROW_LAYOUT =
|
|||||||
const HOME_ROW_BASE = `${HOME_ROW_LAYOUT} border-0`
|
const HOME_ROW_BASE = `${HOME_ROW_LAYOUT} border-0`
|
||||||
const HOME_ROW = `${HOME_ROW_BASE} [font-weight:530] text-v2-text-text-muted hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover`
|
const HOME_ROW = `${HOME_ROW_BASE} [font-weight:530] text-v2-text-text-muted hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover`
|
||||||
const HOME_PROJECT_NAV_LABEL = "min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap"
|
const HOME_PROJECT_NAV_LABEL = "min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap"
|
||||||
const HOME_PROJECT_NAV_ROW = `${HOME_ROW_LAYOUT} h-7 gap-2 px-1.5 [font-weight:440] text-v2-text-text-muted hover:bg-v2-background-bg-layer-01 hover:text-v2-text-text-base hover:[box-shadow:inset_0_0_0_0.5px_var(--v2-border-border-muted)] data-[selected]:bg-v2-background-bg-layer-02 data-[selected]:text-v2-text-text-base data-[selected]:[box-shadow:inset_0_0_0_0.5px_var(--v2-border-border-muted)] data-[selected]:hover:bg-v2-background-bg-layer-02 focus-visible:bg-v2-background-bg-layer-01 focus-visible:text-v2-text-text-base focus-visible:[box-shadow:inset_0_0_0_0.5px_var(--v2-border-border-muted)]`
|
const HOME_PROJECT_NAV_ROW = `${HOME_ROW_LAYOUT} h-7 gap-2 px-1.5 [font-weight:440] text-v2-text-text-muted hover:bg-v2-background-bg-layer-01 hover:text-v2-text-text-base hover:[box-shadow:inset_0_0_0_0.5px_var(--v2-border-border-muted)] data-[selected]:bg-v2-background-bg-layer-03 data-[selected]:text-v2-text-text-base data-[selected]:[box-shadow:inset_0_0_0_0.5px_var(--v2-border-border-muted)] data-[selected]:hover:bg-v2-background-bg-layer-03 focus-visible:bg-v2-background-bg-layer-01 focus-visible:text-v2-text-text-base focus-visible:[box-shadow:inset_0_0_0_0.5px_var(--v2-border-border-muted)]`
|
||||||
const HOME_SECTION_LABEL = "text-v2-text-text-muted [font-weight:440]"
|
const HOME_SECTION_LABEL = "text-v2-text-text-muted [font-weight:440]"
|
||||||
|
|
||||||
type HomeSessionRecord = {
|
type HomeSessionRecord = {
|
||||||
@@ -125,6 +126,7 @@ function HomeDesign() {
|
|||||||
const sync = useServerSync()
|
const sync = useServerSync()
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
|
const pickDirectory = useDirectoryPicker()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
@@ -314,26 +316,19 @@ function HomeDesign() {
|
|||||||
navigateOnServer(conn, `/${base64Encode(session.directory)}/session/${session.id}`)
|
navigateOnServer(conn, `/${base64Encode(session.directory)}/session/${session.id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function chooseProject(conn: ServerConnection.Any) {
|
function chooseProject(conn: ServerConnection.Any) {
|
||||||
function resolve(result: string | string[] | null) {
|
function resolve(result: string | string[] | null) {
|
||||||
addProjects(conn, homeProjectDirectories(result))
|
addProjects(conn, homeProjectDirectories(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = global.createServerCtx(conn)
|
const server = global.createServerCtx(conn)
|
||||||
|
|
||||||
if (platform.openDirectoryPickerDialog && server.isLocal) {
|
pickDirectory({
|
||||||
const result = await platform.openDirectoryPickerDialog?.({
|
server: conn,
|
||||||
title: language.t("command.project.open"),
|
title: language.t("command.project.open"),
|
||||||
multiple: true,
|
multiple: true,
|
||||||
})
|
onSelect: resolve,
|
||||||
resolve(result)
|
})
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog.show(
|
|
||||||
() => <DialogSelectDirectory multiple={true} onSelect={resolve} server={conn} />,
|
|
||||||
() => resolve(null),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openSettings() {
|
function openSettings() {
|
||||||
@@ -390,7 +385,7 @@ function HomeDesign() {
|
|||||||
onClose={closeSearch}
|
onClose={closeSearch}
|
||||||
onSelect={selectSearchSession}
|
onSelect={selectSearchSession}
|
||||||
/>
|
/>
|
||||||
<div class="mt-3 min-h-0 flex-1 overflow-y-auto">
|
<ScrollView class="mt-3 min-h-0 flex-1">
|
||||||
<div class="pt-3 flex flex-col gap-6">
|
<div class="pt-3 flex flex-col gap-6">
|
||||||
<Show
|
<Show
|
||||||
when={!sessionLoad.isLoading}
|
when={!sessionLoad.isLoading}
|
||||||
@@ -432,7 +427,7 @@ function HomeDesign() {
|
|||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ScrollView>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -890,7 +885,7 @@ function HomeSessionSearch(props: {
|
|||||||
<label
|
<label
|
||||||
class="relative z-20 flex h-9 w-full items-center gap-2 rounded-[6px] py-1 pl-3 pr-2 text-v2-icon-icon-muted transition-[background-color,box-shadow] duration-[120ms] ease-in-out"
|
class="relative z-20 flex h-9 w-full items-center gap-2 rounded-[6px] py-1 pl-3 pr-2 text-v2-icon-icon-muted transition-[background-color,box-shadow] duration-[120ms] ease-in-out"
|
||||||
classList={{
|
classList={{
|
||||||
"bg-v2-background-bg-deep focus-within:bg-v2-background-bg-base focus-within:shadow-[0_0_0_0.5px_var(--v2-border-border-focus),var(--v2-elevation-raised)]":
|
"bg-v2-background-bg-layer-03 focus-within:bg-v2-background-bg-layer-03 focus-within:shadow-[0_0_0_0.5px_var(--v2-border-border-focus),var(--v2-elevation-raised)]":
|
||||||
!props.open,
|
!props.open,
|
||||||
"bg-transparent shadow-[0_0_0_0.5px_var(--v2-border-border-focus)]": props.open,
|
"bg-transparent shadow-[0_0_0_0.5px_var(--v2-border-border-focus)]": props.open,
|
||||||
}}
|
}}
|
||||||
@@ -1101,6 +1096,7 @@ function groupSessions(records: HomeSessionRecord[], language: ReturnType<typeof
|
|||||||
function LegacyHome() {
|
function LegacyHome() {
|
||||||
const sync = useServerSync()
|
const sync = useServerSync()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
|
const pickDirectory = useDirectoryPicker()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const global = useGlobal()
|
const global = useGlobal()
|
||||||
@@ -1128,7 +1124,7 @@ function LegacyHome() {
|
|||||||
navigate(`/${base64Encode(directory)}`)
|
navigate(`/${base64Encode(directory)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function chooseProject() {
|
function chooseProject() {
|
||||||
const s = server.current
|
const s = server.current
|
||||||
if (!s) return
|
if (!s) return
|
||||||
|
|
||||||
@@ -1142,18 +1138,12 @@ function LegacyHome() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (platform.openDirectoryPickerDialog && server.isLocal()) {
|
pickDirectory({
|
||||||
const result = await platform.openDirectoryPickerDialog?.({
|
server: s,
|
||||||
title: language.t("command.project.open"),
|
title: language.t("command.project.open"),
|
||||||
multiple: true,
|
multiple: true,
|
||||||
})
|
onSelect: resolve,
|
||||||
resolve(result)
|
})
|
||||||
} else {
|
|
||||||
dialog.show(
|
|
||||||
() => <DialogSelectDirectory multiple={true} onSelect={resolve} server={s} />,
|
|
||||||
() => resolve(null),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
} from "solid-js"
|
} from "solid-js"
|
||||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||||
import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
||||||
import { useQuery } from "@tanstack/solid-query"
|
|
||||||
import { useLayout, LocalProject } from "@/context/layout"
|
import { useLayout, LocalProject } from "@/context/layout"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useServerSync } from "@/context/server-sync"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
@@ -64,7 +63,9 @@ import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context"
|
|||||||
import { useCommand, type CommandOption } from "@/context/command"
|
import { useCommand, type CommandOption } from "@/context/command"
|
||||||
import { ConstrainDragXAxis, getDraggableId } from "@/utils/solid-dnd"
|
import { ConstrainDragXAxis, getDraggableId } from "@/utils/solid-dnd"
|
||||||
import { DebugBar } from "@/components/debug-bar"
|
import { DebugBar } from "@/components/debug-bar"
|
||||||
|
import { HelpButton } from "@/components/help-button"
|
||||||
import { Titlebar, type TitlebarUpdate } from "@/components/titlebar"
|
import { Titlebar, type TitlebarUpdate } from "@/components/titlebar"
|
||||||
|
import { useDirectoryPicker } from "@/components/directory-picker"
|
||||||
import { ServerConnection, useServer } from "@/context/server"
|
import { ServerConnection, useServer } from "@/context/server"
|
||||||
import { useLanguage, type Locale } from "@/context/language"
|
import { useLanguage, type Locale } from "@/context/language"
|
||||||
import { pathKey } from "@/utils/path-key"
|
import { pathKey } from "@/utils/path-key"
|
||||||
@@ -90,7 +91,6 @@ import {
|
|||||||
} from "./layout/sidebar-workspace"
|
} from "./layout/sidebar-workspace"
|
||||||
import { ProjectDragOverlay, SortableProject, type ProjectSidebarContext } from "./layout/sidebar-project"
|
import { ProjectDragOverlay, SortableProject, type ProjectSidebarContext } from "./layout/sidebar-project"
|
||||||
import { SidebarContent } from "./layout/sidebar-shell"
|
import { SidebarContent } from "./layout/sidebar-shell"
|
||||||
import { runUpdateAndRestart } from "./layout/update"
|
|
||||||
|
|
||||||
export default function Layout(props: ParentProps) {
|
export default function Layout(props: ParentProps) {
|
||||||
const serverSDK = useServerSDK()
|
const serverSDK = useServerSDK()
|
||||||
@@ -119,6 +119,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
const layoutReady = createMemo(() => layout.ready())
|
const layoutReady = createMemo(() => layout.ready())
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
|
const pickDirectory = useDirectoryPicker()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
const notification = useNotification()
|
const notification = useNotification()
|
||||||
@@ -168,28 +169,15 @@ export default function Layout(props: ParentProps) {
|
|||||||
peeked: false,
|
peeked: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const [update, setUpdate] = createStore({
|
|
||||||
installing: false,
|
|
||||||
})
|
|
||||||
const updateQuery = useQuery(() => ({
|
|
||||||
queryKey: ["desktop", "update"] as const,
|
|
||||||
enabled: () =>
|
|
||||||
!!platform.checkUpdate && !!platform.updateAndRestart && settings.ready() && settings.updates.startup(),
|
|
||||||
queryFn: () => platform.checkUpdate?.() ?? Promise.resolve({ updateAvailable: false, version: undefined }),
|
|
||||||
refetchInterval: (query) => (query.state.data?.updateAvailable ? false : 10 * 60 * 1000),
|
|
||||||
}))
|
|
||||||
const updateVersion = () => {
|
const updateVersion = () => {
|
||||||
if (!settings.ready()) return
|
const state = platform.updater?.state()
|
||||||
if (!settings.updates.startup()) return
|
if (state?.status !== "ready") return
|
||||||
if (!updateQuery.data?.updateAvailable) return
|
return state.version
|
||||||
return updateQuery.data.version ?? ""
|
|
||||||
}
|
|
||||||
const installUpdate = () => {
|
|
||||||
runUpdateAndRestart(platform.updateAndRestart, (installing) => setUpdate("installing", installing))
|
|
||||||
}
|
}
|
||||||
|
const installUpdate = () => void platform.updater?.install()
|
||||||
const titlebarUpdate: TitlebarUpdate = {
|
const titlebarUpdate: TitlebarUpdate = {
|
||||||
version: updateVersion,
|
version: updateVersion,
|
||||||
installing: () => update.installing,
|
installing: () => platform.updater?.state().status === "installing",
|
||||||
install: installUpdate,
|
install: installUpdate,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1472,7 +1460,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function chooseProject() {
|
function chooseProject() {
|
||||||
const conn = server.current
|
const conn = server.current
|
||||||
if (!conn) return
|
if (!conn) return
|
||||||
function resolve(result: string | string[] | null) {
|
function resolve(result: string | string[] | null) {
|
||||||
@@ -1486,22 +1474,12 @@ export default function Layout(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (platform.openDirectoryPickerDialog && server.isLocal()) {
|
pickDirectory({
|
||||||
const result = await platform.openDirectoryPickerDialog?.({
|
server: conn,
|
||||||
title: language.t("command.project.open"),
|
title: language.t("command.project.open"),
|
||||||
multiple: true,
|
multiple: true,
|
||||||
})
|
onSelect: resolve,
|
||||||
resolve(result)
|
})
|
||||||
} else {
|
|
||||||
const run = ++dialogRun
|
|
||||||
void import("@/components/dialog-select-directory").then((x) => {
|
|
||||||
if (dialogDead || dialogRun !== run) return
|
|
||||||
dialog.show(
|
|
||||||
() => <x.DialogSelectDirectory multiple={true} onSelect={resolve} server={conn} />,
|
|
||||||
() => resolve(null),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteWorkspace = async (root: string, directory: string, leaveDeletedWorkspace = false) => {
|
const deleteWorkspace = async (root: string, directory: string, leaveDeletedWorkspace = false) => {
|
||||||
@@ -2387,6 +2365,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
</Show>
|
</Show>
|
||||||
</main>
|
</main>
|
||||||
{import.meta.env.DEV && <DebugBar />}
|
{import.meta.env.DEV && <DebugBar />}
|
||||||
|
<HelpButton />
|
||||||
<ToastRegion v2={newDesign()} />
|
<ToastRegion v2={newDesign()} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -2540,6 +2519,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
</div>
|
</div>
|
||||||
{import.meta.env.DEV && <DebugBar />}
|
{import.meta.env.DEV && <DebugBar />}
|
||||||
</div>
|
</div>
|
||||||
|
<HelpButton />
|
||||||
<ToastRegion v2={newDesign()} />
|
<ToastRegion v2={newDesign()} />
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
|
||||||
import { runUpdateAndRestart } from "./update"
|
|
||||||
|
|
||||||
describe("runUpdateAndRestart", () => {
|
|
||||||
test("clears the installing state when restart resolves without exiting", async () => {
|
|
||||||
const states: boolean[] = []
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
runUpdateAndRestart(
|
|
||||||
async () => {},
|
|
||||||
(installing) => {
|
|
||||||
states.push(installing)
|
|
||||||
if (states.length === 2) resolve()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(states).toEqual([true, false])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
export function runUpdateAndRestart(
|
|
||||||
updateAndRestart: (() => Promise<void>) | undefined,
|
|
||||||
setInstalling: (installing: boolean) => void,
|
|
||||||
) {
|
|
||||||
if (!updateAndRestart) return
|
|
||||||
setInstalling(true)
|
|
||||||
void updateAndRestart()
|
|
||||||
.catch(() => undefined)
|
|
||||||
.finally(() => setInstalling(false))
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { createEffect, createMemo, onMount, untrack } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { useSearchParams } from "@solidjs/router"
|
||||||
|
import { NewSessionDesignView } from "@/components/session"
|
||||||
|
import { useComments } from "@/context/comments"
|
||||||
|
import { usePrompt } from "@/context/prompt"
|
||||||
|
import { useSDK } from "@/context/sdk"
|
||||||
|
import { useSync } from "@/context/sync"
|
||||||
|
import { createSessionComposerState, SessionComposerRegion } from "@/pages/session/composer"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `/new-session` draft page. Unlike `session.tsx`, this only renders the prompt
|
||||||
|
* composer for a brand-new session — no terminal, review pane, file tree, or message
|
||||||
|
* timeline. Submitting promotes the draft into a real session (see prompt-input/submit).
|
||||||
|
*/
|
||||||
|
export default function NewSessionPage() {
|
||||||
|
const prompt = usePrompt()
|
||||||
|
const sdk = useSDK()
|
||||||
|
const sync = useSync()
|
||||||
|
const comments = useComments()
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>()
|
||||||
|
|
||||||
|
let inputRef: HTMLDivElement | undefined
|
||||||
|
|
||||||
|
const composer = createSessionComposerState()
|
||||||
|
|
||||||
|
const [store, setStore] = createStore({
|
||||||
|
worktree: "main",
|
||||||
|
})
|
||||||
|
|
||||||
|
const newSessionWorktree = createMemo(() => {
|
||||||
|
if (store.worktree === "create") return "create"
|
||||||
|
const project = sync.project
|
||||||
|
if (project && sdk.directory !== project.worktree) return sdk.directory
|
||||||
|
return "main"
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!prompt.ready()) return
|
||||||
|
untrack(() => {
|
||||||
|
const text = searchParams.prompt
|
||||||
|
if (!text) return
|
||||||
|
prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length)
|
||||||
|
setSearchParams({ ...searchParams, prompt: undefined })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
requestAnimationFrame(() => inputRef?.focus())
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="relative size-full overflow-hidden flex flex-col">
|
||||||
|
<div class="flex-1 min-h-0 flex flex-col gap-2 p-2">
|
||||||
|
<div class="@container relative flex flex-col min-h-0 h-full bg-background-stronger flex-1">
|
||||||
|
<div class="flex-1 min-h-0 overflow-hidden rounded-[10px]">
|
||||||
|
<NewSessionDesignView>
|
||||||
|
<SessionComposerRegion
|
||||||
|
state={composer}
|
||||||
|
ready
|
||||||
|
centered={false}
|
||||||
|
placement="inline"
|
||||||
|
inputRef={(el) => {
|
||||||
|
inputRef = el
|
||||||
|
}}
|
||||||
|
newSessionWorktree={newSessionWorktree()}
|
||||||
|
onNewSessionWorktreeReset={() => setStore("worktree", "main")}
|
||||||
|
onSubmit={() => comments.clear()}
|
||||||
|
onResponseSubmit={() => {}}
|
||||||
|
setPromptDockRef={() => {}}
|
||||||
|
/>
|
||||||
|
</NewSessionDesignView>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -31,13 +31,14 @@ import { Button } from "@opencode-ai/ui/button"
|
|||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { checksum } from "@opencode-ai/core/util/encode"
|
import { checksum } from "@opencode-ai/core/util/encode"
|
||||||
import { useLocation, useSearchParams } from "@solidjs/router"
|
import { useLocation, useSearchParams } from "@solidjs/router"
|
||||||
import { NewSessionDesignView, NewSessionView, SessionHeader } from "@/components/session"
|
import { NewSessionView, SessionHeader } from "@/components/session"
|
||||||
import { useComments } from "@/context/comments"
|
import { useComments } from "@/context/comments"
|
||||||
import { getSessionPrefetch, SESSION_PREFETCH_TTL } from "@/context/global-sync/session-prefetch"
|
import { getSessionPrefetch, SESSION_PREFETCH_TTL } from "@/context/global-sync/session-prefetch"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useServerSync } from "@/context/server-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { usePrompt } from "@/context/prompt"
|
import { usePrompt } from "@/context/prompt"
|
||||||
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { useServerSDK } from "@/context/server-sdk"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
@@ -51,6 +52,7 @@ import {
|
|||||||
createSizing,
|
createSizing,
|
||||||
focusTerminalById,
|
focusTerminalById,
|
||||||
shouldFocusTerminalOnKeyDown,
|
shouldFocusTerminalOnKeyDown,
|
||||||
|
shouldShowFileTree,
|
||||||
} from "@/pages/session/helpers"
|
} from "@/pages/session/helpers"
|
||||||
import { MessageTimeline } from "@/pages/session/message-timeline"
|
import { MessageTimeline } from "@/pages/session/message-timeline"
|
||||||
import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab"
|
import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab"
|
||||||
@@ -61,7 +63,6 @@ import { SessionSidePanel } from "@/pages/session/session-side-panel"
|
|||||||
import { TerminalPanel } from "@/pages/session/terminal-panel"
|
import { TerminalPanel } from "@/pages/session/terminal-panel"
|
||||||
import { useSessionCommands } from "@/pages/session/use-session-commands"
|
import { useSessionCommands } from "@/pages/session/use-session-commands"
|
||||||
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
|
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
|
||||||
import { shouldUseV2NewSessionPage } from "@/pages/session/new-session-layout"
|
|
||||||
import { Identifier } from "@/utils/id"
|
import { Identifier } from "@/utils/id"
|
||||||
import { diffs as list } from "@/utils/diffs"
|
import { diffs as list } from "@/utils/diffs"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
@@ -194,6 +195,7 @@ export default function Page() {
|
|||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const serverSDK = useServerSDK()
|
const serverSDK = useServerSDK()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
|
const platform = usePlatform()
|
||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
const comments = useComments()
|
const comments = useComments()
|
||||||
const terminal = useTerminal()
|
const terminal = useTerminal()
|
||||||
@@ -268,10 +270,16 @@ export default function Page() {
|
|||||||
|
|
||||||
const isDesktop = createMediaQuery("(min-width: 768px)")
|
const isDesktop = createMediaQuery("(min-width: 768px)")
|
||||||
const size = createSizing()
|
const size = createSizing()
|
||||||
const isV2NewSessionPage = () =>
|
const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
|
||||||
shouldUseV2NewSessionPage({ newLayoutDesigns: newSessionDesign(), sessionID: params.id })
|
const desktopFileTreeOpen = createMemo(
|
||||||
const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened() && !isV2NewSessionPage())
|
() =>
|
||||||
const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened() && !isV2NewSessionPage())
|
isDesktop() &&
|
||||||
|
shouldShowFileTree({
|
||||||
|
desktopV2: platform.platform === "desktop" && settings.general.newLayoutDesigns(),
|
||||||
|
showFileTree: settings.general.showFileTree(),
|
||||||
|
opened: layout.fileTree.opened(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
const desktopSidePanelOpen = createMemo(() => desktopReviewOpen() || desktopFileTreeOpen())
|
const desktopSidePanelOpen = createMemo(() => desktopReviewOpen() || desktopFileTreeOpen())
|
||||||
const sessionPanelWidth = createMemo(() => {
|
const sessionPanelWidth = createMemo(() => {
|
||||||
if (!desktopSidePanelOpen()) return "100%"
|
if (!desktopSidePanelOpen()) return "100%"
|
||||||
@@ -1745,81 +1753,80 @@ export default function Page() {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
"@container relative shrink-0 flex flex-col min-h-0 h-full bg-background-stronger flex-1 md:flex-none": true,
|
"@container relative shrink-0 flex flex-col min-h-0 h-full flex-1 md:flex-none transition-[width]": true,
|
||||||
"duration-[240ms] ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
|
"duration-[240ms] ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
|
||||||
!size.active() && !ui.reviewSnap,
|
!size.active() && !ui.reviewSnap,
|
||||||
"transition-[width]": !isV2NewSessionPage(),
|
|
||||||
"rounded-[10px] shadow-[var(--v2-elevation-raised)]": settings.general.newLayoutDesigns() && !!params.id,
|
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
width: sessionPanelWidth(),
|
width: sessionPanelWidth(),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex-1 min-h-0 overflow-hidden"
|
|
||||||
classList={{
|
classList={{
|
||||||
"rounded-[10px]": settings.general.newLayoutDesigns(),
|
"flex-1 min-h-0 flex flex-col bg-background-stronger": true,
|
||||||
|
"rounded-[10px] overflow-hidden": settings.general.newLayoutDesigns(),
|
||||||
|
"shadow-[var(--v2-elevation-raised)]": settings.general.newLayoutDesigns() && !!params.id,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Switch>
|
<div class="flex-1 min-h-0 overflow-hidden">
|
||||||
<Match when={params.id && mobileChanges()}>
|
<Switch>
|
||||||
<div class="relative h-full overflow-hidden">
|
<Match when={params.id && mobileChanges()}>
|
||||||
{reviewContent({
|
<div class="relative h-full overflow-hidden">
|
||||||
diffStyle: "unified",
|
{reviewContent({
|
||||||
classes: {
|
diffStyle: "unified",
|
||||||
root: "pb-8",
|
classes: {
|
||||||
header: "px-4",
|
root: "pb-8",
|
||||||
container: "px-4",
|
header: "px-4",
|
||||||
},
|
container: "px-4",
|
||||||
loadingClass: "px-4 py-4 text-text-weak",
|
},
|
||||||
emptyClass: "h-full pb-64 -mt-4 flex flex-col items-center justify-center text-center gap-6",
|
loadingClass: "px-4 py-4 text-text-weak",
|
||||||
})}
|
emptyClass: "h-full pb-64 -mt-4 flex flex-col items-center justify-center text-center gap-6",
|
||||||
</div>
|
})}
|
||||||
</Match>
|
</div>
|
||||||
<Match when={params.id}>
|
</Match>
|
||||||
<Show when={messagesReady()}>
|
<Match when={params.id}>
|
||||||
<MessageTimeline
|
<Show when={messagesReady()}>
|
||||||
actions={actions}
|
<MessageTimeline
|
||||||
scroll={ui.scroll}
|
actions={actions}
|
||||||
onResumeScroll={resumeScroll}
|
scroll={ui.scroll}
|
||||||
setScrollRef={setScrollRef}
|
onResumeScroll={resumeScroll}
|
||||||
onScheduleScrollState={scheduleScrollState}
|
setScrollRef={setScrollRef}
|
||||||
onAutoScrollHandleScroll={autoScroll.handleScroll}
|
onScheduleScrollState={scheduleScrollState}
|
||||||
onMarkScrollGesture={markScrollGesture}
|
onAutoScrollHandleScroll={autoScroll.handleScroll}
|
||||||
hasScrollGesture={hasScrollGesture}
|
onMarkScrollGesture={markScrollGesture}
|
||||||
onUserScroll={markUserScroll}
|
hasScrollGesture={hasScrollGesture}
|
||||||
onHistoryScroll={historyLoader.onScrollerScroll}
|
onUserScroll={markUserScroll}
|
||||||
onAutoScrollInteraction={autoScroll.handleInteraction}
|
onHistoryScroll={historyLoader.onScrollerScroll}
|
||||||
shouldAnchorBottom={() =>
|
onAutoScrollInteraction={autoScroll.handleInteraction}
|
||||||
!location.hash && !store.messageId && !ui.pendingMessage && !autoScroll.userScrolled()
|
shouldAnchorBottom={() =>
|
||||||
}
|
!location.hash && !store.messageId && !ui.pendingMessage && !autoScroll.userScrolled()
|
||||||
centered={centered()}
|
}
|
||||||
setContentRef={(el) => {
|
centered={centered()}
|
||||||
content = el
|
setContentRef={(el) => {
|
||||||
autoScroll.contentRef(el)
|
content = el
|
||||||
|
autoScroll.contentRef(el)
|
||||||
|
|
||||||
const root = scroller
|
const root = scroller
|
||||||
if (root) scheduleScrollState(root)
|
if (root) scheduleScrollState(root)
|
||||||
}}
|
}}
|
||||||
historyShift={historyLoader.shift()}
|
historyShift={historyLoader.shift()}
|
||||||
userMessages={historyLoader.userMessages()}
|
userMessages={historyLoader.userMessages()}
|
||||||
anchor={anchor}
|
anchor={anchor}
|
||||||
setRevealMessage={(fn) => {
|
setRevealMessage={(fn) => {
|
||||||
revealMessage = fn
|
revealMessage = fn
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<Show when={newSessionDesign()} fallback={<NewSessionView worktree={newSessionWorktree()} />}>
|
<NewSessionView worktree={newSessionWorktree()} />
|
||||||
<NewSessionDesignView>{composerRegion("inline")}</NewSessionDesignView>
|
</Match>
|
||||||
</Show>
|
</Switch>
|
||||||
</Match>
|
</div>
|
||||||
</Switch>
|
|
||||||
|
<Show when={params.id || !newSessionDesign()}>{composerRegion("dock")}</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={params.id || !newSessionDesign()}>{composerRegion("dock")}</Show>
|
|
||||||
|
|
||||||
<Show when={desktopReviewOpen()}>
|
<Show when={desktopReviewOpen()}>
|
||||||
<div onPointerDown={() => size.start()}>
|
<div onPointerDown={() => size.start()}>
|
||||||
<ResizeHandle
|
<ResizeHandle
|
||||||
|
|||||||
@@ -8,8 +8,17 @@ import {
|
|||||||
focusTerminalById,
|
focusTerminalById,
|
||||||
getTabReorderIndex,
|
getTabReorderIndex,
|
||||||
shouldFocusTerminalOnKeyDown,
|
shouldFocusTerminalOnKeyDown,
|
||||||
|
shouldShowFileTree,
|
||||||
} from "./helpers"
|
} from "./helpers"
|
||||||
|
|
||||||
|
describe("shouldShowFileTree", () => {
|
||||||
|
test("does not reserve space for a disabled v2 file tree", () => {
|
||||||
|
expect(shouldShowFileTree({ desktopV2: true, showFileTree: false, opened: true })).toBe(false)
|
||||||
|
expect(shouldShowFileTree({ desktopV2: false, showFileTree: false, opened: true })).toBe(true)
|
||||||
|
expect(shouldShowFileTree({ desktopV2: true, showFileTree: true, opened: true })).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("createOpenReviewFile", () => {
|
describe("createOpenReviewFile", () => {
|
||||||
test("opens and loads selected review file", () => {
|
test("opens and loads selected review file", () => {
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ type TabsInput = {
|
|||||||
|
|
||||||
export const getSessionKey = (dir: string | undefined, id: string | undefined) => `${dir ?? ""}${id ? `/${id}` : ""}`
|
export const getSessionKey = (dir: string | undefined, id: string | undefined) => `${dir ?? ""}${id ? `/${id}` : ""}`
|
||||||
|
|
||||||
|
export function shouldShowFileTree(input: { desktopV2: boolean; showFileTree: boolean; opened: boolean }) {
|
||||||
|
return input.opened && (!input.desktopV2 || input.showFileTree)
|
||||||
|
}
|
||||||
|
|
||||||
export const createSessionTabs = (input: TabsInput) => {
|
export const createSessionTabs = (input: TabsInput) => {
|
||||||
const review = input.review ?? (() => false)
|
const review = input.review ?? (() => false)
|
||||||
const hasReview = input.hasReview ?? (() => false)
|
const hasReview = input.hasReview ?? (() => false)
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
|
||||||
import { shouldUseV2NewSessionPage } from "./new-session-layout"
|
|
||||||
|
|
||||||
describe("shouldUseV2NewSessionPage", () => {
|
|
||||||
test("keeps disabled pages on the legacy layout", () => {
|
|
||||||
expect(shouldUseV2NewSessionPage({ newLayoutDesigns: false, sessionID: "ses_123" })).toBe(false)
|
|
||||||
expect(shouldUseV2NewSessionPage({ newLayoutDesigns: false })).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("uses the v2 layout only for enabled new-session pages", () => {
|
|
||||||
expect(shouldUseV2NewSessionPage({ newLayoutDesigns: true })).toBe(true)
|
|
||||||
expect(shouldUseV2NewSessionPage({ newLayoutDesigns: true, sessionID: "ses_123" })).toBe(false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,6 +1,2 @@
|
|||||||
/** Inline new-session content width — keep in sync with session composer `placement === "inline"`. */
|
/** Inline new-session content width — keep in sync with session composer `placement === "inline"`. */
|
||||||
export const NEW_SESSION_CONTENT_WIDTH = "w-full max-w-[720px] px-0"
|
export const NEW_SESSION_CONTENT_WIDTH = "w-full max-w-[720px] px-0"
|
||||||
|
|
||||||
export function shouldUseV2NewSessionPage(input: { newLayoutDesigns: boolean; sessionID?: string }) {
|
|
||||||
return input.newLayoutDesigns && !input.sessionID
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -24,7 +24,13 @@ import { useSettings } from "@/context/settings"
|
|||||||
import { useSync } from "@/context/sync"
|
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,
|
||||||
|
shouldShowFileTree,
|
||||||
|
type Sizing,
|
||||||
|
} from "@/pages/session/helpers"
|
||||||
import { setSessionHandoff } from "@/pages/session/handoff"
|
import { setSessionHandoff } from "@/pages/session/handoff"
|
||||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
|
|
||||||
@@ -59,10 +65,18 @@ export function SessionSidePanel(props: {
|
|||||||
|
|
||||||
const isDesktop = createMediaQuery("(min-width: 768px)")
|
const isDesktop = createMediaQuery("(min-width: 768px)")
|
||||||
const desktopV2 = () => platform.platform === "desktop" && settings.general.newLayoutDesigns()
|
const desktopV2 = () => platform.platform === "desktop" && settings.general.newLayoutDesigns()
|
||||||
const shown = createMemo(() => (desktopV2() ? settings.general.showFileTree() : true))
|
const shown = createMemo(() => !desktopV2() || settings.general.showFileTree())
|
||||||
|
|
||||||
const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
|
const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
|
||||||
const fileOpen = createMemo(() => isDesktop() && shown() && layout.fileTree.opened())
|
const fileOpen = createMemo(
|
||||||
|
() =>
|
||||||
|
isDesktop() &&
|
||||||
|
shouldShowFileTree({
|
||||||
|
desktopV2: desktopV2(),
|
||||||
|
showFileTree: settings.general.showFileTree(),
|
||||||
|
opened: 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(() => {
|
||||||
|
|||||||
@@ -547,6 +547,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
description: language.t("command.agent.cycle.description"),
|
description: language.t("command.agent.cycle.description"),
|
||||||
keybind: "mod+.",
|
keybind: "mod+.",
|
||||||
slash: "agent",
|
slash: "agent",
|
||||||
|
disabled: desktopV2() && !settings.general.showCustomAgents(),
|
||||||
onSelect: () => local.agent.move(1),
|
onSelect: () => local.agent.move(1),
|
||||||
}),
|
}),
|
||||||
agentCommand({
|
agentCommand({
|
||||||
@@ -554,6 +555,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
title: language.t("command.agent.cycle.reverse"),
|
title: language.t("command.agent.cycle.reverse"),
|
||||||
description: language.t("command.agent.cycle.reverse.description"),
|
description: language.t("command.agent.cycle.reverse.description"),
|
||||||
keybind: "shift+mod+.",
|
keybind: "shift+mod+.",
|
||||||
|
disabled: desktopV2() && !settings.general.showCustomAgents(),
|
||||||
onSelect: () => local.agent.move(-1),
|
onSelect: () => local.agent.move(-1),
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { Accessor } from "solid-js"
|
||||||
|
|
||||||
|
export type UpdaterState =
|
||||||
|
| { status: "disabled" }
|
||||||
|
| { status: "idle" }
|
||||||
|
| { status: "checking" }
|
||||||
|
| { status: "downloading"; version: string; percent?: number }
|
||||||
|
| { status: "ready"; version: string }
|
||||||
|
| { status: "up-to-date" }
|
||||||
|
| { status: "installing"; version: string }
|
||||||
|
| { status: "error"; message: string }
|
||||||
|
|
||||||
|
export type UpdaterPlatform = {
|
||||||
|
state: Accessor<UpdaterState>
|
||||||
|
check(): Promise<UpdaterState>
|
||||||
|
install(): Promise<void>
|
||||||
|
}
|
||||||
@@ -166,6 +166,24 @@ describe("persist localStorage resilience", () => {
|
|||||||
expect(storage.getItem(`${target.legacyStorageNames![0]}:${target.key}`)).toBeNull()
|
expect(storage.getItem(`${target.legacyStorageNames![0]}:${target.key}`)).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("draft target isolates storage per draft and namespaces keys", () => {
|
||||||
|
const a = Persist.draft("draft-a", "prompt")
|
||||||
|
const b = Persist.draft("draft-b", "prompt")
|
||||||
|
|
||||||
|
expect(a.key).toBe("draft:prompt")
|
||||||
|
expect(a.storage).not.toBe(b.storage)
|
||||||
|
expect(a.storage).not.toBe(Persist.workspace("/home/luke/repo", "prompt").storage)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("removes draft storage when removing persisted target", () => {
|
||||||
|
const target = Persist.draft("draft-a", "prompt")
|
||||||
|
storage.setItem(`${target.storage}:${target.key}`, '{"value":1}')
|
||||||
|
|
||||||
|
removePersisted(target)
|
||||||
|
|
||||||
|
expect(storage.getItem(`${target.storage}:${target.key}`)).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
test("server workspace target preserves local storage and isolates remote storage", () => {
|
test("server workspace target preserves local storage and isolates remote storage", () => {
|
||||||
const local = Persist.serverWorkspace(ServerScope.local, "/home/luke/repo", "prompt")
|
const local = Persist.serverWorkspace(ServerScope.local, "/home/luke/repo", "prompt")
|
||||||
const windows = Persist.serverWorkspace("https://windows.example" as ServerScope, "/home/luke/repo", "prompt")
|
const windows = Persist.serverWorkspace("https://windows.example" as ServerScope, "/home/luke/repo", "prompt")
|
||||||
|
|||||||
@@ -341,6 +341,12 @@ function workspaceStorage(dir: string) {
|
|||||||
return `opencode.workspace.${head}.${sum}.dat`
|
return `opencode.workspace.${head}.${sum}.dat`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function draftStorage(draftID: string) {
|
||||||
|
const head = (draftID.slice(0, 12) || "draft").replace(/[^a-zA-Z0-9._-]/g, "-")
|
||||||
|
const sum = checksum(draftID) ?? "0"
|
||||||
|
return `opencode.draft.${head}.${sum}.dat`
|
||||||
|
}
|
||||||
|
|
||||||
function legacyWorkspaceStorage(dir: string) {
|
function legacyWorkspaceStorage(dir: string) {
|
||||||
const storage = workspaceStorage(pathKey(dir))
|
const storage = workspaceStorage(pathKey(dir))
|
||||||
const result = new Set<string>()
|
const result = new Set<string>()
|
||||||
@@ -450,6 +456,12 @@ function localStorageDirect(): SyncStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DRAFT_PERSISTED_KEYS = ["prompt", "comments", "model-selection", "file-view", "layout"]
|
||||||
|
|
||||||
|
export function draftPersistedKeys() {
|
||||||
|
return DRAFT_PERSISTED_KEYS
|
||||||
|
}
|
||||||
|
|
||||||
export const PersistTesting = {
|
export const PersistTesting = {
|
||||||
localStorageDirect,
|
localStorageDirect,
|
||||||
localStorageWithPrefix,
|
localStorageWithPrefix,
|
||||||
@@ -462,6 +474,9 @@ export const Persist = {
|
|||||||
global(key: string, legacy?: string[]): PersistTarget {
|
global(key: string, legacy?: string[]): PersistTarget {
|
||||||
return { storage: GLOBAL_STORAGE, key, legacy }
|
return { storage: GLOBAL_STORAGE, key, legacy }
|
||||||
},
|
},
|
||||||
|
draft(draftID: string, key: string, legacy?: string[]): PersistTarget {
|
||||||
|
return { storage: draftStorage(draftID), key: `draft:${key}`, legacy }
|
||||||
|
},
|
||||||
serverGlobal(scope: ServerScopeValue, key: string, legacy?: string[]): PersistTarget {
|
serverGlobal(scope: ServerScopeValue, key: string, legacy?: string[]): PersistTarget {
|
||||||
if (scope === ServerScope.local) return Persist.global(key, legacy)
|
if (scope === ServerScope.local) return Persist.global(key, legacy)
|
||||||
return { storage: GLOBAL_STORAGE, key: ScopedKey.from(scope, key) }
|
return { storage: GLOBAL_STORAGE, key: ScopedKey.from(scope, key) }
|
||||||
|
|||||||
@@ -71,6 +71,17 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
|
|||||||
if (!distro) return null
|
if (!distro) return null
|
||||||
return current()?.opencodeChecks[distro] ?? null
|
return current()?.opencodeChecks[distro] ?? null
|
||||||
})
|
})
|
||||||
|
const wslReady = createMemo(() => !!current()?.runtime?.available && !current()?.pendingRestart)
|
||||||
|
const distroReady = createMemo(() => {
|
||||||
|
const probe = selectedProbe()
|
||||||
|
if (!probe || !selectedDistro()) return false
|
||||||
|
if (selectedInstalled()?.version === 1) return false
|
||||||
|
return probe.canExecute && probe.hasBash && probe.hasCurl
|
||||||
|
})
|
||||||
|
const opencodeReady = createMemo(() => {
|
||||||
|
const check = opencodeCheck()
|
||||||
|
return !!check?.resolvedPath && !check.error
|
||||||
|
})
|
||||||
const distroWarningProbe = createMemo(() => {
|
const distroWarningProbe = createMemo(() => {
|
||||||
const probe = selectedProbe()
|
const probe = selectedProbe()
|
||||||
if (!probe) return null
|
if (!probe) return null
|
||||||
@@ -106,17 +117,6 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
|
|||||||
const job = current()?.job
|
const job = current()?.job
|
||||||
return job?.kind === "install-opencode" && job.distro === selectedDistro()
|
return job?.kind === "install-opencode" && job.distro === selectedDistro()
|
||||||
})
|
})
|
||||||
const wslReady = createMemo(() => !!current()?.runtime?.available && !current()?.pendingRestart)
|
|
||||||
const distroReady = createMemo(() => {
|
|
||||||
const probe = selectedProbe()
|
|
||||||
if (!probe || !selectedDistro()) return false
|
|
||||||
if (selectedInstalled()?.version === 1) return false
|
|
||||||
return probe.canExecute && probe.hasBash && probe.hasCurl
|
|
||||||
})
|
|
||||||
const opencodeReady = createMemo(() => {
|
|
||||||
const check = opencodeCheck()
|
|
||||||
return !!check?.resolvedPath && !check.error
|
|
||||||
})
|
|
||||||
const allReady = createMemo(() => wslReady() && distroReady() && opencodeReady())
|
const allReady = createMemo(() => wslReady() && distroReady() && opencodeReady())
|
||||||
const addDisabled = createMemo(() => {
|
const addDisabled = createMemo(() => {
|
||||||
const job = current()?.job
|
const job = current()?.job
|
||||||
|
|||||||
@@ -76,11 +76,7 @@ export function WslServerSettings(props: {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
const remove = (key: ServerConnection.Key) => {
|
const remove = (key: ServerConnection.Key) => {
|
||||||
if (!api) return
|
request.mutate(() => props.controller.handleRemove(key))
|
||||||
request.mutate(async () => {
|
|
||||||
await api.removeServer(key)
|
|
||||||
await props.controller.handleRemove(key)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
preload = ["@opentui/solid/preload"]
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/cli",
|
"name": "@opencode-ai/cli",
|
||||||
"version": "1.16.2",
|
"version": "1.17.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -20,8 +20,12 @@
|
|||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/server": "workspace:*",
|
"@opencode-ai/server": "workspace:*",
|
||||||
|
"@opencode-ai/tui": "workspace:*",
|
||||||
|
"@opentui/core": "catalog:",
|
||||||
|
"@opentui/solid": "catalog:",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"effect": "catalog:"
|
"effect": "catalog:",
|
||||||
|
"solid-js": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import { $ } from "bun"
|
||||||
|
import fs from "fs"
|
||||||
import { rm } from "fs/promises"
|
import { rm } from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Script } from "@opencode-ai/script"
|
import { Script } from "@opencode-ai/script"
|
||||||
|
import { createSolidTransformPlugin } from "@opentui/solid/bun-plugin"
|
||||||
|
import pkg from "../package.json"
|
||||||
import { modelsData } from "./generate"
|
import { modelsData } from "./generate"
|
||||||
|
|
||||||
const dir = path.resolve(import.meta.dirname, "..")
|
const dir = path.resolve(import.meta.dirname, "..")
|
||||||
@@ -13,7 +17,9 @@ await rm("dist", { recursive: true, force: true })
|
|||||||
|
|
||||||
const singleFlag = process.argv.includes("--single")
|
const singleFlag = process.argv.includes("--single")
|
||||||
const baselineFlag = process.argv.includes("--baseline")
|
const baselineFlag = process.argv.includes("--baseline")
|
||||||
|
const skipInstall = process.argv.includes("--skip-install")
|
||||||
const sourcemapsFlag = process.argv.includes("--sourcemaps")
|
const sourcemapsFlag = process.argv.includes("--sourcemaps")
|
||||||
|
const plugin = createSolidTransformPlugin()
|
||||||
|
|
||||||
const allTargets: {
|
const allTargets: {
|
||||||
os: string
|
os: string
|
||||||
@@ -43,6 +49,12 @@ const targets = singleFlag
|
|||||||
})
|
})
|
||||||
: allTargets
|
: allTargets
|
||||||
|
|
||||||
|
if (!skipInstall) await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}`
|
||||||
|
|
||||||
|
const localParserWorker = path.resolve(dir, "node_modules/@opentui/core/parser.worker.js")
|
||||||
|
const rootParserWorker = path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js")
|
||||||
|
const parserWorker = fs.realpathSync(fs.existsSync(localParserWorker) ? localParserWorker : rootParserWorker)
|
||||||
|
|
||||||
for (const item of targets) {
|
for (const item of targets) {
|
||||||
const target = [
|
const target = [
|
||||||
binary,
|
binary,
|
||||||
@@ -56,8 +68,9 @@ for (const item of targets) {
|
|||||||
const name = target.replace(binary, "cli")
|
const name = target.replace(binary, "cli")
|
||||||
console.log(`building ${name}`)
|
console.log(`building ${name}`)
|
||||||
const result = await Bun.build({
|
const result = await Bun.build({
|
||||||
entrypoints: ["./src/index.ts"],
|
entrypoints: ["./src/index.ts", parserWorker],
|
||||||
tsconfig: "./tsconfig.json",
|
tsconfig: "./tsconfig.json",
|
||||||
|
plugins: [plugin],
|
||||||
external: ["node-gyp"],
|
external: ["node-gyp"],
|
||||||
format: "esm",
|
format: "esm",
|
||||||
minify: true,
|
minify: true,
|
||||||
@@ -79,6 +92,13 @@ for (const item of targets) {
|
|||||||
OPENCODE_MODELS_DEV: modelsData,
|
OPENCODE_MODELS_DEV: modelsData,
|
||||||
OPENCODE_CHANNEL: `'${Script.channel}'`,
|
OPENCODE_CHANNEL: `'${Script.channel}'`,
|
||||||
OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "undefined",
|
OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "undefined",
|
||||||
|
// FFF_LIBC selects the fff native lib variant: "musl" or "gnu".
|
||||||
|
FFF_LIBC: item.os === "linux" ? `'${item.abi ?? "gnu"}'` : "undefined",
|
||||||
|
OTUI_TREE_SITTER_WORKER_PATH:
|
||||||
|
(item.os === "win32" ? '"B:/~BUN/root/' : '"/$bunfs/root/') +
|
||||||
|
path.relative(dir, parserWorker).replaceAll("\\", "/") +
|
||||||
|
'"',
|
||||||
|
...(item.os === "linux" ? { "process.env.OPENTUI_LIBC": JSON.stringify(item.abi ?? "glibc") } : {}),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Commands } from "../commands"
|
||||||
|
import { Runtime } from "../../framework/runtime"
|
||||||
|
import { Effect } from "effect"
|
||||||
|
import { Daemon } from "../../services/daemon"
|
||||||
|
|
||||||
|
export default Runtime.handler(Commands, () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const daemon = yield* Daemon.Service
|
||||||
|
const transport = yield* daemon.transport()
|
||||||
|
const { runTui } = yield* Effect.promise(() => import("../../tui"))
|
||||||
|
yield* runTui(transport)
|
||||||
|
}),
|
||||||
|
)
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { NodeHttpServer } from "@effect/platform-node"
|
import { NodeHttpServer } from "@effect/platform-node"
|
||||||
|
import { Credential } from "@opencode-ai/core/credential"
|
||||||
|
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||||
import { Context, Layer, Option } from "effect"
|
import { Context, Layer, Option } from "effect"
|
||||||
import * as Effect from "effect/Effect"
|
import * as Effect from "effect/Effect"
|
||||||
import { HttpRouter, HttpServer } from "effect/unstable/http"
|
import { HttpRouter, HttpServer } from "effect/unstable/http"
|
||||||
@@ -34,6 +36,8 @@ function bind(hostname: string, port: number, password: string) {
|
|||||||
return Layer.build(
|
return Layer.build(
|
||||||
HttpRouter.serve(createRoutes(password), { disableListenLog: true, disableLogger: true }).pipe(
|
HttpRouter.serve(createRoutes(password), { disableListenLog: true, disableLogger: true }).pipe(
|
||||||
Layer.provideMerge(NodeHttpServer.layer(() => createServer(), { port, host: hostname })),
|
Layer.provideMerge(NodeHttpServer.layer(() => createServer(), { port, host: hostname })),
|
||||||
|
Layer.provide(Credential.defaultLayer),
|
||||||
|
Layer.provide(PermissionSaved.defaultLayer),
|
||||||
),
|
),
|
||||||
).pipe(Effect.map((context) => Context.get(context, HttpServer.HttpServer).address))
|
).pipe(Effect.map((context) => Context.get(context, HttpServer.HttpServer).address))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,19 +60,19 @@ export function run(commands: Spec.Any, handlers: ReadonlyArray<LazyHandler>, op
|
|||||||
}
|
}
|
||||||
|
|
||||||
function provide(node: Spec.Any, handlers: ReadonlyArray<LazyHandler>): ProvidedCommand {
|
function provide(node: Spec.Any, handlers: ReadonlyArray<LazyHandler>): ProvidedCommand {
|
||||||
const spec: Command.Command.Any = Object.keys(node.commands).length
|
const handler = handlers.find((handler) => handler.spec === node.spec)
|
||||||
? (node.spec as Command.Command<string, unknown>).pipe(
|
const spec = handler
|
||||||
Command.withSubcommands(Object.values(node.commands).map((child) => provide(child, handlers))),
|
? node.spec.pipe(
|
||||||
|
Command.withHandler((input) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* Effect.flatMap(Effect.promise(handler.load), (module) => module.default(input))
|
||||||
|
}),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: node.spec
|
: node.spec
|
||||||
const handler = handlers.find((handler) => handler.spec === node.spec)
|
if (!Object.keys(node.commands).length) return spec as ProvidedCommand
|
||||||
if (!handler) return spec as ProvidedCommand
|
|
||||||
return spec.pipe(
|
return spec.pipe(
|
||||||
Command.withHandler((input) =>
|
Command.withSubcommands(Object.values(node.commands).map((child) => provide(child, handlers))),
|
||||||
Effect.gen(function* () {
|
|
||||||
yield* Effect.flatMap(Effect.promise(handler.load), (module) => module.default(input))
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
) as ProvidedCommand
|
) as ProvidedCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Runtime } from "./framework/runtime"
|
|||||||
import { Daemon } from "./services/daemon"
|
import { Daemon } from "./services/daemon"
|
||||||
|
|
||||||
const Handlers = Runtime.handlers(Commands, {
|
const Handlers = Runtime.handlers(Commands, {
|
||||||
|
$: () => import("./commands/handlers/default"),
|
||||||
debug: {
|
debug: {
|
||||||
agents: () => import("./commands/handlers/debug/agents"),
|
agents: () => import("./commands/handlers/debug/agents"),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import { ServerAuth } from "@opencode-ai/server/auth"
|
|||||||
import { Context, Effect, FileSystem, Layer, Option, Schedule, Schema, Scope } from "effect"
|
import { Context, Effect, FileSystem, Layer, Option, Schedule, Schema, Scope } from "effect"
|
||||||
import { HttpServer } from "effect/unstable/http"
|
import { HttpServer } from "effect/unstable/http"
|
||||||
import { randomBytes, randomUUID } from "crypto"
|
import { randomBytes, randomUUID } from "crypto"
|
||||||
|
import { spawn } from "node:child_process"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly client: () => Effect.Effect<ReturnType<typeof createOpencodeClient>, unknown>
|
readonly client: () => Effect.Effect<ReturnType<typeof createOpencodeClient>, unknown>
|
||||||
|
readonly transport: () => Effect.Effect<{ url: string; headers: RequestInit["headers"] }, unknown>
|
||||||
readonly start: () => Effect.Effect<string, Error>
|
readonly start: () => Effect.Effect<string, Error>
|
||||||
readonly status: () => Effect.Effect<string | undefined>
|
readonly status: () => Effect.Effect<string | undefined>
|
||||||
readonly stop: () => Effect.Effect<void, unknown>
|
readonly stop: () => Effect.Effect<void, unknown>
|
||||||
@@ -108,16 +110,21 @@ export const layer = Layer.effect(
|
|||||||
const start = Effect.fn("cli.daemon.start")(function* () {
|
const start = Effect.fn("cli.daemon.start")(function* () {
|
||||||
const existing = yield* healthy().pipe(Effect.option)
|
const existing = yield* healthy().pipe(Effect.option)
|
||||||
const found = Option.getOrUndefined(existing)
|
const found = Option.getOrUndefined(existing)
|
||||||
if (found?.version === InstallationVersion) return found.url
|
const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun"
|
||||||
|
if (found?.version === InstallationVersion && compiled) return found.url
|
||||||
if (found) yield* stopProcess(found).pipe(Effect.ignore)
|
if (found) yield* stopProcess(found).pipe(Effect.ignore)
|
||||||
|
|
||||||
yield* Effect.sync(() => {
|
const entrypoint = compiled ? undefined : process.argv[1]
|
||||||
const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun"
|
if (!compiled && entrypoint === undefined)
|
||||||
Bun.spawn([process.execPath, ...(compiled ? [] : [Bun.main]), "serve", "--register"], {
|
return yield* Effect.fail(new Error("Failed to resolve CLI entrypoint"))
|
||||||
stdin: "ignore",
|
yield* Effect.try({
|
||||||
stdout: "ignore",
|
try: () => {
|
||||||
stderr: "ignore",
|
spawn(process.execPath, [...(entrypoint ? [entrypoint] : []), "serve", "--register"], {
|
||||||
}).unref()
|
detached: true,
|
||||||
|
stdio: "ignore",
|
||||||
|
}).unref()
|
||||||
|
},
|
||||||
|
catch: (cause) => new Error("Failed to start server", { cause }),
|
||||||
})
|
})
|
||||||
|
|
||||||
return yield* compatible().pipe(
|
return yield* compatible().pipe(
|
||||||
@@ -127,8 +134,13 @@ export const layer = Layer.effect(
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const transport = Effect.fn("cli.daemon.transport")(function* () {
|
||||||
|
return { url: yield* start(), headers: ServerAuth.headers({ password: yield* password() }) }
|
||||||
|
})
|
||||||
|
|
||||||
const client = Effect.fn("cli.daemon.client")(function* () {
|
const client = Effect.fn("cli.daemon.client")(function* () {
|
||||||
return yield* createClient(yield* start())
|
const connection = yield* transport()
|
||||||
|
return createOpencodeClient({ baseUrl: connection.url, headers: connection.headers })
|
||||||
})
|
})
|
||||||
|
|
||||||
const status = Effect.fn("cli.daemon.status")(function* () {
|
const status = Effect.fn("cli.daemon.status")(function* () {
|
||||||
@@ -173,7 +185,7 @@ export const layer = Layer.effect(
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
return Service.of({ client, start, status, stop, password, register })
|
return Service.of({ client, transport, start, status, stop, password, register })
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { run } from "@opencode-ai/tui"
|
||||||
|
import { TuiConfig } from "@opencode-ai/tui/config"
|
||||||
|
import { Effect } from "effect"
|
||||||
|
import { Global } from "@opencode-ai/core/global"
|
||||||
|
|
||||||
|
export function runTui(transport: { url: string; headers: RequestInit["headers"] }) {
|
||||||
|
const config = TuiConfig.resolve({}, { terminalSuspend: false })
|
||||||
|
return run({
|
||||||
|
...transport,
|
||||||
|
args: {},
|
||||||
|
config,
|
||||||
|
fetch: gracefulFetch,
|
||||||
|
pluginHost: {
|
||||||
|
async start() {},
|
||||||
|
async dispose() {},
|
||||||
|
},
|
||||||
|
}).pipe(Effect.provide(Global.defaultLayer))
|
||||||
|
}
|
||||||
|
|
||||||
|
const legacyDefaults: Record<string, unknown> = {
|
||||||
|
"/config/providers": { providers: [], default: {} },
|
||||||
|
"/provider": { all: [], default: {}, connected: [] },
|
||||||
|
"/agent": [],
|
||||||
|
"/config": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const gracefulFetch = Object.assign(
|
||||||
|
async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||||
|
const response = await fetch(input, init)
|
||||||
|
if (response.status !== 404) return response
|
||||||
|
const fallback = legacyDefaults[new URL(input instanceof Request ? input.url : input).pathname]
|
||||||
|
if (fallback === undefined) return response
|
||||||
|
return Response.json(fallback)
|
||||||
|
},
|
||||||
|
{ preconnect: fetch.preconnect },
|
||||||
|
)
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
"$schema": "https://json.schemastore.org/tsconfig",
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
"extends": "@tsconfig/bun/tsconfig.json",
|
"extends": "@tsconfig/bun/tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"jsx": "preserve",
|
||||||
|
"jsxImportSource": "@opentui/solid",
|
||||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||||
"noUncheckedIndexedAccess": false
|
"noUncheckedIndexedAccess": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.16.2",
|
"version": "1.17.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -13,10 +13,9 @@ import copyLogoSvgLight from "../asset/lander/opencode-logo-light.svg"
|
|||||||
import copyLogoSvgDark from "../asset/lander/opencode-logo-dark.svg"
|
import copyLogoSvgDark from "../asset/lander/opencode-logo-dark.svg"
|
||||||
import copyWordmarkSvgLight from "../asset/lander/opencode-wordmark-light.svg"
|
import copyWordmarkSvgLight from "../asset/lander/opencode-wordmark-light.svg"
|
||||||
import copyWordmarkSvgDark from "../asset/lander/opencode-wordmark-dark.svg"
|
import copyWordmarkSvgDark from "../asset/lander/opencode-wordmark-dark.svg"
|
||||||
import { A, createAsync, useNavigate } from "@solidjs/router"
|
import { A, useNavigate } from "@solidjs/router"
|
||||||
import { createMemo, Match, Show, Switch } from "solid-js"
|
import { createMemo, Match, Show, Switch } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { github } from "~/lib/github"
|
|
||||||
import { createEffect, onCleanup } from "solid-js"
|
import { createEffect, onCleanup } from "solid-js"
|
||||||
import { config } from "~/config"
|
import { config } from "~/config"
|
||||||
import { useI18n } from "~/context/i18n"
|
import { useI18n } from "~/context/i18n"
|
||||||
@@ -40,16 +39,6 @@ export function Header(props: { zen?: boolean; go?: boolean; hideGetStarted?: bo
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const githubData = createAsync(() => github())
|
|
||||||
const starCount = createMemo(() =>
|
|
||||||
githubData()?.stars
|
|
||||||
? new Intl.NumberFormat("en-US", {
|
|
||||||
notation: "compact",
|
|
||||||
compactDisplay: "short",
|
|
||||||
maximumFractionDigits: 0,
|
|
||||||
}).format(githubData()?.stars)
|
|
||||||
: config.github.starsFormatted.compact,
|
|
||||||
)
|
|
||||||
|
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
mobileMenuOpen: false,
|
mobileMenuOpen: false,
|
||||||
@@ -155,12 +144,15 @@ export function Header(props: { zen?: boolean; go?: boolean; hideGetStarted?: bo
|
|||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<a href={config.github.repoUrl} target="_blank" style="white-space: nowrap;">
|
<a href={config.github.repoUrl} target="_blank" style="white-space: nowrap;">
|
||||||
{i18n.t("nav.github")} <span>[{starCount()}]</span>
|
{i18n.t("nav.github")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href={language.route("/docs")}>{i18n.t("nav.docs")}</a>
|
<a href={language.route("/docs")}>{i18n.t("nav.docs")}</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href={language.route("/data")}>{i18n.t("nav.data")}</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<A href={language.route("/zen")}>{i18n.t("nav.zen")}</A>
|
<A href={language.route("/zen")}>{i18n.t("nav.zen")}</A>
|
||||||
</li>
|
</li>
|
||||||
@@ -252,12 +244,15 @@ export function Header(props: { zen?: boolean; go?: boolean; hideGetStarted?: bo
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href={config.github.repoUrl} target="_blank" style="white-space: nowrap;">
|
<a href={config.github.repoUrl} target="_blank" style="white-space: nowrap;">
|
||||||
{i18n.t("nav.github")} <span>[{starCount()}]</span>
|
{i18n.t("nav.github")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href={language.route("/docs")}>{i18n.t("nav.docs")}</a>
|
<a href={language.route("/docs")}>{i18n.t("nav.docs")}</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href={language.route("/data")}>{i18n.t("nav.data")}</a>
|
||||||
|
</li>
|
||||||
<Show when={!props.zen}>
|
<Show when={!props.zen}>
|
||||||
<li>
|
<li>
|
||||||
<A href={language.route("/zen")}>{i18n.t("nav.zen")}</A>
|
<A href={language.route("/zen")}>{i18n.t("nav.zen")}</A>
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "الاستثناءات التالية",
|
"zen.privacy.exceptionsLink": "الاستثناءات التالية",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع",
|
"go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع",
|
||||||
|
"go.banner.text": "MiniMax M3: حد استخدام أكبر 3 مرات لفترة محدودة",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash.",
|
"يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash.",
|
||||||
"go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع",
|
"go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع",
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "seguintes exceções",
|
"zen.privacy.exceptionsLink": "seguintes exceções",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos",
|
"go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos",
|
||||||
|
"go.banner.text": "MiniMax M3 tem limite de uso 3x maior por tempo limitado",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de solicitação de 5 horas para GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash.",
|
"O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de solicitação de 5 horas para GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Modelos de codificação de baixo custo para todos",
|
"go.hero.title": "Modelos de codificação de baixo custo para todos",
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "følgende undtagelser",
|
"zen.privacy.exceptionsLink": "følgende undtagelser",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle",
|
"go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle",
|
||||||
|
"go.banner.text": "MiniMax M3 får tredoblet brugsgrænse i en begrænset periode",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go starter ved $5 for den første måned, derefter $10/måned, med generøse 5-timers anmodningsgrænser for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash.",
|
"Go starter ved $5 for den første måned, derefter $10/måned, med generøse 5-timers anmodningsgrænser for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Kodningsmodeller til lav pris for alle",
|
"go.hero.title": "Kodningsmodeller til lav pris for alle",
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "folgenden Ausnahmen",
|
"zen.privacy.exceptionsLink": "folgenden Ausnahmen",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle",
|
"go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle",
|
||||||
|
"go.banner.text": "MiniMax M3 erhält für begrenzte Zeit 3x Nutzungslimits",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go beginnt bei $5 für den ersten Monat, danach $10/Monat, mit großzügigen 5-Stunden-Anfragelimits für GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro und DeepSeek V4 Flash.",
|
"Go beginnt bei $5 für den ersten Monat, danach $10/Monat, mit großzügigen 5-Stunden-Anfragelimits für GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro und DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Kostengünstige Coding-Modelle für alle",
|
"go.hero.title": "Kostengünstige Coding-Modelle für alle",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export const dict = {
|
export const dict = {
|
||||||
"nav.github": "GitHub",
|
"nav.github": "GitHub",
|
||||||
"nav.docs": "Docs",
|
"nav.docs": "Docs",
|
||||||
|
"nav.data": "Data",
|
||||||
"nav.changelog": "Changelog",
|
"nav.changelog": "Changelog",
|
||||||
"nav.discord": "Discord",
|
"nav.discord": "Discord",
|
||||||
"nav.x": "X",
|
"nav.x": "X",
|
||||||
@@ -247,6 +248,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "following exceptions",
|
"zen.privacy.exceptionsLink": "following exceptions",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Low cost coding models for everyone",
|
"go.title": "OpenCode Go | Low cost coding models for everyone",
|
||||||
|
"go.banner.text": "MiniMax M3 gets 3× usage limits for a limited time",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go starts at $5 for your first month, then $10/month, with generous 5-hour request limits for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, and DeepSeek V4 Flash.",
|
"Go starts at $5 for your first month, then $10/month, with generous 5-hour request limits for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, and DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Low cost coding models for everyone",
|
"go.hero.title": "Low cost coding models for everyone",
|
||||||
|
|||||||
@@ -253,6 +253,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "siguientes excepciones",
|
"zen.privacy.exceptionsLink": "siguientes excepciones",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Modelos de programación de bajo coste para todos",
|
"go.title": "OpenCode Go | Modelos de programación de bajo coste para todos",
|
||||||
|
"go.banner.text": "MiniMax M3 tiene límites de uso 3x mayores por tiempo limitado",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go comienza en $5 el primer mes, luego 10 $/mes, con generosos límites de solicitudes de 5 horas para GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro y DeepSeek V4 Flash.",
|
"Go comienza en $5 el primer mes, luego 10 $/mes, con generosos límites de solicitudes de 5 horas para GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro y DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Modelos de programación de bajo coste para todos",
|
"go.hero.title": "Modelos de programación de bajo coste para todos",
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "exceptions suivantes",
|
"zen.privacy.exceptionsLink": "exceptions suivantes",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Modèles de code à faible coût pour tous",
|
"go.title": "OpenCode Go | Modèles de code à faible coût pour tous",
|
||||||
|
"go.banner.text": "MiniMax M3 bénéficie de limites d’utilisation 3x supérieures pour une durée limitée",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites de requêtes généreuses sur 5 heures pour GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro et DeepSeek V4 Flash.",
|
"Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites de requêtes généreuses sur 5 heures pour GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro et DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Modèles de code à faible coût pour tous",
|
"go.hero.title": "Modèles de code à faible coût pour tous",
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "seguenti eccezioni",
|
"zen.privacy.exceptionsLink": "seguenti eccezioni",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Modelli di coding a basso costo per tutti",
|
"go.title": "OpenCode Go | Modelli di coding a basso costo per tutti",
|
||||||
|
"go.banner.text": "MiniMax M3 offre limiti di utilizzo 3x superiori per un periodo limitato",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go inizia a $5 per il primo mese, poi $10/mese, con generosi limiti di richiesta di 5 ore per GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash.",
|
"Go inizia a $5 per il primo mese, poi $10/mese, con generosi limiti di richiesta di 5 ore per GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Modelli di coding a basso costo per tutti",
|
"go.hero.title": "Modelli di coding a basso costo per tutti",
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "以下の例外",
|
"zen.privacy.exceptionsLink": "以下の例外",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル",
|
"go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル",
|
||||||
|
"go.banner.text": "MiniMax M3の利用上限が期間限定で3倍に",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Goは最初の月$5、その後$10/月で、GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flashに対して5時間のゆとりあるリクエスト上限があります。",
|
"Goは最初の月$5、その後$10/月で、GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flashに対して5時間のゆとりあるリクエスト上限があります。",
|
||||||
"go.hero.title": "すべての人のための低価格なコーディングモデル",
|
"go.hero.title": "すべての人のための低価格なコーディングモデル",
|
||||||
|
|||||||
@@ -246,6 +246,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "다음 예외",
|
"zen.privacy.exceptionsLink": "다음 예외",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델",
|
"go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델",
|
||||||
|
"go.banner.text": "MiniMax M3 사용 한도가 한시적으로 3배 확대됩니다",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash에 대해 넉넉한 5시간 요청 한도를 제공합니다.",
|
"Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash에 대해 넉넉한 5시간 요청 한도를 제공합니다.",
|
||||||
"go.hero.title": "모두를 위한 저비용 코딩 모델",
|
"go.hero.title": "모두를 위한 저비용 코딩 모델",
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "følgende unntak",
|
"zen.privacy.exceptionsLink": "følgende unntak",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Rimelige kodemodeller for alle",
|
"go.title": "OpenCode Go | Rimelige kodemodeller for alle",
|
||||||
|
"go.banner.text": "MiniMax M3 får 3x bruksgrense i en begrenset periode",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse 5-timers forespørselsgrenser for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash.",
|
"Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse 5-timers forespørselsgrenser for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Rimelige kodemodeller for alle",
|
"go.hero.title": "Rimelige kodemodeller for alle",
|
||||||
|
|||||||
@@ -251,6 +251,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "następującymi wyjątkami",
|
"zen.privacy.exceptionsLink": "następującymi wyjątkami",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego",
|
"go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego",
|
||||||
|
"go.banner.text": "MiniMax M3 oferuje 3x wyższe limity użycia przez ograniczony czas",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi 5-godzinnymi limitami zapytań dla GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro i DeepSeek V4 Flash.",
|
"Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi 5-godzinnymi limitami zapytań dla GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro i DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Niskokosztowe modele do kodowania dla każdego",
|
"go.hero.title": "Niskokosztowe modele do kodowania dla każdego",
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "следующими исключениями",
|
"zen.privacy.exceptionsLink": "следующими исключениями",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Недорогие модели для кодинга для всех",
|
"go.title": "OpenCode Go | Недорогие модели для кодинга для всех",
|
||||||
|
"go.banner.text": "MiniMax M3 получает 3x лимиты использования на ограниченное время",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами запросов за 5 часов для GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro и DeepSeek V4 Flash.",
|
"Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами запросов за 5 часов для GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro и DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Недорогие модели для кодинга для всех",
|
"go.hero.title": "Недорогие модели для кодинга для всех",
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "ข้อยกเว้นดังนี้",
|
"zen.privacy.exceptionsLink": "ข้อยกเว้นดังนี้",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
|
"go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
|
||||||
|
"go.banner.text": "MiniMax M3 เพิ่มโควตาการใช้งานเป็น 3 เท่าในช่วงเวลาจำกัด",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดคำขอ 5 ชั่วโมงที่เอื้อเฟื้อสำหรับ GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro และ DeepSeek V4 Flash",
|
"Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดคำขอ 5 ชั่วโมงที่เอื้อเฟื้อสำหรับ GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro และ DeepSeek V4 Flash",
|
||||||
"go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
|
"go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "aşağıdaki istisnalar",
|
"zen.privacy.exceptionsLink": "aşağıdaki istisnalar",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri",
|
"go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri",
|
||||||
|
"go.banner.text": "MiniMax M3 sınırlı bir süre için 3x kullanım limiti sunuyor",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro ve DeepSeek V4 Flash için cömert 5 saatlik istek limitleri sunar.",
|
"Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro ve DeepSeek V4 Flash için cömert 5 saatlik istek limitleri sunar.",
|
||||||
"go.hero.title": "Herkes için düşük maliyetli kodlama modelleri",
|
"go.hero.title": "Herkes için düşük maliyetli kodlama modelleri",
|
||||||
|
|||||||
@@ -251,6 +251,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "такими винятками",
|
"zen.privacy.exceptionsLink": "такими винятками",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Недорогі моделі кодування для всіх",
|
"go.title": "OpenCode Go | Недорогі моделі кодування для всіх",
|
||||||
|
"go.banner.text": "MiniMax M3 отримує 3x ліміти використання протягом обмеженого часу",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go починається від $5 за перший місяць, потім $10/місяць, з generous 5-годинними лімітами запитів для GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro та DeepSeek V4 Flash.",
|
"Go починається від $5 за перший місяць, потім $10/місяць, з generous 5-годинними лімітами запитів для GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro та DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Недорогі моделі кодування для всіх",
|
"go.hero.title": "Недорогі моделі кодування для всіх",
|
||||||
|
|||||||
@@ -240,6 +240,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "以下例外情况除外",
|
"zen.privacy.exceptionsLink": "以下例外情况除外",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | 人人可用的低成本编程模型",
|
"go.title": "OpenCode Go | 人人可用的低成本编程模型",
|
||||||
|
"go.banner.text": "MiniMax M3 限时享受 3 倍使用额度",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go 首月 $5,之后 $10/月,提供对 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小时充裕请求额度。",
|
"Go 首月 $5,之后 $10/月,提供对 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小时充裕请求额度。",
|
||||||
"go.hero.title": "人人可用的低成本编程模型",
|
"go.hero.title": "人人可用的低成本编程模型",
|
||||||
|
|||||||
@@ -240,6 +240,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "以下例外情況",
|
"zen.privacy.exceptionsLink": "以下例外情況",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | 低成本全民編碼模型",
|
"go.title": "OpenCode Go | 低成本全民編碼模型",
|
||||||
|
"go.banner.text": "MiniMax M3 限時享有 3 倍使用額度",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go 首月 $5,之後 $10/月,提供對 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小時充裕請求額度。",
|
"Go 首月 $5,之後 $10/月,提供對 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小時充裕請求額度。",
|
||||||
"go.hero.title": "低成本全民編碼模型",
|
"go.hero.title": "低成本全民編碼模型",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user