Compare commits
175
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ff4da5639 | ||
|
|
24f9df5463 | ||
|
|
12b8e1c2be | ||
|
|
d70099b059 | ||
|
|
ce845a0b1b | ||
|
|
05d3e65f76 | ||
|
|
51618e9cef | ||
|
|
e78944e9a4 | ||
|
|
bfdc38e421 | ||
|
|
83023e4f0f | ||
|
|
d0a57305ef | ||
|
|
27a70ad70f | ||
|
|
b6e94fd2a8 | ||
|
|
0bbf26a1ce | ||
|
|
83cdb4de64 | ||
|
|
4989632245 | ||
|
|
d460614cd7 | ||
|
|
7866dbcfcc | ||
|
|
e71a21e0a8 | ||
|
|
1071aca91f | ||
|
|
b3d0446d13 | ||
|
|
949191ab74 | ||
|
|
92cd908fb5 | ||
|
|
6fcc970def | ||
|
|
52a7a04ad8 | ||
|
|
3d5c041129 | ||
|
|
37b8662a9d | ||
|
|
ddcb32ae0b | ||
|
|
3af557512b | ||
|
|
2c056c90da | ||
|
|
812d1bb32a | ||
|
|
9a58c43ef4 | ||
|
|
63585db6a7 | ||
|
|
bd44489ada | ||
|
|
b53a95fd81 | ||
|
|
a6ef9e9206 | ||
|
|
6e09a1d904 | ||
|
|
4f21757e0d | ||
|
|
2dbcd79fd2 | ||
|
|
8e11a46fe0 | ||
|
|
48a7f0fd93 | ||
|
|
8ab4d84057 | ||
|
|
4066247988 | ||
|
|
b9de3ad370 | ||
|
|
d69962b0f7 | ||
|
|
a6f23cb08e | ||
|
|
0540751897 | ||
|
|
baa204193c | ||
|
|
aeece6166b | ||
|
|
0d7e62a532 | ||
|
|
41aa254db4 | ||
|
|
d178d8249f | ||
|
|
e6f5214779 | ||
|
|
84f60d97a0 | ||
|
|
cbf4b68fee | ||
|
|
bd4527b4f2 | ||
|
|
f4a9fe29a3 | ||
|
|
5a0bfa7061 | ||
|
|
1ac1a0287c | ||
|
|
8e09e8c612 | ||
|
|
84e62fc662 | ||
|
|
a7ea93528b | ||
|
|
d90e3a2833 | ||
|
|
1c74c2741a | ||
|
|
5d2f8d77f9 | ||
|
|
81be544981 | ||
|
|
773c1192dc | ||
|
|
5ddfe4ada5 | ||
|
|
a93d98bd94 | ||
|
|
54ed87d53c | ||
|
|
8ee939c741 | ||
|
|
1b0096bf61 | ||
|
|
8006c29db3 | ||
|
|
3f1c96a0bb | ||
|
|
3558deba4a | ||
|
|
c3ddc85cca | ||
|
|
a800583aea | ||
|
|
171e69c2fc | ||
|
|
822bb7b336 | ||
|
|
47cf267c23 | ||
|
|
976aae7e42 | ||
|
|
0ca51eebcf | ||
|
|
3256886e25 | ||
|
|
d2194f6dde | ||
|
|
bfd4787fcd | ||
|
|
58dce0148a | ||
|
|
79635b8b41 | ||
|
|
331dacf9db | ||
|
|
4ba7d3b406 | ||
|
|
a43783a6d4 | ||
|
|
37c5295111 | ||
|
|
56102ff642 | ||
|
|
1b86c27fb8 | ||
|
|
fe43bdb699 | ||
|
|
a849a17e93 | ||
|
|
0292f1b559 | ||
|
|
5dfe86dcb1 | ||
|
|
4b4dd2b882 | ||
|
|
bc949af623 | ||
|
|
9e7c136de7 | ||
|
|
fee3c196c5 | ||
|
|
6c047391bb | ||
|
|
350df0b261 | ||
|
|
fbabc97c4c | ||
|
|
7daea69e13 | ||
|
|
0772a95918 | ||
|
|
dadddc9c8c | ||
|
|
6708c3f6cf | ||
|
|
ba22976568 | ||
|
|
0afeaea21f | ||
|
|
b07b5a9b7f | ||
|
|
dbbe931a18 | ||
|
|
e14e874e51 | ||
|
|
544315dff7 | ||
|
|
f13da808ff | ||
|
|
e416e59ea6 | ||
|
|
cb69501098 | ||
|
|
a64f604d54 | ||
|
|
d7093abf61 | ||
|
|
60af447908 | ||
|
|
1cdc558ac0 | ||
|
|
3849822769 | ||
|
|
e9a17e4480 | ||
|
|
68809365df | ||
|
|
8da511dfa8 | ||
|
|
69381f6aea | ||
|
|
df6508530f | ||
|
|
335356280c | ||
|
|
03d84f49c2 | ||
|
|
2cbdf04ec9 | ||
|
|
410fbd8a00 | ||
|
|
e5cbecf17c | ||
|
|
ca3af5dc6a | ||
|
|
9e740d9947 | ||
|
|
d4694d058c | ||
|
|
469c3a4204 | ||
|
|
4cb29967f6 | ||
|
|
e718db624f | ||
|
|
15b27e0d18 | ||
|
|
c523aac586 | ||
|
|
51fcd04a70 | ||
|
|
4d7cbdcbef | ||
|
|
59c530cc6c | ||
|
|
c2ca1494e5 | ||
|
|
4ee426ba54 | ||
|
|
510374207d | ||
|
|
aedbecedf7 | ||
|
|
9c00669927 | ||
|
|
b9f6b40e3a | ||
|
|
ad06d8f496 | ||
|
|
2fc06c5a17 | ||
|
|
52877d8765 | ||
|
|
8f957b8f90 | ||
|
|
0befa1e57e | ||
|
|
f015154314 | ||
|
|
689d9e14ea | ||
|
|
66e8c57ed1 | ||
|
|
b698f14e55 | ||
|
|
cec1255b36 | ||
|
|
88226f3061 | ||
|
|
8c53b2b470 | ||
|
|
f2d3a4c70f | ||
|
|
4b9b86b544 | ||
|
|
f54abe58cf | ||
|
|
d954026dd8 | ||
|
|
4ad8116ce3 | ||
|
|
5c7088338c | ||
|
|
389daa03df | ||
|
|
1cbe7b0854 | ||
|
|
050d71bcf9 | ||
|
|
ffde837e83 | ||
|
|
536abea2e2 | ||
|
|
c7a52b6a2d | ||
|
|
c4ccb50c37 | ||
|
|
5aaf1ddfb7 |
@@ -21,3 +21,4 @@ r44vc0rp
|
|||||||
rekram1-node
|
rekram1-node
|
||||||
-spider-yamet clawdbot/llm psychosis, spam pinging the team
|
-spider-yamet clawdbot/llm psychosis, spam pinging the team
|
||||||
thdxr
|
thdxr
|
||||||
|
-OpenCode2026
|
||||||
|
|||||||
@@ -41,5 +41,13 @@ runs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install
|
run: |
|
||||||
|
# Workaround for patched peer variants
|
||||||
|
# e.g. ./patches/ for standard-openapi
|
||||||
|
# https://github.com/oven-sh/bun/issues/28147
|
||||||
|
if [ "$RUNNER_OS" = "Windows" ]; then
|
||||||
|
bun install --linker hoisted
|
||||||
|
else
|
||||||
|
bun install
|
||||||
|
fi
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
@@ -50,20 +50,17 @@ jobs:
|
|||||||
|
|
||||||
e2e:
|
e2e:
|
||||||
name: e2e (${{ matrix.settings.name }})
|
name: e2e (${{ matrix.settings.name }})
|
||||||
needs: unit
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
settings:
|
settings:
|
||||||
- name: linux
|
- name: linux
|
||||||
host: blacksmith-4vcpu-ubuntu-2404
|
host: blacksmith-4vcpu-ubuntu-2404
|
||||||
playwright: bunx playwright install --with-deps
|
|
||||||
- name: windows
|
- name: windows
|
||||||
host: blacksmith-4vcpu-windows-2025
|
host: blacksmith-4vcpu-windows-2025
|
||||||
playwright: bunx playwright install
|
|
||||||
runs-on: ${{ matrix.settings.host }}
|
runs-on: ${{ matrix.settings.host }}
|
||||||
env:
|
env:
|
||||||
PLAYWRIGHT_BROWSERS_PATH: 0
|
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.playwright-browsers
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -76,9 +73,28 @@ jobs:
|
|||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: ./.github/actions/setup-bun
|
uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
- name: Install Playwright browsers
|
- name: Read Playwright version
|
||||||
|
id: playwright-version
|
||||||
|
run: |
|
||||||
|
version=$(node -e 'console.log(require("./packages/app/package.json").devDependencies["@playwright/test"])')
|
||||||
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Cache Playwright browsers
|
||||||
|
id: playwright-cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ github.workspace }}/.playwright-browsers
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright-version.outputs.version }}-chromium
|
||||||
|
|
||||||
|
- name: Install Playwright system dependencies
|
||||||
|
if: runner.os == 'Linux'
|
||||||
working-directory: packages/app
|
working-directory: packages/app
|
||||||
run: ${{ matrix.settings.playwright }}
|
run: bunx playwright install-deps chromium
|
||||||
|
|
||||||
|
- name: Install Playwright browsers
|
||||||
|
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||||
|
working-directory: packages/app
|
||||||
|
run: bunx playwright install chromium
|
||||||
|
|
||||||
- name: Run app e2e tests
|
- name: Run app e2e tests
|
||||||
run: bun --cwd packages/app test:e2e:local
|
run: bun --cwd packages/app test:e2e:local
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
plans/
|
node_modules
|
||||||
bun.lock
|
plans
|
||||||
package.json
|
package.json
|
||||||
|
bun.lock
|
||||||
|
.gitignore
|
||||||
package-lock.json
|
package-lock.json
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
description: Translate content for a specified locale while preserving technical terms
|
description: Translate content for a specified locale while preserving technical terms
|
||||||
mode: subagent
|
mode: subagent
|
||||||
model: opencode/gemini-3-pro
|
model: opencode/gpt-5.4
|
||||||
---
|
---
|
||||||
|
|
||||||
You are a professional translator and localization specialist.
|
You are a professional translator and localization specialist.
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
/// <reference path="../env.d.ts" />
|
/// <reference path="../env.d.ts" />
|
||||||
import { tool } from "@opencode-ai/plugin"
|
import { tool } from "@opencode-ai/plugin"
|
||||||
import DESCRIPTION from "./github-pr-search.txt"
|
|
||||||
|
|
||||||
async function githubFetch(endpoint: string, options: RequestInit = {}) {
|
async function githubFetch(endpoint: string, options: RequestInit = {}) {
|
||||||
const response = await fetch(`https://api.github.com${endpoint}`, {
|
const response = await fetch(`https://api.github.com${endpoint}`, {
|
||||||
...options,
|
...options,
|
||||||
@@ -24,7 +22,16 @@ interface PR {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default tool({
|
export default tool({
|
||||||
description: DESCRIPTION,
|
description: `Use this tool to search GitHub pull requests by title and description.
|
||||||
|
|
||||||
|
This tool searches PRs in the anomalyco/opencode repository and returns LLM-friendly results including:
|
||||||
|
- PR number and title
|
||||||
|
- Author
|
||||||
|
- State (open/closed/merged)
|
||||||
|
- Labels
|
||||||
|
- Description snippet
|
||||||
|
|
||||||
|
Use the query parameter to search for keywords that might appear in PR titles or descriptions.`,
|
||||||
args: {
|
args: {
|
||||||
query: tool.schema.string().describe("Search query for PR titles and descriptions"),
|
query: tool.schema.string().describe("Search query for PR titles and descriptions"),
|
||||||
limit: tool.schema.number().describe("Maximum number of results to return").default(10),
|
limit: tool.schema.number().describe("Maximum number of results to return").default(10),
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
Use this tool to search GitHub pull requests by title and description.
|
|
||||||
|
|
||||||
This tool searches PRs in the anomalyco/opencode repository and returns LLM-friendly results including:
|
|
||||||
- PR number and title
|
|
||||||
- Author
|
|
||||||
- State (open/closed/merged)
|
|
||||||
- Labels
|
|
||||||
- Description snippet
|
|
||||||
|
|
||||||
Use the query parameter to search for keywords that might appear in PR titles or descriptions.
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
/// <reference path="../env.d.ts" />
|
/// <reference path="../env.d.ts" />
|
||||||
import { tool } from "@opencode-ai/plugin"
|
import { tool } from "@opencode-ai/plugin"
|
||||||
import DESCRIPTION from "./github-triage.txt"
|
|
||||||
|
|
||||||
const TEAM = {
|
const TEAM = {
|
||||||
desktop: ["adamdotdevin", "iamdavidhill", "Brendonovich", "nexxeln"],
|
desktop: ["adamdotdevin", "iamdavidhill", "Brendonovich", "nexxeln"],
|
||||||
zen: ["fwang", "MrMushrooooom"],
|
zen: ["fwang", "MrMushrooooom"],
|
||||||
@@ -40,7 +38,12 @@ async function githubFetch(endpoint: string, options: RequestInit = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default tool({
|
export default tool({
|
||||||
description: DESCRIPTION,
|
description: `Use this tool to assign and/or label a GitHub issue.
|
||||||
|
|
||||||
|
Choose labels and assignee using the current triage policy and ownership rules.
|
||||||
|
Pick the most fitting labels for the issue and assign one owner.
|
||||||
|
|
||||||
|
If unsure, choose the team/section with the most overlap with the issue and assign a member from that team at random.`,
|
||||||
args: {
|
args: {
|
||||||
assignee: tool.schema
|
assignee: tool.schema
|
||||||
.enum(ASSIGNEES as [string, ...string[]])
|
.enum(ASSIGNEES as [string, ...string[]])
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
Use this tool to assign and/or label a GitHub issue.
|
|
||||||
|
|
||||||
Choose labels and assignee using the current triage policy and ownership rules.
|
|
||||||
Pick the most fitting labels for the issue and assign one owner.
|
|
||||||
|
|
||||||
If unsure, choose the team/section with the most overlap with the issue and assign a member from that team at random.
|
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.2.25",
|
"version": "1.2.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
"@solidjs/router": "catalog:",
|
"@solidjs/router": "catalog:",
|
||||||
"@thisbeyond/solid-dnd": "0.7.5",
|
"@thisbeyond/solid-dnd": "0.7.5",
|
||||||
"diff": "catalog:",
|
"diff": "catalog:",
|
||||||
"effect": "4.0.0-beta.31",
|
"effect": "catalog:",
|
||||||
"fuzzysort": "catalog:",
|
"fuzzysort": "catalog:",
|
||||||
"ghostty-web": "github:anomalyco/ghostty-web#main",
|
"ghostty-web": "github:anomalyco/ghostty-web#main",
|
||||||
"luxon": "catalog:",
|
"luxon": "catalog:",
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.2.25",
|
"version": "1.2.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.2.25",
|
"version": "1.2.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-sts": "3.782.0",
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
"@jsx-email/render": "1.1.1",
|
"@jsx-email/render": "1.1.1",
|
||||||
@@ -138,7 +138,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.2.25",
|
"version": "1.2.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "2.0.0",
|
"@ai-sdk/anthropic": "2.0.0",
|
||||||
"@ai-sdk/openai": "2.0.2",
|
"@ai-sdk/openai": "2.0.2",
|
||||||
@@ -162,7 +162,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.2.25",
|
"version": "1.2.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
@@ -186,7 +186,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.2.25",
|
"version": "1.2.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/app": "workspace:*",
|
"@opencode-ai/app": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
@@ -219,7 +219,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop-electron": {
|
"packages/desktop-electron": {
|
||||||
"name": "@opencode-ai/desktop-electron",
|
"name": "@opencode-ai/desktop-electron",
|
||||||
"version": "1.2.25",
|
"version": "1.2.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/app": "workspace:*",
|
"@opencode-ai/app": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
@@ -227,7 +227,7 @@
|
|||||||
"@solid-primitives/storage": "catalog:",
|
"@solid-primitives/storage": "catalog:",
|
||||||
"@solidjs/meta": "catalog:",
|
"@solidjs/meta": "catalog:",
|
||||||
"@solidjs/router": "0.15.4",
|
"@solidjs/router": "0.15.4",
|
||||||
"effect": "4.0.0-beta.31",
|
"effect": "catalog:",
|
||||||
"electron-log": "^5",
|
"electron-log": "^5",
|
||||||
"electron-store": "^10",
|
"electron-store": "^10",
|
||||||
"electron-updater": "^6",
|
"electron-updater": "^6",
|
||||||
@@ -250,7 +250,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.2.25",
|
"version": "1.2.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
@@ -279,7 +279,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.2.25",
|
"version": "1.2.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -295,7 +295,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.2.25",
|
"version": "1.2.27",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -324,8 +324,7 @@
|
|||||||
"@ai-sdk/xai": "2.0.51",
|
"@ai-sdk/xai": "2.0.51",
|
||||||
"@aws-sdk/credential-providers": "3.993.0",
|
"@aws-sdk/credential-providers": "3.993.0",
|
||||||
"@clack/prompts": "1.0.0-alpha.1",
|
"@clack/prompts": "1.0.0-alpha.1",
|
||||||
"@gitlab/gitlab-ai-provider": "3.6.0",
|
"@effect/platform-node": "catalog:",
|
||||||
"@gitlab/opencode-gitlab-auth": "1.3.3",
|
|
||||||
"@hono/standard-validator": "0.1.5",
|
"@hono/standard-validator": "0.1.5",
|
||||||
"@hono/zod-validator": "catalog:",
|
"@hono/zod-validator": "catalog:",
|
||||||
"@modelcontextprotocol/sdk": "1.25.2",
|
"@modelcontextprotocol/sdk": "1.25.2",
|
||||||
@@ -351,11 +350,13 @@
|
|||||||
"bun-pty": "0.4.8",
|
"bun-pty": "0.4.8",
|
||||||
"chokidar": "4.0.3",
|
"chokidar": "4.0.3",
|
||||||
"clipboardy": "4.0.0",
|
"clipboardy": "4.0.0",
|
||||||
|
"cross-spawn": "^7.0.6",
|
||||||
"decimal.js": "10.5.0",
|
"decimal.js": "10.5.0",
|
||||||
"diff": "catalog:",
|
"diff": "catalog:",
|
||||||
"drizzle-orm": "1.0.0-beta.16-ea816b6",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
|
"gitlab-ai-provider": "5.2.2",
|
||||||
"glob": "13.0.5",
|
"glob": "13.0.5",
|
||||||
"google-auth-library": "10.5.0",
|
"google-auth-library": "10.5.0",
|
||||||
"gray-matter": "4.0.3",
|
"gray-matter": "4.0.3",
|
||||||
@@ -366,6 +367,7 @@
|
|||||||
"mime-types": "3.0.2",
|
"mime-types": "3.0.2",
|
||||||
"minimatch": "10.0.3",
|
"minimatch": "10.0.3",
|
||||||
"open": "10.1.2",
|
"open": "10.1.2",
|
||||||
|
"opencode-gitlab-auth": "2.0.0",
|
||||||
"opentui-spinner": "0.0.6",
|
"opentui-spinner": "0.0.6",
|
||||||
"partial-json": "0.1.7",
|
"partial-json": "0.1.7",
|
||||||
"remeda": "catalog:",
|
"remeda": "catalog:",
|
||||||
@@ -400,14 +402,15 @@
|
|||||||
"@tsconfig/bun": "catalog:",
|
"@tsconfig/bun": "catalog:",
|
||||||
"@types/babel__core": "7.20.5",
|
"@types/babel__core": "7.20.5",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
|
"@types/cross-spawn": "6.0.6",
|
||||||
"@types/mime-types": "3.0.1",
|
"@types/mime-types": "3.0.1",
|
||||||
"@types/semver": "^7.5.8",
|
"@types/semver": "^7.5.8",
|
||||||
"@types/turndown": "5.0.5",
|
"@types/turndown": "5.0.5",
|
||||||
"@types/which": "3.0.4",
|
"@types/which": "3.0.4",
|
||||||
"@types/yargs": "17.0.33",
|
"@types/yargs": "17.0.33",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
"drizzle-kit": "1.0.0-beta.16-ea816b6",
|
"drizzle-kit": "catalog:",
|
||||||
"drizzle-orm": "1.0.0-beta.16-ea816b6",
|
"drizzle-orm": "catalog:",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"vscode-languageserver-types": "3.17.5",
|
"vscode-languageserver-types": "3.17.5",
|
||||||
"why-is-node-running": "3.2.2",
|
"why-is-node-running": "3.2.2",
|
||||||
@@ -416,7 +419,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.2.25",
|
"version": "1.2.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
@@ -440,7 +443,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.2.25",
|
"version": "1.2.27",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hey-api/openapi-ts": "0.90.10",
|
"@hey-api/openapi-ts": "0.90.10",
|
||||||
"@tsconfig/node22": "catalog:",
|
"@tsconfig/node22": "catalog:",
|
||||||
@@ -451,7 +454,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.2.25",
|
"version": "1.2.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -486,7 +489,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.2.25",
|
"version": "1.2.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -532,7 +535,7 @@
|
|||||||
},
|
},
|
||||||
"packages/util": {
|
"packages/util": {
|
||||||
"name": "@opencode-ai/util",
|
"name": "@opencode-ai/util",
|
||||||
"version": "1.2.25",
|
"version": "1.2.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -543,7 +546,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.2.25",
|
"version": "1.2.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
@@ -583,6 +586,8 @@
|
|||||||
],
|
],
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch",
|
"@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch",
|
||||||
|
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
||||||
|
"@ai-sdk/xai@2.0.51": "patches/@ai-sdk%2Fxai@2.0.51.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",
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
@@ -591,6 +596,7 @@
|
|||||||
},
|
},
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"@cloudflare/workers-types": "4.20251008.0",
|
"@cloudflare/workers-types": "4.20251008.0",
|
||||||
|
"@effect/platform-node": "4.0.0-beta.35",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
"@kobalte/core": "0.13.11",
|
"@kobalte/core": "0.13.11",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
@@ -612,9 +618,9 @@
|
|||||||
"ai": "5.0.124",
|
"ai": "5.0.124",
|
||||||
"diff": "8.0.2",
|
"diff": "8.0.2",
|
||||||
"dompurify": "3.3.1",
|
"dompurify": "3.3.1",
|
||||||
"drizzle-kit": "1.0.0-beta.16-ea816b6",
|
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
||||||
"drizzle-orm": "1.0.0-beta.16-ea816b6",
|
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
||||||
"effect": "4.0.0-beta.31",
|
"effect": "4.0.0-beta.35",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
"hono": "4.10.7",
|
"hono": "4.10.7",
|
||||||
"hono-openapi": "1.1.2",
|
"hono-openapi": "1.1.2",
|
||||||
@@ -972,6 +978,10 @@
|
|||||||
|
|
||||||
"@effect/language-service": ["@effect/language-service@0.79.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-DEmIOsg1GjjP6s9HXH1oJrW+gDmzkhVv9WOZl6to5eNyyCrjz1S2PDqQ7aYrW/HuifhfwI5Bik1pK4pj7Z+lrg=="],
|
"@effect/language-service": ["@effect/language-service@0.79.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-DEmIOsg1GjjP6s9HXH1oJrW+gDmzkhVv9WOZl6to5eNyyCrjz1S2PDqQ7aYrW/HuifhfwI5Bik1pK4pj7Z+lrg=="],
|
||||||
|
|
||||||
|
"@effect/platform-node": ["@effect/platform-node@4.0.0-beta.35", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.35", "mime": "^4.1.0", "undici": "^7.24.0" }, "peerDependencies": { "effect": "^4.0.0-beta.35", "ioredis": "^5.7.0" } }, "sha512-HPc2xZASl9F9y/xJ01bQgFD6Jf9XP4Fcv/BlVTvG0Yr/uN63lwKZYr/VXor5K5krHfBDeCBD8y7/SICPYZoq3A=="],
|
||||||
|
|
||||||
|
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.35", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.35" } }, "sha512-9bPqNV988itKJ7MQoJuzmR014DB9EZRDOnhJt/+iJlb8qLoR9HnCzNJb9gfBdYhFmVYc8DMsQxG81rdJzpv9tg=="],
|
||||||
|
|
||||||
"@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
|
"@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
|
||||||
|
|
||||||
"@electron/fuses": ["@electron/fuses@1.8.0", "", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="],
|
"@electron/fuses": ["@electron/fuses@1.8.0", "", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="],
|
||||||
@@ -1100,10 +1110,6 @@
|
|||||||
|
|
||||||
"@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
|
"@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
|
||||||
|
|
||||||
"@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.6.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-8LmcIQ86xkMtC7L4P1/QYVEC+yKMTRerfPeniaaQGalnzXKtX6iMHLjLPOL9Rxp55lOXi6ed0WrFuJzZx+fNRg=="],
|
|
||||||
|
|
||||||
"@gitlab/opencode-gitlab-auth": ["@gitlab/opencode-gitlab-auth@1.3.3", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-FT+KsCmAJjtqWr1YAq0MywGgL9kaLQ4apmsoowAXrPqHtoYf2i/nY10/A+L06kNj22EATeEDRpbB1NWXMto/SA=="],
|
|
||||||
|
|
||||||
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
|
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
|
||||||
|
|
||||||
"@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="],
|
"@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="],
|
||||||
@@ -1168,6 +1174,8 @@
|
|||||||
|
|
||||||
"@internationalized/number": ["@internationalized/number@3.6.5", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g=="],
|
"@internationalized/number": ["@internationalized/number@3.6.5", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g=="],
|
||||||
|
|
||||||
|
"@ioredis/commands": ["@ioredis/commands@1.5.1", "", {}, "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw=="],
|
||||||
|
|
||||||
"@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="],
|
"@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="],
|
||||||
|
|
||||||
"@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.1", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ=="],
|
"@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.1", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ=="],
|
||||||
@@ -2052,6 +2060,8 @@
|
|||||||
|
|
||||||
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
|
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
|
||||||
|
|
||||||
|
"@types/cross-spawn": ["@types/cross-spawn@6.0.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA=="],
|
||||||
|
|
||||||
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
||||||
|
|
||||||
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
||||||
@@ -2536,6 +2546,8 @@
|
|||||||
|
|
||||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
||||||
|
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
|
||||||
|
|
||||||
"collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="],
|
"collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="],
|
||||||
|
|
||||||
"color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
|
"color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
|
||||||
@@ -2722,9 +2734,9 @@
|
|||||||
|
|
||||||
"dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="],
|
"dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="],
|
||||||
|
|
||||||
"drizzle-kit": ["drizzle-kit@1.0.0-beta.16-ea816b6", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-GiJQqCNPZP8Kk+i7/sFa3rtXbq26tLDNi3LbMx9aoLuwF2ofk8CS7cySUGdI+r4J3q0a568quC8FZeaFTCw4IA=="],
|
"drizzle-kit": ["drizzle-kit@1.0.0-beta.19-d95b7a4", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "get-tsconfig": "^4.13.6", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-M0sqc+42TYBod6kEZ3AsW6+JWe3+76gR1aDFbHH5DmuLKEwewmbzlhBG6qnvV6YA1cIIbkuam3dC7r6PREOCXw=="],
|
||||||
|
|
||||||
"drizzle-orm": ["drizzle-orm@1.0.0-beta.16-ea816b6", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-k9gT4f0O9Qvah5YK/zL+FZonQ8TPyVxcG/ojN4dzO0fHP8hs8tBno8lqmJo53g0JLWv3Q2nsTUoyBRKM2TljFw=="],
|
"drizzle-orm": ["drizzle-orm@1.0.0-beta.19-d95b7a4", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-bZZKKeoRKrMVU6zKTscjrSH0+WNb1WEi3N0Jl4wEyQ7aQpTgHzdYY6IJQ1P0M74HuSJVeX4UpkFB/S6dtqLEJg=="],
|
||||||
|
|
||||||
"dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="],
|
"dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="],
|
||||||
|
|
||||||
@@ -2738,7 +2750,7 @@
|
|||||||
|
|
||||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||||
|
|
||||||
"effect": ["effect@4.0.0-beta.31", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-w3QwJnlaLtWWiUSzhCXUTIisnULPsxLzpO6uqaBFjXybKx6FvCqsLJT6v4dV7G9eA9jeTtG6Gv7kF+jGe3HxzA=="],
|
"effect": ["effect@4.0.0-beta.35", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-64j8dgJmoEMeq6Y3WLYcZIRqPZ5E/lqnULCf6QW5te3hQ/sa13UodWLGwBEviEqBoq72U8lArhVX+T7ntzhJGQ=="],
|
||||||
|
|
||||||
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
||||||
|
|
||||||
@@ -3006,6 +3018,8 @@
|
|||||||
|
|
||||||
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
|
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
|
||||||
|
|
||||||
|
"get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="],
|
||||||
|
|
||||||
"ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#4af877d", {}, "anomalyco-ghostty-web-4af877d", "sha512-fbEK8mtr7ar4ySsF+JUGjhaZrane7dKphanN+SxHt5XXI6yLMAh/Hpf6sNCOyyVa2UlGCd7YpXG/T2v2RUAX+A=="],
|
"ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#4af877d", {}, "anomalyco-ghostty-web-4af877d", "sha512-fbEK8mtr7ar4ySsF+JUGjhaZrane7dKphanN+SxHt5XXI6yLMAh/Hpf6sNCOyyVa2UlGCd7YpXG/T2v2RUAX+A=="],
|
||||||
|
|
||||||
"gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="],
|
"gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="],
|
||||||
@@ -3014,6 +3028,8 @@
|
|||||||
|
|
||||||
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
||||||
|
|
||||||
|
"gitlab-ai-provider": ["gitlab-ai-provider@5.2.2", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-ejwnie62rimfVHbjYZ2tsnqwLjF9YLgXD3OQA458gHz8hUvw7vEnhuyuMv5PmWQtyS3ISAghiX7r5SBhUWeCTA=="],
|
||||||
|
|
||||||
"glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
|
"glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
|
||||||
|
|
||||||
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
@@ -3176,6 +3192,8 @@
|
|||||||
|
|
||||||
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
||||||
|
|
||||||
|
"ioredis": ["ioredis@5.10.0", "", { "dependencies": { "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-HVBe9OFuqs+Z6n64q09PQvP1/R4Bm+30PAyyD4wIEqssh3v9L21QjCVk4kRLucMBcDokJTcLjsGeVRlq/nH6DA=="],
|
||||||
|
|
||||||
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
|
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
|
||||||
|
|
||||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||||
@@ -3408,10 +3426,14 @@
|
|||||||
|
|
||||||
"lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="],
|
"lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="],
|
||||||
|
|
||||||
|
"lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
|
||||||
|
|
||||||
"lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="],
|
"lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="],
|
||||||
|
|
||||||
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
|
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
|
||||||
|
|
||||||
|
"lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="],
|
||||||
|
|
||||||
"lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
|
"lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
|
||||||
|
|
||||||
"lodash.isequal": ["lodash.isequal@4.5.0", "", {}, "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="],
|
"lodash.isequal": ["lodash.isequal@4.5.0", "", {}, "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="],
|
||||||
@@ -3598,7 +3620,7 @@
|
|||||||
|
|
||||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||||
|
|
||||||
"mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
|
"mime": ["mime@4.1.0", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw=="],
|
||||||
|
|
||||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||||
|
|
||||||
@@ -3760,6 +3782,8 @@
|
|||||||
|
|
||||||
"opencode": ["opencode@workspace:packages/opencode"],
|
"opencode": ["opencode@workspace:packages/opencode"],
|
||||||
|
|
||||||
|
"opencode-gitlab-auth": ["opencode-gitlab-auth@2.0.0", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-jmZOOvYIurRScQCtdBqIW5HbP1JbmIiq7UtI7NGgn2vjke46g9d4NVPBg5/ZmFFVIBwZcgyFgJ7b8kGEOR9ujA=="],
|
||||||
|
|
||||||
"opencontrol": ["opencontrol@0.0.6", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.6.1", "@tsconfig/bun": "1.0.7", "hono": "4.7.4", "zod": "3.24.2", "zod-to-json-schema": "3.24.3" }, "bin": { "opencontrol": "bin/index.mjs" } }, "sha512-QeCrpOK5D15QV8kjnGVeD/BHFLwcVr+sn4T6KKmP0WAMs2pww56e4h+eOGHb5iPOufUQXbdbBKi6WV2kk7tefQ=="],
|
"opencontrol": ["opencontrol@0.0.6", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.6.1", "@tsconfig/bun": "1.0.7", "hono": "4.7.4", "zod": "3.24.2", "zod-to-json-schema": "3.24.3" }, "bin": { "opencontrol": "bin/index.mjs" } }, "sha512-QeCrpOK5D15QV8kjnGVeD/BHFLwcVr+sn4T6KKmP0WAMs2pww56e4h+eOGHb5iPOufUQXbdbBKi6WV2kk7tefQ=="],
|
||||||
|
|
||||||
"openid-client": ["openid-client@5.6.4", "", { "dependencies": { "jose": "^4.15.4", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" } }, "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA=="],
|
"openid-client": ["openid-client@5.6.4", "", { "dependencies": { "jose": "^4.15.4", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" } }, "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA=="],
|
||||||
@@ -4022,6 +4046,10 @@
|
|||||||
|
|
||||||
"redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="],
|
"redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="],
|
||||||
|
|
||||||
|
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
|
||||||
|
|
||||||
|
"redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="],
|
||||||
|
|
||||||
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
|
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
|
||||||
|
|
||||||
"regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
|
"regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
|
||||||
@@ -4084,6 +4112,8 @@
|
|||||||
|
|
||||||
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
|
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
|
||||||
|
|
||||||
|
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||||
|
|
||||||
"responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="],
|
"responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="],
|
||||||
|
|
||||||
"restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="],
|
"restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="],
|
||||||
@@ -4216,7 +4246,7 @@
|
|||||||
|
|
||||||
"socket.io-client": ["socket.io-client@4.8.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g=="],
|
"socket.io-client": ["socket.io-client@4.8.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g=="],
|
||||||
|
|
||||||
"socket.io-parser": ["socket.io-parser@4.2.5", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ=="],
|
"socket.io-parser": ["socket.io-parser@4.2.6", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg=="],
|
||||||
|
|
||||||
"socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="],
|
"socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="],
|
||||||
|
|
||||||
@@ -4280,6 +4310,8 @@
|
|||||||
|
|
||||||
"stage-js": ["stage-js@1.0.1", "", {}, "sha512-cz14aPp/wY0s3bkb/B93BPP5ZAEhgBbRmAT3CCDqert8eCAqIpQ0RB2zpK8Ksxf+Pisl5oTzvPHtL4CVzzeHcw=="],
|
"stage-js": ["stage-js@1.0.1", "", {}, "sha512-cz14aPp/wY0s3bkb/B93BPP5ZAEhgBbRmAT3CCDqert8eCAqIpQ0RB2zpK8Ksxf+Pisl5oTzvPHtL4CVzzeHcw=="],
|
||||||
|
|
||||||
|
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
|
||||||
|
|
||||||
"stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="],
|
"stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="],
|
||||||
|
|
||||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||||
@@ -4986,12 +5018,18 @@
|
|||||||
|
|
||||||
"@bufbuild/protoplugin/typescript": ["typescript@5.4.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ=="],
|
"@bufbuild/protoplugin/typescript": ["typescript@5.4.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ=="],
|
||||||
|
|
||||||
|
"@cloudflare/kv-asset-handler/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
|
||||||
|
|
||||||
"@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
|
"@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
|
||||||
|
|
||||||
"@develar/schema-utils/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
"@develar/schema-utils/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
||||||
|
|
||||||
"@dot/log/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
"@dot/log/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||||
|
|
||||||
|
"@effect/platform-node/undici": ["undici@7.24.4", "", {}, "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w=="],
|
||||||
|
|
||||||
|
"@effect/platform-node-shared/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
|
||||||
|
|
||||||
"@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="],
|
"@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="],
|
||||||
|
|
||||||
"@electron/asar/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
"@electron/asar/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||||
@@ -5022,16 +5060,14 @@
|
|||||||
|
|
||||||
"@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
|
"@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
|
||||||
|
|
||||||
"@gitlab/gitlab-ai-provider/openai": ["openai@6.27.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-osTKySlrdYrLYTt0zjhY8yp0JUBmWDCN+Q+QxsV4xMQnnoVFpylgKGgxwN8sSdTNw0G4y+WUXs4eCMWpyDNWZQ=="],
|
|
||||||
|
|
||||||
"@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
|
||||||
|
|
||||||
"@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
|
"@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
|
||||||
|
|
||||||
"@hey-api/openapi-ts/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
"@hey-api/openapi-ts/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||||
|
|
||||||
"@hono/zod-validator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
"@hono/zod-validator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
|
"@jimp/core/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
|
||||||
|
|
||||||
"@jimp/plugin-blit/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
"@jimp/plugin-blit/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
"@jimp/plugin-circle/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
"@jimp/plugin-circle/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
@@ -5352,6 +5388,8 @@
|
|||||||
|
|
||||||
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
|
"db0/drizzle-orm": ["drizzle-orm@1.0.0-beta.16-ea816b6", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-k9gT4f0O9Qvah5YK/zL+FZonQ8TPyVxcG/ojN4dzO0fHP8hs8tBno8lqmJo53g0JLWv3Q2nsTUoyBRKM2TljFw=="],
|
||||||
|
|
||||||
"defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="],
|
"defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="],
|
||||||
|
|
||||||
"dir-compare/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
"dir-compare/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||||
@@ -5418,6 +5456,10 @@
|
|||||||
|
|
||||||
"gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
"gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
||||||
|
|
||||||
|
"gitlab-ai-provider/openai": ["openai@6.32.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-j3k+BjydAf8yQlcOI7WUQMQTbbF5GEIMAE2iZYCOzwwB3S2pCheaWYp+XZRNAch4jWVc52PMDGRRjutao3lLCg=="],
|
||||||
|
|
||||||
|
"gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
"glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
|
"glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
|
||||||
|
|
||||||
"globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
"globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||||
@@ -5494,6 +5536,8 @@
|
|||||||
|
|
||||||
"opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="],
|
"opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="],
|
||||||
|
|
||||||
|
"opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
|
||||||
|
|
||||||
"opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="],
|
"opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="],
|
||||||
|
|
||||||
"opencontrol/@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="],
|
"opencontrol/@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="],
|
||||||
@@ -6244,6 +6288,8 @@
|
|||||||
|
|
||||||
"node-gyp/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
|
"node-gyp/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
|
||||||
|
|
||||||
|
"opencode-gitlab-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
|
||||||
|
|
||||||
"opencode/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
|
"opencode/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
|
||||||
|
|
||||||
"opencode/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
|
"opencode/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ const ZEN_LITE_PRICE = new sst.Linkable("ZEN_LITE_PRICE", {
|
|||||||
properties: {
|
properties: {
|
||||||
product: zenLiteProduct.id,
|
product: zenLiteProduct.id,
|
||||||
price: zenLitePrice.id,
|
price: zenLitePrice.id,
|
||||||
|
priceInr: 92900,
|
||||||
firstMonth50Coupon: zenLiteCouponFirstMonth50.id,
|
firstMonth50Coupon: zenLiteCouponFirstMonth50.id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -201,6 +202,10 @@ const bucketNew = new sst.cloudflare.Bucket("ZenDataNew")
|
|||||||
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
|
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
|
||||||
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
|
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
|
||||||
|
|
||||||
|
const SALESFORCE_CLIENT_ID = new sst.Secret("SALESFORCE_CLIENT_ID")
|
||||||
|
const SALESFORCE_CLIENT_SECRET = new sst.Secret("SALESFORCE_CLIENT_SECRET")
|
||||||
|
const SALESFORCE_INSTANCE_URL = new sst.Secret("SALESFORCE_INSTANCE_URL")
|
||||||
|
|
||||||
const logProcessor = new sst.cloudflare.Worker("LogProcessor", {
|
const logProcessor = new sst.cloudflare.Worker("LogProcessor", {
|
||||||
handler: "packages/console/function/src/log-processor.ts",
|
handler: "packages/console/function/src/log-processor.ts",
|
||||||
link: [new sst.Secret("HONEYCOMB_API_KEY")],
|
link: [new sst.Secret("HONEYCOMB_API_KEY")],
|
||||||
@@ -219,6 +224,9 @@ new sst.cloudflare.x.SolidStart("Console", {
|
|||||||
EMAILOCTOPUS_API_KEY,
|
EMAILOCTOPUS_API_KEY,
|
||||||
AWS_SES_ACCESS_KEY_ID,
|
AWS_SES_ACCESS_KEY_ID,
|
||||||
AWS_SES_SECRET_ACCESS_KEY,
|
AWS_SES_SECRET_ACCESS_KEY,
|
||||||
|
SALESFORCE_CLIENT_ID,
|
||||||
|
SALESFORCE_CLIENT_SECRET,
|
||||||
|
SALESFORCE_INSTANCE_URL,
|
||||||
ZEN_BLACK_PRICE,
|
ZEN_BLACK_PRICE,
|
||||||
ZEN_LITE_PRICE,
|
ZEN_LITE_PRICE,
|
||||||
new sst.Secret("ZEN_LIMITS"),
|
new sst.Secret("ZEN_LIMITS"),
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-WJgo6UclmtQOEubnKMZybdIEhZ1uRTucF61yojjd+l0=",
|
"x86_64-linux": "sha256-P0RJfQF8APTYVGP6hLJRrOkRSl5nVDNxdcGcZECPPJE=",
|
||||||
"aarch64-linux": "sha256-QfZ/g7EZFpe6ndR3dG8WvVfMj5Kyd/R/4kkTJfGJxL4=",
|
"aarch64-linux": "sha256-ZtMjTcd35X3JhJIdn3DilFsp7i/IZIcNaKZFnSzW/nk=",
|
||||||
"aarch64-darwin": "sha256-ezr/R70XJr9eN5l3mgb7HzLF6QsofNEKUOtuxbfli80=",
|
"aarch64-darwin": "sha256-Uw/okFDRxxKQMfEsj8MXuHyhpugxZGgIKtu89Getlz8=",
|
||||||
"x86_64-darwin": "sha256-MbsBGS415uEU/n1RQ/5H5pqh+udLY3+oimJ+eS5uJVI="
|
"x86_64-darwin": "sha256-ZySIgT1HbWZWnaQ0W0eURKC43BTupRmmply92JDFPWA="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-4
@@ -9,6 +9,7 @@
|
|||||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||||
"dev:desktop": "bun --cwd packages/desktop tauri dev",
|
"dev:desktop": "bun --cwd packages/desktop tauri dev",
|
||||||
"dev:web": "bun --cwd packages/app dev",
|
"dev:web": "bun --cwd packages/app dev",
|
||||||
|
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
|
||||||
"dev:storybook": "bun --cwd packages/storybook storybook",
|
"dev:storybook": "bun --cwd packages/storybook storybook",
|
||||||
"typecheck": "bun turbo typecheck",
|
"typecheck": "bun turbo typecheck",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
@@ -24,6 +25,7 @@
|
|||||||
"packages/slack"
|
"packages/slack"
|
||||||
],
|
],
|
||||||
"catalog": {
|
"catalog": {
|
||||||
|
"@effect/platform-node": "4.0.0-beta.35",
|
||||||
"@types/bun": "1.3.9",
|
"@types/bun": "1.3.9",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
@@ -41,9 +43,9 @@
|
|||||||
"@tailwindcss/vite": "4.1.11",
|
"@tailwindcss/vite": "4.1.11",
|
||||||
"diff": "8.0.2",
|
"diff": "8.0.2",
|
||||||
"dompurify": "3.3.1",
|
"dompurify": "3.3.1",
|
||||||
"drizzle-kit": "1.0.0-beta.16-ea816b6",
|
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
||||||
"drizzle-orm": "1.0.0-beta.16-ea816b6",
|
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
||||||
"effect": "4.0.0-beta.31",
|
"effect": "4.0.0-beta.35",
|
||||||
"ai": "5.0.124",
|
"ai": "5.0.124",
|
||||||
"hono": "4.10.7",
|
"hono": "4.10.7",
|
||||||
"hono-openapi": "1.1.2",
|
"hono-openapi": "1.1.2",
|
||||||
@@ -110,6 +112,8 @@
|
|||||||
},
|
},
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"@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",
|
||||||
"@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch"
|
"@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch",
|
||||||
|
"@ai-sdk/xai@2.0.51": "patches/@ai-sdk%2Fxai@2.0.51.patch",
|
||||||
|
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -174,6 +174,8 @@ await page.keyboard.press(`${modKey}+Comma`) // Open settings
|
|||||||
- In terminal tests, type through the browser. Do not write to the PTY through the SDK.
|
- In terminal tests, type through the browser. Do not write to the PTY through the SDK.
|
||||||
- Use `waitTerminalReady(page, { term? })` and `runTerminal(page, { cmd, token, term?, timeout? })` from `actions.ts`.
|
- Use `waitTerminalReady(page, { term? })` and `runTerminal(page, { cmd, token, term?, timeout? })` from `actions.ts`.
|
||||||
- These helpers use the fixture-enabled test-only terminal driver and wait for output after the terminal writer settles.
|
- These helpers use the fixture-enabled test-only terminal driver and wait for output after the terminal writer settles.
|
||||||
|
- After opening the terminal, use `waitTerminalFocusIdle(...)` before the next keyboard action when prompt focus or keyboard routing matters.
|
||||||
|
- This avoids racing terminal mount, focus handoff, and prompt readiness when the next step types or sends shortcuts.
|
||||||
- Avoid `waitForTimeout` and custom DOM or `data-*` readiness checks.
|
- Avoid `waitForTimeout` and custom DOM or `data-*` readiness checks.
|
||||||
|
|
||||||
### Wait on state
|
### Wait on state
|
||||||
@@ -182,6 +184,9 @@ await page.keyboard.press(`${modKey}+Comma`) // Open settings
|
|||||||
- Avoid race-prone flows that assume work is finished after an action
|
- Avoid race-prone flows that assume work is finished after an action
|
||||||
- Wait or poll on observable state with `expect(...)`, `expect.poll(...)`, or existing helpers
|
- Wait or poll on observable state with `expect(...)`, `expect.poll(...)`, or existing helpers
|
||||||
- Prefer locator assertions like `toBeVisible()`, `toHaveCount(0)`, and `toHaveAttribute(...)` for normal UI state, and reserve `expect.poll(...)` for probe, mock, or backend state
|
- Prefer locator assertions like `toBeVisible()`, `toHaveCount(0)`, and `toHaveAttribute(...)` for normal UI state, and reserve `expect.poll(...)` for probe, mock, or backend state
|
||||||
|
- Prefer semantic app state over transient DOM visibility when behavior depends on active selection, focus ownership, or async retry loops
|
||||||
|
- Do not treat a visible element as proof that the app will route the next action to it
|
||||||
|
- When fixing a flake, validate with `--repeat-each` and multiple workers when practical
|
||||||
|
|
||||||
### Add hooks
|
### Add hooks
|
||||||
|
|
||||||
@@ -189,11 +194,16 @@ await page.keyboard.press(`${modKey}+Comma`) // Open settings
|
|||||||
- Keep these hooks minimal and purpose-built, following the style of `packages/app/src/testing/terminal.ts`
|
- Keep these hooks minimal and purpose-built, following the style of `packages/app/src/testing/terminal.ts`
|
||||||
- Test-only hooks must be inert unless explicitly enabled; do not add normal-runtime listeners, reactive subscriptions, or per-update allocations for e2e ceremony
|
- Test-only hooks must be inert unless explicitly enabled; do not add normal-runtime listeners, reactive subscriptions, or per-update allocations for e2e ceremony
|
||||||
- When mocking routes or APIs, expose explicit mock state and wait on that before asserting post-action UI
|
- When mocking routes or APIs, expose explicit mock state and wait on that before asserting post-action UI
|
||||||
|
- Add minimal test-only probes for semantic state like the active list item or selected command when DOM intermediates are unstable
|
||||||
|
- Prefer probing committed app state over asserting on transient highlight, visibility, or animation states
|
||||||
|
|
||||||
### Prefer helpers
|
### Prefer helpers
|
||||||
|
|
||||||
- Prefer fluent helpers and drivers when they make intent obvious and reduce locator-heavy noise
|
- Prefer fluent helpers and drivers when they make intent obvious and reduce locator-heavy noise
|
||||||
- Use direct locators when the interaction is simple and a helper would not add clarity
|
- Use direct locators when the interaction is simple and a helper would not add clarity
|
||||||
|
- Prefer helpers that both perform an action and verify the app consumed it
|
||||||
|
- Avoid composing helpers redundantly when one already includes the other or already waits for the resulting state
|
||||||
|
- If a helper already covers the required wait or verification, use it directly instead of layering extra clicks, keypresses, or assertions
|
||||||
|
|
||||||
## Writing New Tests
|
## Writing New Tests
|
||||||
|
|
||||||
|
|||||||
+240
-7
@@ -1,3 +1,4 @@
|
|||||||
|
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
|
||||||
import { expect, type Locator, type Page } from "@playwright/test"
|
import { expect, type Locator, type Page } from "@playwright/test"
|
||||||
import fs from "node:fs/promises"
|
import fs from "node:fs/promises"
|
||||||
import os from "node:os"
|
import os from "node:os"
|
||||||
@@ -8,6 +9,7 @@ import { createSdk, modKey, resolveDirectory, serverUrl } from "./utils"
|
|||||||
import {
|
import {
|
||||||
dropdownMenuTriggerSelector,
|
dropdownMenuTriggerSelector,
|
||||||
dropdownMenuContentSelector,
|
dropdownMenuContentSelector,
|
||||||
|
projectSwitchSelector,
|
||||||
projectMenuTriggerSelector,
|
projectMenuTriggerSelector,
|
||||||
projectCloseMenuSelector,
|
projectCloseMenuSelector,
|
||||||
projectWorkspacesToggleSelector,
|
projectWorkspacesToggleSelector,
|
||||||
@@ -16,11 +18,22 @@ import {
|
|||||||
listItemSelector,
|
listItemSelector,
|
||||||
listItemKeySelector,
|
listItemKeySelector,
|
||||||
listItemKeyStartsWithSelector,
|
listItemKeyStartsWithSelector,
|
||||||
|
promptSelector,
|
||||||
terminalSelector,
|
terminalSelector,
|
||||||
workspaceItemSelector,
|
workspaceItemSelector,
|
||||||
workspaceMenuTriggerSelector,
|
workspaceMenuTriggerSelector,
|
||||||
} from "./selectors"
|
} from "./selectors"
|
||||||
|
|
||||||
|
const phase = new WeakMap<Page, "test" | "cleanup">()
|
||||||
|
|
||||||
|
export function setHealthPhase(page: Page, value: "test" | "cleanup") {
|
||||||
|
phase.set(page, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function healthPhase(page: Page) {
|
||||||
|
return phase.get(page) ?? "test"
|
||||||
|
}
|
||||||
|
|
||||||
export async function defocus(page: Page) {
|
export async function defocus(page: Page) {
|
||||||
await page
|
await page
|
||||||
.evaluate(() => {
|
.evaluate(() => {
|
||||||
@@ -61,6 +74,15 @@ async function terminalReady(page: Page, term?: Locator) {
|
|||||||
}, id)
|
}, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function terminalFocusIdle(page: Page, term?: Locator) {
|
||||||
|
const next = term ?? page.locator(terminalSelector).first()
|
||||||
|
const id = await terminalID(next)
|
||||||
|
return page.evaluate((id) => {
|
||||||
|
const state = (window as E2EWindow).__opencode_e2e?.terminal?.terminals?.[id]
|
||||||
|
return (state?.focusing ?? 0) === 0
|
||||||
|
}, id)
|
||||||
|
}
|
||||||
|
|
||||||
async function terminalHas(page: Page, input: { term?: Locator; token: string }) {
|
async function terminalHas(page: Page, input: { term?: Locator; token: string }) {
|
||||||
const next = input.term ?? page.locator(terminalSelector).first()
|
const next = input.term ?? page.locator(terminalSelector).first()
|
||||||
const id = await terminalID(next)
|
const id = await terminalID(next)
|
||||||
@@ -73,6 +95,29 @@ async function terminalHas(page: Page, input: { term?: Locator; token: string })
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function promptSlashActive(page: Page, id: string) {
|
||||||
|
return page.evaluate((id) => {
|
||||||
|
const state = (window as E2EWindow).__opencode_e2e?.prompt?.current
|
||||||
|
if (state?.popover !== "slash") return false
|
||||||
|
if (!state.slash.ids.includes(id)) return false
|
||||||
|
return state.slash.active === id
|
||||||
|
}, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptSlashSelects(page: Page) {
|
||||||
|
return page.evaluate(() => {
|
||||||
|
return (window as E2EWindow).__opencode_e2e?.prompt?.current?.selects ?? 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptSlashSelected(page: Page, input: { id: string; count: number }) {
|
||||||
|
return page.evaluate((input) => {
|
||||||
|
const state = (window as E2EWindow).__opencode_e2e?.prompt?.current
|
||||||
|
if (!state) return false
|
||||||
|
return state.selected === input.id && state.selects >= input.count
|
||||||
|
}, input)
|
||||||
|
}
|
||||||
|
|
||||||
export async function waitTerminalReady(page: Page, input?: { term?: Locator; timeout?: number }) {
|
export async function waitTerminalReady(page: Page, input?: { term?: Locator; timeout?: number }) {
|
||||||
const term = input?.term ?? page.locator(terminalSelector).first()
|
const term = input?.term ?? page.locator(terminalSelector).first()
|
||||||
const timeout = input?.timeout ?? 10_000
|
const timeout = input?.timeout ?? 10_000
|
||||||
@@ -81,6 +126,43 @@ export async function waitTerminalReady(page: Page, input?: { term?: Locator; ti
|
|||||||
await expect.poll(() => terminalReady(page, term), { timeout }).toBe(true)
|
await expect.poll(() => terminalReady(page, term), { timeout }).toBe(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function waitTerminalFocusIdle(page: Page, input?: { term?: Locator; timeout?: number }) {
|
||||||
|
const term = input?.term ?? page.locator(terminalSelector).first()
|
||||||
|
const timeout = input?.timeout ?? 10_000
|
||||||
|
await waitTerminalReady(page, { term, timeout })
|
||||||
|
await expect.poll(() => terminalFocusIdle(page, term), { timeout }).toBe(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function showPromptSlash(
|
||||||
|
page: Page,
|
||||||
|
input: { id: string; text: string; prompt?: Locator; timeout?: number },
|
||||||
|
) {
|
||||||
|
const prompt = input.prompt ?? page.locator(promptSelector)
|
||||||
|
const timeout = input.timeout ?? 10_000
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
await prompt.click().catch(() => false)
|
||||||
|
await prompt.fill(input.text).catch(() => false)
|
||||||
|
return promptSlashActive(page, input.id).catch(() => false)
|
||||||
|
},
|
||||||
|
{ timeout },
|
||||||
|
)
|
||||||
|
.toBe(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runPromptSlash(
|
||||||
|
page: Page,
|
||||||
|
input: { id: string; text: string; prompt?: Locator; timeout?: number },
|
||||||
|
) {
|
||||||
|
const prompt = input.prompt ?? page.locator(promptSelector)
|
||||||
|
const timeout = input.timeout ?? 10_000
|
||||||
|
const count = await promptSlashSelects(page)
|
||||||
|
await showPromptSlash(page, input)
|
||||||
|
await prompt.press("Enter")
|
||||||
|
await expect.poll(() => promptSlashSelected(page, { id: input.id, count: count + 1 }), { timeout }).toBe(true)
|
||||||
|
}
|
||||||
|
|
||||||
export async function runTerminal(page: Page, input: { cmd: string; token: string; term?: Locator; timeout?: number }) {
|
export async function runTerminal(page: Page, input: { cmd: string; token: string; term?: Locator; timeout?: number }) {
|
||||||
const term = input.term ?? page.locator(terminalSelector).first()
|
const term = input.term ?? page.locator(terminalSelector).first()
|
||||||
const timeout = input.timeout ?? 10_000
|
const timeout = input.timeout ?? 10_000
|
||||||
@@ -125,11 +207,51 @@ export async function closeDialog(page: Page, dialog: Locator) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function isSidebarClosed(page: Page) {
|
export async function isSidebarClosed(page: Page) {
|
||||||
const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
|
const button = await waitSidebarButton(page, "isSidebarClosed")
|
||||||
await expect(button).toBeVisible()
|
|
||||||
return (await button.getAttribute("aria-expanded")) !== "true"
|
return (await button.getAttribute("aria-expanded")) !== "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function errorBoundaryText(page: Page) {
|
||||||
|
const title = page.getByRole("heading", { name: /something went wrong/i }).first()
|
||||||
|
if (!(await title.isVisible().catch(() => false))) return
|
||||||
|
|
||||||
|
const description = await page
|
||||||
|
.getByText(/an error occurred while loading the application\./i)
|
||||||
|
.first()
|
||||||
|
.textContent()
|
||||||
|
.catch(() => "")
|
||||||
|
const detail = await page
|
||||||
|
.getByRole("textbox", { name: /error details/i })
|
||||||
|
.first()
|
||||||
|
.inputValue()
|
||||||
|
.catch(async () =>
|
||||||
|
(
|
||||||
|
(await page
|
||||||
|
.getByRole("textbox", { name: /error details/i })
|
||||||
|
.first()
|
||||||
|
.textContent()
|
||||||
|
.catch(() => "")) ?? ""
|
||||||
|
).trim(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return [title ? "Error boundary" : "", description ?? "", detail ?? ""].filter(Boolean).join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function assertHealthy(page: Page, context: string) {
|
||||||
|
const text = await errorBoundaryText(page)
|
||||||
|
if (!text) return
|
||||||
|
console.log(`[e2e:error-boundary][${context}]\n${text}`)
|
||||||
|
throw new Error(`Error boundary during ${context}\n${text}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitSidebarButton(page: Page, context: string) {
|
||||||
|
const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
|
||||||
|
const boundary = page.getByRole("heading", { name: /something went wrong/i }).first()
|
||||||
|
await button.or(boundary).first().waitFor({ state: "visible", timeout: 10_000 })
|
||||||
|
await assertHealthy(page, context)
|
||||||
|
return button
|
||||||
|
}
|
||||||
|
|
||||||
export async function toggleSidebar(page: Page) {
|
export async function toggleSidebar(page: Page) {
|
||||||
await defocus(page)
|
await defocus(page)
|
||||||
await page.keyboard.press(`${modKey}+B`)
|
await page.keyboard.press(`${modKey}+B`)
|
||||||
@@ -138,7 +260,7 @@ export async function toggleSidebar(page: Page) {
|
|||||||
export async function openSidebar(page: Page) {
|
export async function openSidebar(page: Page) {
|
||||||
if (!(await isSidebarClosed(page))) return
|
if (!(await isSidebarClosed(page))) return
|
||||||
|
|
||||||
const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
|
const button = await waitSidebarButton(page, "openSidebar")
|
||||||
await button.click()
|
await button.click()
|
||||||
|
|
||||||
const opened = await expect(button)
|
const opened = await expect(button)
|
||||||
@@ -155,7 +277,7 @@ export async function openSidebar(page: Page) {
|
|||||||
export async function closeSidebar(page: Page) {
|
export async function closeSidebar(page: Page) {
|
||||||
if (await isSidebarClosed(page)) return
|
if (await isSidebarClosed(page)) return
|
||||||
|
|
||||||
const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
|
const button = await waitSidebarButton(page, "closeSidebar")
|
||||||
await button.click()
|
await button.click()
|
||||||
|
|
||||||
const closed = await expect(button)
|
const closed = await expect(button)
|
||||||
@@ -170,6 +292,7 @@ export async function closeSidebar(page: Page) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function openSettings(page: Page) {
|
export async function openSettings(page: Page) {
|
||||||
|
await assertHealthy(page, "openSettings")
|
||||||
await defocus(page)
|
await defocus(page)
|
||||||
|
|
||||||
const dialog = page.getByRole("dialog")
|
const dialog = page.getByRole("dialog")
|
||||||
@@ -182,6 +305,8 @@ export async function openSettings(page: Page) {
|
|||||||
|
|
||||||
if (opened) return dialog
|
if (opened) return dialog
|
||||||
|
|
||||||
|
await assertHealthy(page, "openSettings")
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Settings" }).first().click()
|
await page.getByRole("button", { name: "Settings" }).first().click()
|
||||||
await expect(dialog).toBeVisible()
|
await expect(dialog).toBeVisible()
|
||||||
return dialog
|
return dialog
|
||||||
@@ -243,10 +368,12 @@ export async function seedProjects(page: Page, input: { directory: string; extra
|
|||||||
|
|
||||||
export async function createTestProject() {
|
export async function createTestProject() {
|
||||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-e2e-project-"))
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-e2e-project-"))
|
||||||
|
const id = `e2e-${path.basename(root)}`
|
||||||
|
|
||||||
await fs.writeFile(path.join(root, "README.md"), "# e2e\n")
|
await fs.writeFile(path.join(root, "README.md"), `# e2e\n\n${id}\n`)
|
||||||
|
|
||||||
execSync("git init", { cwd: root, stdio: "ignore" })
|
execSync("git init", { cwd: root, stdio: "ignore" })
|
||||||
|
await fs.writeFile(path.join(root, ".git", "opencode"), id)
|
||||||
execSync("git config core.fsmonitor false", { cwd: root, stdio: "ignore" })
|
execSync("git config core.fsmonitor false", { cwd: root, stdio: "ignore" })
|
||||||
execSync("git add -A", { cwd: root, stdio: "ignore" })
|
execSync("git add -A", { cwd: root, stdio: "ignore" })
|
||||||
execSync('git -c user.name="e2e" -c user.email="e2e@example.com" commit -m "init" --allow-empty', {
|
execSync('git -c user.name="e2e" -c user.email="e2e@example.com" commit -m "init" --allow-empty', {
|
||||||
@@ -268,12 +395,24 @@ export function slugFromUrl(url: string) {
|
|||||||
return /\/([^/]+)\/session(?:[/?#]|$)/.exec(url)?.[1] ?? ""
|
return /\/([^/]+)\/session(?:[/?#]|$)/.exec(url)?.[1] ?? ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function probeSession(page: Page) {
|
||||||
|
return page
|
||||||
|
.evaluate(() => {
|
||||||
|
const win = window as E2EWindow
|
||||||
|
const current = win.__opencode_e2e?.model?.current
|
||||||
|
if (!current) return null
|
||||||
|
return { dir: current.dir, sessionID: current.sessionID }
|
||||||
|
})
|
||||||
|
.catch(() => null as { dir?: string; sessionID?: string } | null)
|
||||||
|
}
|
||||||
|
|
||||||
export async function waitSlug(page: Page, skip: string[] = []) {
|
export async function waitSlug(page: Page, skip: string[] = []) {
|
||||||
let prev = ""
|
let prev = ""
|
||||||
let next = ""
|
let next = ""
|
||||||
await expect
|
await expect
|
||||||
.poll(
|
.poll(
|
||||||
() => {
|
async () => {
|
||||||
|
await assertHealthy(page, "waitSlug")
|
||||||
const slug = slugFromUrl(page.url())
|
const slug = slugFromUrl(page.url())
|
||||||
if (!slug) return ""
|
if (!slug) return ""
|
||||||
if (skip.includes(slug)) return ""
|
if (skip.includes(slug)) return ""
|
||||||
@@ -291,6 +430,94 @@ export async function waitSlug(page: Page, skip: string[] = []) {
|
|||||||
return next
|
return next
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function resolveSlug(slug: string) {
|
||||||
|
const directory = base64Decode(slug)
|
||||||
|
if (!directory) throw new Error(`Failed to decode workspace slug: ${slug}`)
|
||||||
|
const resolved = await resolveDirectory(directory)
|
||||||
|
return { directory: resolved, slug: base64Encode(resolved), raw: slug }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function waitDir(page: Page, directory: string) {
|
||||||
|
const target = await resolveDirectory(directory)
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
await assertHealthy(page, "waitDir")
|
||||||
|
const slug = slugFromUrl(page.url())
|
||||||
|
if (!slug) return ""
|
||||||
|
return resolveSlug(slug)
|
||||||
|
.then((item) => item.directory)
|
||||||
|
.catch(() => "")
|
||||||
|
},
|
||||||
|
{ timeout: 45_000 },
|
||||||
|
)
|
||||||
|
.toBe(target)
|
||||||
|
return { directory: target, slug: base64Encode(target) }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function waitSession(page: Page, input: { directory: string; sessionID?: string }) {
|
||||||
|
const target = await resolveDirectory(input.directory)
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
await assertHealthy(page, "waitSession")
|
||||||
|
const slug = slugFromUrl(page.url())
|
||||||
|
if (!slug) return false
|
||||||
|
const resolved = await resolveSlug(slug).catch(() => undefined)
|
||||||
|
if (!resolved || resolved.directory !== target) return false
|
||||||
|
if (input.sessionID && sessionIDFromUrl(page.url()) !== input.sessionID) return false
|
||||||
|
|
||||||
|
const state = await probeSession(page)
|
||||||
|
if (input.sessionID && (!state || state.sessionID !== input.sessionID)) return false
|
||||||
|
if (state?.dir) {
|
||||||
|
const dir = await resolveDirectory(state.dir).catch(() => state.dir ?? "")
|
||||||
|
if (dir !== target) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return page
|
||||||
|
.locator(promptSelector)
|
||||||
|
.first()
|
||||||
|
.isVisible()
|
||||||
|
.catch(() => false)
|
||||||
|
},
|
||||||
|
{ timeout: 45_000 },
|
||||||
|
)
|
||||||
|
.toBe(true)
|
||||||
|
return { directory: target, slug: base64Encode(target) }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function waitSessionSaved(directory: string, sessionID: string, timeout = 30_000) {
|
||||||
|
const sdk = createSdk(directory)
|
||||||
|
const target = await resolveDirectory(directory)
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const data = await sdk.session
|
||||||
|
.get({ sessionID })
|
||||||
|
.then((x) => x.data)
|
||||||
|
.catch(() => undefined)
|
||||||
|
if (!data?.directory) return ""
|
||||||
|
return resolveDirectory(data.directory).catch(() => data.directory)
|
||||||
|
},
|
||||||
|
{ timeout },
|
||||||
|
)
|
||||||
|
.toBe(target)
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const items = await sdk.session
|
||||||
|
.messages({ sessionID, limit: 20 })
|
||||||
|
.then((x) => x.data ?? [])
|
||||||
|
.catch(() => [])
|
||||||
|
return items.some((item) => item.info.role === "user")
|
||||||
|
},
|
||||||
|
{ timeout },
|
||||||
|
)
|
||||||
|
.toBe(true)
|
||||||
|
}
|
||||||
|
|
||||||
export function sessionIDFromUrl(url: string) {
|
export function sessionIDFromUrl(url: string) {
|
||||||
const match = /\/session\/([^/?#]+)/.exec(url)
|
const match = /\/session\/([^/?#]+)/.exec(url)
|
||||||
return match?.[1]
|
return match?.[1]
|
||||||
@@ -702,8 +929,14 @@ export async function openStatusPopover(page: Page) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function openProjectMenu(page: Page, projectSlug: string) {
|
export async function openProjectMenu(page: Page, projectSlug: string) {
|
||||||
|
await openSidebar(page)
|
||||||
|
const item = page.locator(projectSwitchSelector(projectSlug)).first()
|
||||||
|
await expect(item).toBeVisible()
|
||||||
|
await item.hover()
|
||||||
|
|
||||||
const trigger = page.locator(projectMenuTriggerSelector(projectSlug)).first()
|
const trigger = page.locator(projectMenuTriggerSelector(projectSlug)).first()
|
||||||
await expect(trigger).toHaveCount(1)
|
await expect(trigger).toHaveCount(1)
|
||||||
|
await expect(trigger).toBeVisible()
|
||||||
|
|
||||||
const menu = page
|
const menu = page
|
||||||
.locator(dropdownMenuContentSelector)
|
.locator(dropdownMenuContentSelector)
|
||||||
@@ -712,7 +945,7 @@ export async function openProjectMenu(page: Page, projectSlug: string) {
|
|||||||
const close = menu.locator(projectCloseMenuSelector(projectSlug)).first()
|
const close = menu.locator(projectCloseMenuSelector(projectSlug)).first()
|
||||||
|
|
||||||
const clicked = await trigger
|
const clicked = await trigger
|
||||||
.click({ timeout: 1500 })
|
.click({ force: true, timeout: 1500 })
|
||||||
.then(() => true)
|
.then(() => true)
|
||||||
.catch(() => false)
|
.catch(() => false)
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ import { serverNamePattern } from "../utils"
|
|||||||
|
|
||||||
test("home renders and shows core entrypoints", async ({ page }) => {
|
test("home renders and shows core entrypoints", async ({ page }) => {
|
||||||
await page.goto("/")
|
await page.goto("/")
|
||||||
|
const nav = page.locator('[data-component="sidebar-nav-desktop"]')
|
||||||
|
|
||||||
await expect(page.getByRole("button", { name: "Open project" }).first()).toBeVisible()
|
await expect(page.getByRole("button", { name: "Open project" }).first()).toBeVisible()
|
||||||
|
await expect(nav.getByText("No projects open")).toBeVisible()
|
||||||
|
await expect(nav.getByText("Open a project to get started")).toBeVisible()
|
||||||
await expect(page.getByRole("button", { name: serverNamePattern })).toBeVisible()
|
await expect(page.getByRole("button", { name: serverNamePattern })).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
import { test as base, expect, type Page } from "@playwright/test"
|
import { test as base, expect, type Page } from "@playwright/test"
|
||||||
import type { E2EWindow } from "../src/testing/terminal"
|
import type { E2EWindow } from "../src/testing/terminal"
|
||||||
import { cleanupSession, cleanupTestProject, createTestProject, seedProjects, sessionIDFromUrl } from "./actions"
|
import {
|
||||||
import { promptSelector } from "./selectors"
|
healthPhase,
|
||||||
|
cleanupSession,
|
||||||
|
cleanupTestProject,
|
||||||
|
createTestProject,
|
||||||
|
setHealthPhase,
|
||||||
|
seedProjects,
|
||||||
|
sessionIDFromUrl,
|
||||||
|
waitSlug,
|
||||||
|
waitSession,
|
||||||
|
} from "./actions"
|
||||||
import { createSdk, dirSlug, getWorktree, sessionPath } from "./utils"
|
import { createSdk, dirSlug, getWorktree, sessionPath } from "./utils"
|
||||||
|
|
||||||
export const settingsKey = "settings.v3"
|
export const settingsKey = "settings.v3"
|
||||||
@@ -27,6 +36,29 @@ type WorkerFixtures = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const test = base.extend<TestFixtures, WorkerFixtures>({
|
export const test = base.extend<TestFixtures, WorkerFixtures>({
|
||||||
|
page: async ({ page }, use) => {
|
||||||
|
let boundary: string | undefined
|
||||||
|
setHealthPhase(page, "test")
|
||||||
|
const consoleHandler = (msg: { text(): string }) => {
|
||||||
|
const text = msg.text()
|
||||||
|
if (!text.includes("[e2e:error-boundary]")) return
|
||||||
|
if (healthPhase(page) === "cleanup") {
|
||||||
|
console.warn(`[e2e:error-boundary][cleanup-warning]\n${text}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
boundary ||= text
|
||||||
|
console.log(text)
|
||||||
|
}
|
||||||
|
const pageErrorHandler = (err: Error) => {
|
||||||
|
console.log(`[e2e:pageerror] ${err.stack || err.message}`)
|
||||||
|
}
|
||||||
|
page.on("console", consoleHandler)
|
||||||
|
page.on("pageerror", pageErrorHandler)
|
||||||
|
await use(page)
|
||||||
|
page.off("console", consoleHandler)
|
||||||
|
page.off("pageerror", pageErrorHandler)
|
||||||
|
if (boundary) throw new Error(boundary)
|
||||||
|
},
|
||||||
directory: [
|
directory: [
|
||||||
async ({}, use) => {
|
async ({}, use) => {
|
||||||
const directory = await getWorktree()
|
const directory = await getWorktree()
|
||||||
@@ -48,21 +80,20 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
|
|||||||
|
|
||||||
const gotoSession = async (sessionID?: string) => {
|
const gotoSession = async (sessionID?: string) => {
|
||||||
await page.goto(sessionPath(directory, sessionID))
|
await page.goto(sessionPath(directory, sessionID))
|
||||||
await expect(page.locator(promptSelector)).toBeVisible()
|
await waitSession(page, { directory, sessionID })
|
||||||
}
|
}
|
||||||
await use(gotoSession)
|
await use(gotoSession)
|
||||||
},
|
},
|
||||||
withProject: async ({ page }, use) => {
|
withProject: async ({ page }, use) => {
|
||||||
await use(async (callback, options) => {
|
await use(async (callback, options) => {
|
||||||
const root = await createTestProject()
|
const root = await createTestProject()
|
||||||
const slug = dirSlug(root)
|
|
||||||
const sessions = new Map<string, string>()
|
const sessions = new Map<string, string>()
|
||||||
const dirs = new Set<string>()
|
const dirs = new Set<string>()
|
||||||
await seedStorage(page, { directory: root, extra: options?.extra })
|
await seedStorage(page, { directory: root, extra: options?.extra })
|
||||||
|
|
||||||
const gotoSession = async (sessionID?: string) => {
|
const gotoSession = async (sessionID?: string) => {
|
||||||
await page.goto(sessionPath(root, sessionID))
|
await page.goto(sessionPath(root, sessionID))
|
||||||
await expect(page.locator(promptSelector)).toBeVisible()
|
await waitSession(page, { directory: root, sessionID })
|
||||||
const current = sessionIDFromUrl(page.url())
|
const current = sessionIDFromUrl(page.url())
|
||||||
if (current) trackSession(current)
|
if (current) trackSession(current)
|
||||||
}
|
}
|
||||||
@@ -77,13 +108,16 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await gotoSession()
|
await gotoSession()
|
||||||
|
const slug = await waitSlug(page)
|
||||||
return await callback({ directory: root, slug, gotoSession, trackSession, trackDirectory })
|
return await callback({ directory: root, slug, gotoSession, trackSession, trackDirectory })
|
||||||
} finally {
|
} finally {
|
||||||
|
setHealthPhase(page, "cleanup")
|
||||||
await Promise.allSettled(
|
await Promise.allSettled(
|
||||||
Array.from(sessions, ([sessionID, directory]) => cleanupSession({ sessionID, directory })),
|
Array.from(sessions, ([sessionID, directory]) => cleanupSession({ sessionID, directory })),
|
||||||
)
|
)
|
||||||
await Promise.allSettled(Array.from(dirs, (directory) => cleanupTestProject(directory)))
|
await Promise.allSettled(Array.from(dirs, (directory) => cleanupTestProject(directory)))
|
||||||
await cleanupTestProject(root)
|
await cleanupTestProject(root)
|
||||||
|
setHealthPhase(page, "test")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -95,6 +129,12 @@ async function seedStorage(page: Page, input: { directory: string; extra?: strin
|
|||||||
const win = window as E2EWindow
|
const win = window as E2EWindow
|
||||||
win.__opencode_e2e = {
|
win.__opencode_e2e = {
|
||||||
...win.__opencode_e2e,
|
...win.__opencode_e2e,
|
||||||
|
model: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
prompt: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
terminal: {
|
terminal: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
terminals: {},
|
terminals: {},
|
||||||
|
|||||||
@@ -1,41 +1,19 @@
|
|||||||
import { base64Decode } from "@opencode-ai/util/encode"
|
import { base64Decode } from "@opencode-ai/util/encode"
|
||||||
import type { Page } from "@playwright/test"
|
|
||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { defocus, createTestProject, cleanupTestProject, openSidebar, sessionIDFromUrl, waitSlug } from "../actions"
|
import {
|
||||||
|
defocus,
|
||||||
|
createTestProject,
|
||||||
|
cleanupTestProject,
|
||||||
|
openSidebar,
|
||||||
|
sessionIDFromUrl,
|
||||||
|
setWorkspacesEnabled,
|
||||||
|
waitSession,
|
||||||
|
waitSessionSaved,
|
||||||
|
waitSlug,
|
||||||
|
} from "../actions"
|
||||||
import { projectSwitchSelector, promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
|
import { projectSwitchSelector, promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
|
||||||
import { dirSlug, resolveDirectory } from "../utils"
|
import { dirSlug, resolveDirectory } from "../utils"
|
||||||
|
|
||||||
async function workspaces(page: Page, directory: string, enabled: boolean) {
|
|
||||||
await page.evaluate(
|
|
||||||
({ directory, enabled }: { directory: string; enabled: boolean }) => {
|
|
||||||
const key = "opencode.global.dat:layout"
|
|
||||||
const raw = localStorage.getItem(key)
|
|
||||||
const data = raw ? JSON.parse(raw) : {}
|
|
||||||
const sidebar = data.sidebar && typeof data.sidebar === "object" ? data.sidebar : {}
|
|
||||||
const current =
|
|
||||||
sidebar.workspaces && typeof sidebar.workspaces === "object" && !Array.isArray(sidebar.workspaces)
|
|
||||||
? sidebar.workspaces
|
|
||||||
: {}
|
|
||||||
const next = { ...current }
|
|
||||||
|
|
||||||
if (enabled) next[directory] = true
|
|
||||||
if (!enabled) delete next[directory]
|
|
||||||
|
|
||||||
localStorage.setItem(
|
|
||||||
key,
|
|
||||||
JSON.stringify({
|
|
||||||
...data,
|
|
||||||
sidebar: {
|
|
||||||
...sidebar,
|
|
||||||
workspaces: next,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{ directory, enabled },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
test("can switch between projects from sidebar", async ({ page, withProject }) => {
|
test("can switch between projects from sidebar", async ({ page, withProject }) => {
|
||||||
await page.setViewportSize({ width: 1400, height: 800 })
|
await page.setViewportSize({ width: 1400, height: 800 })
|
||||||
|
|
||||||
@@ -76,9 +54,7 @@ test("switching back to a project opens the latest workspace session", async ({
|
|||||||
await withProject(
|
await withProject(
|
||||||
async ({ directory, slug, trackSession, trackDirectory }) => {
|
async ({ directory, slug, trackSession, trackDirectory }) => {
|
||||||
await defocus(page)
|
await defocus(page)
|
||||||
await workspaces(page, directory, true)
|
await setWorkspacesEnabled(page, slug, true)
|
||||||
await page.reload()
|
|
||||||
await expect(page.locator(promptSelector)).toBeVisible()
|
|
||||||
await openSidebar(page)
|
await openSidebar(page)
|
||||||
await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible()
|
await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible()
|
||||||
|
|
||||||
@@ -100,11 +76,7 @@ test("switching back to a project opens the latest workspace session", async ({
|
|||||||
await expect(btn).toBeVisible()
|
await expect(btn).toBeVisible()
|
||||||
await btn.click({ force: true })
|
await btn.click({ force: true })
|
||||||
|
|
||||||
// A new workspace can be discovered via a transient slug before the route and sidebar
|
await waitSession(page, { directory: space })
|
||||||
// settle to the canonical workspace path on Windows, so interact with either and assert
|
|
||||||
// against the resolved workspace slug.
|
|
||||||
await waitSlug(page)
|
|
||||||
await expect(page).toHaveURL(new RegExp(`/${next}/session(?:[/?#]|$)`))
|
|
||||||
|
|
||||||
// Create a session by sending a prompt
|
// Create a session by sending a prompt
|
||||||
const prompt = page.locator(promptSelector)
|
const prompt = page.locator(promptSelector)
|
||||||
@@ -118,6 +90,7 @@ test("switching back to a project opens the latest workspace session", async ({
|
|||||||
const created = sessionIDFromUrl(page.url())
|
const created = sessionIDFromUrl(page.url())
|
||||||
if (!created) throw new Error(`Failed to get session ID from url: ${page.url()}`)
|
if (!created) throw new Error(`Failed to get session ID from url: ${page.url()}`)
|
||||||
trackSession(created, space)
|
trackSession(created, space)
|
||||||
|
await waitSessionSaved(space, created)
|
||||||
|
|
||||||
await expect(page).toHaveURL(new RegExp(`/${next}/session/${created}(?:[/?#]|$)`))
|
await expect(page).toHaveURL(new RegExp(`/${next}/session/${created}(?:[/?#]|$)`))
|
||||||
|
|
||||||
@@ -125,14 +98,14 @@ test("switching back to a project opens the latest workspace session", async ({
|
|||||||
|
|
||||||
const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
|
const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
|
||||||
await expect(otherButton).toBeVisible()
|
await expect(otherButton).toBeVisible()
|
||||||
await otherButton.click()
|
await otherButton.click({ force: true })
|
||||||
await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`))
|
await waitSession(page, { directory: other })
|
||||||
|
|
||||||
const rootButton = page.locator(projectSwitchSelector(slug)).first()
|
const rootButton = page.locator(projectSwitchSelector(slug)).first()
|
||||||
await expect(rootButton).toBeVisible()
|
await expect(rootButton).toBeVisible()
|
||||||
await rootButton.click()
|
await rootButton.click({ force: true })
|
||||||
|
|
||||||
await expect.poll(() => sessionIDFromUrl(page.url()) ?? "").toBe(created)
|
await waitSession(page, { directory: space, sessionID: created })
|
||||||
await expect(page).toHaveURL(new RegExp(`/session/${created}(?:[/?#]|$)`))
|
await expect(page).toHaveURL(new RegExp(`/session/${created}(?:[/?#]|$)`))
|
||||||
},
|
},
|
||||||
{ extra: [other] },
|
{ extra: [other] },
|
||||||
|
|||||||
@@ -1,109 +1,94 @@
|
|||||||
import { base64Decode } from "@opencode-ai/util/encode"
|
|
||||||
import type { Page } from "@playwright/test"
|
import type { Page } from "@playwright/test"
|
||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { openSidebar, sessionIDFromUrl, setWorkspacesEnabled, slugFromUrl, waitSlug } from "../actions"
|
import {
|
||||||
|
openSidebar,
|
||||||
|
resolveSlug,
|
||||||
|
sessionIDFromUrl,
|
||||||
|
setWorkspacesEnabled,
|
||||||
|
waitDir,
|
||||||
|
waitSession,
|
||||||
|
waitSessionSaved,
|
||||||
|
waitSlug,
|
||||||
|
} from "../actions"
|
||||||
import { promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
|
import { promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
|
||||||
import { createSdk } from "../utils"
|
import { createSdk } from "../utils"
|
||||||
|
|
||||||
async function waitWorkspaceReady(page: Page, slug: string) {
|
function item(space: { slug: string; raw: string }) {
|
||||||
await openSidebar(page)
|
return `${workspaceItemSelector(space.slug)}, ${workspaceItemSelector(space.raw)}`
|
||||||
await expect
|
|
||||||
.poll(
|
|
||||||
async () => {
|
|
||||||
const item = page.locator(workspaceItemSelector(slug)).first()
|
|
||||||
try {
|
|
||||||
await item.hover({ timeout: 500 })
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{ timeout: 60_000 },
|
function button(space: { slug: string; raw: string }) {
|
||||||
)
|
return `${workspaceNewSessionSelector(space.slug)}, ${workspaceNewSessionSelector(space.raw)}`
|
||||||
.toBe(true)
|
}
|
||||||
|
|
||||||
|
async function waitWorkspaceReady(page: Page, space: { slug: string; raw: string }) {
|
||||||
|
await openSidebar(page)
|
||||||
|
await expect(page.locator(item(space)).first()).toBeVisible({ timeout: 60_000 })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createWorkspace(page: Page, root: string, seen: string[]) {
|
async function createWorkspace(page: Page, root: string, seen: string[]) {
|
||||||
await openSidebar(page)
|
await openSidebar(page)
|
||||||
await page.getByRole("button", { name: "New workspace" }).first().click()
|
await page.getByRole("button", { name: "New workspace" }).first().click()
|
||||||
|
|
||||||
const slug = await waitSlug(page, [root, ...seen])
|
const next = await resolveSlug(await waitSlug(page, [root, ...seen]))
|
||||||
const directory = base64Decode(slug)
|
await waitDir(page, next.directory)
|
||||||
if (!directory) throw new Error(`Failed to decode workspace slug: ${slug}`)
|
|
||||||
return { slug, directory }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openWorkspaceNewSession(page: Page, slug: string) {
|
|
||||||
await waitWorkspaceReady(page, slug)
|
|
||||||
|
|
||||||
const item = page.locator(workspaceItemSelector(slug)).first()
|
|
||||||
await item.hover()
|
|
||||||
|
|
||||||
const button = page.locator(workspaceNewSessionSelector(slug)).first()
|
|
||||||
await expect(button).toBeVisible()
|
|
||||||
await button.click({ force: true })
|
|
||||||
|
|
||||||
const next = await waitSlug(page)
|
|
||||||
await expect(page).toHaveURL(new RegExp(`/${next}/session(?:[/?#]|$)`))
|
|
||||||
return next
|
return next
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createSessionFromWorkspace(page: Page, slug: string, text: string) {
|
async function openWorkspaceNewSession(page: Page, space: { slug: string; raw: string; directory: string }) {
|
||||||
const next = await openWorkspaceNewSession(page, slug)
|
await waitWorkspaceReady(page, space)
|
||||||
|
|
||||||
|
const row = page.locator(item(space)).first()
|
||||||
|
await row.hover()
|
||||||
|
|
||||||
|
const next = page.locator(button(space)).first()
|
||||||
|
await expect(next).toBeVisible()
|
||||||
|
await next.click({ force: true })
|
||||||
|
|
||||||
|
await waitSession(page, { directory: space.directory })
|
||||||
|
await expect.poll(() => sessionIDFromUrl(page.url()) ?? "").toBe("")
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createSessionFromWorkspace(
|
||||||
|
page: Page,
|
||||||
|
space: { slug: string; raw: string; directory: string },
|
||||||
|
text: string,
|
||||||
|
) {
|
||||||
|
await openWorkspaceNewSession(page, space)
|
||||||
|
|
||||||
const prompt = page.locator(promptSelector)
|
const prompt = page.locator(promptSelector)
|
||||||
await expect(prompt).toBeVisible()
|
await expect(prompt).toBeVisible()
|
||||||
await expect(prompt).toBeEditable()
|
|
||||||
await prompt.click()
|
|
||||||
await expect(prompt).toBeFocused()
|
|
||||||
await prompt.fill(text)
|
await prompt.fill(text)
|
||||||
await expect.poll(async () => ((await prompt.textContent()) ?? "").trim()).toContain(text)
|
await page.keyboard.press("Enter")
|
||||||
await prompt.press("Enter")
|
|
||||||
|
|
||||||
await expect.poll(() => slugFromUrl(page.url())).toBe(next)
|
|
||||||
await expect.poll(() => sessionIDFromUrl(page.url()) ?? "", { timeout: 30_000 }).not.toBe("")
|
|
||||||
|
|
||||||
|
await expect.poll(() => sessionIDFromUrl(page.url()) ?? "", { timeout: 15_000 }).not.toBe("")
|
||||||
const sessionID = sessionIDFromUrl(page.url())
|
const sessionID = sessionIDFromUrl(page.url())
|
||||||
if (!sessionID) throw new Error(`Failed to parse session id from url: ${page.url()}`)
|
if (!sessionID) throw new Error(`Failed to parse session id from url: ${page.url()}`)
|
||||||
await expect(page).toHaveURL(new RegExp(`/${next}/session/${sessionID}(?:[/?#]|$)`))
|
|
||||||
return { sessionID, slug: next }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sessionDirectory(directory: string, sessionID: string) {
|
await waitSessionSaved(space.directory, sessionID)
|
||||||
const info = await createSdk(directory)
|
await createSdk(space.directory)
|
||||||
.session.get({ sessionID })
|
.session.abort({ sessionID })
|
||||||
.then((x) => x.data)
|
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
if (!info) return ""
|
return sessionID
|
||||||
return info.directory
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test("new sessions from sidebar workspace actions stay in selected workspace", async ({ page, withProject }) => {
|
test("new sessions from sidebar workspace actions stay in selected workspace", async ({ page, withProject }) => {
|
||||||
await page.setViewportSize({ width: 1400, height: 800 })
|
await page.setViewportSize({ width: 1400, height: 800 })
|
||||||
|
|
||||||
await withProject(async ({ directory, slug: root, trackSession, trackDirectory }) => {
|
await withProject(async ({ slug: root, trackDirectory, trackSession }) => {
|
||||||
await openSidebar(page)
|
await openSidebar(page)
|
||||||
await setWorkspacesEnabled(page, root, true)
|
await setWorkspacesEnabled(page, root, true)
|
||||||
|
|
||||||
const first = await createWorkspace(page, root, [])
|
const first = await createWorkspace(page, root, [])
|
||||||
trackDirectory(first.directory)
|
trackDirectory(first.directory)
|
||||||
await waitWorkspaceReady(page, first.slug)
|
await waitWorkspaceReady(page, first)
|
||||||
|
|
||||||
const second = await createWorkspace(page, root, [first.slug])
|
const second = await createWorkspace(page, root, [first.slug])
|
||||||
trackDirectory(second.directory)
|
trackDirectory(second.directory)
|
||||||
await waitWorkspaceReady(page, second.slug)
|
await waitWorkspaceReady(page, second)
|
||||||
|
|
||||||
const firstSession = await createSessionFromWorkspace(page, first.slug, `workspace one ${Date.now()}`)
|
trackSession(await createSessionFromWorkspace(page, first, `workspace one ${Date.now()}`), first.directory)
|
||||||
trackSession(firstSession.sessionID, first.directory)
|
trackSession(await createSessionFromWorkspace(page, second, `workspace two ${Date.now()}`), second.directory)
|
||||||
|
trackSession(await createSessionFromWorkspace(page, first, `workspace one again ${Date.now()}`), first.directory)
|
||||||
const secondSession = await createSessionFromWorkspace(page, second.slug, `workspace two ${Date.now()}`)
|
|
||||||
trackSession(secondSession.sessionID, second.directory)
|
|
||||||
|
|
||||||
const thirdSession = await createSessionFromWorkspace(page, first.slug, `workspace one again ${Date.now()}`)
|
|
||||||
trackSession(thirdSession.sessionID, first.directory)
|
|
||||||
|
|
||||||
await expect.poll(() => sessionDirectory(first.directory, firstSession.sessionID)).toBe(first.directory)
|
|
||||||
await expect.poll(() => sessionDirectory(second.directory, secondSession.sessionID)).toBe(second.directory)
|
|
||||||
await expect.poll(() => sessionDirectory(first.directory, thirdSession.sessionID)).toBe(first.directory)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { base64Decode } from "@opencode-ai/util/encode"
|
|
||||||
import fs from "node:fs/promises"
|
import fs from "node:fs/promises"
|
||||||
import os from "node:os"
|
import os from "node:os"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
|
import { base64Decode } from "@opencode-ai/util/encode"
|
||||||
import type { Page } from "@playwright/test"
|
import type { Page } from "@playwright/test"
|
||||||
|
|
||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
@@ -13,8 +13,10 @@ import {
|
|||||||
confirmDialog,
|
confirmDialog,
|
||||||
openSidebar,
|
openSidebar,
|
||||||
openWorkspaceMenu,
|
openWorkspaceMenu,
|
||||||
|
resolveSlug,
|
||||||
setWorkspacesEnabled,
|
setWorkspacesEnabled,
|
||||||
slugFromUrl,
|
slugFromUrl,
|
||||||
|
waitDir,
|
||||||
waitSlug,
|
waitSlug,
|
||||||
} from "../actions"
|
} from "../actions"
|
||||||
import { dropdownMenuContentSelector, inlineInputSelector, workspaceItemSelector } from "../selectors"
|
import { dropdownMenuContentSelector, inlineInputSelector, workspaceItemSelector } from "../selectors"
|
||||||
@@ -27,15 +29,15 @@ async function setupWorkspaceTest(page: Page, project: { slug: string }) {
|
|||||||
await setWorkspacesEnabled(page, rootSlug, true)
|
await setWorkspacesEnabled(page, rootSlug, true)
|
||||||
|
|
||||||
await page.getByRole("button", { name: "New workspace" }).first().click()
|
await page.getByRole("button", { name: "New workspace" }).first().click()
|
||||||
const slug = await waitSlug(page, [rootSlug])
|
const next = await resolveSlug(await waitSlug(page, [rootSlug]))
|
||||||
const dir = base64Decode(slug)
|
await waitDir(page, next.directory)
|
||||||
|
|
||||||
await openSidebar(page)
|
await openSidebar(page)
|
||||||
|
|
||||||
await expect
|
await expect
|
||||||
.poll(
|
.poll(
|
||||||
async () => {
|
async () => {
|
||||||
const item = page.locator(workspaceItemSelector(slug)).first()
|
const item = page.locator(workspaceItemSelector(next.slug)).first()
|
||||||
try {
|
try {
|
||||||
await item.hover({ timeout: 500 })
|
await item.hover({ timeout: 500 })
|
||||||
return true
|
return true
|
||||||
@@ -47,7 +49,7 @@ async function setupWorkspaceTest(page: Page, project: { slug: string }) {
|
|||||||
)
|
)
|
||||||
.toBe(true)
|
.toBe(true)
|
||||||
|
|
||||||
return { rootSlug, slug, directory: dir }
|
return { rootSlug, slug: next.slug, directory: next.directory }
|
||||||
}
|
}
|
||||||
|
|
||||||
test("can enable and disable workspaces from project menu", async ({ page, withProject }) => {
|
test("can enable and disable workspaces from project menu", async ({ page, withProject }) => {
|
||||||
@@ -79,15 +81,15 @@ test("can create a workspace", async ({ page, withProject }) => {
|
|||||||
await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible()
|
await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible()
|
||||||
|
|
||||||
await page.getByRole("button", { name: "New workspace" }).first().click()
|
await page.getByRole("button", { name: "New workspace" }).first().click()
|
||||||
const workspaceSlug = await waitSlug(page, [slug])
|
const next = await resolveSlug(await waitSlug(page, [slug]))
|
||||||
const workspaceDir = base64Decode(workspaceSlug)
|
await waitDir(page, next.directory)
|
||||||
|
|
||||||
await openSidebar(page)
|
await openSidebar(page)
|
||||||
|
|
||||||
await expect
|
await expect
|
||||||
.poll(
|
.poll(
|
||||||
async () => {
|
async () => {
|
||||||
const item = page.locator(workspaceItemSelector(workspaceSlug)).first()
|
const item = page.locator(workspaceItemSelector(next.slug)).first()
|
||||||
try {
|
try {
|
||||||
await item.hover({ timeout: 500 })
|
await item.hover({ timeout: 500 })
|
||||||
return true
|
return true
|
||||||
@@ -99,9 +101,9 @@ test("can create a workspace", async ({ page, withProject }) => {
|
|||||||
)
|
)
|
||||||
.toBe(true)
|
.toBe(true)
|
||||||
|
|
||||||
await expect(page.locator(workspaceItemSelector(workspaceSlug)).first()).toBeVisible()
|
await expect(page.locator(workspaceItemSelector(next.slug)).first()).toBeVisible()
|
||||||
|
|
||||||
await cleanupTestProject(workspaceDir)
|
await cleanupTestProject(next.directory)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -119,7 +121,7 @@ test("non-git projects keep workspace mode disabled", async ({ page, withProject
|
|||||||
|
|
||||||
await expect.poll(() => slugFromUrl(page.url()), { timeout: 30_000 }).not.toBe("")
|
await expect.poll(() => slugFromUrl(page.url()), { timeout: 30_000 }).not.toBe("")
|
||||||
|
|
||||||
const activeDir = base64Decode(slugFromUrl(page.url()))
|
const activeDir = await resolveSlug(slugFromUrl(page.url())).then((item) => item.directory)
|
||||||
expect(path.basename(activeDir)).toContain("opencode-e2e-project-nongit-")
|
expect(path.basename(activeDir)).toContain("opencode-e2e-project-nongit-")
|
||||||
|
|
||||||
await openSidebar(page)
|
await openSidebar(page)
|
||||||
@@ -331,9 +333,9 @@ test("can reorder workspaces by drag and drop", async ({ page, withProject }) =>
|
|||||||
for (const _ of [0, 1]) {
|
for (const _ of [0, 1]) {
|
||||||
const prev = slugFromUrl(page.url())
|
const prev = slugFromUrl(page.url())
|
||||||
await page.getByRole("button", { name: "New workspace" }).first().click()
|
await page.getByRole("button", { name: "New workspace" }).first().click()
|
||||||
const slug = await waitSlug(page, [rootSlug, prev])
|
const next = await resolveSlug(await waitSlug(page, [rootSlug, prev]))
|
||||||
const dir = base64Decode(slug)
|
await waitDir(page, next.directory)
|
||||||
workspaces.push({ slug, directory: dir })
|
workspaces.push(next)
|
||||||
|
|
||||||
await openSidebar(page)
|
await openSidebar(page)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,18 @@ test("shift+enter inserts a newline without submitting", async ({ page, gotoSess
|
|||||||
await expect(page).toHaveURL(/\/session\/?$/)
|
await expect(page).toHaveURL(/\/session\/?$/)
|
||||||
|
|
||||||
const prompt = page.locator(promptSelector)
|
const prompt = page.locator(promptSelector)
|
||||||
await prompt.click()
|
await prompt.focus()
|
||||||
await page.keyboard.type("line one")
|
await expect(prompt).toBeFocused()
|
||||||
await page.keyboard.press("Shift+Enter")
|
|
||||||
await page.keyboard.type("line two")
|
await prompt.pressSequentially("line one")
|
||||||
|
await expect(prompt).toBeFocused()
|
||||||
|
|
||||||
|
await prompt.press("Shift+Enter")
|
||||||
|
await expect(page).toHaveURL(/\/session\/?$/)
|
||||||
|
await expect(prompt).toBeFocused()
|
||||||
|
|
||||||
|
await prompt.pressSequentially("line two")
|
||||||
|
|
||||||
await expect(page).toHaveURL(/\/session\/?$/)
|
await expect(page).toHaveURL(/\/session\/?$/)
|
||||||
await expect(prompt).toContainText("line one")
|
await expect.poll(() => prompt.evaluate((el) => el.innerText)).toBe("line one\nline two")
|
||||||
await expect(prompt).toContainText("line two")
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { waitTerminalReady } from "../actions"
|
import { runPromptSlash, waitTerminalFocusIdle } from "../actions"
|
||||||
import { promptSelector, terminalSelector } from "../selectors"
|
import { promptSelector, terminalSelector } from "../selectors"
|
||||||
|
|
||||||
test("/terminal toggles the terminal panel", async ({ page, gotoSession }) => {
|
test("/terminal toggles the terminal panel", async ({ page, gotoSession }) => {
|
||||||
@@ -7,29 +7,12 @@ test("/terminal toggles the terminal panel", async ({ page, gotoSession }) => {
|
|||||||
|
|
||||||
const prompt = page.locator(promptSelector)
|
const prompt = page.locator(promptSelector)
|
||||||
const terminal = page.locator(terminalSelector)
|
const terminal = page.locator(terminalSelector)
|
||||||
const slash = page.locator('[data-slash-id="terminal.toggle"]').first()
|
|
||||||
|
|
||||||
await expect(terminal).not.toBeVisible()
|
await expect(terminal).not.toBeVisible()
|
||||||
|
|
||||||
await prompt.fill("/terminal")
|
await runPromptSlash(page, { prompt, text: "/terminal", id: "terminal.toggle" })
|
||||||
await expect(slash).toBeVisible()
|
await waitTerminalFocusIdle(page, { term: terminal })
|
||||||
await page.keyboard.press("Enter")
|
|
||||||
await waitTerminalReady(page, { term: terminal })
|
|
||||||
|
|
||||||
// Terminal panel retries focus (immediate, RAF, 120ms, 240ms) after opening,
|
await runPromptSlash(page, { prompt, text: "/terminal", id: "terminal.toggle" })
|
||||||
// which can steal focus from the prompt and prevent fill() from triggering
|
|
||||||
// the slash popover. Re-attempt click+fill until all retries are exhausted
|
|
||||||
// and the popover appears.
|
|
||||||
await expect
|
|
||||||
.poll(
|
|
||||||
async () => {
|
|
||||||
await prompt.click().catch(() => false)
|
|
||||||
await prompt.fill("/terminal").catch(() => false)
|
|
||||||
return slash.isVisible().catch(() => false)
|
|
||||||
},
|
|
||||||
{ timeout: 10_000 },
|
|
||||||
)
|
|
||||||
.toBe(true)
|
|
||||||
await page.keyboard.press("Enter")
|
|
||||||
await expect(terminal).not.toBeVisible()
|
await expect(terminal).not.toBeVisible()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ export const sessionTodoToggleButtonSelector = '[data-action="session-todo-toggl
|
|||||||
export const sessionTodoListSelector = '[data-slot="session-todo-list"]'
|
export const sessionTodoListSelector = '[data-slot="session-todo-list"]'
|
||||||
|
|
||||||
export const modelVariantCycleSelector = '[data-action="model-variant-cycle"]'
|
export const modelVariantCycleSelector = '[data-action="model-variant-cycle"]'
|
||||||
|
export const promptAgentSelector = '[data-component="prompt-agent-control"]'
|
||||||
|
export const promptModelSelector = '[data-component="prompt-model-control"]'
|
||||||
|
export const promptVariantSelector = '[data-component="prompt-variant-control"]'
|
||||||
export const settingsLanguageSelectSelector = '[data-action="settings-language"]'
|
export const settingsLanguageSelectSelector = '[data-action="settings-language"]'
|
||||||
export const settingsColorSchemeSelector = '[data-action="settings-color-scheme"]'
|
export const settingsColorSchemeSelector = '[data-action="settings-color-scheme"]'
|
||||||
export const settingsThemeSelector = '[data-action="settings-theme"]'
|
export const settingsThemeSelector = '[data-action="settings-theme"]'
|
||||||
|
|||||||
@@ -0,0 +1,359 @@
|
|||||||
|
import type { Locator, Page } from "@playwright/test"
|
||||||
|
import { test, expect } from "../fixtures"
|
||||||
|
import {
|
||||||
|
openSidebar,
|
||||||
|
resolveSlug,
|
||||||
|
sessionIDFromUrl,
|
||||||
|
setWorkspacesEnabled,
|
||||||
|
waitSession,
|
||||||
|
waitSessionIdle,
|
||||||
|
waitSlug,
|
||||||
|
} from "../actions"
|
||||||
|
import {
|
||||||
|
promptAgentSelector,
|
||||||
|
promptModelSelector,
|
||||||
|
promptSelector,
|
||||||
|
promptVariantSelector,
|
||||||
|
workspaceItemSelector,
|
||||||
|
workspaceNewSessionSelector,
|
||||||
|
} from "../selectors"
|
||||||
|
import { createSdk, sessionPath } from "../utils"
|
||||||
|
|
||||||
|
type Footer = {
|
||||||
|
agent: string
|
||||||
|
model: string
|
||||||
|
variant: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Probe = {
|
||||||
|
dir?: string
|
||||||
|
sessionID?: string
|
||||||
|
model?: { providerID: string; modelID: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
const escape = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||||
|
|
||||||
|
const text = async (locator: Locator) => ((await locator.textContent()) ?? "").trim()
|
||||||
|
|
||||||
|
const modelKey = (state: Probe | null) => (state?.model ? `${state.model.providerID}:${state.model.modelID}` : null)
|
||||||
|
|
||||||
|
async function probe(page: Page): Promise<Probe | null> {
|
||||||
|
return page.evaluate(() => {
|
||||||
|
const win = window as Window & {
|
||||||
|
__opencode_e2e?: {
|
||||||
|
model?: {
|
||||||
|
current?: Probe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return win.__opencode_e2e?.model?.current ?? null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function read(page: Page): Promise<Footer> {
|
||||||
|
return {
|
||||||
|
agent: await text(page.locator(`${promptAgentSelector} [data-slot="select-select-trigger-value"]`).first()),
|
||||||
|
model: await text(page.locator(`${promptModelSelector} [data-action="prompt-model"] span`).first()),
|
||||||
|
variant: await text(page.locator(`${promptVariantSelector} [data-slot="select-select-trigger-value"]`).first()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitFooter(page: Page, expected: Partial<Footer>) {
|
||||||
|
let hit: Footer | null = null
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const state = await read(page)
|
||||||
|
const ok = Object.entries(expected).every(([key, value]) => state[key as keyof Footer] === value)
|
||||||
|
if (ok) hit = state
|
||||||
|
return ok
|
||||||
|
},
|
||||||
|
{ timeout: 30_000 },
|
||||||
|
)
|
||||||
|
.toBe(true)
|
||||||
|
if (!hit) throw new Error("Failed to resolve prompt footer state")
|
||||||
|
return hit
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitModel(page: Page, value: string) {
|
||||||
|
await expect.poll(() => probe(page).then(modelKey), { timeout: 30_000 }).toBe(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function choose(page: Page, root: string, value: string) {
|
||||||
|
const select = page.locator(root)
|
||||||
|
await expect(select).toBeVisible()
|
||||||
|
await select.locator('[data-action], [data-slot="select-select-trigger"]').first().click()
|
||||||
|
const item = page
|
||||||
|
.locator('[data-slot="select-select-item"]')
|
||||||
|
.filter({ hasText: new RegExp(`^\\s*${escape(value)}\\s*$`) })
|
||||||
|
.first()
|
||||||
|
await expect(item).toBeVisible()
|
||||||
|
await item.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function variantCount(page: Page) {
|
||||||
|
const select = page.locator(promptVariantSelector)
|
||||||
|
await expect(select).toBeVisible()
|
||||||
|
await select.locator('[data-slot="select-select-trigger"]').click()
|
||||||
|
const count = await page.locator('[data-slot="select-select-item"]').count()
|
||||||
|
await page.keyboard.press("Escape")
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
async function agents(page: Page) {
|
||||||
|
const select = page.locator(promptAgentSelector)
|
||||||
|
await expect(select).toBeVisible()
|
||||||
|
await select.locator('[data-action], [data-slot="select-select-trigger"]').first().click()
|
||||||
|
const labels = await page.locator('[data-slot="select-select-item-label"]').allTextContents()
|
||||||
|
await page.keyboard.press("Escape")
|
||||||
|
return labels.map((item) => item.trim()).filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureVariant(page: Page, directory: string): Promise<Footer> {
|
||||||
|
const current = await read(page)
|
||||||
|
if ((await variantCount(page)) >= 2) return current
|
||||||
|
|
||||||
|
const cfg = await createSdk(directory)
|
||||||
|
.config.get()
|
||||||
|
.then((x) => x.data)
|
||||||
|
const visible = new Set(await agents(page))
|
||||||
|
const entry = Object.entries(cfg?.agent ?? {}).find((item) => {
|
||||||
|
const value = item[1]
|
||||||
|
return !!value && typeof value === "object" && "variant" in value && "model" in value && visible.has(item[0])
|
||||||
|
})
|
||||||
|
const name = entry?.[0]
|
||||||
|
test.skip(!name, "no agent with alternate variants available")
|
||||||
|
if (!name) return current
|
||||||
|
|
||||||
|
await choose(page, promptAgentSelector, name)
|
||||||
|
await expect.poll(() => variantCount(page), { timeout: 30_000 }).toBeGreaterThanOrEqual(2)
|
||||||
|
return waitFooter(page, { agent: name })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function chooseDifferentVariant(page: Page): Promise<Footer> {
|
||||||
|
const current = await read(page)
|
||||||
|
const select = page.locator(promptVariantSelector)
|
||||||
|
await expect(select).toBeVisible()
|
||||||
|
await select.locator('[data-slot="select-select-trigger"]').click()
|
||||||
|
|
||||||
|
const items = page.locator('[data-slot="select-select-item"]')
|
||||||
|
const count = await items.count()
|
||||||
|
if (count < 2) throw new Error("Current model has no alternate variant to select")
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const item = items.nth(i)
|
||||||
|
const next = await text(item.locator('[data-slot="select-select-item-label"]').first())
|
||||||
|
if (!next || next === current.variant) continue
|
||||||
|
await item.click()
|
||||||
|
return waitFooter(page, { agent: current.agent, model: current.model, variant: next })
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Failed to choose a different variant")
|
||||||
|
}
|
||||||
|
|
||||||
|
async function chooseOtherModel(page: Page): Promise<Footer> {
|
||||||
|
const current = await read(page)
|
||||||
|
const button = page.locator(`${promptModelSelector} [data-action="prompt-model"]`)
|
||||||
|
await expect(button).toBeVisible()
|
||||||
|
await button.click()
|
||||||
|
|
||||||
|
const dialog = page.getByRole("dialog")
|
||||||
|
await expect(dialog).toBeVisible()
|
||||||
|
const items = dialog.locator('[data-slot="list-item"]')
|
||||||
|
const count = await items.count()
|
||||||
|
expect(count).toBeGreaterThan(1)
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const item = items.nth(i)
|
||||||
|
const selected = (await item.getAttribute("data-selected")) === "true"
|
||||||
|
if (selected) continue
|
||||||
|
await item.click()
|
||||||
|
await expect(dialog).toHaveCount(0)
|
||||||
|
await expect.poll(async () => (await read(page)).model !== current.model, { timeout: 30_000 }).toBe(true)
|
||||||
|
return read(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Failed to choose a different model")
|
||||||
|
}
|
||||||
|
|
||||||
|
async function goto(page: Page, directory: string, sessionID?: string) {
|
||||||
|
await page.goto(sessionPath(directory, sessionID))
|
||||||
|
await waitSession(page, { directory, sessionID })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit(page: Page, value: string) {
|
||||||
|
const prompt = page.locator(promptSelector)
|
||||||
|
await expect(prompt).toBeVisible()
|
||||||
|
await prompt.click()
|
||||||
|
await prompt.fill(value)
|
||||||
|
await prompt.press("Enter")
|
||||||
|
|
||||||
|
await expect.poll(() => sessionIDFromUrl(page.url()) ?? "", { timeout: 30_000 }).not.toBe("")
|
||||||
|
const id = sessionIDFromUrl(page.url())
|
||||||
|
if (!id) throw new Error(`Failed to resolve session id from ${page.url()}`)
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitUser(directory: string, sessionID: string) {
|
||||||
|
const sdk = createSdk(directory)
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const items = await sdk.session.messages({ sessionID, limit: 20 }).then((x) => x.data ?? [])
|
||||||
|
return items.some((item) => item.info.role === "user")
|
||||||
|
},
|
||||||
|
{ timeout: 30_000 },
|
||||||
|
)
|
||||||
|
.toBe(true)
|
||||||
|
await sdk.session.abort({ sessionID }).catch(() => undefined)
|
||||||
|
await waitSessionIdle(sdk, sessionID, 30_000).catch(() => undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createWorkspace(page: Page, root: string, seen: string[]) {
|
||||||
|
await openSidebar(page)
|
||||||
|
await page.getByRole("button", { name: "New workspace" }).first().click()
|
||||||
|
|
||||||
|
const next = await resolveSlug(await waitSlug(page, [root, ...seen]))
|
||||||
|
await waitSession(page, { directory: next.directory })
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitWorkspace(page: Page, slug: string) {
|
||||||
|
await openSidebar(page)
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const item = page.locator(workspaceItemSelector(slug)).first()
|
||||||
|
try {
|
||||||
|
await item.hover({ timeout: 500 })
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ timeout: 60_000 },
|
||||||
|
)
|
||||||
|
.toBe(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function newWorkspaceSession(page: Page, slug: string) {
|
||||||
|
await waitWorkspace(page, slug)
|
||||||
|
const item = page.locator(workspaceItemSelector(slug)).first()
|
||||||
|
await item.hover()
|
||||||
|
|
||||||
|
const button = page.locator(workspaceNewSessionSelector(slug)).first()
|
||||||
|
await expect(button).toBeVisible()
|
||||||
|
await button.click({ force: true })
|
||||||
|
|
||||||
|
const next = await resolveSlug(await waitSlug(page))
|
||||||
|
return waitSession(page, { directory: next.directory }).then((item) => item.directory)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("session model and variant restore per session without leaking into new sessions", async ({
|
||||||
|
page,
|
||||||
|
withProject,
|
||||||
|
}) => {
|
||||||
|
await page.setViewportSize({ width: 1440, height: 900 })
|
||||||
|
|
||||||
|
await withProject(async ({ directory, gotoSession, trackSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
await ensureVariant(page, directory)
|
||||||
|
const firstState = await chooseDifferentVariant(page)
|
||||||
|
const first = await submit(page, `session variant ${Date.now()}`)
|
||||||
|
trackSession(first)
|
||||||
|
await waitUser(directory, first)
|
||||||
|
|
||||||
|
await page.reload()
|
||||||
|
await waitSession(page, { directory, sessionID: first })
|
||||||
|
await waitFooter(page, firstState)
|
||||||
|
|
||||||
|
await gotoSession()
|
||||||
|
const fresh = await ensureVariant(page, directory)
|
||||||
|
expect(fresh.variant).not.toBe(firstState.variant)
|
||||||
|
|
||||||
|
const secondState = await chooseOtherModel(page)
|
||||||
|
const second = await submit(page, `session model ${Date.now()}`)
|
||||||
|
trackSession(second)
|
||||||
|
await waitUser(directory, second)
|
||||||
|
|
||||||
|
await goto(page, directory, first)
|
||||||
|
await waitFooter(page, firstState)
|
||||||
|
|
||||||
|
await goto(page, directory, second)
|
||||||
|
await waitFooter(page, secondState)
|
||||||
|
|
||||||
|
await gotoSession()
|
||||||
|
await waitFooter(page, fresh)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("session model restore across workspaces", async ({ page, withProject }) => {
|
||||||
|
await page.setViewportSize({ width: 1440, height: 900 })
|
||||||
|
|
||||||
|
await withProject(async ({ directory: root, slug, gotoSession, trackDirectory, trackSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
await ensureVariant(page, root)
|
||||||
|
const firstState = await chooseDifferentVariant(page)
|
||||||
|
const first = await submit(page, `root session ${Date.now()}`)
|
||||||
|
trackSession(first, root)
|
||||||
|
await waitUser(root, first)
|
||||||
|
|
||||||
|
await openSidebar(page)
|
||||||
|
await setWorkspacesEnabled(page, slug, true)
|
||||||
|
|
||||||
|
const one = await createWorkspace(page, slug, [])
|
||||||
|
const oneDir = await newWorkspaceSession(page, one.slug)
|
||||||
|
trackDirectory(oneDir)
|
||||||
|
|
||||||
|
const secondState = await chooseOtherModel(page)
|
||||||
|
const second = await submit(page, `workspace one ${Date.now()}`)
|
||||||
|
trackSession(second, oneDir)
|
||||||
|
await waitUser(oneDir, second)
|
||||||
|
|
||||||
|
const two = await createWorkspace(page, slug, [one.slug])
|
||||||
|
const twoDir = await newWorkspaceSession(page, two.slug)
|
||||||
|
trackDirectory(twoDir)
|
||||||
|
|
||||||
|
await ensureVariant(page, twoDir)
|
||||||
|
const thirdState = await chooseDifferentVariant(page)
|
||||||
|
const third = await submit(page, `workspace two ${Date.now()}`)
|
||||||
|
trackSession(third, twoDir)
|
||||||
|
await waitUser(twoDir, third)
|
||||||
|
|
||||||
|
await goto(page, root, first)
|
||||||
|
await waitFooter(page, firstState)
|
||||||
|
|
||||||
|
await goto(page, oneDir, second)
|
||||||
|
await waitFooter(page, secondState)
|
||||||
|
|
||||||
|
await goto(page, twoDir, third)
|
||||||
|
await waitFooter(page, thirdState)
|
||||||
|
|
||||||
|
await goto(page, root, first)
|
||||||
|
await waitFooter(page, firstState)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("variant preserved when switching agent modes", async ({ page, withProject }) => {
|
||||||
|
await page.setViewportSize({ width: 1440, height: 900 })
|
||||||
|
|
||||||
|
await withProject(async ({ directory, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
await ensureVariant(page, directory)
|
||||||
|
const updated = await chooseDifferentVariant(page)
|
||||||
|
|
||||||
|
const available = await agents(page)
|
||||||
|
const other = available.find((name) => name !== updated.agent)
|
||||||
|
test.skip(!other, "only one agent available")
|
||||||
|
if (!other) return
|
||||||
|
|
||||||
|
await choose(page, promptAgentSelector, other)
|
||||||
|
await waitFooter(page, { agent: other, variant: updated.variant })
|
||||||
|
|
||||||
|
await choose(page, promptAgentSelector, updated.agent)
|
||||||
|
await waitFooter(page, { agent: updated.agent, variant: updated.variant })
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -123,6 +123,215 @@ async function spot(page: Parameters<typeof test>[0]["page"], file: string) {
|
|||||||
}, file)
|
}, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function comment(page: Parameters<typeof test>[0]["page"], file: string, note: string) {
|
||||||
|
const row = page.locator(`[data-file="${file}"]`).first()
|
||||||
|
await expect(row).toBeVisible()
|
||||||
|
|
||||||
|
const line = row.locator('diffs-container [data-line="2"]').first()
|
||||||
|
await expect(line).toBeVisible()
|
||||||
|
await line.hover()
|
||||||
|
|
||||||
|
const add = row.getByRole("button", { name: /^Comment$/ }).first()
|
||||||
|
await expect(add).toBeVisible()
|
||||||
|
await add.click()
|
||||||
|
|
||||||
|
const area = row.locator('[data-slot="line-comment-textarea"]').first()
|
||||||
|
await expect(area).toBeVisible()
|
||||||
|
await area.fill(note)
|
||||||
|
|
||||||
|
const submit = row.locator('[data-slot="line-comment-action"][data-variant="primary"]').first()
|
||||||
|
await expect(submit).toBeEnabled()
|
||||||
|
await submit.click()
|
||||||
|
|
||||||
|
await expect(row.locator('[data-slot="line-comment-content"]').filter({ hasText: note }).first()).toBeVisible()
|
||||||
|
await expect(row.locator('[data-slot="line-comment-tools"]').first()).toBeVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function overflow(page: Parameters<typeof test>[0]["page"], file: string) {
|
||||||
|
const row = page.locator(`[data-file="${file}"]`).first()
|
||||||
|
const view = page.locator('[data-slot="session-review-scroll"] .scroll-view__viewport').first()
|
||||||
|
const pop = row.locator('[data-slot="line-comment-popover"][data-inline-body]').first()
|
||||||
|
const tools = row.locator('[data-slot="line-comment-tools"]').first()
|
||||||
|
|
||||||
|
const [width, viewBox, popBox, toolsBox] = await Promise.all([
|
||||||
|
view.evaluate((el) => el.scrollWidth - el.clientWidth),
|
||||||
|
view.boundingBox(),
|
||||||
|
pop.boundingBox(),
|
||||||
|
tools.boundingBox(),
|
||||||
|
])
|
||||||
|
|
||||||
|
if (!viewBox || !popBox || !toolsBox) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
width,
|
||||||
|
pop: popBox.x + popBox.width - (viewBox.x + viewBox.width),
|
||||||
|
tools: toolsBox.x + toolsBox.width - (viewBox.x + viewBox.width),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openReviewFile(page: Parameters<typeof test>[0]["page"], file: string) {
|
||||||
|
const row = page.locator(`[data-file="${file}"]`).first()
|
||||||
|
await expect(row).toBeVisible()
|
||||||
|
await row.hover()
|
||||||
|
|
||||||
|
const open = row.getByRole("button", { name: /^Open file$/i }).first()
|
||||||
|
await expect(open).toBeVisible()
|
||||||
|
await open.click()
|
||||||
|
|
||||||
|
const tab = page.getByRole("tab", { name: file }).first()
|
||||||
|
await expect(tab).toBeVisible()
|
||||||
|
await tab.click()
|
||||||
|
|
||||||
|
const viewer = page.locator('[data-component="file"][data-mode="text"]').first()
|
||||||
|
await expect(viewer).toBeVisible()
|
||||||
|
return viewer
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fileComment(page: Parameters<typeof test>[0]["page"], note: string) {
|
||||||
|
const viewer = page.locator('[data-component="file"][data-mode="text"]').first()
|
||||||
|
await expect(viewer).toBeVisible()
|
||||||
|
|
||||||
|
const line = viewer.locator('diffs-container [data-line="2"]').first()
|
||||||
|
await expect(line).toBeVisible()
|
||||||
|
await line.hover()
|
||||||
|
|
||||||
|
const add = viewer.getByRole("button", { name: /^Comment$/ }).first()
|
||||||
|
await expect(add).toBeVisible()
|
||||||
|
await add.click()
|
||||||
|
|
||||||
|
const area = viewer.locator('[data-slot="line-comment-textarea"]').first()
|
||||||
|
await expect(area).toBeVisible()
|
||||||
|
await area.fill(note)
|
||||||
|
|
||||||
|
const submit = viewer.locator('[data-slot="line-comment-action"][data-variant="primary"]').first()
|
||||||
|
await expect(submit).toBeEnabled()
|
||||||
|
await submit.click()
|
||||||
|
|
||||||
|
await expect(viewer.locator('[data-slot="line-comment-content"]').filter({ hasText: note }).first()).toBeVisible()
|
||||||
|
await expect(viewer.locator('[data-slot="line-comment-tools"]').first()).toBeVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fileOverflow(page: Parameters<typeof test>[0]["page"]) {
|
||||||
|
const viewer = page.locator('[data-component="file"][data-mode="text"]').first()
|
||||||
|
const view = page.locator('[role="tabpanel"] .scroll-view__viewport').first()
|
||||||
|
const pop = viewer.locator('[data-slot="line-comment-popover"][data-inline-body]').first()
|
||||||
|
const tools = viewer.locator('[data-slot="line-comment-tools"]').first()
|
||||||
|
|
||||||
|
const [width, viewBox, popBox, toolsBox] = await Promise.all([
|
||||||
|
view.evaluate((el) => el.scrollWidth - el.clientWidth),
|
||||||
|
view.boundingBox(),
|
||||||
|
pop.boundingBox(),
|
||||||
|
tools.boundingBox(),
|
||||||
|
])
|
||||||
|
|
||||||
|
if (!viewBox || !popBox || !toolsBox) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
width,
|
||||||
|
pop: popBox.x + popBox.width - (viewBox.x + viewBox.width),
|
||||||
|
tools: toolsBox.x + toolsBox.width - (viewBox.x + viewBox.width),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("review applies inline comment clicks without horizontal overflow", async ({ page, withProject }) => {
|
||||||
|
test.setTimeout(180_000)
|
||||||
|
|
||||||
|
const tag = `review-comment-${Date.now()}`
|
||||||
|
const file = `review-comment-${tag}.txt`
|
||||||
|
const note = `comment ${tag}`
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1280, height: 900 })
|
||||||
|
|
||||||
|
await withProject(async (project) => {
|
||||||
|
const sdk = createSdk(project.directory)
|
||||||
|
|
||||||
|
await withSession(sdk, `e2e review comment ${tag}`, async (session) => {
|
||||||
|
await patch(sdk, session.id, seed([{ file, mark: tag }]))
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const diff = await sdk.session.diff({ sessionID: session.id }).then((res) => res.data ?? [])
|
||||||
|
return diff.length
|
||||||
|
},
|
||||||
|
{ timeout: 60_000 },
|
||||||
|
)
|
||||||
|
.toBe(1)
|
||||||
|
|
||||||
|
await project.gotoSession(session.id)
|
||||||
|
await show(page)
|
||||||
|
|
||||||
|
const tab = page.getByRole("tab", { name: /Review/i }).first()
|
||||||
|
await expect(tab).toBeVisible()
|
||||||
|
await tab.click()
|
||||||
|
|
||||||
|
await expand(page)
|
||||||
|
await waitMark(page, file, tag)
|
||||||
|
await comment(page, file, note)
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(async () => (await overflow(page, file))?.width ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
|
||||||
|
.toBeLessThanOrEqual(1)
|
||||||
|
await expect
|
||||||
|
.poll(async () => (await overflow(page, file))?.pop ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
|
||||||
|
.toBeLessThanOrEqual(1)
|
||||||
|
await expect
|
||||||
|
.poll(async () => (await overflow(page, file))?.tools ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
|
||||||
|
.toBeLessThanOrEqual(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("review file comments submit on click without clipping actions", async ({ page, withProject }) => {
|
||||||
|
test.setTimeout(180_000)
|
||||||
|
|
||||||
|
const tag = `review-file-comment-${Date.now()}`
|
||||||
|
const file = `review-file-comment-${tag}.txt`
|
||||||
|
const note = `comment ${tag}`
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1280, height: 900 })
|
||||||
|
|
||||||
|
await withProject(async (project) => {
|
||||||
|
const sdk = createSdk(project.directory)
|
||||||
|
|
||||||
|
await withSession(sdk, `e2e review file comment ${tag}`, async (session) => {
|
||||||
|
await patch(sdk, session.id, seed([{ file, mark: tag }]))
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const diff = await sdk.session.diff({ sessionID: session.id }).then((res) => res.data ?? [])
|
||||||
|
return diff.length
|
||||||
|
},
|
||||||
|
{ timeout: 60_000 },
|
||||||
|
)
|
||||||
|
.toBe(1)
|
||||||
|
|
||||||
|
await project.gotoSession(session.id)
|
||||||
|
await show(page)
|
||||||
|
|
||||||
|
const tab = page.getByRole("tab", { name: /Review/i }).first()
|
||||||
|
await expect(tab).toBeVisible()
|
||||||
|
await tab.click()
|
||||||
|
|
||||||
|
await expand(page)
|
||||||
|
await waitMark(page, file, tag)
|
||||||
|
await openReviewFile(page, file)
|
||||||
|
await fileComment(page, note)
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(async () => (await fileOverflow(page))?.width ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
|
||||||
|
.toBeLessThanOrEqual(1)
|
||||||
|
await expect
|
||||||
|
.poll(async () => (await fileOverflow(page))?.pop ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
|
||||||
|
.toBeLessThanOrEqual(1)
|
||||||
|
await expect
|
||||||
|
.poll(async () => (await fileOverflow(page))?.tools ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
|
||||||
|
.toBeLessThanOrEqual(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test("review keeps scroll position after a live diff update", async ({ page, withProject }) => {
|
test("review keeps scroll position after a live diff update", async ({ page, withProject }) => {
|
||||||
test.skip(Boolean(process.env.CI), "Flaky in CI for now.")
|
test.skip(Boolean(process.env.CI), "Flaky in CI for now.")
|
||||||
test.setTimeout(180_000)
|
test.setTimeout(180_000)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { openSettings, closeDialog, waitTerminalReady, withSession } from "../actions"
|
import { openSettings, closeDialog, waitTerminalFocusIdle, withSession } from "../actions"
|
||||||
import { keybindButtonSelector, terminalSelector } from "../selectors"
|
import { keybindButtonSelector, terminalSelector } from "../selectors"
|
||||||
import { modKey } from "../utils"
|
import { modKey } from "../utils"
|
||||||
|
|
||||||
@@ -302,7 +302,7 @@ test("changing terminal toggle keybind works", async ({ page, gotoSession }) =>
|
|||||||
await expect(terminal).not.toBeVisible()
|
await expect(terminal).not.toBeVisible()
|
||||||
|
|
||||||
await page.keyboard.press(`${modKey}+Y`)
|
await page.keyboard.press(`${modKey}+Y`)
|
||||||
await waitTerminalReady(page, { term: terminal })
|
await waitTerminalFocusIdle(page, { term: terminal })
|
||||||
|
|
||||||
await page.keyboard.press(`${modKey}+Y`)
|
await page.keyboard.press(`${modKey}+Y`)
|
||||||
await expect(terminal).not.toBeVisible()
|
await expect(terminal).not.toBeVisible()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { waitTerminalReady } from "../actions"
|
import { waitTerminalFocusIdle, waitTerminalReady } from "../actions"
|
||||||
import { promptSelector, terminalSelector } from "../selectors"
|
import { promptSelector, terminalSelector } from "../selectors"
|
||||||
import { terminalToggleKey } from "../utils"
|
import { terminalToggleKey } from "../utils"
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ test("smoke terminal mounts and can create a second tab", async ({ page, gotoSes
|
|||||||
await page.keyboard.press(terminalToggleKey)
|
await page.keyboard.press(terminalToggleKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
await waitTerminalReady(page, { term: terminals.first() })
|
await waitTerminalFocusIdle(page, { term: terminals.first() })
|
||||||
await expect(terminals).toHaveCount(1)
|
await expect(terminals).toHaveCount(1)
|
||||||
|
|
||||||
// Ghostty captures a lot of keybinds when focused; move focus back
|
// Ghostty captures a lot of keybinds when focused; move focus back
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.2.25",
|
"version": "1.2.27",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
"@solidjs/router": "catalog:",
|
"@solidjs/router": "catalog:",
|
||||||
"@thisbeyond/solid-dnd": "0.7.5",
|
"@thisbeyond/solid-dnd": "0.7.5",
|
||||||
"diff": "catalog:",
|
"diff": "catalog:",
|
||||||
"effect": "4.0.0-beta.31",
|
"effect": "catalog:",
|
||||||
"fuzzysort": "catalog:",
|
"fuzzysort": "catalog:",
|
||||||
"ghostty-web": "github:anomalyco/ghostty-web#main",
|
"ghostty-web": "github:anomalyco/ghostty-web#main",
|
||||||
"luxon": "catalog:",
|
"luxon": "catalog:",
|
||||||
|
|||||||
@@ -46,21 +46,13 @@ 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 Home = lazy(() => import("@/pages/home"))
|
const HomeRoute = lazy(() => import("@/pages/home"))
|
||||||
const Session = lazy(() => import("@/pages/session"))
|
const Session = lazy(() => import("@/pages/session"))
|
||||||
const Loading = () => <div class="size-full" />
|
const Loading = () => <div class="size-full" />
|
||||||
|
|
||||||
const HomeRoute = () => (
|
|
||||||
<Suspense fallback={<Loading />}>
|
|
||||||
<Home />
|
|
||||||
</Suspense>
|
|
||||||
)
|
|
||||||
|
|
||||||
const SessionRoute = () => (
|
const SessionRoute = () => (
|
||||||
<SessionProviders>
|
<SessionProviders>
|
||||||
<Suspense fallback={<Loading />}>
|
|
||||||
<Session />
|
<Session />
|
||||||
</Suspense>
|
|
||||||
</SessionProviders>
|
</SessionProviders>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -124,8 +116,10 @@ function SessionProviders(props: ParentProps) {
|
|||||||
function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
|
function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
|
||||||
return (
|
return (
|
||||||
<AppShellProviders>
|
<AppShellProviders>
|
||||||
|
<Suspense fallback={<Loading />}>
|
||||||
{props.appChildren}
|
{props.appChildren}
|
||||||
{props.children}
|
{props.children}
|
||||||
|
</Suspense>
|
||||||
</AppShellProviders>
|
</AppShellProviders>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -265,6 +259,15 @@ function ConnectionError(props: { onRetry?: () => void; onServerSelected?: (key:
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ServerKey(props: ParentProps) {
|
||||||
|
const server = useServer()
|
||||||
|
return (
|
||||||
|
<Show when={server.key} keyed>
|
||||||
|
{props.children}
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function AppInterface(props: {
|
export function AppInterface(props: {
|
||||||
children?: JSX.Element
|
children?: JSX.Element
|
||||||
defaultServer: ServerConnection.Key
|
defaultServer: ServerConnection.Key
|
||||||
@@ -275,6 +278,7 @@ export function AppInterface(props: {
|
|||||||
return (
|
return (
|
||||||
<ServerProvider defaultServer={props.defaultServer} servers={props.servers}>
|
<ServerProvider defaultServer={props.defaultServer} servers={props.servers}>
|
||||||
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
|
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
|
||||||
|
<ServerKey>
|
||||||
<GlobalSDKProvider>
|
<GlobalSDKProvider>
|
||||||
<GlobalSyncProvider>
|
<GlobalSyncProvider>
|
||||||
<Dynamic
|
<Dynamic
|
||||||
@@ -289,6 +293,7 @@ export function AppInterface(props: {
|
|||||||
</Dynamic>
|
</Dynamic>
|
||||||
</GlobalSyncProvider>
|
</GlobalSyncProvider>
|
||||||
</GlobalSDKProvider>
|
</GlobalSDKProvider>
|
||||||
|
</ServerKey>
|
||||||
</ConnectionGate>
|
</ConnectionGate>
|
||||||
</ServerProvider>
|
</ServerProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { Link } from "@/components/link"
|
|||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { usePlatform } from "@/context/platform"
|
|
||||||
import { DialogSelectModel } from "./dialog-select-model"
|
import { DialogSelectModel } from "./dialog-select-model"
|
||||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||||
|
|
||||||
@@ -23,7 +22,6 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const globalSync = useGlobalSync()
|
const globalSync = useGlobalSync()
|
||||||
const globalSDK = useGlobalSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
const platform = usePlatform()
|
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
|
||||||
const alive = { value: true }
|
const alive = { value: true }
|
||||||
@@ -49,13 +47,14 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
methodIndex: undefined as undefined | number,
|
methodIndex: undefined as undefined | number,
|
||||||
authorization: undefined as undefined | ProviderAuthAuthorization,
|
authorization: undefined as undefined | ProviderAuthAuthorization,
|
||||||
state: "pending" as undefined | "pending" | "complete" | "error",
|
state: "pending" as undefined | "pending" | "complete" | "error" | "prompt",
|
||||||
error: undefined as string | undefined,
|
error: undefined as string | undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
type Action =
|
type Action =
|
||||||
| { type: "method.select"; index: number }
|
| { type: "method.select"; index: number }
|
||||||
| { type: "method.reset" }
|
| { type: "method.reset" }
|
||||||
|
| { type: "auth.prompt" }
|
||||||
| { type: "auth.pending" }
|
| { type: "auth.pending" }
|
||||||
| { type: "auth.complete"; authorization: ProviderAuthAuthorization }
|
| { type: "auth.complete"; authorization: ProviderAuthAuthorization }
|
||||||
| { type: "auth.error"; error: string }
|
| { type: "auth.error"; error: string }
|
||||||
@@ -77,6 +76,11 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
draft.error = undefined
|
draft.error = undefined
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (action.type === "auth.prompt") {
|
||||||
|
draft.state = "prompt"
|
||||||
|
draft.error = undefined
|
||||||
|
return
|
||||||
|
}
|
||||||
if (action.type === "auth.pending") {
|
if (action.type === "auth.pending") {
|
||||||
draft.state = "pending"
|
draft.state = "pending"
|
||||||
draft.error = undefined
|
draft.error = undefined
|
||||||
@@ -120,7 +124,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectMethod(index: number) {
|
async function selectMethod(index: number, inputs?: Record<string, string>) {
|
||||||
if (timer.current !== undefined) {
|
if (timer.current !== undefined) {
|
||||||
clearTimeout(timer.current)
|
clearTimeout(timer.current)
|
||||||
timer.current = undefined
|
timer.current = undefined
|
||||||
@@ -130,6 +134,10 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
dispatch({ type: "method.select", index })
|
dispatch({ type: "method.select", index })
|
||||||
|
|
||||||
if (method.type === "oauth") {
|
if (method.type === "oauth") {
|
||||||
|
if (method.prompts?.length && !inputs) {
|
||||||
|
dispatch({ type: "auth.prompt" })
|
||||||
|
return
|
||||||
|
}
|
||||||
dispatch({ type: "auth.pending" })
|
dispatch({ type: "auth.pending" })
|
||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
await globalSDK.client.provider.oauth
|
await globalSDK.client.provider.oauth
|
||||||
@@ -137,6 +145,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
{
|
{
|
||||||
providerID: props.provider,
|
providerID: props.provider,
|
||||||
method: index,
|
method: index,
|
||||||
|
inputs,
|
||||||
},
|
},
|
||||||
{ throwOnError: true },
|
{ throwOnError: true },
|
||||||
)
|
)
|
||||||
@@ -163,6 +172,122 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function OAuthPromptsView() {
|
||||||
|
const [formStore, setFormStore] = createStore({
|
||||||
|
value: {} as Record<string, string>,
|
||||||
|
index: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
const prompts = createMemo(() => method()?.prompts ?? [])
|
||||||
|
const matches = (prompt: NonNullable<ReturnType<typeof prompts>[number]>, value: Record<string, string>) => {
|
||||||
|
if (!prompt.when) return true
|
||||||
|
const actual = value[prompt.when.key]
|
||||||
|
if (actual === undefined) return false
|
||||||
|
return prompt.when.op === "eq" ? actual === prompt.when.value : actual !== prompt.when.value
|
||||||
|
}
|
||||||
|
const current = createMemo(() => {
|
||||||
|
const all = prompts()
|
||||||
|
const index = all.findIndex((prompt, index) => index >= formStore.index && matches(prompt, formStore.value))
|
||||||
|
if (index === -1) return
|
||||||
|
return {
|
||||||
|
index,
|
||||||
|
prompt: all[index],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const valid = createMemo(() => {
|
||||||
|
const item = current()
|
||||||
|
if (!item || item.prompt.type !== "text") return false
|
||||||
|
const value = formStore.value[item.prompt.key] ?? ""
|
||||||
|
return value.trim().length > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
async function next(index: number, value: Record<string, string>) {
|
||||||
|
if (store.methodIndex === undefined) return
|
||||||
|
const next = prompts().findIndex((prompt, i) => i > index && matches(prompt, value))
|
||||||
|
if (next !== -1) {
|
||||||
|
setFormStore("index", next)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await selectMethod(store.methodIndex, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit(e: SubmitEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
const item = current()
|
||||||
|
if (!item || item.prompt.type !== "text") return
|
||||||
|
if (!valid()) return
|
||||||
|
await next(item.index, formStore.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = () => current()
|
||||||
|
const text = createMemo(() => {
|
||||||
|
const prompt = item()?.prompt
|
||||||
|
if (!prompt || prompt.type !== "text") return
|
||||||
|
return prompt
|
||||||
|
})
|
||||||
|
const select = createMemo(() => {
|
||||||
|
const prompt = item()?.prompt
|
||||||
|
if (!prompt || prompt.type !== "select") return
|
||||||
|
return prompt
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
|
||||||
|
<Switch>
|
||||||
|
<Match when={item()?.prompt.type === "text"}>
|
||||||
|
<TextField
|
||||||
|
type="text"
|
||||||
|
label={text()?.message ?? ""}
|
||||||
|
placeholder={text()?.placeholder}
|
||||||
|
value={text() ? (formStore.value[text()!.key] ?? "") : ""}
|
||||||
|
onChange={(value) => {
|
||||||
|
const prompt = text()
|
||||||
|
if (!prompt) return
|
||||||
|
setFormStore("value", prompt.key, value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button class="w-auto" type="submit" size="large" variant="primary" disabled={!valid()}>
|
||||||
|
{language.t("common.continue")}
|
||||||
|
</Button>
|
||||||
|
</Match>
|
||||||
|
<Match when={item()?.prompt.type === "select"}>
|
||||||
|
<div class="w-full flex flex-col gap-1.5">
|
||||||
|
<div class="text-14-regular text-text-base">{select()?.message}</div>
|
||||||
|
<div>
|
||||||
|
<List
|
||||||
|
items={select()?.options ?? []}
|
||||||
|
key={(x) => x.value}
|
||||||
|
current={select()?.options.find((x) => x.value === formStore.value[select()!.key])}
|
||||||
|
onSelect={(value) => {
|
||||||
|
if (!value) return
|
||||||
|
const prompt = select()
|
||||||
|
if (!prompt) return
|
||||||
|
const nextValue = {
|
||||||
|
...formStore.value,
|
||||||
|
[prompt.key]: value.value,
|
||||||
|
}
|
||||||
|
setFormStore("value", prompt.key, value.value)
|
||||||
|
void next(item()!.index, nextValue)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(option) => (
|
||||||
|
<div class="w-full flex items-center gap-x-2">
|
||||||
|
<div class="w-4 h-2 rounded-[1px] bg-input-base shadow-xs-border-base flex items-center justify-center">
|
||||||
|
<div class="w-2.5 h-0.5 ml-0 bg-icon-strong-base hidden" data-slot="list-item-extra-icon" />
|
||||||
|
</div>
|
||||||
|
<span>{option.label}</span>
|
||||||
|
<span class="text-14-regular text-text-weak">{option.hint}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
let listRef: ListRef | undefined
|
let listRef: ListRef | undefined
|
||||||
function handleKey(e: KeyboardEvent) {
|
function handleKey(e: KeyboardEvent) {
|
||||||
if (e.key === "Enter" && e.target instanceof HTMLInputElement) {
|
if (e.key === "Enter" && e.target instanceof HTMLInputElement) {
|
||||||
@@ -301,7 +426,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
error={formStore.error}
|
error={formStore.error}
|
||||||
/>
|
/>
|
||||||
<Button class="w-auto" type="submit" size="large" variant="primary">
|
<Button class="w-auto" type="submit" size="large" variant="primary">
|
||||||
{language.t("common.submit")}
|
{language.t("common.continue")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -314,12 +439,6 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
error: undefined as string | undefined,
|
error: undefined as string | undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
if (store.authorization?.method === "code" && store.authorization?.url) {
|
|
||||||
platform.openLink(store.authorization.url)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
async function handleSubmit(e: SubmitEvent) {
|
async function handleSubmit(e: SubmitEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
@@ -368,7 +487,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
error={formStore.error}
|
error={formStore.error}
|
||||||
/>
|
/>
|
||||||
<Button class="w-auto" type="submit" size="large" variant="primary">
|
<Button class="w-auto" type="submit" size="large" variant="primary">
|
||||||
{language.t("common.submit")}
|
{language.t("common.continue")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -386,10 +505,6 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
if (store.authorization?.url) {
|
|
||||||
platform.openLink(store.authorization.url)
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await globalSDK.client.provider.oauth
|
const result = await globalSDK.client.provider.oauth
|
||||||
.callback({
|
.callback({
|
||||||
providerID: props.provider,
|
providerID: props.provider,
|
||||||
@@ -470,6 +585,9 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
|
<Match when={store.state === "prompt"}>
|
||||||
|
<OAuthPromptsView />
|
||||||
|
</Match>
|
||||||
<Match when={store.state === "error"}>
|
<Match when={store.state === "error"}>
|
||||||
<div class="text-14-regular text-text-base">
|
<div class="text-14-regular text-text-base">
|
||||||
<div class="flex items-center gap-x-2">
|
<div class="flex items-center gap-x-2">
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export const DialogFork: Component = () => {
|
|||||||
directory: sdk.directory,
|
directory: sdk.directory,
|
||||||
attachmentName: language.t("common.attachment"),
|
attachmentName: language.t("common.attachment"),
|
||||||
})
|
})
|
||||||
|
const dir = base64Encode(sdk.directory)
|
||||||
|
|
||||||
sdk.client.session
|
sdk.client.session
|
||||||
.fork({ sessionID, messageID: item.id })
|
.fork({ sessionID, messageID: item.id })
|
||||||
@@ -75,10 +76,8 @@ export const DialogFork: Component = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
dialog.close()
|
dialog.close()
|
||||||
navigate(`/${base64Encode(sdk.directory)}/session/${forked.data.id}`)
|
prompt.set(restored, undefined, { dir, id: forked.data.id })
|
||||||
requestAnimationFrame(() => {
|
navigate(`/${dir}/session/${forked.data.id}`)
|
||||||
prompt.set(restored)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.catch((err: unknown) => {
|
.catch((err: unknown) => {
|
||||||
const message = err instanceof Error ? err.message : String(err)
|
const message = err instanceof Error ? err.message : String(err)
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ import { DialogSelectProvider } from "./dialog-select-provider"
|
|||||||
import { ModelTooltip } from "./model-tooltip"
|
import { ModelTooltip } from "./model-tooltip"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
|
||||||
export const DialogSelectModelUnpaid: Component = () => {
|
type ModelState = ReturnType<typeof useLocal>["model"]
|
||||||
const local = useLocal()
|
|
||||||
|
export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props) => {
|
||||||
|
const model = props.model ?? useLocal().model
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const providers = useProviders()
|
const providers = useProviders()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
@@ -35,8 +37,8 @@ export const DialogSelectModelUnpaid: Component = () => {
|
|||||||
<List
|
<List
|
||||||
class="[&_[data-slot=list-scroll]]:overflow-visible"
|
class="[&_[data-slot=list-scroll]]:overflow-visible"
|
||||||
ref={(ref) => (listRef = ref)}
|
ref={(ref) => (listRef = ref)}
|
||||||
items={local.model.list}
|
items={model.list}
|
||||||
current={local.model.current()}
|
current={model.current()}
|
||||||
key={(x) => `${x.provider.id}:${x.id}`}
|
key={(x) => `${x.provider.id}:${x.id}`}
|
||||||
itemWrapper={(item, node) => (
|
itemWrapper={(item, node) => (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@@ -55,7 +57,7 @@ export const DialogSelectModelUnpaid: Component = () => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
onSelect={(x) => {
|
onSelect={(x) => {
|
||||||
local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
|
model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
|
||||||
recent: true,
|
recent: true,
|
||||||
})
|
})
|
||||||
dialog.close()
|
dialog.close()
|
||||||
|
|||||||
@@ -18,19 +18,22 @@ import { useLanguage } from "@/context/language"
|
|||||||
const isFree = (provider: string, cost: { input: number } | undefined) =>
|
const isFree = (provider: string, cost: { input: number } | undefined) =>
|
||||||
provider === "opencode" && (!cost || cost.input === 0)
|
provider === "opencode" && (!cost || cost.input === 0)
|
||||||
|
|
||||||
|
type ModelState = ReturnType<typeof useLocal>["model"]
|
||||||
|
|
||||||
const ModelList: Component<{
|
const ModelList: Component<{
|
||||||
provider?: string
|
provider?: string
|
||||||
class?: string
|
class?: string
|
||||||
onSelect: () => void
|
onSelect: () => void
|
||||||
action?: JSX.Element
|
action?: JSX.Element
|
||||||
|
model?: ModelState
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const local = useLocal()
|
const model = props.model ?? useLocal().model
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
|
||||||
const models = createMemo(() =>
|
const models = createMemo(() =>
|
||||||
local.model
|
model
|
||||||
.list()
|
.list()
|
||||||
.filter((m) => local.model.visible({ modelID: m.id, providerID: m.provider.id }))
|
.filter((m) => model.visible({ modelID: m.id, providerID: m.provider.id }))
|
||||||
.filter((m) => (props.provider ? m.provider.id === props.provider : true)),
|
.filter((m) => (props.provider ? m.provider.id === props.provider : true)),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -41,7 +44,7 @@ const ModelList: Component<{
|
|||||||
emptyMessage={language.t("dialog.model.empty")}
|
emptyMessage={language.t("dialog.model.empty")}
|
||||||
key={(x) => `${x.provider.id}:${x.id}`}
|
key={(x) => `${x.provider.id}:${x.id}`}
|
||||||
items={models}
|
items={models}
|
||||||
current={local.model.current()}
|
current={model.current()}
|
||||||
filterKeys={["provider.name", "name", "id"]}
|
filterKeys={["provider.name", "name", "id"]}
|
||||||
sortBy={(a, b) => a.name.localeCompare(b.name)}
|
sortBy={(a, b) => a.name.localeCompare(b.name)}
|
||||||
groupBy={(x) => x.provider.name}
|
groupBy={(x) => x.provider.name}
|
||||||
@@ -63,7 +66,7 @@ const ModelList: Component<{
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
onSelect={(x) => {
|
onSelect={(x) => {
|
||||||
local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
|
model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
|
||||||
recent: true,
|
recent: true,
|
||||||
})
|
})
|
||||||
props.onSelect()
|
props.onSelect()
|
||||||
@@ -88,6 +91,7 @@ type ModelSelectorTriggerProps = Omit<ComponentProps<typeof Kobalte.Trigger>, "a
|
|||||||
|
|
||||||
export function ModelSelectorPopover(props: {
|
export function ModelSelectorPopover(props: {
|
||||||
provider?: string
|
provider?: string
|
||||||
|
model?: ModelState
|
||||||
children?: JSX.Element
|
children?: JSX.Element
|
||||||
triggerAs?: ValidComponent
|
triggerAs?: ValidComponent
|
||||||
triggerProps?: ModelSelectorTriggerProps
|
triggerProps?: ModelSelectorTriggerProps
|
||||||
@@ -151,6 +155,7 @@ export function ModelSelectorPopover(props: {
|
|||||||
<Kobalte.Title class="sr-only">{language.t("dialog.model.select.title")}</Kobalte.Title>
|
<Kobalte.Title class="sr-only">{language.t("dialog.model.select.title")}</Kobalte.Title>
|
||||||
<ModelList
|
<ModelList
|
||||||
provider={props.provider}
|
provider={props.provider}
|
||||||
|
model={props.model}
|
||||||
onSelect={() => setStore("open", false)}
|
onSelect={() => setStore("open", false)}
|
||||||
class="p-1"
|
class="p-1"
|
||||||
action={
|
action={
|
||||||
@@ -184,7 +189,7 @@ export function ModelSelectorPopover(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
|
export const DialogSelectModel: Component<{ provider?: string; model?: ModelState }> = (props) => {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
|
||||||
@@ -202,7 +207,7 @@ export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ModelList provider={props.provider} onSelect={() => dialog.close()} />
|
<ModelList provider={props.provider} model={props.model} onSelect={() => dialog.close()} />
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="ml-3 mt-5 mb-6 text-text-base self-start"
|
class="ml-3 mt-5 mb-6 text-text-base self-start"
|
||||||
|
|||||||
@@ -291,8 +291,8 @@ export function DialogSelectServer() {
|
|||||||
navigate("/")
|
navigate("/")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
server.setActive(ServerConnection.key(conn))
|
|
||||||
navigate("/")
|
navigate("/")
|
||||||
|
queueMicrotask(() => server.setActive(ServerConnection.key(conn)))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAddChange = (value: string) => {
|
const handleAddChange = (value: string) => {
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
||||||
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
||||||
import { createEffect, on, Component, Show, onCleanup, Switch, Match, createMemo, createSignal } from "solid-js"
|
import { createEffect, on, Component, Show, onCleanup, createMemo, createSignal } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { createFocusSignal } from "@solid-primitives/active-element"
|
|
||||||
import { useLocal } from "@/context/local"
|
import { useLocal } from "@/context/local"
|
||||||
import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file"
|
import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file"
|
||||||
import {
|
import {
|
||||||
@@ -37,6 +36,7 @@ import { useLanguage } from "@/context/language"
|
|||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
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 { promptEnabled, promptProbe } from "@/testing/prompt"
|
||||||
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 } from "./prompt-input/files"
|
||||||
@@ -121,7 +121,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
let slashPopoverRef!: HTMLDivElement
|
let slashPopoverRef!: HTMLDivElement
|
||||||
|
|
||||||
const mirror = { input: false }
|
const mirror = { input: false }
|
||||||
const inset = 52
|
const inset = 56
|
||||||
const space = `${inset}px`
|
const space = `${inset}px`
|
||||||
|
|
||||||
const scrollCursorIntoView = () => {
|
const scrollCursorIntoView = () => {
|
||||||
@@ -244,6 +244,23 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
const working = createMemo(() => status()?.type !== "idle")
|
const working = createMemo(() => status()?.type !== "idle")
|
||||||
|
const tip = () => {
|
||||||
|
if (working()) {
|
||||||
|
return (
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span>{language.t("prompt.action.stop")}</span>
|
||||||
|
<span class="text-icon-base text-12-medium text-[10px]!">{language.t("common.key.esc")}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span>{language.t("prompt.action.send")}</span>
|
||||||
|
<Icon name="enter" size="small" class="text-icon-base" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
const imageAttachments = createMemo(() =>
|
const imageAttachments = createMemo(() =>
|
||||||
prompt.current().filter((part): part is ImageAttachmentPart => part.type === "image"),
|
prompt.current().filter((part): part is ImageAttachmentPart => part.type === "image"),
|
||||||
)
|
)
|
||||||
@@ -411,7 +428,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFocused = createFocusSignal(() => editorRef)
|
|
||||||
const escBlur = () => platform.platform === "desktop" && platform.os === "macos"
|
const escBlur = () => platform.platform === "desktop" && platform.os === "macos"
|
||||||
|
|
||||||
const pick = () => fileInputRef?.click()
|
const pick = () => fileInputRef?.click()
|
||||||
@@ -606,6 +622,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
|
|
||||||
const handleSlashSelect = (cmd: SlashCommand | undefined) => {
|
const handleSlashSelect = (cmd: SlashCommand | undefined) => {
|
||||||
if (!cmd) return
|
if (!cmd) return
|
||||||
|
promptProbe.select(cmd.id)
|
||||||
closePopover()
|
closePopover()
|
||||||
|
|
||||||
if (cmd.type === "custom") {
|
if (cmd.type === "custom") {
|
||||||
@@ -694,6 +711,20 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (promptEnabled()) {
|
||||||
|
createEffect(() => {
|
||||||
|
promptProbe.set({
|
||||||
|
popover: store.popover,
|
||||||
|
slash: {
|
||||||
|
active: slashActive() ?? null,
|
||||||
|
ids: slashFlat().map((cmd) => cmd.id),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onCleanup(() => promptProbe.clear())
|
||||||
|
}
|
||||||
|
|
||||||
const selectPopoverActive = () => {
|
const selectPopoverActive = () => {
|
||||||
if (store.popover === "at") {
|
if (store.popover === "at") {
|
||||||
const items = atFlat()
|
const items = atFlat()
|
||||||
@@ -1014,7 +1045,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
|
|
||||||
const { addAttachment, removeAttachment, handlePaste } = createPromptAttachments({
|
const { addAttachment, removeAttachment, handlePaste } = createPromptAttachments({
|
||||||
editor: () => editorRef,
|
editor: () => editorRef,
|
||||||
isFocused,
|
|
||||||
isDialogActive: () => !!dialog.active,
|
isDialogActive: () => !!dialog.active,
|
||||||
setDraggingType: (type) => setStore("draggingType", type),
|
setDraggingType: (type) => setStore("draggingType", type),
|
||||||
focusEditor: () => {
|
focusEditor: () => {
|
||||||
@@ -1031,6 +1061,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
if (!id) return permission.isAutoAcceptingDirectory(sdk.directory)
|
if (!id) return permission.isAutoAcceptingDirectory(sdk.directory)
|
||||||
return permission.isAutoAccepting(id, sdk.directory)
|
return permission.isAutoAccepting(id, sdk.directory)
|
||||||
})
|
})
|
||||||
|
const acceptLabel = createMemo(() =>
|
||||||
|
language.t(accepting() ? "command.permissions.autoaccept.disable" : "command.permissions.autoaccept.enable"),
|
||||||
|
)
|
||||||
|
const toggleAccept = () => {
|
||||||
|
if (!params.id) {
|
||||||
|
permission.toggleAutoAcceptDirectory(sdk.directory)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
permission.toggleAutoAccept(params.id, sdk.directory)
|
||||||
|
}
|
||||||
|
|
||||||
const { abort, handleSubmit } = createPromptSubmit({
|
const { abort, handleSubmit } = createPromptSubmit({
|
||||||
info,
|
info,
|
||||||
@@ -1200,6 +1241,20 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
|
|
||||||
// Note: Shift+Enter is handled earlier, before IME check
|
// Note: Shift+Enter is handled earlier, before IME check
|
||||||
if (event.key === "Enter" && !event.shiftKey) {
|
if (event.key === "Enter" && !event.shiftKey) {
|
||||||
|
event.preventDefault()
|
||||||
|
if (event.repeat) return
|
||||||
|
if (
|
||||||
|
working() &&
|
||||||
|
prompt
|
||||||
|
.current()
|
||||||
|
.map((part) => ("content" in part ? part.content : ""))
|
||||||
|
.join("")
|
||||||
|
.trim().length === 0 &&
|
||||||
|
imageAttachments().length === 0 &&
|
||||||
|
commentCount() === 0
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
handleSubmit(event)
|
handleSubmit(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1328,18 +1383,41 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
|
multiple
|
||||||
accept={ACCEPTED_FILE_TYPES.join(",")}
|
accept={ACCEPTED_FILE_TYPES.join(",")}
|
||||||
class="hidden"
|
class="hidden"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const file = e.currentTarget.files?.[0]
|
const list = e.currentTarget.files
|
||||||
if (file) void addAttachment(file)
|
if (list) {
|
||||||
|
for (const file of Array.from(list)) {
|
||||||
|
void addAttachment(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
e.currentTarget.value = ""
|
e.currentTarget.value = ""
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-1 pointer-events-auto">
|
||||||
|
<Tooltip placement="top" inactive={!prompt.dirty() && !working()} value={tip()}>
|
||||||
|
<IconButton
|
||||||
|
data-action="prompt-submit"
|
||||||
|
type="submit"
|
||||||
|
disabled={store.mode !== "normal" || (!prompt.dirty() && !working() && commentCount() === 0)}
|
||||||
|
tabIndex={store.mode === "normal" ? undefined : -1}
|
||||||
|
icon={working() ? "stop" : "arrow-up"}
|
||||||
|
variant="primary"
|
||||||
|
class="size-8"
|
||||||
|
style={buttons()}
|
||||||
|
aria-label={working() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pointer-events-none absolute bottom-2 left-2">
|
||||||
<div
|
<div
|
||||||
aria-hidden={store.mode !== "normal"}
|
aria-hidden={store.mode !== "normal"}
|
||||||
class="flex items-center gap-1"
|
class="pointer-events-auto"
|
||||||
style={{
|
style={{
|
||||||
"pointer-events": buttonsSpring() > 0.5 ? "auto" : "none",
|
"pointer-events": buttonsSpring() > 0.5 ? "auto" : "none",
|
||||||
}}
|
}}
|
||||||
@@ -1363,81 +1441,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
<Icon name="plus" class="size-4.5" />
|
<Icon name="plus" class="size-4.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
|
|
||||||
<Tooltip
|
|
||||||
placement="top"
|
|
||||||
inactive={!prompt.dirty() && !working()}
|
|
||||||
value={
|
|
||||||
<Switch>
|
|
||||||
<Match when={working()}>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span>{language.t("prompt.action.stop")}</span>
|
|
||||||
<span class="text-icon-base text-12-medium text-[10px]!">{language.t("common.key.esc")}</span>
|
|
||||||
</div>
|
|
||||||
</Match>
|
|
||||||
<Match when={true}>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span>{language.t("prompt.action.send")}</span>
|
|
||||||
<Icon name="enter" size="small" class="text-icon-base" />
|
|
||||||
</div>
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
data-action="prompt-submit"
|
|
||||||
type="submit"
|
|
||||||
disabled={store.mode !== "normal" || (!prompt.dirty() && !working() && commentCount() === 0)}
|
|
||||||
tabIndex={store.mode === "normal" ? undefined : -1}
|
|
||||||
icon={working() ? "stop" : "arrow-up"}
|
|
||||||
variant="primary"
|
|
||||||
class="size-8"
|
|
||||||
style={buttons()}
|
|
||||||
aria-label={working() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pointer-events-none absolute bottom-2 left-2">
|
|
||||||
<div class="pointer-events-auto">
|
|
||||||
<TooltipKeybind
|
|
||||||
placement="top"
|
|
||||||
gutter={8}
|
|
||||||
title={language.t(
|
|
||||||
accepting() ? "command.permissions.autoaccept.disable" : "command.permissions.autoaccept.enable",
|
|
||||||
)}
|
|
||||||
keybind={command.keybind("permissions.autoaccept")}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
data-action="prompt-permissions"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => {
|
|
||||||
if (!params.id) {
|
|
||||||
permission.toggleAutoAcceptDirectory(sdk.directory)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
permission.toggleAutoAccept(params.id, sdk.directory)
|
|
||||||
}}
|
|
||||||
classList={{
|
|
||||||
"size-6 flex items-center justify-center": true,
|
|
||||||
"text-text-base": !accepting(),
|
|
||||||
"hover:bg-surface-success-base": accepting(),
|
|
||||||
}}
|
|
||||||
aria-label={
|
|
||||||
accepting()
|
|
||||||
? language.t("command.permissions.autoaccept.disable")
|
|
||||||
: language.t("command.permissions.autoaccept.enable")
|
|
||||||
}
|
|
||||||
aria-pressed={accepting()}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name="chevron-double-right"
|
|
||||||
size="small"
|
|
||||||
classList={{ "text-icon-success-base": accepting() }}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</TooltipKeybind>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1457,6 +1460,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
<div class="size-4 shrink-0" />
|
<div class="size-4 shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1.5 min-w-0 flex-1">
|
<div class="flex items-center gap-1.5 min-w-0 flex-1">
|
||||||
|
<div data-component="prompt-agent-control">
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
placement="top"
|
placement="top"
|
||||||
gutter={4}
|
gutter={4}
|
||||||
@@ -1468,12 +1472,15 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
options={agentNames()}
|
options={agentNames()}
|
||||||
current={local.agent.current()?.name ?? ""}
|
current={local.agent.current()?.name ?? ""}
|
||||||
onSelect={local.agent.set}
|
onSelect={local.agent.set}
|
||||||
class="capitalize max-w-[160px]"
|
class="capitalize max-w-[160px] text-text-base"
|
||||||
valueClass="truncate text-13-regular"
|
valueClass="truncate text-13-regular text-text-base"
|
||||||
triggerStyle={control()}
|
triggerStyle={control()}
|
||||||
|
triggerProps={{ "data-action": "prompt-agent" }}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
/>
|
/>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
|
</div>
|
||||||
|
<div data-component="prompt-model-control">
|
||||||
<Show
|
<Show
|
||||||
when={providers.paid().length > 0}
|
when={providers.paid().length > 0}
|
||||||
fallback={
|
fallback={
|
||||||
@@ -1484,12 +1491,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
keybind={command.keybind("model.choose")}
|
keybind={command.keybind("model.choose")}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
|
data-action="prompt-model"
|
||||||
as="div"
|
as="div"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="normal"
|
size="normal"
|
||||||
class="min-w-0 max-w-[320px] text-13-regular group"
|
class="min-w-0 max-w-[320px] text-13-regular text-text-base group"
|
||||||
style={control()}
|
style={control()}
|
||||||
onClick={() => dialog.show(() => <DialogSelectModelUnpaid />)}
|
onClick={() => dialog.show(() => <DialogSelectModelUnpaid model={local.model} />)}
|
||||||
>
|
>
|
||||||
<Show when={local.model.current()?.provider?.id}>
|
<Show when={local.model.current()?.provider?.id}>
|
||||||
<ProviderIcon
|
<ProviderIcon
|
||||||
@@ -1513,12 +1521,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
keybind={command.keybind("model.choose")}
|
keybind={command.keybind("model.choose")}
|
||||||
>
|
>
|
||||||
<ModelSelectorPopover
|
<ModelSelectorPopover
|
||||||
|
model={local.model}
|
||||||
triggerAs={Button}
|
triggerAs={Button}
|
||||||
triggerProps={{
|
triggerProps={{
|
||||||
variant: "ghost",
|
variant: "ghost",
|
||||||
size: "normal",
|
size: "normal",
|
||||||
style: control(),
|
style: control(),
|
||||||
class: "min-w-0 max-w-[320px] text-13-regular group",
|
class: "min-w-0 max-w-[320px] text-13-regular text-text-base group",
|
||||||
|
"data-action": "prompt-model",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show when={local.model.current()?.provider?.id}>
|
<Show when={local.model.current()?.provider?.id}>
|
||||||
@@ -1535,6 +1545,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
</ModelSelectorPopover>
|
</ModelSelectorPopover>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</Show>
|
</Show>
|
||||||
|
</div>
|
||||||
|
<div data-component="prompt-variant-control">
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
placement="top"
|
placement="top"
|
||||||
gutter={4}
|
gutter={4}
|
||||||
@@ -1547,13 +1559,37 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
current={local.model.variant.current() ?? "default"}
|
current={local.model.variant.current() ?? "default"}
|
||||||
label={(x) => (x === "default" ? language.t("common.default") : x)}
|
label={(x) => (x === "default" ? language.t("common.default") : x)}
|
||||||
onSelect={(x) => local.model.variant.set(x === "default" ? undefined : x)}
|
onSelect={(x) => local.model.variant.set(x === "default" ? undefined : x)}
|
||||||
class="capitalize max-w-[160px]"
|
class="capitalize max-w-[160px] text-text-base"
|
||||||
valueClass="truncate text-13-regular"
|
valueClass="truncate text-13-regular text-text-base"
|
||||||
triggerStyle={control()}
|
triggerStyle={control()}
|
||||||
|
triggerProps={{ "data-action": "prompt-model-variant" }}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
/>
|
/>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</div>
|
</div>
|
||||||
|
<TooltipKeybind
|
||||||
|
placement="top"
|
||||||
|
gutter={8}
|
||||||
|
title={acceptLabel()}
|
||||||
|
keybind={command.keybind("permissions.autoaccept")}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
data-action="prompt-permissions"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={toggleAccept}
|
||||||
|
classList={{
|
||||||
|
"h-7 w-7 p-0 shrink-0 flex items-center justify-center": true,
|
||||||
|
"text-text-base": !accepting(),
|
||||||
|
"hover:bg-surface-success-base": accepting(),
|
||||||
|
}}
|
||||||
|
style={control()}
|
||||||
|
aria-label={acceptLabel()}
|
||||||
|
aria-pressed={accepting()}
|
||||||
|
>
|
||||||
|
<Icon name="shield" size="small" classList={{ "text-icon-success-base": accepting() }} />
|
||||||
|
</Button>
|
||||||
|
</TooltipKeybind>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DockTray>
|
</DockTray>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { attachmentMime } from "./files"
|
import { attachmentMime } from "./files"
|
||||||
|
import { pasteMode } from "./paste"
|
||||||
|
|
||||||
describe("attachmentMime", () => {
|
describe("attachmentMime", () => {
|
||||||
test("keeps PDFs when the browser reports the mime", async () => {
|
test("keeps PDFs when the browser reports the mime", async () => {
|
||||||
@@ -22,3 +23,22 @@ describe("attachmentMime", () => {
|
|||||||
expect(await attachmentMime(file)).toBeUndefined()
|
expect(await attachmentMime(file)).toBeUndefined()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("pasteMode", () => {
|
||||||
|
test("uses native paste for short single-line text", () => {
|
||||||
|
expect(pasteMode("hello world")).toBe("native")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("uses manual paste for multiline text", () => {
|
||||||
|
expect(
|
||||||
|
pasteMode(`{
|
||||||
|
"ok": true
|
||||||
|
}`),
|
||||||
|
).toBe("manual")
|
||||||
|
expect(pasteMode("a\r\nb")).toBe("manual")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("uses manual paste for large text", () => {
|
||||||
|
expect(pasteMode("x".repeat(8000))).toBe("manual")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import { useLanguage } from "@/context/language"
|
|||||||
import { uuid } from "@/utils/uuid"
|
import { uuid } from "@/utils/uuid"
|
||||||
import { getCursorPosition } from "./editor-dom"
|
import { getCursorPosition } from "./editor-dom"
|
||||||
import { attachmentMime } from "./files"
|
import { attachmentMime } from "./files"
|
||||||
const LARGE_PASTE_CHARS = 8000
|
import { normalizePaste, pasteMode } from "./paste"
|
||||||
const LARGE_PASTE_BREAKS = 120
|
|
||||||
|
|
||||||
function dataUrl(file: File, mime: string) {
|
function dataUrl(file: File, mime: string) {
|
||||||
return new Promise<string>((resolve) => {
|
return new Promise<string>((resolve) => {
|
||||||
@@ -25,20 +24,8 @@ function dataUrl(file: File, mime: string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function largePaste(text: string) {
|
|
||||||
if (text.length >= LARGE_PASTE_CHARS) return true
|
|
||||||
let breaks = 0
|
|
||||||
for (const char of text) {
|
|
||||||
if (char !== "\n") continue
|
|
||||||
breaks += 1
|
|
||||||
if (breaks >= LARGE_PASTE_BREAKS) return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
type PromptAttachmentsInput = {
|
type PromptAttachmentsInput = {
|
||||||
editor: () => HTMLDivElement | undefined
|
editor: () => HTMLDivElement | undefined
|
||||||
isFocused: () => boolean
|
|
||||||
isDialogActive: () => boolean
|
isDialogActive: () => boolean
|
||||||
setDraggingType: (type: "image" | "@mention" | null) => void
|
setDraggingType: (type: "image" | "@mention" | null) => void
|
||||||
focusEditor: () => void
|
focusEditor: () => void
|
||||||
@@ -91,7 +78,6 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handlePaste = async (event: ClipboardEvent) => {
|
const handlePaste = async (event: ClipboardEvent) => {
|
||||||
if (!input.isFocused()) return
|
|
||||||
const clipboardData = event.clipboardData
|
const clipboardData = event.clipboardData
|
||||||
if (!clipboardData) return
|
if (!clipboardData) return
|
||||||
|
|
||||||
@@ -126,16 +112,23 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
|||||||
|
|
||||||
if (!plainText) return
|
if (!plainText) return
|
||||||
|
|
||||||
if (largePaste(plainText)) {
|
const text = normalizePaste(plainText)
|
||||||
if (input.addPart({ type: "text", content: plainText, start: 0, end: 0 })) return
|
|
||||||
|
const put = () => {
|
||||||
|
if (input.addPart({ type: "text", content: text, start: 0, end: 0 })) return true
|
||||||
input.focusEditor()
|
input.focusEditor()
|
||||||
if (input.addPart({ type: "text", content: plainText, start: 0, end: 0 })) return
|
return input.addPart({ type: "text", content: text, start: 0, end: 0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const inserted = typeof document.execCommand === "function" && document.execCommand("insertText", false, plainText)
|
if (pasteMode(text) === "manual") {
|
||||||
|
put()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const inserted = typeof document.execCommand === "function" && document.execCommand("insertText", false, text)
|
||||||
if (inserted) return
|
if (inserted) return
|
||||||
|
|
||||||
input.addPart({ type: "text", content: plainText, start: 0, end: 0 })
|
put()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleGlobalDragOver = (event: DragEvent) => {
|
const handleGlobalDragOver = (event: DragEvent) => {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
|
import { ACCEPTED_FILE_TYPES, ACCEPTED_IMAGE_TYPES } from "@/constants/file-picker"
|
||||||
|
|
||||||
|
export { ACCEPTED_FILE_TYPES }
|
||||||
|
|
||||||
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([
|
||||||
@@ -18,61 +20,6 @@ const TEXT_MIMES = new Set([
|
|||||||
"application/yaml",
|
"application/yaml",
|
||||||
])
|
])
|
||||||
|
|
||||||
export const ACCEPTED_FILE_TYPES = [
|
|
||||||
...ACCEPTED_IMAGE_TYPES,
|
|
||||||
"application/pdf",
|
|
||||||
"text/*",
|
|
||||||
"application/json",
|
|
||||||
"application/ld+json",
|
|
||||||
"application/toml",
|
|
||||||
"application/x-toml",
|
|
||||||
"application/x-yaml",
|
|
||||||
"application/xml",
|
|
||||||
"application/yaml",
|
|
||||||
".c",
|
|
||||||
".cc",
|
|
||||||
".cjs",
|
|
||||||
".conf",
|
|
||||||
".cpp",
|
|
||||||
".css",
|
|
||||||
".csv",
|
|
||||||
".cts",
|
|
||||||
".env",
|
|
||||||
".go",
|
|
||||||
".gql",
|
|
||||||
".graphql",
|
|
||||||
".h",
|
|
||||||
".hh",
|
|
||||||
".hpp",
|
|
||||||
".htm",
|
|
||||||
".html",
|
|
||||||
".ini",
|
|
||||||
".java",
|
|
||||||
".js",
|
|
||||||
".json",
|
|
||||||
".jsx",
|
|
||||||
".log",
|
|
||||||
".md",
|
|
||||||
".mdx",
|
|
||||||
".mjs",
|
|
||||||
".mts",
|
|
||||||
".py",
|
|
||||||
".rb",
|
|
||||||
".rs",
|
|
||||||
".sass",
|
|
||||||
".scss",
|
|
||||||
".sh",
|
|
||||||
".sql",
|
|
||||||
".toml",
|
|
||||||
".ts",
|
|
||||||
".tsx",
|
|
||||||
".txt",
|
|
||||||
".xml",
|
|
||||||
".yaml",
|
|
||||||
".yml",
|
|
||||||
".zsh",
|
|
||||||
]
|
|
||||||
|
|
||||||
const SAMPLE = 4096
|
const SAMPLE = 4096
|
||||||
|
|
||||||
function kind(type: string) {
|
function kind(type: string) {
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
const LARGE_PASTE_CHARS = 8000
|
||||||
|
const LARGE_PASTE_BREAKS = 120
|
||||||
|
|
||||||
|
function largePaste(text: string) {
|
||||||
|
if (text.length >= LARGE_PASTE_CHARS) return true
|
||||||
|
let breaks = 0
|
||||||
|
for (const char of text) {
|
||||||
|
if (char !== "\n") continue
|
||||||
|
breaks += 1
|
||||||
|
if (breaks >= LARGE_PASTE_BREAKS) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizePaste(text: string) {
|
||||||
|
if (!text.includes("\r")) return text
|
||||||
|
return text.replace(/\r\n?/g, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pasteMode(text: string) {
|
||||||
|
if (largePaste(text)) return "manual"
|
||||||
|
if (text.includes("\n") || text.includes("\r")) return "manual"
|
||||||
|
return "native"
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ const optimistic: Array<{
|
|||||||
}> = []
|
}> = []
|
||||||
const optimisticSeeded: boolean[] = []
|
const optimisticSeeded: boolean[] = []
|
||||||
const storedSessions: Record<string, Array<{ id: string; title?: string }>> = {}
|
const storedSessions: Record<string, Array<{ id: string; title?: string }>> = {}
|
||||||
|
const promoted: Array<{ directory: string; sessionID: string }> = []
|
||||||
const sentShell: string[] = []
|
const sentShell: string[] = []
|
||||||
const syncedDirectories: string[] = []
|
const syncedDirectories: string[] = []
|
||||||
|
|
||||||
@@ -86,6 +87,11 @@ beforeAll(async () => {
|
|||||||
agent: {
|
agent: {
|
||||||
current: () => ({ name: "agent" }),
|
current: () => ({ name: "agent" }),
|
||||||
},
|
},
|
||||||
|
session: {
|
||||||
|
promote(directory: string, sessionID: string) {
|
||||||
|
promoted.push({ directory, sessionID })
|
||||||
|
},
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -201,6 +207,7 @@ beforeEach(() => {
|
|||||||
enabledAutoAccept.length = 0
|
enabledAutoAccept.length = 0
|
||||||
optimistic.length = 0
|
optimistic.length = 0
|
||||||
optimisticSeeded.length = 0
|
optimisticSeeded.length = 0
|
||||||
|
promoted.length = 0
|
||||||
params = {}
|
params = {}
|
||||||
sentShell.length = 0
|
sentShell.length = 0
|
||||||
syncedDirectories.length = 0
|
syncedDirectories.length = 0
|
||||||
@@ -240,6 +247,11 @@ describe("prompt submit worktree selection", () => {
|
|||||||
expect(createdSessions).toEqual(["/repo/worktree-a", "/repo/worktree-b"])
|
expect(createdSessions).toEqual(["/repo/worktree-a", "/repo/worktree-b"])
|
||||||
expect(sentShell).toEqual(["/repo/worktree-a", "/repo/worktree-b"])
|
expect(sentShell).toEqual(["/repo/worktree-a", "/repo/worktree-b"])
|
||||||
expect(syncedDirectories).toEqual(["/repo/worktree-a", "/repo/worktree-a", "/repo/worktree-b", "/repo/worktree-b"])
|
expect(syncedDirectories).toEqual(["/repo/worktree-a", "/repo/worktree-a", "/repo/worktree-b", "/repo/worktree-b"])
|
||||||
|
expect(promoted).toEqual([
|
||||||
|
{ directory: "/repo/worktree-a", sessionID: "session-1" },
|
||||||
|
{ directory: "/repo/worktree-b", sessionID: "session-2" },
|
||||||
|
])
|
||||||
|
expect(syncedDirectories).toEqual(["/repo/worktree-a", "/repo/worktree-a", "/repo/worktree-b", "/repo/worktree-b"])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("applies auto-accept to newly created sessions", async () => {
|
test("applies auto-accept to newly created sessions", async () => {
|
||||||
|
|||||||
@@ -296,6 +296,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
|
|
||||||
const currentModel = local.model.current()
|
const currentModel = local.model.current()
|
||||||
const currentAgent = local.agent.current()
|
const currentAgent = local.agent.current()
|
||||||
|
const variant = local.model.variant.current()
|
||||||
if (!currentModel || !currentAgent) {
|
if (!currentModel || !currentAgent) {
|
||||||
showToast({
|
showToast({
|
||||||
title: language.t("prompt.toast.modelAgentRequired.title"),
|
title: language.t("prompt.toast.modelAgentRequired.title"),
|
||||||
@@ -370,6 +371,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
seed(sessionDirectory, created)
|
seed(sessionDirectory, created)
|
||||||
session = created
|
session = created
|
||||||
if (shouldAutoAccept) permission.enableAutoAccept(session.id, sessionDirectory)
|
if (shouldAutoAccept) permission.enableAutoAccept(session.id, sessionDirectory)
|
||||||
|
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}`)
|
navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`)
|
||||||
}
|
}
|
||||||
@@ -387,7 +389,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
providerID: currentModel.provider.id,
|
providerID: currentModel.provider.id,
|
||||||
}
|
}
|
||||||
const agent = currentAgent.name
|
const agent = currentAgent.name
|
||||||
const variant = local.model.variant.current()
|
|
||||||
const context = prompt.context.items().slice()
|
const context = prompt.context.items().slice()
|
||||||
const draft: FollowupDraft = {
|
const draft: FollowupDraft = {
|
||||||
sessionID: session.id,
|
sessionID: session.id,
|
||||||
|
|||||||
@@ -16,9 +16,11 @@ import { useLanguage } from "@/context/language"
|
|||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useServer } from "@/context/server"
|
import { useServer } from "@/context/server"
|
||||||
|
import { useSync } from "@/context/sync"
|
||||||
import { useTerminal } from "@/context/terminal"
|
import { useTerminal } from "@/context/terminal"
|
||||||
import { focusTerminalById } from "@/pages/session/helpers"
|
import { focusTerminalById } from "@/pages/session/helpers"
|
||||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
|
import { messageAgentColor } from "@/utils/agent"
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { StatusPopover } from "../status-popover"
|
import { StatusPopover } from "../status-popover"
|
||||||
@@ -132,6 +134,7 @@ export function SessionHeader() {
|
|||||||
const server = useServer()
|
const server = useServer()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
const sync = useSync()
|
||||||
const terminal = useTerminal()
|
const terminal = useTerminal()
|
||||||
const { params, view } = useSessionLayout()
|
const { params, view } = useSessionLayout()
|
||||||
|
|
||||||
@@ -218,6 +221,9 @@ export function SessionHeader() {
|
|||||||
({ id: "finder", label: fileManager().label, icon: fileManager().icon } as const),
|
({ id: "finder", label: fileManager().label, icon: fileManager().icon } as const),
|
||||||
)
|
)
|
||||||
const opening = createMemo(() => openRequest.app !== undefined)
|
const opening = createMemo(() => openRequest.app !== undefined)
|
||||||
|
const tint = createMemo(() =>
|
||||||
|
messageAgentColor(params.id ? sync.data.message[params.id] : undefined, sync.data.agent),
|
||||||
|
)
|
||||||
|
|
||||||
const selectApp = (app: OpenApp) => {
|
const selectApp = (app: OpenApp) => {
|
||||||
if (!options().some((item) => item.id === app)) return
|
if (!options().some((item) => item.id === app)) return
|
||||||
@@ -268,12 +274,11 @@ export function SessionHeader() {
|
|||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="small"
|
size="small"
|
||||||
class="hidden md:flex w-[240px] max-w-full min-w-0 pl-0.5 pr-2 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-panel shadow-none cursor-default"
|
class="hidden md:flex w-[240px] max-w-full min-w-0 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-panel shadow-none cursor-default"
|
||||||
onClick={() => command.trigger("file.open")}
|
onClick={() => command.trigger("file.open")}
|
||||||
aria-label={language.t("session.header.searchFiles")}
|
aria-label={language.t("session.header.searchFiles")}
|
||||||
>
|
>
|
||||||
<div class="flex min-w-0 flex-1 items-center gap-1.5 overflow-visible">
|
<div class="flex min-w-0 flex-1 items-center overflow-visible">
|
||||||
<Icon name="magnifying-glass" size="small" class="icon-base shrink-0 size-4" />
|
|
||||||
<span class="flex-1 min-w-0 text-12-regular text-text-weak truncate text-left">
|
<span class="flex-1 min-w-0 text-12-regular text-text-weak truncate text-left">
|
||||||
{language.t("session.header.search.placeholder", {
|
{language.t("session.header.search.placeholder", {
|
||||||
project: name(),
|
project: name(),
|
||||||
@@ -320,7 +325,7 @@ export function SessionHeader() {
|
|||||||
<div class="flex h-[24px] box-border items-center rounded-md border border-border-weak-base bg-surface-panel overflow-hidden">
|
<div class="flex h-[24px] box-border items-center rounded-md border border-border-weak-base bg-surface-panel overflow-hidden">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="rounded-none h-full py-0 pr-1.5 pl-px gap-1.5 border-none shadow-none disabled:!cursor-default"
|
class="rounded-none h-full px-0.5 border-none shadow-none disabled:!cursor-default"
|
||||||
classList={{
|
classList={{
|
||||||
"bg-surface-raised-base-active": opening(),
|
"bg-surface-raised-base-active": opening(),
|
||||||
}}
|
}}
|
||||||
@@ -330,10 +335,9 @@ export function SessionHeader() {
|
|||||||
>
|
>
|
||||||
<div class="flex size-5 shrink-0 items-center justify-center [&_[data-component=app-icon]]:size-5">
|
<div class="flex size-5 shrink-0 items-center justify-center [&_[data-component=app-icon]]:size-5">
|
||||||
<Show when={opening()} fallback={<AppIcon id={current().icon} />}>
|
<Show when={opening()} fallback={<AppIcon id={current().icon} />}>
|
||||||
<Spinner class="size-3.5 text-icon-base" />
|
<Spinner class="size-3.5" style={{ color: tint() ?? "var(--icon-base)" }} />
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-12-regular text-text-strong">{language.t("common.open")}</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
gutter={4}
|
gutter={4}
|
||||||
|
|||||||
@@ -277,8 +277,8 @@ export function StatusPopover() {
|
|||||||
aria-disabled={isBlocked()}
|
aria-disabled={isBlocked()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isBlocked()) return
|
if (isBlocked()) return
|
||||||
server.setActive(key)
|
|
||||||
navigate("/")
|
navigate("/")
|
||||||
|
queueMicrotask(() => server.setActive(key))
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ServerHealthIndicator health={health[key]} />
|
<ServerHealthIndicator health={health[key]} />
|
||||||
|
|||||||
@@ -65,14 +65,11 @@ const debugTerminal = (...values: unknown[]) => {
|
|||||||
console.debug("[terminal]", ...values)
|
console.debug("[terminal]", ...values)
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorStatus = (err: unknown) => {
|
const errorName = (err: unknown) => {
|
||||||
if (!err || typeof err !== "object") return
|
if (!err || typeof err !== "object") return
|
||||||
if (!("data" in err)) return
|
if (!("name" in err)) return
|
||||||
const data = err.data
|
const errorName = err.name
|
||||||
if (!data || typeof data !== "object") return
|
return typeof errorName === "string" ? errorName : undefined
|
||||||
if (!("statusCode" in data)) return
|
|
||||||
const status = data.statusCode
|
|
||||||
return typeof status === "number" ? status : undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const useTerminalUiBindings = (input: {
|
const useTerminalUiBindings = (input: {
|
||||||
@@ -168,6 +165,12 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
|
const directory = sdk.directory
|
||||||
|
const client = sdk.client
|
||||||
|
const url = sdk.url
|
||||||
|
const auth = server.current?.http
|
||||||
|
const username = auth?.username ?? "opencode"
|
||||||
|
const password = auth?.password ?? ""
|
||||||
let container!: HTMLDivElement
|
let container!: HTMLDivElement
|
||||||
const [local, others] = splitProps(props, ["pty", "class", "classList", "autoFocus", "onConnect", "onConnectError"])
|
const [local, others] = splitProps(props, ["pty", "class", "classList", "autoFocus", "onConnect", "onConnectError"])
|
||||||
const id = local.pty.id
|
const id = local.pty.id
|
||||||
@@ -218,7 +221,7 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pushSize = (cols: number, rows: number) => {
|
const pushSize = (cols: number, rows: number) => {
|
||||||
return sdk.client.pty
|
return client.pty
|
||||||
.update({
|
.update({
|
||||||
ptyID: id,
|
ptyID: id,
|
||||||
size: { cols, rows },
|
size: { cols, rows },
|
||||||
@@ -477,11 +480,11 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const gone = () =>
|
const gone = () =>
|
||||||
sdk.client.pty
|
client.pty
|
||||||
.get({ ptyID: id })
|
.get({ ptyID: id })
|
||||||
.then(() => false)
|
.then(() => false)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (errorStatus(err) === 404) return true
|
if (errorName(err) === "NotFoundError") return true
|
||||||
debugTerminal("failed to inspect terminal session", err)
|
debugTerminal("failed to inspect terminal session", err)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@@ -509,14 +512,14 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
if (disposed) return
|
if (disposed) return
|
||||||
drop?.()
|
drop?.()
|
||||||
|
|
||||||
const url = new URL(sdk.url + `/pty/${id}/connect`)
|
const next = new URL(url + `/pty/${id}/connect`)
|
||||||
url.searchParams.set("directory", sdk.directory)
|
next.searchParams.set("directory", directory)
|
||||||
url.searchParams.set("cursor", String(seek))
|
next.searchParams.set("cursor", String(seek))
|
||||||
url.protocol = url.protocol === "https:" ? "wss:" : "ws:"
|
next.protocol = next.protocol === "https:" ? "wss:" : "ws:"
|
||||||
url.username = server.current?.http.username ?? "opencode"
|
next.username = username
|
||||||
url.password = server.current?.http.password ?? ""
|
next.password = password
|
||||||
|
|
||||||
const socket = new WebSocket(url)
|
const socket = new WebSocket(next)
|
||||||
socket.binaryType = "arraybuffer"
|
socket.binaryType = "arraybuffer"
|
||||||
ws = socket
|
ws = socket
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export function Titlebar() {
|
|||||||
|
|
||||||
const canBack = createMemo(() => history.index > 0)
|
const canBack = createMemo(() => history.index > 0)
|
||||||
const canForward = createMemo(() => history.index < history.stack.length - 1)
|
const canForward = createMemo(() => history.index < history.stack.length - 1)
|
||||||
|
const hasProjects = createMemo(() => layout.projects.list().length > 0)
|
||||||
|
|
||||||
const back = () => {
|
const back = () => {
|
||||||
const next = backPath(history)
|
const next = backPath(history)
|
||||||
@@ -217,6 +218,17 @@ export function Titlebar() {
|
|||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
<div class="hidden xl:flex items-center shrink-0">
|
<div class="hidden xl:flex items-center shrink-0">
|
||||||
<Show when={params.dir}>
|
<Show when={params.dir}>
|
||||||
|
<div
|
||||||
|
class="flex items-center shrink-0 w-8 mr-1"
|
||||||
|
aria-hidden={layout.sidebar.opened() ? "true" : undefined}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="transition-opacity"
|
||||||
|
classList={{
|
||||||
|
"opacity-100 duration-120 ease-out": !layout.sidebar.opened(),
|
||||||
|
"opacity-0 duration-120 ease-in delay-0 pointer-events-none": layout.sidebar.opened(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
title={language.t("command.session.new")}
|
title={language.t("command.session.new")}
|
||||||
@@ -227,6 +239,8 @@ export function Titlebar() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
icon={creating() ? "new-session-active" : "new-session"}
|
icon={creating() ? "new-session-active" : "new-session"}
|
||||||
class="titlebar-icon w-8 h-6 p-0 box-border"
|
class="titlebar-icon w-8 h-6 p-0 box-border"
|
||||||
|
disabled={layout.sidebar.opened()}
|
||||||
|
tabIndex={layout.sidebar.opened() ? -1 : undefined}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!params.dir) return
|
if (!params.dir) return
|
||||||
navigate(`/${params.dir}/session`)
|
navigate(`/${params.dir}/session`)
|
||||||
@@ -235,8 +249,19 @@ export function Titlebar() {
|
|||||||
aria-current={creating() ? "page" : undefined}
|
aria-current={creating() ? "page" : undefined}
|
||||||
/>
|
/>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div class="flex items-center gap-0" classList={{ "ml-1": !!params.dir }}>
|
<Show when={hasProjects()}>
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-0 transition-transform"
|
||||||
|
classList={{
|
||||||
|
"translate-x-0": !layout.sidebar.opened(),
|
||||||
|
"-translate-x-[36px]": layout.sidebar.opened(),
|
||||||
|
"duration-180 ease-out": !layout.sidebar.opened(),
|
||||||
|
"duration-180 ease-in": layout.sidebar.opened(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
|
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -258,6 +283,7 @@ export function Titlebar() {
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
|
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
|
||||||
|
|
||||||
|
export const ACCEPTED_FILE_TYPES = [
|
||||||
|
...ACCEPTED_IMAGE_TYPES,
|
||||||
|
"application/pdf",
|
||||||
|
"text/*",
|
||||||
|
"application/json",
|
||||||
|
"application/ld+json",
|
||||||
|
"application/toml",
|
||||||
|
"application/x-toml",
|
||||||
|
"application/x-yaml",
|
||||||
|
"application/xml",
|
||||||
|
"application/yaml",
|
||||||
|
".c",
|
||||||
|
".cc",
|
||||||
|
".cjs",
|
||||||
|
".conf",
|
||||||
|
".cpp",
|
||||||
|
".css",
|
||||||
|
".csv",
|
||||||
|
".cts",
|
||||||
|
".env",
|
||||||
|
".go",
|
||||||
|
".gql",
|
||||||
|
".graphql",
|
||||||
|
".h",
|
||||||
|
".hh",
|
||||||
|
".hpp",
|
||||||
|
".htm",
|
||||||
|
".html",
|
||||||
|
".ini",
|
||||||
|
".java",
|
||||||
|
".js",
|
||||||
|
".json",
|
||||||
|
".jsx",
|
||||||
|
".log",
|
||||||
|
".md",
|
||||||
|
".mdx",
|
||||||
|
".mjs",
|
||||||
|
".mts",
|
||||||
|
".py",
|
||||||
|
".rb",
|
||||||
|
".rs",
|
||||||
|
".sass",
|
||||||
|
".scss",
|
||||||
|
".sh",
|
||||||
|
".sql",
|
||||||
|
".toml",
|
||||||
|
".ts",
|
||||||
|
".tsx",
|
||||||
|
".txt",
|
||||||
|
".xml",
|
||||||
|
".yaml",
|
||||||
|
".yml",
|
||||||
|
".zsh",
|
||||||
|
]
|
||||||
|
|
||||||
|
const MIME_EXT = new Map([
|
||||||
|
["image/png", "png"],
|
||||||
|
["image/jpeg", "jpg"],
|
||||||
|
["image/gif", "gif"],
|
||||||
|
["image/webp", "webp"],
|
||||||
|
["application/pdf", "pdf"],
|
||||||
|
["application/json", "json"],
|
||||||
|
["application/ld+json", "jsonld"],
|
||||||
|
["application/toml", "toml"],
|
||||||
|
["application/x-toml", "toml"],
|
||||||
|
["application/x-yaml", "yaml"],
|
||||||
|
["application/xml", "xml"],
|
||||||
|
["application/yaml", "yaml"],
|
||||||
|
])
|
||||||
|
|
||||||
|
const TEXT_EXT = ["txt", "text", "md", "markdown", "log", "csv"]
|
||||||
|
|
||||||
|
export const ACCEPTED_FILE_EXTENSIONS = Array.from(
|
||||||
|
new Set(
|
||||||
|
ACCEPTED_FILE_TYPES.flatMap((item) => {
|
||||||
|
if (item.startsWith(".")) return [item.slice(1)]
|
||||||
|
if (item === "text/*") return TEXT_EXT
|
||||||
|
const out = MIME_EXT.get(item)
|
||||||
|
return out ? [out] : []
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).sort()
|
||||||
|
|
||||||
|
export function filePickerFilters(ext?: string[]) {
|
||||||
|
if (!ext || ext.length === 0) return undefined
|
||||||
|
return [{ name: "Files", extensions: ext }]
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { createEffect, createMemo, onCleanup, onMount, type Accessor } from "solid-js"
|
|
||||||
import { createStore } from "solid-js/store"
|
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { dict as en } from "@/i18n/en"
|
import { type Accessor, createEffect, createMemo, onCleanup, onMount } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
|
import { dict as en } from "@/i18n/en"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
|
|
||||||
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
||||||
@@ -238,9 +238,10 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||||||
})
|
})
|
||||||
const warnedDuplicates = new Set<string>()
|
const warnedDuplicates = new Set<string>()
|
||||||
|
|
||||||
|
type CommandCatalog = Record<string, CommandCatalogItem>
|
||||||
const [catalog, setCatalog, _, catalogReady] = persisted(
|
const [catalog, setCatalog, _, catalogReady] = persisted(
|
||||||
Persist.global("command.catalog.v1"),
|
Persist.global("command.catalog.v1"),
|
||||||
createStore<Record<string, CommandCatalogItem>>({}),
|
createStore<CommandCatalog>({}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const bind = (id: string, def: KeybindConfig | undefined) => {
|
const bind = (id: string, def: KeybindConfig | undefined) => {
|
||||||
@@ -259,7 +260,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||||||
if (seen.has(opt.id)) {
|
if (seen.has(opt.id)) {
|
||||||
if (import.meta.env.DEV && !warnedDuplicates.has(opt.id)) {
|
if (import.meta.env.DEV && !warnedDuplicates.has(opt.id)) {
|
||||||
warnedDuplicates.add(opt.id)
|
warnedDuplicates.add(opt.id)
|
||||||
console.warn(`[command] duplicate command id \"${opt.id}\" registered; keeping first entry`)
|
console.warn(`[command] duplicate command id "${opt.id}" registered; keeping first entry`)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -274,16 +275,19 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!catalogReady()) return
|
if (!catalogReady()) return
|
||||||
|
|
||||||
for (const opt of registered()) {
|
setCatalog(
|
||||||
|
registered().reduce((acc, opt) => {
|
||||||
const id = actionId(opt.id)
|
const id = actionId(opt.id)
|
||||||
setCatalog(id, {
|
acc[id] = {
|
||||||
title: opt.title,
|
title: opt.title,
|
||||||
description: opt.description,
|
description: opt.description,
|
||||||
category: opt.category,
|
category: opt.category,
|
||||||
keybind: opt.keybind,
|
keybind: opt.keybind,
|
||||||
slash: opt.slash,
|
slash: opt.slash,
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
return acc
|
||||||
|
}, {} as CommandCatalog),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const catalogOptions = createMemo(() => Object.entries(catalog).map(([id, meta]) => ({ id, ...meta })))
|
const catalogOptions = createMemo(() => Object.entries(catalog).map(([id, meta]) => ({ id, ...meta })))
|
||||||
|
|||||||
@@ -378,6 +378,7 @@ function createGlobalSync() {
|
|||||||
return globalStore.error
|
return globalStore.error
|
||||||
},
|
},
|
||||||
child: children.child,
|
child: children.child,
|
||||||
|
peek: children.peek,
|
||||||
bootstrap,
|
bootstrap,
|
||||||
updateConfig,
|
updateConfig,
|
||||||
project: projectApi,
|
project: projectApi,
|
||||||
|
|||||||
@@ -226,6 +226,15 @@ export function createChildStoreManager(input: {
|
|||||||
return childStore
|
return childStore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function peek(directory: string, options: ChildOptions = {}) {
|
||||||
|
const childStore = ensureChild(directory)
|
||||||
|
const shouldBootstrap = options.bootstrap ?? true
|
||||||
|
if (shouldBootstrap && childStore[0].status === "loading") {
|
||||||
|
input.onBootstrap(directory)
|
||||||
|
}
|
||||||
|
return childStore
|
||||||
|
}
|
||||||
|
|
||||||
function projectMeta(directory: string, patch: ProjectMeta) {
|
function projectMeta(directory: string, patch: ProjectMeta) {
|
||||||
const [store, setStore] = ensureChild(directory)
|
const [store, setStore] = ensureChild(directory)
|
||||||
const cached = metaCache.get(directory)
|
const cached = metaCache.get(directory)
|
||||||
@@ -256,6 +265,7 @@ export function createChildStoreManager(input: {
|
|||||||
children,
|
children,
|
||||||
ensureChild,
|
ensureChild,
|
||||||
child,
|
child,
|
||||||
|
peek,
|
||||||
projectMeta,
|
projectMeta,
|
||||||
projectIcon,
|
projectIcon,
|
||||||
mark,
|
mark,
|
||||||
|
|||||||
+331
-161
@@ -1,214 +1,319 @@
|
|||||||
import { createStore } from "solid-js/store"
|
|
||||||
import { batch, createMemo } from "solid-js"
|
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
|
import { base64Encode } from "@opencode-ai/util/encode"
|
||||||
|
import { useParams } from "@solidjs/router"
|
||||||
|
import { batch, createEffect, createMemo, onCleanup } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { useModels } from "@/context/models"
|
||||||
|
import { useProviders } from "@/hooks/use-providers"
|
||||||
|
import { modelEnabled, modelProbe } from "@/testing/model-selection"
|
||||||
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
|
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
|
||||||
import { useSDK } from "./sdk"
|
import { useSDK } from "./sdk"
|
||||||
import { useSync } from "./sync"
|
import { useSync } from "./sync"
|
||||||
import { base64Encode } from "@opencode-ai/util/encode"
|
|
||||||
import { useProviders } from "@/hooks/use-providers"
|
|
||||||
import { useModels } from "@/context/models"
|
|
||||||
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
|
|
||||||
|
|
||||||
export type ModelKey = { providerID: string; modelID: string }
|
export type ModelKey = { providerID: string; modelID: string }
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
agent?: string
|
||||||
|
model?: ModelKey
|
||||||
|
variant?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
type Saved = {
|
||||||
|
session: Record<string, State | undefined>
|
||||||
|
}
|
||||||
|
|
||||||
|
const WORKSPACE_KEY = "__workspace__"
|
||||||
|
const handoff = new Map<string, State>()
|
||||||
|
|
||||||
|
const handoffKey = (dir: string, id: string) => `${dir}\n${id}`
|
||||||
|
|
||||||
|
const migrate = (value: unknown) => {
|
||||||
|
if (!value || typeof value !== "object") return { session: {} }
|
||||||
|
|
||||||
|
const item = value as {
|
||||||
|
session?: Record<string, State | undefined>
|
||||||
|
pick?: Record<string, State | undefined>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.session && typeof item.session === "object") return { session: item.session }
|
||||||
|
if (!item.pick || typeof item.pick !== "object") return { session: {} }
|
||||||
|
|
||||||
|
return {
|
||||||
|
session: Object.fromEntries(Object.entries(item.pick).filter(([key]) => key !== WORKSPACE_KEY)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clone = (value: State | undefined) => {
|
||||||
|
if (!value) return undefined
|
||||||
|
return {
|
||||||
|
...value,
|
||||||
|
model: value.model ? { ...value.model } : undefined,
|
||||||
|
} satisfies State
|
||||||
|
}
|
||||||
|
|
||||||
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||||
name: "Local",
|
name: "Local",
|
||||||
init: () => {
|
init: () => {
|
||||||
|
const params = useParams()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const providers = useProviders()
|
const providers = useProviders()
|
||||||
const connected = createMemo(() => new Set(providers.connected().map((provider) => provider.id)))
|
|
||||||
|
|
||||||
function isModelValid(model: ModelKey) {
|
|
||||||
const provider = providers.all().find((x) => x.id === model.providerID)
|
|
||||||
return !!provider?.models[model.modelID] && connected().has(model.providerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFirstValidModel(...modelFns: (() => ModelKey | undefined)[]) {
|
|
||||||
for (const modelFn of modelFns) {
|
|
||||||
const model = modelFn()
|
|
||||||
if (!model) continue
|
|
||||||
if (isModelValid(model)) return model
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let setModel: (model: ModelKey | undefined, options?: { recent?: boolean }) => void = () => undefined
|
|
||||||
|
|
||||||
const agent = (() => {
|
|
||||||
const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden))
|
|
||||||
const models = useModels()
|
const models = useModels()
|
||||||
|
|
||||||
|
const id = createMemo(() => params.id || undefined)
|
||||||
|
const list = createMemo(() => sync.data.agent.filter((item) => item.mode !== "subagent" && !item.hidden))
|
||||||
|
const connected = createMemo(() => new Set(providers.connected().map((item) => item.id)))
|
||||||
|
|
||||||
|
const [saved, setSaved] = persisted(
|
||||||
|
{
|
||||||
|
...Persist.workspace(sdk.directory, "model-selection", ["model-selection.v1"]),
|
||||||
|
migrate,
|
||||||
|
},
|
||||||
|
createStore<Saved>({
|
||||||
|
session: {},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const [store, setStore] = createStore<{
|
const [store, setStore] = createStore<{
|
||||||
current?: string
|
current?: string
|
||||||
|
draft?: State
|
||||||
|
last?: {
|
||||||
|
type: "agent" | "model" | "variant"
|
||||||
|
agent?: string
|
||||||
|
model?: ModelKey | null
|
||||||
|
variant?: string | null
|
||||||
|
}
|
||||||
}>({
|
}>({
|
||||||
current: list()[0]?.name,
|
current: list()[0]?.name,
|
||||||
|
draft: undefined,
|
||||||
|
last: undefined,
|
||||||
})
|
})
|
||||||
return {
|
|
||||||
list,
|
const validModel = (model: ModelKey) => {
|
||||||
current() {
|
const provider = providers.all().find((item) => item.id === model.providerID)
|
||||||
const available = list()
|
return !!provider?.models[model.modelID] && connected().has(model.providerID)
|
||||||
if (available.length === 0) return undefined
|
}
|
||||||
return available.find((x) => x.name === store.current) ?? available[0]
|
|
||||||
},
|
const firstModel = (...items: Array<() => ModelKey | undefined>) => {
|
||||||
set(name: string | undefined) {
|
for (const item of items) {
|
||||||
const available = list()
|
const model = item()
|
||||||
if (available.length === 0) {
|
if (!model) continue
|
||||||
setStore("current", undefined)
|
if (validModel(model)) return model
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pickAgent = (name: string | undefined) => {
|
||||||
|
const items = list()
|
||||||
|
if (items.length === 0) return undefined
|
||||||
|
return items.find((item) => item.name === name) ?? items[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const items = list()
|
||||||
|
if (items.length === 0) {
|
||||||
|
if (store.current !== undefined) setStore("current", undefined)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const match = name ? available.find((x) => x.name === name) : undefined
|
if (items.some((item) => item.name === store.current)) return
|
||||||
const value = match ?? available[0]
|
setStore("current", items[0]?.name)
|
||||||
if (!value) return
|
|
||||||
setStore("current", value.name)
|
|
||||||
if (!value.model) return
|
|
||||||
setModel({
|
|
||||||
providerID: value.model.providerID,
|
|
||||||
modelID: value.model.modelID,
|
|
||||||
})
|
})
|
||||||
if (value.variant)
|
|
||||||
models.variant.set({ providerID: value.model.providerID, modelID: value.model.modelID }, value.variant)
|
const scope = createMemo<State | undefined>(() => {
|
||||||
},
|
const session = id()
|
||||||
move(direction: 1 | -1) {
|
if (!session) return store.draft
|
||||||
const available = list()
|
return saved.session[session] ?? handoff.get(handoffKey(sdk.directory, session))
|
||||||
if (available.length === 0) {
|
})
|
||||||
setStore("current", undefined)
|
|
||||||
|
createEffect(() => {
|
||||||
|
const session = id()
|
||||||
|
if (!session) return
|
||||||
|
|
||||||
|
const key = handoffKey(sdk.directory, session)
|
||||||
|
const next = handoff.get(key)
|
||||||
|
if (!next) return
|
||||||
|
if (saved.session[session] !== undefined) {
|
||||||
|
handoff.delete(key)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let next = available.findIndex((x) => x.name === store.current) + direction
|
|
||||||
if (next < 0) next = available.length - 1
|
|
||||||
if (next >= available.length) next = 0
|
|
||||||
const value = available[next]
|
|
||||||
if (!value) return
|
|
||||||
setStore("current", value.name)
|
|
||||||
if (!value.model) return
|
|
||||||
setModel({
|
|
||||||
providerID: value.model.providerID,
|
|
||||||
modelID: value.model.modelID,
|
|
||||||
})
|
|
||||||
if (value.variant)
|
|
||||||
models.variant.set({ providerID: value.model.providerID, modelID: value.model.modelID }, value.variant)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
const model = (() => {
|
setSaved("session", session, clone(next))
|
||||||
const models = useModels()
|
handoff.delete(key)
|
||||||
|
|
||||||
const [ephemeral, setEphemeral] = createStore<{
|
|
||||||
model: Record<string, ModelKey | undefined>
|
|
||||||
}>({
|
|
||||||
model: {},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const resolveConfigured = () => {
|
const configuredModel = () => {
|
||||||
if (!sync.data.config.model) return
|
if (!sync.data.config.model) return
|
||||||
const [providerID, modelID] = sync.data.config.model.split("/")
|
const [providerID, modelID] = sync.data.config.model.split("/")
|
||||||
const key = { providerID, modelID }
|
const model = { providerID, modelID }
|
||||||
if (isModelValid(key)) return key
|
if (validModel(model)) return model
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolveRecent = () => {
|
const recentModel = () => {
|
||||||
for (const item of models.recent.list()) {
|
for (const item of models.recent.list()) {
|
||||||
if (isModelValid(item)) return item
|
if (validModel(item)) return item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolveDefault = () => {
|
const defaultModel = () => {
|
||||||
const defaults = providers.default()
|
const defaults = providers.default()
|
||||||
for (const provider of providers.connected()) {
|
for (const provider of providers.connected()) {
|
||||||
const configured = defaults[provider.id]
|
const configured = defaults[provider.id]
|
||||||
if (configured) {
|
if (configured) {
|
||||||
const key = { providerID: provider.id, modelID: configured }
|
const model = { providerID: provider.id, modelID: configured }
|
||||||
if (isModelValid(key)) return key
|
if (validModel(model)) return model
|
||||||
}
|
}
|
||||||
|
|
||||||
const first = Object.values(provider.models)[0]
|
const first = Object.values(provider.models)[0]
|
||||||
if (!first) continue
|
if (!first) continue
|
||||||
const key = { providerID: provider.id, modelID: first.id }
|
const model = { providerID: provider.id, modelID: first.id }
|
||||||
if (isModelValid(key)) return key
|
if (validModel(model)) return model
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fallbackModel = createMemo<ModelKey | undefined>(() => {
|
const fallback = createMemo<ModelKey | undefined>(() => configuredModel() ?? recentModel() ?? defaultModel())
|
||||||
return resolveConfigured() ?? resolveRecent() ?? resolveDefault()
|
|
||||||
|
const agent = {
|
||||||
|
list,
|
||||||
|
current() {
|
||||||
|
return pickAgent(scope()?.agent ?? store.current)
|
||||||
|
},
|
||||||
|
set(name: string | undefined) {
|
||||||
|
const item = pickAgent(name)
|
||||||
|
if (!item) {
|
||||||
|
setStore("current", undefined)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
batch(() => {
|
||||||
|
setStore("current", item.name)
|
||||||
|
setStore("last", {
|
||||||
|
type: "agent",
|
||||||
|
agent: item.name,
|
||||||
|
model: item.model,
|
||||||
|
variant: item.variant ?? null,
|
||||||
})
|
})
|
||||||
|
const prev = scope()
|
||||||
|
const next = {
|
||||||
|
agent: item.name,
|
||||||
|
model: item.model ?? prev?.model,
|
||||||
|
variant: item.variant ?? prev?.variant,
|
||||||
|
} satisfies State
|
||||||
|
const session = id()
|
||||||
|
if (session) {
|
||||||
|
setSaved("session", session, next)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setStore("draft", next)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
move(direction: 1 | -1) {
|
||||||
|
const items = list()
|
||||||
|
if (items.length === 0) {
|
||||||
|
setStore("current", undefined)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const current = createMemo(() => {
|
let next = items.findIndex((item) => item.name === agent.current()?.name) + direction
|
||||||
const a = agent.current()
|
if (next < 0) next = items.length - 1
|
||||||
if (!a) return undefined
|
if (next >= items.length) next = 0
|
||||||
const key = getFirstValidModel(
|
const item = items[next]
|
||||||
() => ephemeral.model[a.name],
|
if (!item) return
|
||||||
() => a.model,
|
agent.set(item.name)
|
||||||
fallbackModel,
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = () => {
|
||||||
|
const item = firstModel(
|
||||||
|
() => scope()?.model,
|
||||||
|
() => agent.current()?.model,
|
||||||
|
fallback,
|
||||||
)
|
)
|
||||||
if (!key) return undefined
|
if (!item) return undefined
|
||||||
return models.find(key)
|
return models.find(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
const configured = () => {
|
||||||
|
const item = agent.current()
|
||||||
|
const model = current()
|
||||||
|
if (!item || !model) return undefined
|
||||||
|
return getConfiguredAgentVariant({
|
||||||
|
agent: { model: item.model, variant: item.variant },
|
||||||
|
model: { providerID: model.provider.id, modelID: model.id, variants: model.variants },
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = () => scope()?.variant
|
||||||
|
|
||||||
|
const snapshot = () => {
|
||||||
|
const model = current()
|
||||||
|
return {
|
||||||
|
agent: agent.current()?.name,
|
||||||
|
model: model ? { providerID: model.provider.id, modelID: model.id } : undefined,
|
||||||
|
variant: selected(),
|
||||||
|
} satisfies State
|
||||||
|
}
|
||||||
|
|
||||||
|
const write = (next: Partial<State>) => {
|
||||||
|
const state = {
|
||||||
|
...(scope() ?? { agent: agent.current()?.name }),
|
||||||
|
...next,
|
||||||
|
} satisfies State
|
||||||
|
|
||||||
|
const session = id()
|
||||||
|
if (session) {
|
||||||
|
setSaved("session", session, state)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setStore("draft", state)
|
||||||
|
}
|
||||||
|
|
||||||
const recent = createMemo(() => models.recent.list().map(models.find).filter(Boolean))
|
const recent = createMemo(() => models.recent.list().map(models.find).filter(Boolean))
|
||||||
|
|
||||||
const cycle = (direction: 1 | -1) => {
|
const model = {
|
||||||
const recentList = recent()
|
|
||||||
const currentModel = current()
|
|
||||||
if (!currentModel) return
|
|
||||||
|
|
||||||
const index = recentList.findIndex(
|
|
||||||
(x) => x?.provider.id === currentModel.provider.id && x?.id === currentModel.id,
|
|
||||||
)
|
|
||||||
if (index === -1) return
|
|
||||||
|
|
||||||
let next = index + direction
|
|
||||||
if (next < 0) next = recentList.length - 1
|
|
||||||
if (next >= recentList.length) next = 0
|
|
||||||
|
|
||||||
const val = recentList[next]
|
|
||||||
if (!val) return
|
|
||||||
|
|
||||||
model.set({
|
|
||||||
providerID: val.provider.id,
|
|
||||||
modelID: val.id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const set = (model: ModelKey | undefined, options?: { recent?: boolean }) => {
|
|
||||||
batch(() => {
|
|
||||||
const currentAgent = agent.current()
|
|
||||||
const next = model ?? fallbackModel()
|
|
||||||
if (currentAgent) setEphemeral("model", currentAgent.name, next)
|
|
||||||
if (model) models.setVisibility(model, true)
|
|
||||||
if (options?.recent && model) models.recent.push(model)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
setModel = set
|
|
||||||
|
|
||||||
return {
|
|
||||||
ready: models.ready,
|
ready: models.ready,
|
||||||
current,
|
current,
|
||||||
recent,
|
recent,
|
||||||
list: models.list,
|
list: models.list,
|
||||||
cycle,
|
cycle(direction: 1 | -1) {
|
||||||
set,
|
const items = recent()
|
||||||
visible(model: ModelKey) {
|
const item = current()
|
||||||
return models.visible(model)
|
if (!item) return
|
||||||
|
|
||||||
|
const index = items.findIndex((entry) => entry?.provider.id === item.provider.id && entry?.id === item.id)
|
||||||
|
if (index === -1) return
|
||||||
|
|
||||||
|
let next = index + direction
|
||||||
|
if (next < 0) next = items.length - 1
|
||||||
|
if (next >= items.length) next = 0
|
||||||
|
|
||||||
|
const entry = items[next]
|
||||||
|
if (!entry) return
|
||||||
|
model.set({ providerID: entry.provider.id, modelID: entry.id })
|
||||||
},
|
},
|
||||||
setVisibility(model: ModelKey, visible: boolean) {
|
set(item: ModelKey | undefined, options?: { recent?: boolean }) {
|
||||||
models.setVisibility(model, visible)
|
batch(() => {
|
||||||
},
|
setStore("last", {
|
||||||
variant: {
|
type: "model",
|
||||||
configured() {
|
agent: agent.current()?.name,
|
||||||
const a = agent.current()
|
model: item ?? null,
|
||||||
const m = current()
|
variant: selected(),
|
||||||
if (!a || !m) return undefined
|
})
|
||||||
return getConfiguredAgentVariant({
|
write({ model: item })
|
||||||
agent: { model: a.model, variant: a.variant },
|
if (!item) return
|
||||||
model: { providerID: m.provider.id, modelID: m.id, variants: m.variants },
|
models.setVisibility(item, true)
|
||||||
|
if (!options?.recent) return
|
||||||
|
models.recent.push(item)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
selected() {
|
visible(item: ModelKey) {
|
||||||
const m = current()
|
return models.visible(item)
|
||||||
if (!m) return undefined
|
|
||||||
return models.variant.get({ providerID: m.provider.id, modelID: m.id })
|
|
||||||
},
|
},
|
||||||
|
setVisibility(item: ModelKey, visible: boolean) {
|
||||||
|
models.setVisibility(item, visible)
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
configured,
|
||||||
|
selected,
|
||||||
current() {
|
current() {
|
||||||
return resolveModelVariant({
|
return resolveModelVariant({
|
||||||
variants: this.list(),
|
variants: this.list(),
|
||||||
@@ -217,22 +322,28 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
list() {
|
list() {
|
||||||
const m = current()
|
const item = current()
|
||||||
if (!m) return []
|
if (!item?.variants) return []
|
||||||
if (!m.variants) return []
|
return Object.keys(item.variants)
|
||||||
return Object.keys(m.variants)
|
|
||||||
},
|
},
|
||||||
set(value: string | undefined) {
|
set(value: string | undefined) {
|
||||||
const m = current()
|
batch(() => {
|
||||||
if (!m) return
|
const model = current()
|
||||||
models.variant.set({ providerID: m.provider.id, modelID: m.id }, value)
|
setStore("last", {
|
||||||
|
type: "variant",
|
||||||
|
agent: agent.current()?.name,
|
||||||
|
model: model ? { providerID: model.provider.id, modelID: model.id } : null,
|
||||||
|
variant: value ?? null,
|
||||||
|
})
|
||||||
|
write({ variant: value ?? null })
|
||||||
|
})
|
||||||
},
|
},
|
||||||
cycle() {
|
cycle() {
|
||||||
const variants = this.list()
|
const items = this.list()
|
||||||
if (variants.length === 0) return
|
if (items.length === 0) return
|
||||||
this.set(
|
this.set(
|
||||||
cycleModelVariant({
|
cycleModelVariant({
|
||||||
variants,
|
variants: items,
|
||||||
selected: this.selected(),
|
selected: this.selected(),
|
||||||
configured: this.configured(),
|
configured: this.configured(),
|
||||||
}),
|
}),
|
||||||
@@ -240,13 +351,72 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})()
|
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
slug: createMemo(() => base64Encode(sdk.directory)),
|
slug: createMemo(() => base64Encode(sdk.directory)),
|
||||||
model,
|
model,
|
||||||
agent,
|
agent,
|
||||||
|
session: {
|
||||||
|
reset() {
|
||||||
|
setStore("draft", undefined)
|
||||||
|
},
|
||||||
|
promote(dir: string, session: string) {
|
||||||
|
const next = clone(snapshot())
|
||||||
|
if (!next) return
|
||||||
|
|
||||||
|
if (dir === sdk.directory) {
|
||||||
|
setSaved("session", session, next)
|
||||||
|
setStore("draft", undefined)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handoff.set(handoffKey(dir, session), next)
|
||||||
|
setStore("draft", undefined)
|
||||||
|
},
|
||||||
|
restore(msg: { sessionID: string; agent: string; model: ModelKey; variant?: string }) {
|
||||||
|
const session = id()
|
||||||
|
if (!session) return
|
||||||
|
if (msg.sessionID !== session) return
|
||||||
|
if (saved.session[session] !== undefined) return
|
||||||
|
if (handoff.has(handoffKey(sdk.directory, session))) return
|
||||||
|
|
||||||
|
setSaved("session", session, {
|
||||||
|
agent: msg.agent,
|
||||||
|
model: msg.model,
|
||||||
|
variant: msg.variant ?? null,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelEnabled()) {
|
||||||
|
createEffect(() => {
|
||||||
|
const agent = result.agent.current()
|
||||||
|
const model = result.model.current()
|
||||||
|
modelProbe.set({
|
||||||
|
dir: sdk.directory,
|
||||||
|
sessionID: id(),
|
||||||
|
last: store.last,
|
||||||
|
agent: agent?.name,
|
||||||
|
model: model
|
||||||
|
? {
|
||||||
|
providerID: model.provider.id,
|
||||||
|
modelID: model.id,
|
||||||
|
name: model.name,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
variant: result.model.variant.current() ?? null,
|
||||||
|
selected: result.model.variant.selected(),
|
||||||
|
configured: result.model.variant.configured(),
|
||||||
|
pick: scope(),
|
||||||
|
base: undefined,
|
||||||
|
current: store.current,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onCleanup(() => modelProbe.clear())
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -44,6 +44,16 @@ describe("model variant", () => {
|
|||||||
expect(value).toBe("high")
|
expect(value).toBe("high")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("lets an explicit default override the configured variant", () => {
|
||||||
|
const value = resolveModelVariant({
|
||||||
|
variants: ["low", "high", "xhigh"],
|
||||||
|
selected: null,
|
||||||
|
configured: "xhigh",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(value).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
test("cycles from configured variant to next", () => {
|
test("cycles from configured variant to next", () => {
|
||||||
const value = cycleModelVariant({
|
const value = cycleModelVariant({
|
||||||
variants: ["low", "high", "xhigh"],
|
variants: ["low", "high", "xhigh"],
|
||||||
@@ -63,4 +73,14 @@ describe("model variant", () => {
|
|||||||
|
|
||||||
expect(value).toBe("low")
|
expect(value).toBe("low")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("cycles from an explicit default to the first variant", () => {
|
||||||
|
const value = cycleModelVariant({
|
||||||
|
variants: ["low", "high", "xhigh"],
|
||||||
|
selected: null,
|
||||||
|
configured: "xhigh",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(value).toBe("low")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ type Model = AgentModel & {
|
|||||||
|
|
||||||
type VariantInput = {
|
type VariantInput = {
|
||||||
variants: string[]
|
variants: string[]
|
||||||
selected: string | undefined
|
selected: string | null | undefined
|
||||||
configured: string | undefined
|
configured: string | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ export function getConfiguredAgentVariant(input: { agent: Agent | undefined; mod
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function resolveModelVariant(input: VariantInput) {
|
export function resolveModelVariant(input: VariantInput) {
|
||||||
|
if (input.selected === null) return undefined
|
||||||
if (input.selected && input.variants.includes(input.selected)) return input.selected
|
if (input.selected && input.variants.includes(input.selected)) return input.selected
|
||||||
if (input.configured && input.variants.includes(input.configured)) return input.configured
|
if (input.configured && input.variants.includes(input.configured)) return input.configured
|
||||||
return undefined
|
return undefined
|
||||||
@@ -36,6 +37,7 @@ export function resolveModelVariant(input: VariantInput) {
|
|||||||
|
|
||||||
export function cycleModelVariant(input: VariantInput) {
|
export function cycleModelVariant(input: VariantInput) {
|
||||||
if (input.variants.length === 0) return undefined
|
if (input.variants.length === 0) return undefined
|
||||||
|
if (input.selected === null) return input.variants[0]
|
||||||
if (input.selected && input.variants.includes(input.selected)) {
|
if (input.selected && input.variants.includes(input.selected)) {
|
||||||
const index = input.variants.indexOf(input.selected)
|
const index = input.variants.indexOf(input.selected)
|
||||||
if (index === input.variants.length - 1) return undefined
|
if (index === input.variants.length - 1) return undefined
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { ServerConnection } from "./server"
|
|||||||
|
|
||||||
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 }
|
type OpenFilePickerOptions = { title?: string; multiple?: boolean; accept?: string[]; extensions?: string[] }
|
||||||
type SaveFilePickerOptions = { title?: string; defaultPath?: string }
|
type SaveFilePickerOptions = { title?: string; defaultPath?: string }
|
||||||
type UpdateInfo = { updateAvailable: boolean; version?: string }
|
type UpdateInfo = { updateAvailable: boolean; version?: string }
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { createStore, type SetStoreFunction } from "solid-js/store"
|
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { batch, createMemo, createRoot, onCleanup } from "solid-js"
|
import { checksum } from "@opencode-ai/util/encode"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
|
import { batch, createMemo, createRoot, getOwner, onCleanup } from "solid-js"
|
||||||
|
import { createStore, type SetStoreFunction } from "solid-js/store"
|
||||||
import type { FileSelection } from "@/context/file"
|
import type { FileSelection } from "@/context/file"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { checksum } from "@opencode-ai/util/encode"
|
|
||||||
|
|
||||||
interface PartBase {
|
interface PartBase {
|
||||||
content: string
|
content: string
|
||||||
@@ -151,6 +151,11 @@ const MAX_PROMPT_SESSIONS = 20
|
|||||||
|
|
||||||
type PromptSession = ReturnType<typeof createPromptSession>
|
type PromptSession = ReturnType<typeof createPromptSession>
|
||||||
|
|
||||||
|
type Scope = {
|
||||||
|
dir: string
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
type PromptCacheEntry = {
|
type PromptCacheEntry = {
|
||||||
value: PromptSession
|
value: PromptSession
|
||||||
dispose: VoidFunction
|
dispose: VoidFunction
|
||||||
@@ -245,6 +250,7 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const owner = getOwner()
|
||||||
const load = (dir: string, id: string | undefined) => {
|
const load = (dir: string, id: string | undefined) => {
|
||||||
const key = `${dir}:${id ?? WORKSPACE_KEY}`
|
const key = `${dir}:${id ?? WORKSPACE_KEY}`
|
||||||
const existing = cache.get(key)
|
const existing = cache.get(key)
|
||||||
@@ -254,10 +260,13 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
|
|||||||
return existing.value
|
return existing.value
|
||||||
}
|
}
|
||||||
|
|
||||||
const entry = createRoot((dispose) => ({
|
const entry = createRoot(
|
||||||
|
(dispose) => ({
|
||||||
value: createPromptSession(dir, id),
|
value: createPromptSession(dir, id),
|
||||||
dispose,
|
dispose,
|
||||||
}))
|
}),
|
||||||
|
owner,
|
||||||
|
)
|
||||||
|
|
||||||
cache.set(key, entry)
|
cache.set(key, entry)
|
||||||
prune()
|
prune()
|
||||||
@@ -265,6 +274,7 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const session = createMemo(() => load(params.dir!, params.id))
|
const session = createMemo(() => load(params.dir!, params.id))
|
||||||
|
const pick = (scope?: Scope) => (scope ? load(scope.dir, scope.id) : session())
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ready: () => session().ready(),
|
ready: () => session().ready(),
|
||||||
@@ -280,8 +290,8 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
|
|||||||
session().context.updateComment(path, commentID, next),
|
session().context.updateComment(path, commentID, next),
|
||||||
replaceComments: (items: FileContextItem[]) => session().context.replaceComments(items),
|
replaceComments: (items: FileContextItem[]) => session().context.replaceComments(items),
|
||||||
},
|
},
|
||||||
set: (prompt: Prompt, cursorPosition?: number) => session().set(prompt, cursorPosition),
|
set: (prompt: Prompt, cursorPosition?: number, scope?: Scope) => pick(scope).set(prompt, cursorPosition),
|
||||||
reset: () => session().reset(),
|
reset: (scope?: Scope) => pick(scope).reset(),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -185,6 +185,60 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: str
|
|||||||
})
|
})
|
||||||
onCleanup(unsub)
|
onCleanup(unsub)
|
||||||
|
|
||||||
|
const update = (client: ReturnType<typeof useSDK>["client"], pty: Partial<LocalPTY> & { id: string }) => {
|
||||||
|
const index = store.all.findIndex((x) => x.id === pty.id)
|
||||||
|
const previous = index >= 0 ? store.all[index] : undefined
|
||||||
|
if (index >= 0) {
|
||||||
|
setStore("all", index, (item) => ({ ...item, ...pty }))
|
||||||
|
}
|
||||||
|
client.pty
|
||||||
|
.update({
|
||||||
|
ptyID: pty.id,
|
||||||
|
title: pty.title,
|
||||||
|
size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined,
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
if (previous) {
|
||||||
|
const currentIndex = store.all.findIndex((item) => item.id === pty.id)
|
||||||
|
if (currentIndex >= 0) setStore("all", currentIndex, previous)
|
||||||
|
}
|
||||||
|
console.error("Failed to update terminal", error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const clone = async (client: ReturnType<typeof useSDK>["client"], id: string) => {
|
||||||
|
const index = store.all.findIndex((x) => x.id === id)
|
||||||
|
const pty = store.all[index]
|
||||||
|
if (!pty) return
|
||||||
|
const next = await client.pty
|
||||||
|
.create({
|
||||||
|
title: pty.title,
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
console.error("Failed to clone terminal", error)
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
if (!next?.data) return
|
||||||
|
|
||||||
|
const active = store.active === pty.id
|
||||||
|
|
||||||
|
batch(() => {
|
||||||
|
setStore("all", index, {
|
||||||
|
id: next.data.id,
|
||||||
|
title: next.data.title ?? pty.title,
|
||||||
|
titleNumber: pty.titleNumber,
|
||||||
|
buffer: undefined,
|
||||||
|
cursor: undefined,
|
||||||
|
scrollY: undefined,
|
||||||
|
rows: undefined,
|
||||||
|
cols: undefined,
|
||||||
|
})
|
||||||
|
if (active) {
|
||||||
|
setStore("active", next.data.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ready,
|
ready,
|
||||||
all: createMemo(() => store.all),
|
all: createMemo(() => store.all),
|
||||||
@@ -216,24 +270,7 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: str
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
update(pty: Partial<LocalPTY> & { id: string }) {
|
update(pty: Partial<LocalPTY> & { id: string }) {
|
||||||
const index = store.all.findIndex((x) => x.id === pty.id)
|
update(sdk.client, pty)
|
||||||
const previous = index >= 0 ? store.all[index] : undefined
|
|
||||||
if (index >= 0) {
|
|
||||||
setStore("all", index, (item) => ({ ...item, ...pty }))
|
|
||||||
}
|
|
||||||
sdk.client.pty
|
|
||||||
.update({
|
|
||||||
ptyID: pty.id,
|
|
||||||
title: pty.title,
|
|
||||||
size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined,
|
|
||||||
})
|
|
||||||
.catch((error: unknown) => {
|
|
||||||
if (previous) {
|
|
||||||
const currentIndex = store.all.findIndex((item) => item.id === pty.id)
|
|
||||||
if (currentIndex >= 0) setStore("all", currentIndex, previous)
|
|
||||||
}
|
|
||||||
console.error("Failed to update terminal", error)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
trim(id: string) {
|
trim(id: string) {
|
||||||
const index = store.all.findIndex((x) => x.id === id)
|
const index = store.all.findIndex((x) => x.id === id)
|
||||||
@@ -248,37 +285,23 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: str
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
async clone(id: string) {
|
async clone(id: string) {
|
||||||
|
await clone(sdk.client, id)
|
||||||
|
},
|
||||||
|
bind() {
|
||||||
|
const client = sdk.client
|
||||||
|
return {
|
||||||
|
trim(id: string) {
|
||||||
const index = store.all.findIndex((x) => x.id === id)
|
const index = store.all.findIndex((x) => x.id === id)
|
||||||
const pty = store.all[index]
|
if (index === -1) return
|
||||||
if (!pty) return
|
setStore("all", index, (pty) => trimTerminal(pty))
|
||||||
const clone = await sdk.client.pty
|
},
|
||||||
.create({
|
update(pty: Partial<LocalPTY> & { id: string }) {
|
||||||
title: pty.title,
|
update(client, pty)
|
||||||
})
|
},
|
||||||
.catch((error: unknown) => {
|
async clone(id: string) {
|
||||||
console.error("Failed to clone terminal", error)
|
await clone(client, id)
|
||||||
return undefined
|
},
|
||||||
})
|
|
||||||
if (!clone?.data) return
|
|
||||||
|
|
||||||
const active = store.active === pty.id
|
|
||||||
|
|
||||||
batch(() => {
|
|
||||||
setStore("all", index, {
|
|
||||||
id: clone.data.id,
|
|
||||||
title: clone.data.title ?? pty.title,
|
|
||||||
titleNumber: pty.titleNumber,
|
|
||||||
// New PTY process, so start clean.
|
|
||||||
buffer: undefined,
|
|
||||||
cursor: undefined,
|
|
||||||
scrollY: undefined,
|
|
||||||
rows: undefined,
|
|
||||||
cols: undefined,
|
|
||||||
})
|
|
||||||
if (active) {
|
|
||||||
setStore("active", clone.data.id)
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
},
|
},
|
||||||
open(id: string) {
|
open(id: string) {
|
||||||
setStore("active", id)
|
setStore("active", id)
|
||||||
@@ -403,6 +426,7 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
|
|||||||
trim: (id: string) => workspace().trim(id),
|
trim: (id: string) => workspace().trim(id),
|
||||||
trimAll: () => workspace().trimAll(),
|
trimAll: () => workspace().trimAll(),
|
||||||
clone: (id: string) => workspace().clone(id),
|
clone: (id: string) => workspace().clone(id),
|
||||||
|
bind: () => workspace(),
|
||||||
open: (id: string) => workspace().open(id),
|
open: (id: string) => workspace().open(id),
|
||||||
close: (id: string) => workspace().close(id),
|
close: (id: string) => workspace().close(id),
|
||||||
move: (id: string, to: number) => workspace().move(id, to),
|
move: (id: string, to: number) => workspace().move(id, to),
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ export const dict = {
|
|||||||
"common.cancel": "إلغاء",
|
"common.cancel": "إلغاء",
|
||||||
"common.connect": "اتصال",
|
"common.connect": "اتصال",
|
||||||
"common.disconnect": "قطع الاتصال",
|
"common.disconnect": "قطع الاتصال",
|
||||||
|
"common.continue": "إرسال",
|
||||||
"common.submit": "إرسال",
|
"common.submit": "إرسال",
|
||||||
"common.save": "حفظ",
|
"common.save": "حفظ",
|
||||||
"common.saving": "جارٍ الحفظ...",
|
"common.saving": "جارٍ الحفظ...",
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ export const dict = {
|
|||||||
"common.cancel": "Cancelar",
|
"common.cancel": "Cancelar",
|
||||||
"common.connect": "Conectar",
|
"common.connect": "Conectar",
|
||||||
"common.disconnect": "Desconectar",
|
"common.disconnect": "Desconectar",
|
||||||
|
"common.continue": "Enviar",
|
||||||
"common.submit": "Enviar",
|
"common.submit": "Enviar",
|
||||||
"common.save": "Salvar",
|
"common.save": "Salvar",
|
||||||
"common.saving": "Salvando...",
|
"common.saving": "Salvando...",
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ export const dict = {
|
|||||||
"common.cancel": "Otkaži",
|
"common.cancel": "Otkaži",
|
||||||
"common.connect": "Poveži",
|
"common.connect": "Poveži",
|
||||||
"common.disconnect": "Prekini vezu",
|
"common.disconnect": "Prekini vezu",
|
||||||
|
"common.continue": "Pošalji",
|
||||||
"common.submit": "Pošalji",
|
"common.submit": "Pošalji",
|
||||||
"common.save": "Sačuvaj",
|
"common.save": "Sačuvaj",
|
||||||
"common.saving": "Čuvanje...",
|
"common.saving": "Čuvanje...",
|
||||||
|
|||||||
@@ -219,6 +219,7 @@ export const dict = {
|
|||||||
"common.cancel": "Annuller",
|
"common.cancel": "Annuller",
|
||||||
"common.connect": "Forbind",
|
"common.connect": "Forbind",
|
||||||
"common.disconnect": "Frakobl",
|
"common.disconnect": "Frakobl",
|
||||||
|
"common.continue": "Indsend",
|
||||||
"common.submit": "Indsend",
|
"common.submit": "Indsend",
|
||||||
"common.save": "Gem",
|
"common.save": "Gem",
|
||||||
"common.saving": "Gemmer...",
|
"common.saving": "Gemmer...",
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ export const dict = {
|
|||||||
"common.cancel": "Abbrechen",
|
"common.cancel": "Abbrechen",
|
||||||
"common.connect": "Verbinden",
|
"common.connect": "Verbinden",
|
||||||
"common.disconnect": "Trennen",
|
"common.disconnect": "Trennen",
|
||||||
|
"common.continue": "Absenden",
|
||||||
"common.submit": "Absenden",
|
"common.submit": "Absenden",
|
||||||
"common.save": "Speichern",
|
"common.save": "Speichern",
|
||||||
"common.saving": "Speichert...",
|
"common.saving": "Speichert...",
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ export const dict = {
|
|||||||
"common.open": "Open",
|
"common.open": "Open",
|
||||||
"common.connect": "Connect",
|
"common.connect": "Connect",
|
||||||
"common.disconnect": "Disconnect",
|
"common.disconnect": "Disconnect",
|
||||||
|
"common.continue": "Continue",
|
||||||
"common.submit": "Submit",
|
"common.submit": "Submit",
|
||||||
"common.save": "Save",
|
"common.save": "Save",
|
||||||
"common.saving": "Saving...",
|
"common.saving": "Saving...",
|
||||||
@@ -674,6 +675,8 @@ export const dict = {
|
|||||||
"sidebar.project.recentSessions": "Recent sessions",
|
"sidebar.project.recentSessions": "Recent sessions",
|
||||||
"sidebar.project.viewAllSessions": "View all sessions",
|
"sidebar.project.viewAllSessions": "View all sessions",
|
||||||
"sidebar.project.clearNotifications": "Clear notifications",
|
"sidebar.project.clearNotifications": "Clear notifications",
|
||||||
|
"sidebar.empty.title": "No projects open",
|
||||||
|
"sidebar.empty.description": "Open a project to get started",
|
||||||
|
|
||||||
"debugBar.ariaLabel": "Development performance diagnostics",
|
"debugBar.ariaLabel": "Development performance diagnostics",
|
||||||
"debugBar.na": "n/a",
|
"debugBar.na": "n/a",
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ export const dict = {
|
|||||||
"common.cancel": "Cancelar",
|
"common.cancel": "Cancelar",
|
||||||
"common.connect": "Conectar",
|
"common.connect": "Conectar",
|
||||||
"common.disconnect": "Desconectar",
|
"common.disconnect": "Desconectar",
|
||||||
|
"common.continue": "Enviar",
|
||||||
"common.submit": "Enviar",
|
"common.submit": "Enviar",
|
||||||
"common.save": "Guardar",
|
"common.save": "Guardar",
|
||||||
"common.saving": "Guardando...",
|
"common.saving": "Guardando...",
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ export const dict = {
|
|||||||
"common.cancel": "Annuler",
|
"common.cancel": "Annuler",
|
||||||
"common.connect": "Connecter",
|
"common.connect": "Connecter",
|
||||||
"common.disconnect": "Déconnecter",
|
"common.disconnect": "Déconnecter",
|
||||||
|
"common.continue": "Soumettre",
|
||||||
"common.submit": "Soumettre",
|
"common.submit": "Soumettre",
|
||||||
"common.save": "Enregistrer",
|
"common.save": "Enregistrer",
|
||||||
"common.saving": "Enregistrement...",
|
"common.saving": "Enregistrement...",
|
||||||
|
|||||||
@@ -203,6 +203,7 @@ export const dict = {
|
|||||||
"common.cancel": "キャンセル",
|
"common.cancel": "キャンセル",
|
||||||
"common.connect": "接続",
|
"common.connect": "接続",
|
||||||
"common.disconnect": "切断",
|
"common.disconnect": "切断",
|
||||||
|
"common.continue": "送信",
|
||||||
"common.submit": "送信",
|
"common.submit": "送信",
|
||||||
"common.save": "保存",
|
"common.save": "保存",
|
||||||
"common.saving": "保存中...",
|
"common.saving": "保存中...",
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ export const dict = {
|
|||||||
"common.cancel": "취소",
|
"common.cancel": "취소",
|
||||||
"common.connect": "연결",
|
"common.connect": "연결",
|
||||||
"common.disconnect": "연결 해제",
|
"common.disconnect": "연결 해제",
|
||||||
|
"common.continue": "제출",
|
||||||
"common.submit": "제출",
|
"common.submit": "제출",
|
||||||
"common.save": "저장",
|
"common.save": "저장",
|
||||||
"common.saving": "저장 중...",
|
"common.saving": "저장 중...",
|
||||||
|
|||||||
@@ -223,6 +223,7 @@ export const dict = {
|
|||||||
"common.cancel": "Avbryt",
|
"common.cancel": "Avbryt",
|
||||||
"common.connect": "Koble til",
|
"common.connect": "Koble til",
|
||||||
"common.disconnect": "Koble fra",
|
"common.disconnect": "Koble fra",
|
||||||
|
"common.continue": "Send inn",
|
||||||
"common.submit": "Send inn",
|
"common.submit": "Send inn",
|
||||||
"common.save": "Lagre",
|
"common.save": "Lagre",
|
||||||
"common.saving": "Lagrer...",
|
"common.saving": "Lagrer...",
|
||||||
|
|||||||
@@ -205,6 +205,7 @@ export const dict = {
|
|||||||
"common.cancel": "Anuluj",
|
"common.cancel": "Anuluj",
|
||||||
"common.connect": "Połącz",
|
"common.connect": "Połącz",
|
||||||
"common.disconnect": "Rozłącz",
|
"common.disconnect": "Rozłącz",
|
||||||
|
"common.continue": "Prześlij",
|
||||||
"common.submit": "Prześlij",
|
"common.submit": "Prześlij",
|
||||||
"common.save": "Zapisz",
|
"common.save": "Zapisz",
|
||||||
"common.saving": "Zapisywanie...",
|
"common.saving": "Zapisywanie...",
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ export const dict = {
|
|||||||
"common.cancel": "Отмена",
|
"common.cancel": "Отмена",
|
||||||
"common.connect": "Подключить",
|
"common.connect": "Подключить",
|
||||||
"common.disconnect": "Отключить",
|
"common.disconnect": "Отключить",
|
||||||
|
"common.continue": "Отправить",
|
||||||
"common.submit": "Отправить",
|
"common.submit": "Отправить",
|
||||||
"common.save": "Сохранить",
|
"common.save": "Сохранить",
|
||||||
"common.saving": "Сохранение...",
|
"common.saving": "Сохранение...",
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ export const dict = {
|
|||||||
"common.cancel": "ยกเลิก",
|
"common.cancel": "ยกเลิก",
|
||||||
"common.connect": "เชื่อมต่อ",
|
"common.connect": "เชื่อมต่อ",
|
||||||
"common.disconnect": "ยกเลิกการเชื่อมต่อ",
|
"common.disconnect": "ยกเลิกการเชื่อมต่อ",
|
||||||
|
"common.continue": "ส่ง",
|
||||||
"common.submit": "ส่ง",
|
"common.submit": "ส่ง",
|
||||||
"common.save": "บันทึก",
|
"common.save": "บันทึก",
|
||||||
"common.saving": "กำลังบันทึก...",
|
"common.saving": "กำลังบันทึก...",
|
||||||
|
|||||||
@@ -225,6 +225,7 @@ export const dict = {
|
|||||||
"common.cancel": "İptal",
|
"common.cancel": "İptal",
|
||||||
"common.connect": "Bağlan",
|
"common.connect": "Bağlan",
|
||||||
"common.disconnect": "Bağlantı Kes",
|
"common.disconnect": "Bağlantı Kes",
|
||||||
|
"common.continue": "Gönder",
|
||||||
"common.submit": "Gönder",
|
"common.submit": "Gönder",
|
||||||
"common.save": "Kaydet",
|
"common.save": "Kaydet",
|
||||||
"common.saving": "Kaydediliyor...",
|
"common.saving": "Kaydediliyor...",
|
||||||
|
|||||||
@@ -242,6 +242,7 @@ export const dict = {
|
|||||||
"common.cancel": "取消",
|
"common.cancel": "取消",
|
||||||
"common.connect": "连接",
|
"common.connect": "连接",
|
||||||
"common.disconnect": "断开连接",
|
"common.disconnect": "断开连接",
|
||||||
|
"common.continue": "提交",
|
||||||
"common.submit": "提交",
|
"common.submit": "提交",
|
||||||
"common.save": "保存",
|
"common.save": "保存",
|
||||||
"common.saving": "保存中...",
|
"common.saving": "保存中...",
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ export const dict = {
|
|||||||
"common.cancel": "取消",
|
"common.cancel": "取消",
|
||||||
"common.connect": "連線",
|
"common.connect": "連線",
|
||||||
"common.disconnect": "中斷連線",
|
"common.disconnect": "中斷連線",
|
||||||
|
"common.continue": "提交",
|
||||||
"common.submit": "提交",
|
"common.submit": "提交",
|
||||||
"common.save": "儲存",
|
"common.save": "儲存",
|
||||||
"common.saving": "儲存中...",
|
"common.saving": "儲存中...",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export { AppBaseProviders, AppInterface } from "./app"
|
export { AppBaseProviders, AppInterface } from "./app"
|
||||||
|
export { ACCEPTED_FILE_EXTENSIONS, ACCEPTED_FILE_TYPES, filePickerFilters } from "./constants/file-picker"
|
||||||
export { useCommand } from "./context/command"
|
export { useCommand } from "./context/command"
|
||||||
export { type DisplayBackend, type Platform, PlatformProvider } from "./context/platform"
|
export { type DisplayBackend, type Platform, PlatformProvider } from "./context/platform"
|
||||||
export { ServerConnection } from "./context/server"
|
export { ServerConnection } from "./context/server"
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { batch, createEffect, createMemo, Show, type ParentProps } from "solid-js"
|
import { DataProvider } from "@opencode-ai/ui/context"
|
||||||
import { createStore } from "solid-js/store"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
|
import { base64Encode } from "@opencode-ai/util/encode"
|
||||||
import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
||||||
|
import { createMemo, createResource, type ParentProps, Show } from "solid-js"
|
||||||
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { LocalProvider } from "@/context/local"
|
||||||
import { SDKProvider } from "@/context/sdk"
|
import { SDKProvider } from "@/context/sdk"
|
||||||
import { SyncProvider, useSync } from "@/context/sync"
|
import { SyncProvider, useSync } from "@/context/sync"
|
||||||
import { LocalProvider } from "@/context/local"
|
|
||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
|
||||||
|
|
||||||
import { DataProvider } from "@opencode-ai/ui/context"
|
|
||||||
import { base64Encode } from "@opencode-ai/util/encode"
|
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
|
||||||
import { useLanguage } from "@/context/language"
|
|
||||||
function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
|
function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
@@ -30,19 +29,22 @@ function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
|
|||||||
|
|
||||||
export default function Layout(props: ParentProps) {
|
export default function Layout(props: ParentProps) {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const navigate = useNavigate()
|
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const globalSDK = useGlobalSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
const directory = createMemo(() => decode64(params.dir) ?? "")
|
const navigate = useNavigate()
|
||||||
const [state, setState] = createStore({ invalid: "", resolved: "" })
|
let invalid = ""
|
||||||
|
|
||||||
createEffect(() => {
|
const [resolved] = createResource(
|
||||||
if (!params.dir) return
|
() => {
|
||||||
const raw = directory()
|
if (params.dir) return [location.pathname, params.dir] as const
|
||||||
if (!raw) {
|
},
|
||||||
if (state.invalid === params.dir) return
|
async ([pathname, b64Dir]) => {
|
||||||
setState("invalid", params.dir)
|
const directory = decode64(b64Dir)
|
||||||
|
|
||||||
|
if (!directory) {
|
||||||
|
if (invalid === params.dir) return
|
||||||
|
invalid = b64Dir
|
||||||
showToast({
|
showToast({
|
||||||
variant: "error",
|
variant: "error",
|
||||||
title: language.t("common.requestFailed"),
|
title: language.t("common.requestFailed"),
|
||||||
@@ -52,39 +54,32 @@ export default function Layout(props: ParentProps) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const current = params.dir
|
return await globalSDK
|
||||||
globalSDK
|
|
||||||
.createClient({
|
.createClient({
|
||||||
directory: raw,
|
directory,
|
||||||
throwOnError: true,
|
throwOnError: true,
|
||||||
})
|
})
|
||||||
.path.get()
|
.path.get()
|
||||||
.then((x) => {
|
.then((x) => {
|
||||||
if (params.dir !== current) return
|
const next = x.data?.directory ?? directory
|
||||||
const next = x.data?.directory ?? raw
|
invalid = ""
|
||||||
batch(() => {
|
if (next === directory) return next
|
||||||
setState("invalid", "")
|
const path = pathname.slice(b64Dir.length + 1)
|
||||||
setState("resolved", next)
|
|
||||||
})
|
|
||||||
if (next === raw) return
|
|
||||||
const path = location.pathname.slice(current.length + 1)
|
|
||||||
navigate(`/${base64Encode(next)}${path}${location.search}${location.hash}`, { replace: true })
|
navigate(`/${base64Encode(next)}${path}${location.search}${location.hash}`, { replace: true })
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
if (params.dir !== current) return
|
invalid = ""
|
||||||
batch(() => {
|
return directory
|
||||||
setState("invalid", "")
|
|
||||||
setState("resolved", raw)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={state.resolved}>
|
<Show when={resolved()} keyed>
|
||||||
{(resolved) => (
|
{(resolved) => (
|
||||||
<SDKProvider directory={resolved}>
|
<SDKProvider directory={() => resolved}>
|
||||||
<SyncProvider>
|
<SyncProvider>
|
||||||
<DirectoryDataProvider directory={resolved()}>{props.children}</DirectoryDataProvider>
|
<DirectoryDataProvider directory={resolved}>{props.children}</DirectoryDataProvider>
|
||||||
</SyncProvider>
|
</SyncProvider>
|
||||||
</SDKProvider>
|
</SDKProvider>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
import { Logo } from "@opencode-ai/ui/logo"
|
import { Logo } from "@opencode-ai/ui/logo"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { Component, Show } from "solid-js"
|
import { Component, Show, onMount } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import type { E2EWindow } from "@/testing/terminal"
|
||||||
|
|
||||||
export type InitError = {
|
export type InitError = {
|
||||||
name: string
|
name: string
|
||||||
@@ -226,6 +227,13 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
|
|||||||
actionError: undefined as string | undefined,
|
actionError: undefined as string | undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const win = window as E2EWindow
|
||||||
|
if (!win.__opencode_e2e) return
|
||||||
|
const detail = formatError(props.error, language.t)
|
||||||
|
console.error(`[e2e:error-boundary] ${window.location.pathname}\n${detail}`)
|
||||||
|
})
|
||||||
|
|
||||||
async function checkForUpdates() {
|
async function checkForUpdates() {
|
||||||
if (!platform.checkUpdate) return
|
if (!platform.checkUpdate) return
|
||||||
setStore("checking", true)
|
setStore("checking", true)
|
||||||
|
|||||||
+204
-110
@@ -1,4 +1,17 @@
|
|||||||
import { batch, createEffect, createMemo, For, on, onCleanup, onMount, ParentProps, Show, untrack } from "solid-js"
|
import {
|
||||||
|
batch,
|
||||||
|
createEffect,
|
||||||
|
createMemo,
|
||||||
|
createResource,
|
||||||
|
For,
|
||||||
|
on,
|
||||||
|
onCleanup,
|
||||||
|
onMount,
|
||||||
|
ParentProps,
|
||||||
|
Show,
|
||||||
|
untrack,
|
||||||
|
type Accessor,
|
||||||
|
} from "solid-js"
|
||||||
import { useNavigate, useParams } from "@solidjs/router"
|
import { useNavigate, useParams } from "@solidjs/router"
|
||||||
import { useLayout, LocalProject } from "@/context/layout"
|
import { useLayout, LocalProject } from "@/context/layout"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
@@ -116,6 +129,16 @@ export default function Layout(props: ParentProps) {
|
|||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const initialDirectory = decode64(params.dir)
|
const initialDirectory = decode64(params.dir)
|
||||||
|
const route = createMemo(() => {
|
||||||
|
const slug = params.dir
|
||||||
|
if (!slug) return { slug, dir: "" }
|
||||||
|
const dir = decode64(slug)
|
||||||
|
if (!dir) return { slug, dir: "" }
|
||||||
|
return {
|
||||||
|
slug,
|
||||||
|
dir: globalSync.peek(dir, { bootstrap: false })[0].path.directory || dir,
|
||||||
|
}
|
||||||
|
})
|
||||||
const availableThemeEntries = createMemo(() => Object.entries(theme.themes()))
|
const availableThemeEntries = createMemo(() => Object.entries(theme.themes()))
|
||||||
const colorSchemeOrder: ColorScheme[] = ["system", "light", "dark"]
|
const colorSchemeOrder: ColorScheme[] = ["system", "light", "dark"]
|
||||||
const colorSchemeKey: Record<ColorScheme, "theme.scheme.system" | "theme.scheme.light" | "theme.scheme.dark"> = {
|
const colorSchemeKey: Record<ColorScheme, "theme.scheme.system" | "theme.scheme.light" | "theme.scheme.dark"> = {
|
||||||
@@ -124,7 +147,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
dark: "theme.scheme.dark",
|
dark: "theme.scheme.dark",
|
||||||
}
|
}
|
||||||
const colorSchemeLabel = (scheme: ColorScheme) => language.t(colorSchemeKey[scheme])
|
const colorSchemeLabel = (scheme: ColorScheme) => language.t(colorSchemeKey[scheme])
|
||||||
const currentDir = createMemo(() => decode64(params.dir) ?? "")
|
const currentDir = createMemo(() => route().dir)
|
||||||
|
|
||||||
const [state, setState] = createStore({
|
const [state, setState] = createStore({
|
||||||
autoselect: !initialDirectory,
|
autoselect: !initialDirectory,
|
||||||
@@ -135,7 +158,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
nav: undefined as HTMLElement | undefined,
|
nav: undefined as HTMLElement | undefined,
|
||||||
sortNow: Date.now(),
|
sortNow: Date.now(),
|
||||||
sizing: false,
|
sizing: false,
|
||||||
peek: undefined as LocalProject | undefined,
|
peek: undefined as string | undefined,
|
||||||
peeked: false,
|
peeked: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -233,6 +256,12 @@ export default function Layout(props: ParentProps) {
|
|||||||
return layout.projects.list().find((project) => project.worktree === id)
|
return layout.projects.list().find((project) => project.worktree === id)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const peekProject = createMemo(() => {
|
||||||
|
const id = state.peek
|
||||||
|
if (!id) return
|
||||||
|
return layout.projects.list().find((project) => project.worktree === id)
|
||||||
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const p = hoverProjectData()
|
const p = hoverProjectData()
|
||||||
if (p) {
|
if (p) {
|
||||||
@@ -240,7 +269,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
clearTimeout(peekt)
|
clearTimeout(peekt)
|
||||||
peekt = undefined
|
peekt = undefined
|
||||||
}
|
}
|
||||||
setState("peek", p)
|
setState("peek", p.worktree)
|
||||||
setState("peeked", true)
|
setState("peeked", true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -259,16 +288,6 @@ export default function Layout(props: ParentProps) {
|
|||||||
setHoverProject(undefined)
|
setHoverProject(undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
const autoselecting = createMemo(() => {
|
|
||||||
if (params.dir) return false
|
|
||||||
if (!state.autoselect) return false
|
|
||||||
if (!pageReady()) return true
|
|
||||||
if (!layoutReady()) return true
|
|
||||||
const list = layout.projects.list()
|
|
||||||
if (list.length > 0) return true
|
|
||||||
return !!server.projects.last()
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!state.autoselect) return
|
if (!state.autoselect) return
|
||||||
const dir = params.dir
|
const dir = params.dir
|
||||||
@@ -475,8 +494,8 @@ export default function Layout(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentSession = params.id
|
const currentSession = params.id
|
||||||
if (directory === currentDir() && props.sessionID === currentSession) return
|
if (workspaceKey(directory) === workspaceKey(currentDir()) && props.sessionID === currentSession) return
|
||||||
if (directory === currentDir() && session?.parentID === currentSession) return
|
if (workspaceKey(directory) === workspaceKey(currentDir()) && session?.parentID === currentSession) return
|
||||||
|
|
||||||
dismissSessionAlert(sessionKey)
|
dismissSessionAlert(sessionKey)
|
||||||
|
|
||||||
@@ -534,13 +553,14 @@ export default function Layout(props: ParentProps) {
|
|||||||
const currentProject = createMemo(() => {
|
const currentProject = createMemo(() => {
|
||||||
const directory = currentDir()
|
const directory = currentDir()
|
||||||
if (!directory) return
|
if (!directory) return
|
||||||
|
const key = workspaceKey(directory)
|
||||||
|
|
||||||
const projects = layout.projects.list()
|
const projects = layout.projects.list()
|
||||||
|
|
||||||
const sandbox = projects.find((p) => p.sandboxes?.includes(directory))
|
const sandbox = projects.find((p) => p.sandboxes?.some((item) => workspaceKey(item) === key))
|
||||||
if (sandbox) return sandbox
|
if (sandbox) return sandbox
|
||||||
|
|
||||||
const direct = projects.find((p) => p.worktree === directory)
|
const direct = projects.find((p) => workspaceKey(p.worktree) === key)
|
||||||
if (direct) return direct
|
if (direct) return direct
|
||||||
|
|
||||||
const [child] = globalSync.child(directory, { bootstrap: false })
|
const [child] = globalSync.child(directory, { bootstrap: false })
|
||||||
@@ -554,33 +574,23 @@ export default function Layout(props: ParentProps) {
|
|||||||
return projects.find((p) => p.worktree === root)
|
return projects.find((p) => p.worktree === root)
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(
|
const [autoselecting] = createResource(async () => {
|
||||||
on(
|
await ready.promise
|
||||||
() => ({ ready: pageReady(), layoutReady: layoutReady(), dir: params.dir, list: layout.projects.list() }),
|
await layout.ready.promise
|
||||||
(value) => {
|
if (!untrack(() => state.autoselect)) return
|
||||||
if (!value.ready) return
|
|
||||||
if (!value.layoutReady) return
|
|
||||||
if (!state.autoselect) return
|
|
||||||
if (value.dir) return
|
|
||||||
|
|
||||||
|
const list = layout.projects.list()
|
||||||
const last = server.projects.last()
|
const last = server.projects.last()
|
||||||
|
|
||||||
if (value.list.length === 0) {
|
if (list.length === 0) {
|
||||||
if (!last) return
|
if (!last) return
|
||||||
setState("autoselect", false)
|
await openProject(last, true)
|
||||||
openProject(last, false)
|
} else {
|
||||||
navigateToProject(last)
|
const next = list.find((project) => project.worktree === last) ?? list[0]
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const next = value.list.find((project) => project.worktree === last) ?? value.list[0]
|
|
||||||
if (!next) return
|
if (!next) return
|
||||||
setState("autoselect", false)
|
await openProject(next.worktree, true)
|
||||||
openProject(next.worktree, false)
|
}
|
||||||
navigateToProject(next.worktree)
|
})
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const workspaceName = (directory: string, projectId?: string, branch?: string) => {
|
const workspaceName = (directory: string, projectId?: string, branch?: string) => {
|
||||||
const key = workspaceKey(directory)
|
const key = workspaceKey(directory)
|
||||||
@@ -620,7 +630,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
const activeDir = currentDir()
|
const activeDir = currentDir()
|
||||||
return workspaceIds(project).filter((directory) => {
|
return workspaceIds(project).filter((directory) => {
|
||||||
const expanded = store.workspaceExpanded[directory] ?? directory === project.worktree
|
const expanded = store.workspaceExpanded[directory] ?? directory === project.worktree
|
||||||
const active = directory === activeDir
|
const active = workspaceKey(directory) === workspaceKey(activeDir)
|
||||||
return expanded || active
|
return expanded || active
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -631,7 +641,11 @@ export default function Layout(props: ParentProps) {
|
|||||||
const projects = layout.projects.list()
|
const projects = layout.projects.list()
|
||||||
for (const [directory, expanded] of Object.entries(store.workspaceExpanded)) {
|
for (const [directory, expanded] of Object.entries(store.workspaceExpanded)) {
|
||||||
if (!expanded) continue
|
if (!expanded) continue
|
||||||
const project = projects.find((item) => item.worktree === directory || item.sandboxes?.includes(directory))
|
const key = workspaceKey(directory)
|
||||||
|
const project = projects.find(
|
||||||
|
(item) =>
|
||||||
|
workspaceKey(item.worktree) === key || item.sandboxes?.some((sandbox) => workspaceKey(sandbox) === key),
|
||||||
|
)
|
||||||
if (!project) continue
|
if (!project) continue
|
||||||
if (project.vcs === "git" && layout.sidebar.workspaces(project.worktree)()) continue
|
if (project.vcs === "git" && layout.sidebar.workspaces(project.worktree)()) continue
|
||||||
setStore("workspaceExpanded", directory, false)
|
setStore("workspaceExpanded", directory, false)
|
||||||
@@ -683,7 +697,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
seen: lru,
|
seen: lru,
|
||||||
keep: sessionID,
|
keep: sessionID,
|
||||||
limit: PREFETCH_MAX_SESSIONS_PER_DIR,
|
limit: PREFETCH_MAX_SESSIONS_PER_DIR,
|
||||||
preserve: directory === params.dir && params.id ? [params.id] : undefined,
|
preserve: params.id && workspaceKey(directory) === workspaceKey(currentDir()) ? [params.id] : undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -696,7 +710,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
params.dir
|
route()
|
||||||
globalSDK.url
|
globalSDK.url
|
||||||
|
|
||||||
prefetchToken.value += 1
|
prefetchToken.value += 1
|
||||||
@@ -1156,13 +1170,17 @@ export default function Layout(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function projectRoot(directory: string) {
|
function projectRoot(directory: string) {
|
||||||
|
const key = workspaceKey(directory)
|
||||||
const project = layout.projects
|
const project = layout.projects
|
||||||
.list()
|
.list()
|
||||||
.find((item) => item.worktree === directory || item.sandboxes?.includes(directory))
|
.find(
|
||||||
|
(item) =>
|
||||||
|
workspaceKey(item.worktree) === key || item.sandboxes?.some((sandbox) => workspaceKey(sandbox) === key),
|
||||||
|
)
|
||||||
if (project) return project.worktree
|
if (project) return project.worktree
|
||||||
|
|
||||||
const known = Object.entries(store.workspaceOrder).find(
|
const known = Object.entries(store.workspaceOrder).find(
|
||||||
([root, dirs]) => root === directory || dirs.includes(directory),
|
([root, dirs]) => workspaceKey(root) === key || dirs.some((item) => workspaceKey(item) === key),
|
||||||
)
|
)
|
||||||
if (known) return known[0]
|
if (known) return known[0]
|
||||||
|
|
||||||
@@ -1178,13 +1196,6 @@ export default function Layout(props: ParentProps) {
|
|||||||
return currentProject()?.worktree ?? projectRoot(directory)
|
return currentProject()?.worktree ?? projectRoot(directory)
|
||||||
}
|
}
|
||||||
|
|
||||||
function touchProjectRoute() {
|
|
||||||
const root = currentProject()?.worktree
|
|
||||||
if (!root) return
|
|
||||||
if (server.projects.last() !== root) server.projects.touch(root)
|
|
||||||
return root
|
|
||||||
}
|
|
||||||
|
|
||||||
function rememberSessionRoute(directory: string, id: string, root = activeProjectRoot(directory)) {
|
function rememberSessionRoute(directory: string, id: string, root = activeProjectRoot(directory)) {
|
||||||
setStore("lastProjectSession", root, { directory, id, at: Date.now() })
|
setStore("lastProjectSession", root, { directory, id, at: Date.now() })
|
||||||
return root
|
return root
|
||||||
@@ -1293,7 +1304,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
function openProject(directory: string, navigate = true) {
|
function openProject(directory: string, navigate = true) {
|
||||||
layout.projects.open(directory)
|
layout.projects.open(directory)
|
||||||
if (navigate) navigateToProject(directory)
|
if (navigate) return navigateToProject(directory)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeepLinks = (urls: string[]) => {
|
const handleDeepLinks = (urls: string[]) => {
|
||||||
@@ -1348,8 +1359,9 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
function closeProject(directory: string) {
|
function closeProject(directory: string) {
|
||||||
const list = layout.projects.list()
|
const list = layout.projects.list()
|
||||||
const index = list.findIndex((x) => x.worktree === directory)
|
const key = workspaceKey(directory)
|
||||||
const active = currentProject()?.worktree === directory
|
const index = list.findIndex((x) => workspaceKey(x.worktree) === key)
|
||||||
|
const active = workspaceKey(currentProject()?.worktree ?? "") === key
|
||||||
if (index === -1) return
|
if (index === -1) return
|
||||||
const next = list[index + 1]
|
const next = list[index + 1]
|
||||||
|
|
||||||
@@ -1684,38 +1696,51 @@ export default function Layout(props: ParentProps) {
|
|||||||
const activeRoute = {
|
const activeRoute = {
|
||||||
session: "",
|
session: "",
|
||||||
sessionProject: "",
|
sessionProject: "",
|
||||||
|
directory: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => [pageReady(), params.dir, params.id, currentProject()?.worktree] as const,
|
() => {
|
||||||
([ready, dir, id]) => {
|
return [pageReady(), route().slug, params.id, currentProject()?.worktree, currentDir()] as const
|
||||||
if (!ready || !dir) {
|
},
|
||||||
|
([ready, slug, id, root, dir]) => {
|
||||||
|
if (!ready || !slug || !dir) {
|
||||||
activeRoute.session = ""
|
activeRoute.session = ""
|
||||||
activeRoute.sessionProject = ""
|
activeRoute.sessionProject = ""
|
||||||
|
activeRoute.directory = ""
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const directory = decode64(dir)
|
|
||||||
if (!directory) return
|
|
||||||
|
|
||||||
const root = touchProjectRoute() ?? activeProjectRoot(directory)
|
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
activeRoute.session = ""
|
activeRoute.session = ""
|
||||||
activeRoute.sessionProject = ""
|
activeRoute.sessionProject = ""
|
||||||
|
activeRoute.directory = ""
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = `${dir}/${id}`
|
const session = `${slug}/${id}`
|
||||||
if (session !== activeRoute.session) {
|
|
||||||
|
if (!root) {
|
||||||
activeRoute.session = session
|
activeRoute.session = session
|
||||||
activeRoute.sessionProject = syncSessionRoute(directory, id, root)
|
activeRoute.directory = dir
|
||||||
|
activeRoute.sessionProject = ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.projects.last() !== root) server.projects.touch(root)
|
||||||
|
|
||||||
|
const changed = session !== activeRoute.session || dir !== activeRoute.directory
|
||||||
|
if (changed) {
|
||||||
|
activeRoute.session = session
|
||||||
|
activeRoute.directory = dir
|
||||||
|
activeRoute.sessionProject = syncSessionRoute(dir, id, root)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root === activeRoute.sessionProject) return
|
if (root === activeRoute.sessionProject) return
|
||||||
activeRoute.sessionProject = rememberSessionRoute(directory, id, root)
|
activeRoute.directory = dir
|
||||||
|
activeRoute.sessionProject = rememberSessionRoute(dir, id, root)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -1779,8 +1804,13 @@ export default function Layout(props: ParentProps) {
|
|||||||
const local = project.worktree
|
const local = project.worktree
|
||||||
const dirs = [local, ...(project.sandboxes ?? [])]
|
const dirs = [local, ...(project.sandboxes ?? [])]
|
||||||
const active = currentProject()
|
const active = currentProject()
|
||||||
const directory = active?.worktree === project.worktree ? currentDir() : undefined
|
const directory = workspaceKey(active?.worktree ?? "") === workspaceKey(project.worktree) ? currentDir() : undefined
|
||||||
const extra = directory && directory !== local && !dirs.includes(directory) ? directory : undefined
|
const extra =
|
||||||
|
directory &&
|
||||||
|
workspaceKey(directory) !== workspaceKey(local) &&
|
||||||
|
!dirs.some((item) => workspaceKey(item) === workspaceKey(directory))
|
||||||
|
? directory
|
||||||
|
: undefined
|
||||||
const pending = extra ? WorktreeState.get(extra)?.status === "pending" : false
|
const pending = extra ? WorktreeState.get(extra)?.status === "pending" : false
|
||||||
|
|
||||||
const ordered = effectiveWorkspaceOrder(local, dirs, store.workspaceOrder[project.worktree])
|
const ordered = effectiveWorkspaceOrder(local, dirs, store.workspaceOrder[project.worktree])
|
||||||
@@ -1903,6 +1933,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
const projectSidebarCtx: ProjectSidebarContext = {
|
const projectSidebarCtx: ProjectSidebarContext = {
|
||||||
currentDir,
|
currentDir,
|
||||||
|
currentProject,
|
||||||
sidebarOpened: () => layout.sidebar.opened(),
|
sidebarOpened: () => layout.sidebar.opened(),
|
||||||
sidebarHovering,
|
sidebarHovering,
|
||||||
hoverProject: () => state.hoverProject,
|
hoverProject: () => state.hoverProject,
|
||||||
@@ -1932,17 +1963,33 @@ export default function Layout(props: ParentProps) {
|
|||||||
setHoverSession,
|
setHoverSession,
|
||||||
}
|
}
|
||||||
|
|
||||||
const SidebarPanel = (panelProps: { project: LocalProject | undefined; mobile?: boolean; merged?: boolean }) => {
|
const SidebarPanel = (panelProps: {
|
||||||
|
project: Accessor<LocalProject | undefined>
|
||||||
|
mobile?: boolean
|
||||||
|
merged?: boolean
|
||||||
|
}) => {
|
||||||
|
const project = panelProps.project
|
||||||
const merged = createMemo(() => panelProps.mobile || (panelProps.merged ?? layout.sidebar.opened()))
|
const merged = createMemo(() => panelProps.mobile || (panelProps.merged ?? layout.sidebar.opened()))
|
||||||
const hover = createMemo(() => !panelProps.mobile && panelProps.merged === false && !layout.sidebar.opened())
|
const hover = createMemo(() => !panelProps.mobile && panelProps.merged === false && !layout.sidebar.opened())
|
||||||
const popover = createMemo(() => !!panelProps.mobile || panelProps.merged === false || layout.sidebar.opened())
|
const popover = createMemo(() => !!panelProps.mobile || panelProps.merged === false || layout.sidebar.opened())
|
||||||
|
const empty = createMemo(() => !params.dir && layout.projects.list().length === 0)
|
||||||
const projectName = createMemo(() => {
|
const projectName = createMemo(() => {
|
||||||
const project = panelProps.project
|
const item = project()
|
||||||
if (!project) return ""
|
if (!item) return ""
|
||||||
return project.name || getFilename(project.worktree)
|
return item.name || getFilename(item.worktree)
|
||||||
|
})
|
||||||
|
const projectId = createMemo(() => project()?.id ?? "")
|
||||||
|
const worktree = createMemo(() => project()?.worktree ?? "")
|
||||||
|
const slug = createMemo(() => {
|
||||||
|
const dir = worktree()
|
||||||
|
if (!dir) return ""
|
||||||
|
return base64Encode(dir)
|
||||||
|
})
|
||||||
|
const workspaces = createMemo(() => {
|
||||||
|
const item = project()
|
||||||
|
if (!item) return [] as string[]
|
||||||
|
return workspaceIds(item)
|
||||||
})
|
})
|
||||||
const projectId = createMemo(() => panelProps.project?.id ?? "")
|
|
||||||
const workspaces = createMemo(() => workspaceIds(panelProps.project))
|
|
||||||
const unseenCount = createMemo(() =>
|
const unseenCount = createMemo(() =>
|
||||||
workspaces().reduce((total, directory) => total + notification.project.unseenCount(directory), 0),
|
workspaces().reduce((total, directory) => total + notification.project.unseenCount(directory), 0),
|
||||||
)
|
)
|
||||||
@@ -1951,10 +1998,15 @@ export default function Layout(props: ParentProps) {
|
|||||||
.filter((directory) => notification.project.unseenCount(directory) > 0)
|
.filter((directory) => notification.project.unseenCount(directory) > 0)
|
||||||
.forEach((directory) => notification.project.markViewed(directory))
|
.forEach((directory) => notification.project.markViewed(directory))
|
||||||
const workspacesEnabled = createMemo(() => {
|
const workspacesEnabled = createMemo(() => {
|
||||||
const project = panelProps.project
|
const item = project()
|
||||||
if (!project) return false
|
if (!item) return false
|
||||||
if (project.vcs !== "git") return false
|
if (item.vcs !== "git") return false
|
||||||
return layout.sidebar.workspaces(project.worktree)()
|
return layout.sidebar.workspaces(item.worktree)()
|
||||||
|
})
|
||||||
|
const canToggle = createMemo(() => {
|
||||||
|
const item = project()
|
||||||
|
if (!item) return false
|
||||||
|
return item.vcs === "git" || layout.sidebar.workspaces(item.worktree)()
|
||||||
})
|
})
|
||||||
const homedir = createMemo(() => globalSync.data.path.home)
|
const homedir = createMemo(() => globalSync.data.path.home)
|
||||||
|
|
||||||
@@ -1973,8 +2025,26 @@ export default function Layout(props: ParentProps) {
|
|||||||
width: panelProps.mobile ? undefined : `${Math.max(Math.max(layout.sidebar.width(), 244) - 64, 0)}px`,
|
width: panelProps.mobile ? undefined : `${Math.max(Math.max(layout.sidebar.width(), 244) - 64, 0)}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show when={panelProps.project}>
|
<Show
|
||||||
{(p) => (
|
when={project()}
|
||||||
|
fallback={
|
||||||
|
<Show when={empty()}>
|
||||||
|
<div class="flex-1 min-h-0 -mt-4 flex items-center justify-center px-6 pb-64 text-center">
|
||||||
|
<div class="mt-8 flex max-w-60 flex-col items-center gap-6 text-center">
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div class="text-14-medium text-text-strong">{language.t("sidebar.empty.title")}</div>
|
||||||
|
<div class="text-14-regular text-text-base" style={{ "line-height": "var(--line-height-normal)" }}>
|
||||||
|
{language.t("sidebar.empty.description")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button size="large" icon="folder-add-left" onClick={chooseProject}>
|
||||||
|
{language.t("command.project.open")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
>
|
||||||
<>
|
<>
|
||||||
<div class="shrink-0 pl-1 py-1">
|
<div class="shrink-0 pl-1 py-1">
|
||||||
<div class="group/project flex items-start justify-between gap-2 py-2 pl-2 pr-0">
|
<div class="group/project flex items-start justify-between gap-2 py-2 pl-2 pr-0">
|
||||||
@@ -1982,7 +2052,11 @@ export default function Layout(props: ParentProps) {
|
|||||||
<InlineEditor
|
<InlineEditor
|
||||||
id={`project:${projectId()}`}
|
id={`project:${projectId()}`}
|
||||||
value={projectName}
|
value={projectName}
|
||||||
onSave={(next) => renameProject(p(), next)}
|
onSave={(next) => {
|
||||||
|
const item = project()
|
||||||
|
if (!item) return
|
||||||
|
renameProject(item, next)
|
||||||
|
}}
|
||||||
class="text-14-medium text-text-strong truncate"
|
class="text-14-medium text-text-strong truncate"
|
||||||
displayClass="text-14-medium text-text-strong truncate"
|
displayClass="text-14-medium text-text-strong truncate"
|
||||||
stopPropagation
|
stopPropagation
|
||||||
@@ -1991,7 +2065,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
gutter={2}
|
gutter={2}
|
||||||
value={p().worktree}
|
value={worktree()}
|
||||||
class="shrink-0"
|
class="shrink-0"
|
||||||
contentStyle={{
|
contentStyle={{
|
||||||
"max-width": "640px",
|
"max-width": "640px",
|
||||||
@@ -1999,7 +2073,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span class="text-12-regular text-text-base truncate select-text">
|
<span class="text-12-regular text-text-base truncate select-text">
|
||||||
{p().worktree.replace(homedir(), "~")}
|
{worktree().replace(homedir(), "~")}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -2010,7 +2084,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
icon="dot-grid"
|
icon="dot-grid"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
data-action="project-menu"
|
data-action="project-menu"
|
||||||
data-project={base64Encode(p().worktree)}
|
data-project={slug()}
|
||||||
class="shrink-0 size-6 rounded-md data-[expanded]:bg-surface-base-active"
|
class="shrink-0 size-6 rounded-md data-[expanded]:bg-surface-base-active"
|
||||||
classList={{
|
classList={{
|
||||||
"opacity-0 group-hover/project:opacity-100 data-[expanded]:opacity-100": !panelProps.mobile,
|
"opacity-0 group-hover/project:opacity-100 data-[expanded]:opacity-100": !panelProps.mobile,
|
||||||
@@ -2019,24 +2093,34 @@ export default function Layout(props: ParentProps) {
|
|||||||
/>
|
/>
|
||||||
<DropdownMenu.Portal>
|
<DropdownMenu.Portal>
|
||||||
<DropdownMenu.Content class="mt-1">
|
<DropdownMenu.Content class="mt-1">
|
||||||
<DropdownMenu.Item onSelect={() => showEditProjectDialog(p())}>
|
<DropdownMenu.Item
|
||||||
|
onSelect={() => {
|
||||||
|
const item = project()
|
||||||
|
if (!item) return
|
||||||
|
showEditProjectDialog(item)
|
||||||
|
}}
|
||||||
|
>
|
||||||
<DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
|
<DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
data-action="project-workspaces-toggle"
|
data-action="project-workspaces-toggle"
|
||||||
data-project={base64Encode(p().worktree)}
|
data-project={slug()}
|
||||||
disabled={p().vcs !== "git" && !layout.sidebar.workspaces(p().worktree)()}
|
disabled={!canToggle()}
|
||||||
onSelect={() => toggleProjectWorkspaces(p())}
|
onSelect={() => {
|
||||||
|
const item = project()
|
||||||
|
if (!item) return
|
||||||
|
toggleProjectWorkspaces(item)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.ItemLabel>
|
<DropdownMenu.ItemLabel>
|
||||||
{layout.sidebar.workspaces(p().worktree)()
|
{workspacesEnabled()
|
||||||
? language.t("sidebar.workspaces.disable")
|
? language.t("sidebar.workspaces.disable")
|
||||||
: language.t("sidebar.workspaces.enable")}
|
: language.t("sidebar.workspaces.enable")}
|
||||||
</DropdownMenu.ItemLabel>
|
</DropdownMenu.ItemLabel>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
data-action="project-clear-notifications"
|
data-action="project-clear-notifications"
|
||||||
data-project={base64Encode(p().worktree)}
|
data-project={slug()}
|
||||||
disabled={unseenCount() === 0}
|
disabled={unseenCount() === 0}
|
||||||
onSelect={clearNotifications}
|
onSelect={clearNotifications}
|
||||||
>
|
>
|
||||||
@@ -2047,8 +2131,12 @@ export default function Layout(props: ParentProps) {
|
|||||||
<DropdownMenu.Separator />
|
<DropdownMenu.Separator />
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
data-action="project-close-menu"
|
data-action="project-close-menu"
|
||||||
data-project={base64Encode(p().worktree)}
|
data-project={slug()}
|
||||||
onSelect={() => closeProject(p().worktree)}
|
onSelect={() => {
|
||||||
|
const dir = worktree()
|
||||||
|
if (!dir) return
|
||||||
|
closeProject(dir)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.ItemLabel>{language.t("common.close")}</DropdownMenu.ItemLabel>
|
<DropdownMenu.ItemLabel>{language.t("common.close")}</DropdownMenu.ItemLabel>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
@@ -2066,9 +2154,13 @@ export default function Layout(props: ParentProps) {
|
|||||||
<div class="shrink-0 py-4">
|
<div class="shrink-0 py-4">
|
||||||
<Button
|
<Button
|
||||||
size="large"
|
size="large"
|
||||||
icon="plus-small"
|
icon="new-session"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
onClick={() => navigateWithSidebarReset(`/${base64Encode(p().worktree)}/session`)}
|
onClick={() => {
|
||||||
|
const dir = worktree()
|
||||||
|
if (!dir) return
|
||||||
|
navigateWithSidebarReset(`/${base64Encode(dir)}/session`)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{language.t("command.session.new")}
|
{language.t("command.session.new")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -2076,7 +2168,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
<div class="flex-1 min-h-0">
|
<div class="flex-1 min-h-0">
|
||||||
<LocalWorkspace
|
<LocalWorkspace
|
||||||
ctx={workspaceSidebarCtx}
|
ctx={workspaceSidebarCtx}
|
||||||
project={p()}
|
project={project()!}
|
||||||
sortNow={sortNow}
|
sortNow={sortNow}
|
||||||
mobile={panelProps.mobile}
|
mobile={panelProps.mobile}
|
||||||
popover={popover()}
|
popover={popover()}
|
||||||
@@ -2087,7 +2179,16 @@ export default function Layout(props: ParentProps) {
|
|||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<div class="shrink-0 py-4">
|
<div class="shrink-0 py-4">
|
||||||
<Button size="large" icon="plus-small" class="w-full" onClick={() => createWorkspace(p())}>
|
<Button
|
||||||
|
size="large"
|
||||||
|
icon="plus-small"
|
||||||
|
class="w-full"
|
||||||
|
onClick={() => {
|
||||||
|
const item = project()
|
||||||
|
if (!item) return
|
||||||
|
createWorkspace(item)
|
||||||
|
}}
|
||||||
|
>
|
||||||
{language.t("workspace.new")}
|
{language.t("workspace.new")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -2112,7 +2213,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
<SortableWorkspace
|
<SortableWorkspace
|
||||||
ctx={workspaceSidebarCtx}
|
ctx={workspaceSidebarCtx}
|
||||||
directory={directory}
|
directory={directory}
|
||||||
project={p()}
|
project={project()!}
|
||||||
sortNow={sortNow}
|
sortNow={sortNow}
|
||||||
mobile={panelProps.mobile}
|
mobile={panelProps.mobile}
|
||||||
popover={popover()}
|
popover={popover()}
|
||||||
@@ -2134,7 +2235,6 @@ export default function Layout(props: ParentProps) {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -2193,13 +2293,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
helpLabel={() => language.t("sidebar.help")}
|
helpLabel={() => language.t("sidebar.help")}
|
||||||
onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
|
onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
|
||||||
renderPanel={() =>
|
renderPanel={() =>
|
||||||
mobile ? (
|
mobile ? <SidebarPanel project={currentProject} mobile /> : <SidebarPanel project={currentProject} merged />
|
||||||
<SidebarPanel project={currentProject()} mobile />
|
|
||||||
) : (
|
|
||||||
<Show when={currentProject()}>
|
|
||||||
<SidebarPanel project={currentProject()} merged />
|
|
||||||
</Show>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -2300,7 +2394,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
"size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base bg-background-base xl:border-l xl:rounded-tl-[12px]": true,
|
"size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base bg-background-base xl:border-l xl:rounded-tl-[12px]": true,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show when={!autoselecting()} fallback={<div class="size-full" />}>
|
<Show when={!autoselecting.loading} fallback={<div class="size-full" />}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</Show>
|
</Show>
|
||||||
</main>
|
</main>
|
||||||
@@ -2325,8 +2419,8 @@ export default function Layout(props: ParentProps) {
|
|||||||
arm()
|
arm()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show when={state.peek}>
|
<Show when={peekProject()}>
|
||||||
<SidebarPanel project={state.peek} merged={false} />
|
<SidebarPanel project={peekProject} merged={false} />
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -104,14 +104,14 @@ describe("layout deep links", () => {
|
|||||||
describe("layout workspace helpers", () => {
|
describe("layout workspace helpers", () => {
|
||||||
test("normalizes trailing slash in workspace key", () => {
|
test("normalizes trailing slash in workspace key", () => {
|
||||||
expect(workspaceKey("/tmp/demo///")).toBe("/tmp/demo")
|
expect(workspaceKey("/tmp/demo///")).toBe("/tmp/demo")
|
||||||
expect(workspaceKey("C:\\tmp\\demo\\\\")).toBe("C:\\tmp\\demo")
|
expect(workspaceKey("C:\\tmp\\demo\\\\")).toBe("C:/tmp/demo")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("preserves posix and drive roots in workspace key", () => {
|
test("preserves posix and drive roots in workspace key", () => {
|
||||||
expect(workspaceKey("/")).toBe("/")
|
expect(workspaceKey("/")).toBe("/")
|
||||||
expect(workspaceKey("///")).toBe("/")
|
expect(workspaceKey("///")).toBe("/")
|
||||||
expect(workspaceKey("C:\\")).toBe("C:\\")
|
expect(workspaceKey("C:\\")).toBe("C:/")
|
||||||
expect(workspaceKey("C:\\\\\\")).toBe("C:\\")
|
expect(workspaceKey("C://")).toBe("C:/")
|
||||||
expect(workspaceKey("C:///")).toBe("C:/")
|
expect(workspaceKey("C:///")).toBe("C:/")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import { getFilename } from "@opencode-ai/util/path"
|
import { getFilename } from "@opencode-ai/util/path"
|
||||||
import { type Session } from "@opencode-ai/sdk/v2/client"
|
import { type Session } from "@opencode-ai/sdk/v2/client"
|
||||||
|
|
||||||
|
type SessionStore = {
|
||||||
|
session?: Session[]
|
||||||
|
path: { directory: string }
|
||||||
|
}
|
||||||
|
|
||||||
export const workspaceKey = (directory: string) => {
|
export const workspaceKey = (directory: string) => {
|
||||||
const drive = directory.match(/^([A-Za-z]:)[\\/]+$/)
|
const value = directory.replaceAll("\\", "/")
|
||||||
if (drive) return `${drive[1]}${directory.includes("\\") ? "\\" : "/"}`
|
const drive = value.match(/^([A-Za-z]:)\/+$/)
|
||||||
if (/^[\\/]+$/.test(directory)) return directory.includes("\\") ? "\\" : "/"
|
if (drive) return `${drive[1]}/`
|
||||||
return directory.replace(/[\\/]+$/, "")
|
if (/^\/+$/i.test(value)) return "/"
|
||||||
|
return value.replace(/\/+$/, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortSessions(now: number) {
|
function sortSessions(now: number) {
|
||||||
@@ -25,24 +31,24 @@ function sortSessions(now: number) {
|
|||||||
const isRootVisibleSession = (session: Session, directory: string) =>
|
const isRootVisibleSession = (session: Session, directory: string) =>
|
||||||
workspaceKey(session.directory) === workspaceKey(directory) && !session.parentID && !session.time?.archived
|
workspaceKey(session.directory) === workspaceKey(directory) && !session.parentID && !session.time?.archived
|
||||||
|
|
||||||
export const sortedRootSessions = (store: { session: Session[]; path: { directory: string } }, now: number) =>
|
const roots = (store: SessionStore) =>
|
||||||
store.session.filter((session) => isRootVisibleSession(session, store.path.directory)).sort(sortSessions(now))
|
(store.session ?? []).filter((session) => isRootVisibleSession(session, store.path.directory))
|
||||||
|
|
||||||
export const latestRootSession = (stores: { session: Session[]; path: { directory: string } }[], now: number) =>
|
export const sortedRootSessions = (store: SessionStore, now: number) => roots(store).sort(sortSessions(now))
|
||||||
stores
|
|
||||||
.flatMap((store) => store.session.filter((session) => isRootVisibleSession(session, store.path.directory)))
|
export const latestRootSession = (stores: SessionStore[], now: number) =>
|
||||||
.sort(sortSessions(now))[0]
|
stores.flatMap(roots).sort(sortSessions(now))[0]
|
||||||
|
|
||||||
export function hasProjectPermissions<T>(
|
export function hasProjectPermissions<T>(
|
||||||
request: Record<string, T[] | undefined>,
|
request: Record<string, T[] | undefined> | undefined,
|
||||||
include: (item: T) => boolean = () => true,
|
include: (item: T) => boolean = () => true,
|
||||||
) {
|
) {
|
||||||
return Object.values(request).some((list) => list?.some(include))
|
return Object.values(request ?? {}).some((list) => list?.some(include))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const childMapByParent = (sessions: Session[]) => {
|
export const childMapByParent = (sessions: Session[] | undefined) => {
|
||||||
const map = new Map<string, string[]>()
|
const map = new Map<string, string[]>()
|
||||||
for (const session of sessions) {
|
for (const session of sessions ?? []) {
|
||||||
if (!session.parentID) continue
|
if (!session.parentID) continue
|
||||||
const existing = map.get(session.parentID)
|
const existing = map.get(session.parentID)
|
||||||
if (existing) {
|
if (existing) {
|
||||||
|
|||||||
@@ -9,14 +9,13 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
|
|||||||
import { base64Encode } from "@opencode-ai/util/encode"
|
import { base64Encode } from "@opencode-ai/util/encode"
|
||||||
import { getFilename } from "@opencode-ai/util/path"
|
import { getFilename } from "@opencode-ai/util/path"
|
||||||
import { A, useNavigate, useParams } from "@solidjs/router"
|
import { A, useNavigate, useParams } from "@solidjs/router"
|
||||||
import { type Accessor, createEffect, createMemo, For, type JSX, on, onCleanup, Show } from "solid-js"
|
import { type Accessor, createMemo, For, type JSX, Match, onCleanup, Show, Switch } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout"
|
import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout"
|
||||||
import { useNotification } from "@/context/notification"
|
import { useNotification } from "@/context/notification"
|
||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
import { agentColor } from "@/utils/agent"
|
import { messageAgentColor } from "@/utils/agent"
|
||||||
import { sessionPermissionRequest } from "../session/composer/session-request-tree"
|
import { sessionPermissionRequest } from "../session/composer/session-request-tree"
|
||||||
import { hasProjectPermissions } from "./helpers"
|
import { hasProjectPermissions } from "./helpers"
|
||||||
|
|
||||||
@@ -102,44 +101,10 @@ const SessionRow = (props: {
|
|||||||
warmPress: () => void
|
warmPress: () => void
|
||||||
warmFocus: () => void
|
warmFocus: () => void
|
||||||
cancelHoverPrefetch: () => void
|
cancelHoverPrefetch: () => void
|
||||||
}): JSX.Element => {
|
}): JSX.Element => (
|
||||||
const [slot, setSlot] = createStore({
|
|
||||||
open: false,
|
|
||||||
show: false,
|
|
||||||
fade: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
let f: number | undefined
|
|
||||||
const clear = () => {
|
|
||||||
if (f !== undefined) window.clearTimeout(f)
|
|
||||||
f = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
onCleanup(clear)
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
() => props.isWorking(),
|
|
||||||
(on, prev) => {
|
|
||||||
clear()
|
|
||||||
if (on) {
|
|
||||||
setSlot({ open: true, show: true, fade: false })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (prev) {
|
|
||||||
setSlot({ open: false, show: true, fade: true })
|
|
||||||
f = window.setTimeout(() => setSlot({ show: false, fade: false }), 260)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setSlot({ open: false, show: false, fade: false })
|
|
||||||
},
|
|
||||||
{ defer: true },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<A
|
<A
|
||||||
href={`/${props.slug}/session/${props.session.id}`}
|
href={`/${props.slug}/session/${props.session.id}`}
|
||||||
class={`relative flex items-center min-w-0 text-left w-full focus:outline-none transition-[padding] ${props.mobile ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
|
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] ${props.mobile ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
|
||||||
onPointerDown={props.warmPress}
|
onPointerDown={props.warmPress}
|
||||||
onPointerEnter={props.warmHover}
|
onPointerEnter={props.warmHover}
|
||||||
onPointerLeave={props.cancelHoverPrefetch}
|
onPointerLeave={props.cancelHoverPrefetch}
|
||||||
@@ -150,46 +115,32 @@ const SessionRow = (props: {
|
|||||||
props.clearHoverProjectSoon()
|
props.clearHoverProjectSoon()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show when={!props.isWorking() && (props.hasPermissions() || props.hasError() || props.unseenCount() > 0)}>
|
<div class="flex items-center gap-1 w-full">
|
||||||
<div
|
<div
|
||||||
classList={{
|
class="shrink-0 size-6 flex items-center justify-center"
|
||||||
"absolute left-0 top-1/2 -translate-y-1/2 size-1.5 rounded-full": true,
|
style={{ color: props.tint() ?? "var(--icon-interactive-base)" }}
|
||||||
"bg-surface-warning-strong": props.hasPermissions(),
|
|
||||||
"bg-text-diff-delete-base": !props.hasPermissions() && props.hasError(),
|
|
||||||
"bg-text-interactive-base": !props.hasPermissions() && !props.hasError() && props.unseenCount() > 0,
|
|
||||||
}}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<div class="flex items-center min-w-0 grow-1">
|
|
||||||
<div
|
|
||||||
class="shrink-0 flex items-center justify-center overflow-hidden transition-[width,margin] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)]"
|
|
||||||
style={{
|
|
||||||
width: slot.open ? "16px" : "0px",
|
|
||||||
"margin-right": slot.open ? "8px" : "0px",
|
|
||||||
}}
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
>
|
||||||
<Show when={slot.show}>
|
<Switch fallback={<Icon name="dash" size="small" class="text-icon-weak" />}>
|
||||||
<div
|
<Match when={props.isWorking()}>
|
||||||
class="transition-opacity duration-200 ease-out"
|
<Spinner class="size-[15px]" />
|
||||||
classList={{
|
</Match>
|
||||||
"opacity-0": slot.fade,
|
<Match when={props.hasPermissions()}>
|
||||||
}}
|
<div class="size-1.5 rounded-full bg-surface-warning-strong" />
|
||||||
>
|
</Match>
|
||||||
<Spinner class="size-4" style={{ color: props.tint() ?? "var(--icon-interactive-base)" }} />
|
<Match when={props.hasError()}>
|
||||||
|
<div class="size-1.5 rounded-full bg-text-diff-delete-base" />
|
||||||
|
</Match>
|
||||||
|
<Match when={props.unseenCount() > 0}>
|
||||||
|
<div class="size-1.5 rounded-full bg-text-interactive-base" />
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate">
|
<span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate">
|
||||||
{props.session.title}
|
{props.session.title}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</A>
|
</A>
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
const SessionHoverPreview = (props: {
|
const SessionHoverPreview = (props: {
|
||||||
mobile?: boolean
|
mobile?: boolean
|
||||||
@@ -268,19 +219,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const tint = createMemo(() => {
|
const tint = createMemo(() => {
|
||||||
const messages = sessionStore.message[props.session.id]
|
return messageAgentColor(sessionStore.message[props.session.id], sessionStore.agent)
|
||||||
if (!messages) return undefined
|
|
||||||
let user: Message | undefined
|
|
||||||
for (let i = messages.length - 1; i >= 0; i--) {
|
|
||||||
const message = messages[i]
|
|
||||||
if (message.role !== "user") continue
|
|
||||||
user = message
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (!user?.agent) return undefined
|
|
||||||
|
|
||||||
const agent = sessionStore.agent.find((a) => a.name === user.agent)
|
|
||||||
return agentColor(user.agent, agent?.color)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const hoverMessages = createMemo(() =>
|
const hoverMessages = createMemo(() =>
|
||||||
@@ -359,7 +298,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-session-id={props.session.id}
|
data-session-id={props.session.id}
|
||||||
class="group/session relative w-full rounded-md cursor-default pl-3 pr-3 transition-colors
|
class="group/session relative w-full rounded-md cursor-default pl-2 pr-3 transition-colors
|
||||||
hover:bg-surface-raised-base-hover [&:has(:focus-visible)]:bg-surface-raised-base-hover has-[[data-expanded]]:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active"
|
hover:bg-surface-raised-base-hover [&:has(:focus-visible)]:bg-surface-raised-base-hover has-[[data-expanded]]:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active"
|
||||||
>
|
>
|
||||||
<Show
|
<Show
|
||||||
@@ -445,7 +384,7 @@ export const NewSessionItem = (props: {
|
|||||||
>
|
>
|
||||||
<div class="flex items-center gap-1 w-full">
|
<div class="flex items-center gap-1 w-full">
|
||||||
<div class="shrink-0 size-6 flex items-center justify-center">
|
<div class="shrink-0 size-6 flex items-center justify-center">
|
||||||
<Icon name="plus-small" size="small" class="text-icon-weak" />
|
<Icon name="new-session" size="small" class="text-icon-weak" />
|
||||||
</div>
|
</div>
|
||||||
<span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate">
|
<span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate">
|
||||||
{label}
|
{label}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { childMapByParent, displayName, sortedRootSessions } from "./helpers"
|
|||||||
|
|
||||||
export type ProjectSidebarContext = {
|
export type ProjectSidebarContext = {
|
||||||
currentDir: Accessor<string>
|
currentDir: Accessor<string>
|
||||||
|
currentProject: Accessor<LocalProject | undefined>
|
||||||
sidebarOpened: Accessor<boolean>
|
sidebarOpened: Accessor<boolean>
|
||||||
sidebarHovering: Accessor<boolean>
|
sidebarHovering: Accessor<boolean>
|
||||||
hoverProject: Accessor<string | undefined>
|
hoverProject: Accessor<string | undefined>
|
||||||
@@ -278,11 +279,7 @@ export const SortableProject = (props: {
|
|||||||
const globalSync = useGlobalSync()
|
const globalSync = useGlobalSync()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const sortable = createSortable(props.project.worktree)
|
const sortable = createSortable(props.project.worktree)
|
||||||
const selected = createMemo(
|
const selected = createMemo(() => props.ctx.currentProject()?.worktree === props.project.worktree)
|
||||||
() =>
|
|
||||||
props.project.worktree === props.ctx.currentDir() ||
|
|
||||||
props.project.sandboxes?.includes(props.ctx.currentDir()) === true,
|
|
||||||
)
|
|
||||||
const workspaces = createMemo(() => props.ctx.workspaceIds(props.project).slice(0, 2))
|
const workspaces = createMemo(() => props.ctx.workspaceIds(props.project).slice(0, 2))
|
||||||
const workspaceEnabled = createMemo(() => props.ctx.workspacesEnabled(props.project))
|
const workspaceEnabled = createMemo(() => props.ctx.workspacesEnabled(props.project))
|
||||||
const dirs = createMemo(() => props.ctx.workspaceIds(props.project))
|
const dirs = createMemo(() => props.ctx.workspaceIds(props.project))
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { type LocalProject } from "@/context/layout"
|
|||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items"
|
import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items"
|
||||||
import { childMapByParent, sortedRootSessions } from "./helpers"
|
import { childMapByParent, sortedRootSessions, workspaceKey } from "./helpers"
|
||||||
|
|
||||||
type InlineEditorComponent = (props: {
|
type InlineEditorComponent = (props: {
|
||||||
id: string
|
id: string
|
||||||
@@ -217,7 +217,7 @@ const WorkspaceActions = (props: {
|
|||||||
<Show when={!props.touch()}>
|
<Show when={!props.touch()}>
|
||||||
<Tooltip value={props.language.t("command.session.new")} placement="top">
|
<Tooltip value={props.language.t("command.session.new")} placement="top">
|
||||||
<IconButton
|
<IconButton
|
||||||
icon="plus-small"
|
icon="new-session"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="size-6 rounded-md opacity-0 pointer-events-none group-hover/workspace:opacity-100 group-hover/workspace:pointer-events-auto group-focus-within/workspace:opacity-100 group-focus-within/workspace:pointer-events-auto"
|
class="size-6 rounded-md opacity-0 pointer-events-none group-hover/workspace:opacity-100 group-hover/workspace:pointer-events-auto group-focus-within/workspace:opacity-100 group-focus-within/workspace:pointer-events-auto"
|
||||||
data-action="workspace-new-session"
|
data-action="workspace-new-session"
|
||||||
@@ -323,7 +323,7 @@ export const SortableWorkspace = (props: {
|
|||||||
const sessions = createMemo(() => sortedRootSessions(workspaceStore, props.sortNow()))
|
const sessions = createMemo(() => sortedRootSessions(workspaceStore, props.sortNow()))
|
||||||
const children = createMemo(() => childMapByParent(workspaceStore.session))
|
const children = createMemo(() => childMapByParent(workspaceStore.session))
|
||||||
const local = createMemo(() => props.directory === props.project.worktree)
|
const local = createMemo(() => props.directory === props.project.worktree)
|
||||||
const active = createMemo(() => props.ctx.currentDir() === props.directory)
|
const active = createMemo(() => workspaceKey(props.ctx.currentDir()) === workspaceKey(props.directory))
|
||||||
const workspaceValue = createMemo(() => {
|
const workspaceValue = createMemo(() => {
|
||||||
const branch = workspaceStore.vcs?.branch
|
const branch = workspaceStore.vcs?.branch
|
||||||
const name = branch ?? getFilename(props.directory)
|
const name = branch ?? getFilename(props.directory)
|
||||||
@@ -332,12 +332,13 @@ export const SortableWorkspace = (props: {
|
|||||||
const open = createMemo(() => props.ctx.workspaceExpanded(props.directory, local()))
|
const open = createMemo(() => props.ctx.workspaceExpanded(props.directory, local()))
|
||||||
const boot = createMemo(() => open() || active())
|
const boot = createMemo(() => open() || active())
|
||||||
const booted = createMemo((prev) => prev || workspaceStore.status === "complete", false)
|
const booted = createMemo((prev) => prev || workspaceStore.status === "complete", false)
|
||||||
const hasMore = createMemo(() => workspaceStore.sessionTotal > sessions().length)
|
const count = createMemo(() => sessions()?.length ?? 0)
|
||||||
|
const hasMore = createMemo(() => workspaceStore.sessionTotal > count())
|
||||||
const busy = createMemo(() => props.ctx.isBusy(props.directory))
|
const busy = createMemo(() => props.ctx.isBusy(props.directory))
|
||||||
const wasBusy = createMemo((prev) => prev || busy(), false)
|
const wasBusy = createMemo((prev) => prev || busy(), false)
|
||||||
const loading = createMemo(() => open() && !booted() && sessions().length === 0 && !wasBusy())
|
const loading = createMemo(() => open() && !booted() && count() === 0 && !wasBusy())
|
||||||
const touch = createMediaQuery("(hover: none)")
|
const touch = createMediaQuery("(hover: none)")
|
||||||
const showNew = createMemo(() => !loading() && (touch() || sessions().length === 0 || (active() && !params.id)))
|
const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id)))
|
||||||
const loadMore = async () => {
|
const loadMore = async () => {
|
||||||
setWorkspaceStore("limit", (limit) => (limit ?? 0) + 5)
|
setWorkspaceStore("limit", (limit) => (limit ?? 0) + 5)
|
||||||
await globalSync.project.loadSessions(props.directory)
|
await globalSync.project.loadSessions(props.directory)
|
||||||
@@ -472,8 +473,9 @@ export const LocalWorkspace = (props: {
|
|||||||
const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow()))
|
const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow()))
|
||||||
const children = createMemo(() => childMapByParent(workspace().store.session))
|
const children = createMemo(() => childMapByParent(workspace().store.session))
|
||||||
const booted = createMemo((prev) => prev || workspace().store.status === "complete", false)
|
const booted = createMemo((prev) => prev || workspace().store.status === "complete", false)
|
||||||
const loading = createMemo(() => !booted() && sessions().length === 0)
|
const count = createMemo(() => sessions()?.length ?? 0)
|
||||||
const hasMore = createMemo(() => workspace().store.sessionTotal > sessions().length)
|
const loading = createMemo(() => !booted() && count() === 0)
|
||||||
|
const hasMore = createMemo(() => workspace().store.sessionTotal > count())
|
||||||
const loadMore = async () => {
|
const loadMore = async () => {
|
||||||
workspace().setStore("limit", (limit) => (limit ?? 0) + 5)
|
workspace().setStore("limit", (limit) => (limit ?? 0) + 5)
|
||||||
await globalSync.project.loadSessions(props.project.worktree)
|
await globalSync.project.loadSessions(props.project.worktree)
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ import { createOpenReviewFile, createSessionTabs, createSizing, focusTerminalByI
|
|||||||
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"
|
||||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
import { resetSessionModel, syncSessionModel } from "@/pages/session/session-model-helpers"
|
import { syncSessionModel } from "@/pages/session/session-model-helpers"
|
||||||
import { SessionSidePanel } from "@/pages/session/session-side-panel"
|
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"
|
||||||
@@ -490,7 +490,7 @@ export default function Page() {
|
|||||||
(next, prev) => {
|
(next, prev) => {
|
||||||
if (!prev) return
|
if (!prev) return
|
||||||
if (next.dir === prev.dir && next.id === prev.id) return
|
if (next.dir === prev.dir && next.id === prev.id) return
|
||||||
if (!next.id) resetSessionModel(local)
|
if (prev.id && !next.id) local.session.reset()
|
||||||
},
|
},
|
||||||
{ defer: true },
|
{ defer: true },
|
||||||
),
|
),
|
||||||
@@ -1477,6 +1477,7 @@ export default function Page() {
|
|||||||
|
|
||||||
const fork = (input: { sessionID: string; messageID: string }) => {
|
const fork = (input: { sessionID: string; messageID: string }) => {
|
||||||
const value = draft(input.messageID)
|
const value = draft(input.messageID)
|
||||||
|
const dir = base64Encode(sdk.directory)
|
||||||
return sdk.client.session
|
return sdk.client.session
|
||||||
.fork(input)
|
.fork(input)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
@@ -1488,10 +1489,8 @@ export default function Page() {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
navigate(`/${base64Encode(sdk.directory)}/session/${next.id}`)
|
prompt.set(value, undefined, { dir, id: next.id })
|
||||||
requestAnimationFrame(() => {
|
navigate(`/${dir}/session/${next.id}`)
|
||||||
prompt.set(value)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.catch(fail)
|
.catch(fail)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -217,17 +217,6 @@ export function FileTabContent(props: { tab: string }) {
|
|||||||
onDelete={controls.remove}
|
onDelete={controls.remove}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
onDraftPopoverFocusOut: (e: FocusEvent) => {
|
|
||||||
const current = e.currentTarget as HTMLDivElement
|
|
||||||
const target = e.relatedTarget
|
|
||||||
if (target instanceof Node && current.contains(target)) return
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!document.activeElement || !current.contains(document.activeElement)) {
|
|
||||||
setNote("commenting", null)
|
|
||||||
}
|
|
||||||
}, 0)
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
@@ -426,7 +415,6 @@ export function FileTabContent(props: { tab: string }) {
|
|||||||
commentsUi.onLineSelectionEnd(range)
|
commentsUi.onLineSelectionEnd(range)
|
||||||
}}
|
}}
|
||||||
search={search}
|
search={search}
|
||||||
overflow="scroll"
|
|
||||||
class="select-text"
|
class="select-text"
|
||||||
media={{
|
media={{
|
||||||
mode: "auto",
|
mode: "auto",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { usePlatform } from "@/context/platform"
|
|||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
|
import { messageAgentColor } from "@/utils/agent"
|
||||||
import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note"
|
import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note"
|
||||||
|
|
||||||
type MessageComment = {
|
type MessageComment = {
|
||||||
@@ -246,6 +247,7 @@ export function MessageTimeline(props: {
|
|||||||
return sync.data.session_status[id] ?? idle
|
return sync.data.session_status[id] ?? idle
|
||||||
})
|
})
|
||||||
const working = createMemo(() => !!pending() || sessionStatus().type !== "idle")
|
const working = createMemo(() => !!pending() || sessionStatus().type !== "idle")
|
||||||
|
const tint = createMemo(() => messageAgentColor(sessionMessages(), sync.data.agent))
|
||||||
|
|
||||||
const [slot, setSlot] = createStore({
|
const [slot, setSlot] = createStore({
|
||||||
open: false,
|
open: false,
|
||||||
@@ -689,7 +691,7 @@ export function MessageTimeline(props: {
|
|||||||
"opacity-0": slot.fade,
|
"opacity-0": slot.fade,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Spinner class="size-4" style={{ color: "var(--icon-interactive-base)" }} />
|
<Spinner class="size-4" style={{ color: tint() ?? "var(--icon-interactive-base)" }} />
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,145 +14,38 @@ const message = (input?: Partial<Pick<UserMessage, "agent" | "model" | "variant"
|
|||||||
}) as UserMessage
|
}) as UserMessage
|
||||||
|
|
||||||
describe("syncSessionModel", () => {
|
describe("syncSessionModel", () => {
|
||||||
test("restores the last message model and variant", () => {
|
test("restores the last message through session state", () => {
|
||||||
const calls: unknown[] = []
|
const calls: unknown[] = []
|
||||||
|
|
||||||
syncSessionModel(
|
syncSessionModel(
|
||||||
{
|
{
|
||||||
agent: {
|
session: {
|
||||||
current() {
|
restore(value) {
|
||||||
return undefined
|
calls.push(value)
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
calls.push(["agent", value])
|
|
||||||
},
|
|
||||||
},
|
|
||||||
model: {
|
|
||||||
set(value) {
|
|
||||||
calls.push(["model", value])
|
|
||||||
},
|
|
||||||
current() {
|
|
||||||
return { id: "claude-sonnet-4", provider: { id: "anthropic" } }
|
|
||||||
},
|
|
||||||
variant: {
|
|
||||||
set(value) {
|
|
||||||
calls.push(["variant", value])
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
reset() {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
message({ variant: "high" }),
|
message({ variant: "high" }),
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(calls).toEqual([
|
expect(calls).toEqual([message({ variant: "high" })])
|
||||||
["agent", "build"],
|
|
||||||
["model", { providerID: "anthropic", modelID: "claude-sonnet-4" }],
|
|
||||||
["variant", "high"],
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
test("skips variant when the model falls back", () => {
|
|
||||||
const calls: unknown[] = []
|
|
||||||
|
|
||||||
syncSessionModel(
|
|
||||||
{
|
|
||||||
agent: {
|
|
||||||
current() {
|
|
||||||
return undefined
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
calls.push(["agent", value])
|
|
||||||
},
|
|
||||||
},
|
|
||||||
model: {
|
|
||||||
set(value) {
|
|
||||||
calls.push(["model", value])
|
|
||||||
},
|
|
||||||
current() {
|
|
||||||
return { id: "gpt-5", provider: { id: "openai" } }
|
|
||||||
},
|
|
||||||
variant: {
|
|
||||||
set(value) {
|
|
||||||
calls.push(["variant", value])
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
message({ variant: "high" }),
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(calls).toEqual([
|
|
||||||
["agent", "build"],
|
|
||||||
["model", { providerID: "anthropic", modelID: "claude-sonnet-4" }],
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("resetSessionModel", () => {
|
describe("resetSessionModel", () => {
|
||||||
test("restores the current agent defaults", () => {
|
test("clears draft session state", () => {
|
||||||
const calls: unknown[] = []
|
const calls: string[] = []
|
||||||
|
|
||||||
resetSessionModel({
|
resetSessionModel({
|
||||||
agent: {
|
session: {
|
||||||
current() {
|
reset() {
|
||||||
return {
|
calls.push("reset")
|
||||||
model: { providerID: "anthropic", modelID: "claude-sonnet-4" },
|
|
||||||
variant: "high",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
set() {},
|
|
||||||
},
|
|
||||||
model: {
|
|
||||||
set(value) {
|
|
||||||
calls.push(["model", value])
|
|
||||||
},
|
|
||||||
current() {
|
|
||||||
return undefined
|
|
||||||
},
|
|
||||||
variant: {
|
|
||||||
set(value) {
|
|
||||||
calls.push(["variant", value])
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
restore() {},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(calls).toEqual([
|
expect(calls).toEqual(["reset"])
|
||||||
["model", { providerID: "anthropic", modelID: "claude-sonnet-4" }],
|
|
||||||
["variant", "high"],
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
test("clears the variant when the agent has none", () => {
|
|
||||||
const calls: unknown[] = []
|
|
||||||
|
|
||||||
resetSessionModel({
|
|
||||||
agent: {
|
|
||||||
current() {
|
|
||||||
return {
|
|
||||||
model: { providerID: "anthropic", modelID: "claude-sonnet-4" },
|
|
||||||
}
|
|
||||||
},
|
|
||||||
set() {},
|
|
||||||
},
|
|
||||||
model: {
|
|
||||||
set(value) {
|
|
||||||
calls.push(["model", value])
|
|
||||||
},
|
|
||||||
current() {
|
|
||||||
return undefined
|
|
||||||
},
|
|
||||||
variant: {
|
|
||||||
set(value) {
|
|
||||||
calls.push(["variant", value])
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(calls).toEqual([
|
|
||||||
["model", { providerID: "anthropic", modelID: "claude-sonnet-4" }],
|
|
||||||
["variant", undefined],
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,48 +1,16 @@
|
|||||||
import type { UserMessage } from "@opencode-ai/sdk/v2"
|
import type { UserMessage } from "@opencode-ai/sdk/v2"
|
||||||
import { batch } from "solid-js"
|
|
||||||
|
|
||||||
type Local = {
|
type Local = {
|
||||||
agent: {
|
session: {
|
||||||
current():
|
reset(): void
|
||||||
| {
|
restore(msg: UserMessage): void
|
||||||
model?: UserMessage["model"]
|
|
||||||
variant?: string
|
|
||||||
}
|
|
||||||
| undefined
|
|
||||||
set(name: string | undefined): void
|
|
||||||
}
|
|
||||||
model: {
|
|
||||||
set(model: UserMessage["model"] | undefined): void
|
|
||||||
current():
|
|
||||||
| {
|
|
||||||
id: string
|
|
||||||
provider: { id: string }
|
|
||||||
}
|
|
||||||
| undefined
|
|
||||||
variant: {
|
|
||||||
set(value: string | undefined): void
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const resetSessionModel = (local: Local) => {
|
export const resetSessionModel = (local: Local) => {
|
||||||
const agent = local.agent.current()
|
local.session.reset()
|
||||||
if (!agent) return
|
|
||||||
batch(() => {
|
|
||||||
local.model.set(agent.model)
|
|
||||||
local.model.variant.set(agent.variant)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const syncSessionModel = (local: Local, msg: UserMessage) => {
|
export const syncSessionModel = (local: Local, msg: UserMessage) => {
|
||||||
batch(() => {
|
local.session.restore(msg)
|
||||||
local.agent.set(msg.agent)
|
|
||||||
local.model.set(msg.model)
|
|
||||||
})
|
|
||||||
|
|
||||||
const model = local.model.current()
|
|
||||||
if (!model) return
|
|
||||||
if (model.provider.id !== msg.model.providerID) return
|
|
||||||
if (model.id !== msg.model.modelID) return
|
|
||||||
local.model.variant.set(msg.variant)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ import { terminalTabLabel } from "@/pages/session/terminal-label"
|
|||||||
import { createSizing, focusTerminalById } from "@/pages/session/helpers"
|
import { createSizing, focusTerminalById } from "@/pages/session/helpers"
|
||||||
import { getTerminalHandoff, setTerminalHandoff } from "@/pages/session/handoff"
|
import { getTerminalHandoff, setTerminalHandoff } from "@/pages/session/handoff"
|
||||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
|
import { terminalProbe } from "@/testing/terminal"
|
||||||
|
|
||||||
export function TerminalPanel() {
|
export function TerminalPanel() {
|
||||||
|
const delays = [120, 240]
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
const terminal = useTerminal()
|
const terminal = useTerminal()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
@@ -79,16 +81,20 @@ export function TerminalPanel() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const focus = (id: string) => {
|
const focus = (id: string) => {
|
||||||
|
const probe = terminalProbe(id)
|
||||||
|
probe.focus(delays.length + 1)
|
||||||
focusTerminalById(id)
|
focusTerminalById(id)
|
||||||
|
|
||||||
const frame = requestAnimationFrame(() => {
|
const frame = requestAnimationFrame(() => {
|
||||||
|
probe.step()
|
||||||
if (!opened()) return
|
if (!opened()) return
|
||||||
if (terminal.active() !== id) return
|
if (terminal.active() !== id) return
|
||||||
focusTerminalById(id)
|
focusTerminalById(id)
|
||||||
})
|
})
|
||||||
|
|
||||||
const timers = [120, 240].map((ms) =>
|
const timers = delays.map((ms) =>
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
|
probe.step()
|
||||||
if (!opened()) return
|
if (!opened()) return
|
||||||
if (terminal.active() !== id) return
|
if (terminal.active() !== id) return
|
||||||
focusTerminalById(id)
|
focusTerminalById(id)
|
||||||
@@ -96,6 +102,7 @@ export function TerminalPanel() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
probe.focus(0)
|
||||||
cancelAnimationFrame(frame)
|
cancelAnimationFrame(frame)
|
||||||
for (const timer of timers) clearTimeout(timer)
|
for (const timer of timers) clearTimeout(timer)
|
||||||
}
|
}
|
||||||
@@ -273,21 +280,24 @@ export function TerminalPanel() {
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
<div class="flex-1 min-h-0 relative">
|
<div class="flex-1 min-h-0 relative">
|
||||||
<Show when={terminal.active()} keyed>
|
<Show when={terminal.active()} keyed>
|
||||||
{(id) => (
|
{(id) => {
|
||||||
|
const ops = terminal.bind()
|
||||||
|
return (
|
||||||
<Show when={all().find((pty) => pty.id === id)}>
|
<Show when={all().find((pty) => pty.id === id)}>
|
||||||
{(pty) => (
|
{(pty) => (
|
||||||
<div id={`terminal-wrapper-${id}`} class="absolute inset-0">
|
<div id={`terminal-wrapper-${id}`} class="absolute inset-0">
|
||||||
<Terminal
|
<Terminal
|
||||||
pty={pty()}
|
pty={pty()}
|
||||||
autoFocus={opened()}
|
autoFocus={opened()}
|
||||||
onConnect={() => terminal.trim(id)}
|
onConnect={() => ops.trim(id)}
|
||||||
onCleanup={terminal.update}
|
onCleanup={ops.update}
|
||||||
onConnectError={() => terminal.clone(id)}
|
onConnectError={() => ops.clone(id)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
)}
|
)
|
||||||
|
}}
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -351,7 +351,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
description: language.t("command.model.choose.description"),
|
description: language.t("command.model.choose.description"),
|
||||||
keybind: "mod+'",
|
keybind: "mod+'",
|
||||||
slash: "model",
|
slash: "model",
|
||||||
onSelect: () => dialog.show(() => <DialogSelectModel />),
|
onSelect: () => dialog.show(() => <DialogSelectModel model={local.model} />),
|
||||||
}),
|
}),
|
||||||
mcpCommand({
|
mcpCommand({
|
||||||
id: "mcp.toggle",
|
id: "mcp.toggle",
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
type ModelKey = {
|
||||||
|
providerID: string
|
||||||
|
modelID: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
agent?: string
|
||||||
|
model?: ModelKey | null
|
||||||
|
variant?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ModelProbeState = {
|
||||||
|
dir?: string
|
||||||
|
sessionID?: string
|
||||||
|
last?: {
|
||||||
|
type: "agent" | "model" | "variant"
|
||||||
|
agent?: string
|
||||||
|
model?: ModelKey | null
|
||||||
|
variant?: string | null
|
||||||
|
}
|
||||||
|
agent?: string
|
||||||
|
model?: (ModelKey & { name?: string }) | undefined
|
||||||
|
variant?: string | null
|
||||||
|
selected?: string | null
|
||||||
|
configured?: string
|
||||||
|
pick?: State
|
||||||
|
base?: State
|
||||||
|
current?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ModelWindow = Window & {
|
||||||
|
__opencode_e2e?: {
|
||||||
|
model?: {
|
||||||
|
enabled?: boolean
|
||||||
|
current?: ModelProbeState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clone = (state?: State) => {
|
||||||
|
if (!state) return undefined
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
model: state.model ? { ...state.model } : state.model,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const modelEnabled = () => {
|
||||||
|
if (typeof window === "undefined") return false
|
||||||
|
return (window as ModelWindow).__opencode_e2e?.model?.enabled === true
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = () => {
|
||||||
|
if (!modelEnabled()) return
|
||||||
|
return (window as ModelWindow).__opencode_e2e?.model
|
||||||
|
}
|
||||||
|
|
||||||
|
export const modelProbe = {
|
||||||
|
set(input: ModelProbeState) {
|
||||||
|
const state = root()
|
||||||
|
if (!state) return
|
||||||
|
state.current = {
|
||||||
|
...input,
|
||||||
|
model: input.model ? { ...input.model } : undefined,
|
||||||
|
last: input.last
|
||||||
|
? {
|
||||||
|
...input.last,
|
||||||
|
model: input.last.model ? { ...input.last.model } : input.last.model,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
pick: clone(input.pick),
|
||||||
|
base: clone(input.base),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clear() {
|
||||||
|
const state = root()
|
||||||
|
if (!state) return
|
||||||
|
state.current = undefined
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import type { E2EWindow } from "./terminal"
|
||||||
|
|
||||||
|
export type PromptProbeState = {
|
||||||
|
popover: "at" | "slash" | null
|
||||||
|
slash: {
|
||||||
|
active: string | null
|
||||||
|
ids: string[]
|
||||||
|
}
|
||||||
|
selected: string | null
|
||||||
|
selects: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const promptEnabled = () => {
|
||||||
|
if (typeof window === "undefined") return false
|
||||||
|
return (window as E2EWindow).__opencode_e2e?.prompt?.enabled === true
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = () => {
|
||||||
|
if (!promptEnabled()) return
|
||||||
|
return (window as E2EWindow).__opencode_e2e?.prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
export const promptProbe = {
|
||||||
|
set(input: Omit<PromptProbeState, "selected" | "selects">) {
|
||||||
|
const state = root()
|
||||||
|
if (!state) return
|
||||||
|
state.current = {
|
||||||
|
popover: input.popover,
|
||||||
|
slash: {
|
||||||
|
active: input.slash.active,
|
||||||
|
ids: [...input.slash.ids],
|
||||||
|
},
|
||||||
|
selected: state.current?.selected ?? null,
|
||||||
|
selects: state.current?.selects ?? 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
select(id: string) {
|
||||||
|
const state = root()
|
||||||
|
if (!state) return
|
||||||
|
const prev = state.current
|
||||||
|
state.current = {
|
||||||
|
popover: prev?.popover ?? null,
|
||||||
|
slash: {
|
||||||
|
active: prev?.slash.active ?? null,
|
||||||
|
ids: [...(prev?.slash.ids ?? [])],
|
||||||
|
},
|
||||||
|
selected: id,
|
||||||
|
selects: (prev?.selects ?? 0) + 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clear() {
|
||||||
|
const state = root()
|
||||||
|
if (!state) return
|
||||||
|
state.current = undefined
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { ModelProbeState } from "./model-selection"
|
||||||
|
|
||||||
export const terminalAttr = "data-pty-id"
|
export const terminalAttr = "data-pty-id"
|
||||||
|
|
||||||
export type TerminalProbeState = {
|
export type TerminalProbeState = {
|
||||||
@@ -5,6 +7,7 @@ export type TerminalProbeState = {
|
|||||||
connects: number
|
connects: number
|
||||||
rendered: string
|
rendered: string
|
||||||
settled: number
|
settled: number
|
||||||
|
focusing: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type TerminalProbeControl = {
|
type TerminalProbeControl = {
|
||||||
@@ -13,6 +16,14 @@ type TerminalProbeControl = {
|
|||||||
|
|
||||||
export type E2EWindow = Window & {
|
export type E2EWindow = Window & {
|
||||||
__opencode_e2e?: {
|
__opencode_e2e?: {
|
||||||
|
model?: {
|
||||||
|
enabled?: boolean
|
||||||
|
current?: ModelProbeState
|
||||||
|
}
|
||||||
|
prompt?: {
|
||||||
|
enabled?: boolean
|
||||||
|
current?: import("./prompt").PromptProbeState
|
||||||
|
}
|
||||||
terminal?: {
|
terminal?: {
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
terminals?: Record<string, TerminalProbeState>
|
terminals?: Record<string, TerminalProbeState>
|
||||||
@@ -26,6 +37,7 @@ const seed = (): TerminalProbeState => ({
|
|||||||
connects: 0,
|
connects: 0,
|
||||||
rendered: "",
|
rendered: "",
|
||||||
settled: 0,
|
settled: 0,
|
||||||
|
focusing: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
const root = () => {
|
const root = () => {
|
||||||
@@ -82,6 +94,15 @@ export const terminalProbe = (id: string) => {
|
|||||||
const prev = state[id] ?? seed()
|
const prev = state[id] ?? seed()
|
||||||
state[id] = { ...prev, settled: prev.settled + 1 }
|
state[id] = { ...prev, settled: prev.settled + 1 }
|
||||||
},
|
},
|
||||||
|
focus(count: number) {
|
||||||
|
set({ focusing: Math.max(0, count) })
|
||||||
|
},
|
||||||
|
step() {
|
||||||
|
const state = terms()
|
||||||
|
if (!state) return
|
||||||
|
const prev = state[id] ?? seed()
|
||||||
|
state[id] = { ...prev, focusing: Math.max(0, prev.focusing - 1) }
|
||||||
|
},
|
||||||
control(next: Partial<TerminalProbeControl>) {
|
control(next: Partial<TerminalProbeControl>) {
|
||||||
const state = controls()
|
const state = controls()
|
||||||
if (!state) return
|
if (!state) return
|
||||||
|
|||||||
@@ -9,3 +9,15 @@ export function agentColor(name: string, custom?: string) {
|
|||||||
if (custom) return custom
|
if (custom) return custom
|
||||||
return defaults[name] ?? defaults[name.toLowerCase()]
|
return defaults[name] ?? defaults[name.toLowerCase()]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function messageAgentColor(
|
||||||
|
list: readonly { role: string; agent?: string }[] | undefined,
|
||||||
|
agents: readonly { name: string; color?: string }[],
|
||||||
|
) {
|
||||||
|
if (!list) return undefined
|
||||||
|
for (let i = list.length - 1; i >= 0; i--) {
|
||||||
|
const item = list[i]
|
||||||
|
if (item.role !== "user" || !item.agent) continue
|
||||||
|
return agentColor(item.agent, agents.find((agent) => agent.name === item.agent)?.color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,12 @@ import { createResource, type Accessor } from "solid-js"
|
|||||||
import type { SetStoreFunction, Store } from "solid-js/store"
|
import type { SetStoreFunction, Store } from "solid-js/store"
|
||||||
|
|
||||||
type InitType = Promise<string> | string | null
|
type InitType = Promise<string> | string | null
|
||||||
type PersistedWithReady<T> = [Store<T>, SetStoreFunction<T>, InitType, Accessor<boolean>]
|
type PersistedWithReady<T> = [
|
||||||
|
Store<T>,
|
||||||
|
SetStoreFunction<T>,
|
||||||
|
InitType,
|
||||||
|
Accessor<boolean> & { promise: undefined | Promise<any> },
|
||||||
|
]
|
||||||
|
|
||||||
type PersistTarget = {
|
type PersistTarget = {
|
||||||
storage?: string
|
storage?: string
|
||||||
@@ -460,5 +465,12 @@ export function persisted<T>(
|
|||||||
{ initialValue: !isAsync },
|
{ initialValue: !isAsync },
|
||||||
)
|
)
|
||||||
|
|
||||||
return [state, setState, init, () => ready() === true]
|
return [
|
||||||
|
state,
|
||||||
|
setState,
|
||||||
|
init,
|
||||||
|
Object.assign(() => ready() === true, {
|
||||||
|
promise: init instanceof Promise ? init : undefined,
|
||||||
|
}),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.2.25",
|
"version": "1.2.27",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -76,6 +76,19 @@ export function IconAlipay(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function IconUpi(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg {...props} viewBox="10 16 100 28" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M95.678 42.9 110 29.835l-6.784-13.516Z" />
|
||||||
|
<path d="M90.854 42.9 105.176 29.835l-6.784-13.516Z" />
|
||||||
|
<path
|
||||||
|
d="M22.41 16.47 16.38 37.945l21.407.15 5.88-21.625h5.427l-7.05 25.14c-.27.96-1.298 1.74-2.295 1.74H12.31c-1.664 0-2.65-1.3-2.2-2.9l6.724-23.98Zm66.182-.15h5.427l-7.538 27.03h-5.58ZM49.698 27.582l27.136-.15 1.81-5.707H51.054l1.658-5.256 29.4-.27c1.83-.017 2.92 1.4 2.438 3.167L81.78 29.49c-.483 1.766-2.36 3.197-4.19 3.197H53.316L50.454 43.8h-5.28Z"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function IconWechat(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
export function IconWechat(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||||
return (
|
return (
|
||||||
<svg {...props} viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
<svg {...props} viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
@@ -191,6 +204,33 @@ export function IconGemini(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function IconXiaomi(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M12 0C8.016 0 4.756.255 2.493 2.516.23 4.776 0 8.033 0 12.012c0 3.98.23 7.235 2.494 9.497C4.757 23.77 8.017 24 12 24c3.983 0 7.243-.23 9.506-2.491C23.77 19.247 24 15.99 24 12.012c0-3.984-.233-7.243-2.502-9.504C19.234.252 15.978 0 12 0zM4.906 7.405h5.624c1.47 0 3.007.068 3.764.827.746.746.827 2.233.83 3.676v4.54a.15.15 0 0 1-.152.147h-1.947a.15.15 0 0 1-.152-.148V11.83c-.002-.806-.048-1.634-.464-2.051-.358-.36-1.026-.441-1.72-.458H7.158a.15.15 0 0 0-.151.147v6.98a.15.15 0 0 1-.152.148H4.906a.15.15 0 0 1-.15-.148V7.554a.15.15 0 0 1 .15-.149zm12.131 0h1.949a.15.15 0 0 1 .15.15v8.892a.15.15 0 0 1-.15.148h-1.949a.15.15 0 0 1-.151-.148V7.554a.15.15 0 0 1 .151-.149zM8.92 10.948h2.046c.083 0 .15.066.15.147v5.352a.15.15 0 0 1-.15.148H8.92a.15.15 0 0 1-.152-.148v-5.352a.15.15 0 0 1 .152-.147Z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconNvidia(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M8.948 8.798v-1.43a6.7 6.7 0 0 1 .424-.018c3.922-.124 6.493 3.374 6.493 3.374s-2.774 3.851-5.75 3.851c-.398 0-.787-.062-1.158-.185v-4.346c1.528.185 1.837.857 2.747 2.385l2.04-1.714s-1.492-1.952-4-1.952a6.016 6.016 0 0 0-.796.035m0-4.735v2.138l.424-.027c5.45-.185 9.01 4.47 9.01 4.47s-4.08 4.964-8.33 4.964c-.37 0-.733-.035-1.095-.097v1.325c.3.035.61.062.91.062 3.957 0 6.82-2.023 9.593-4.408.459.371 2.34 1.263 2.73 1.652-2.633 2.208-8.772 3.984-12.253 3.984-.335 0-.653-.018-.971-.053v1.864H24V4.063zm0 10.326v1.131c-3.657-.654-4.673-4.46-4.673-4.46s1.758-1.944 4.673-2.262v1.237H8.94c-1.528-.186-2.73 1.245-2.73 1.245s.68 2.412 2.739 3.11M2.456 10.9s2.164-3.197 6.5-3.533V6.201C4.153 6.59 0 10.653 0 10.653s2.35 6.802 8.948 7.42v-1.237c-4.84-.6-6.492-5.936-6.492-5.936z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconArcee(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 37 32" fill="currentColor">
|
||||||
|
<path d="M20.4062 3.66602L4.24121 31.5928H0L18.2881 0L20.4062 3.66602Z" />
|
||||||
|
<path d="M25.8838 13.1553L11.0752 31.5928H6.36719L23.9131 9.74512L25.8838 13.1553Z" />
|
||||||
|
<path d="M36.5352 31.5928H21.6611L34.6191 28.2783L36.5352 31.5928Z" />
|
||||||
|
<path d="M31.2627 22.4648L19.1699 31.5898H13.0762L29.4131 19.2617L31.2627 22.4648Z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function IconStealth(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
export function IconStealth(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||||
return (
|
return (
|
||||||
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 18" fill="none">
|
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 18" fill="none">
|
||||||
|
|||||||
@@ -62,5 +62,6 @@
|
|||||||
font-size: var(--font-size-lg);
|
font-size: var(--font-size-lg);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع",
|
"go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ GLM-5 و Kimi K2.5 و MiniMax M2.5.",
|
"يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ GLM-5 و Kimi K2.5 و MiniMax M2.5 وMiniMax M2.7.",
|
||||||
"go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع",
|
"go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع",
|
||||||
"go.hero.body":
|
"go.hero.body":
|
||||||
"يجلب Go البرمجة الوكيلة للمبرمجين حول العالم. يوفر حدودًا سخية ووصولًا موثوقًا إلى أقوى النماذج مفتوحة المصدر، حتى تتمكن من البناء باستخدام وكلاء أقوياء دون القلق بشأن التكلفة أو التوفر.",
|
"يجلب Go البرمجة الوكيلة للمبرمجين حول العالم. يوفر حدودًا سخية ووصولًا موثوقًا إلى أقوى النماذج مفتوحة المصدر، حتى تتمكن من البناء باستخدام وكلاء أقوياء دون القلق بشأن التكلفة أو التوفر.",
|
||||||
@@ -297,7 +297,7 @@ export const dict = {
|
|||||||
"go.problem.item1": "أسعار اشتراك منخفضة التكلفة",
|
"go.problem.item1": "أسعار اشتراك منخفضة التكلفة",
|
||||||
"go.problem.item2": "حدود سخية ووصول موثوق",
|
"go.problem.item2": "حدود سخية ووصول موثوق",
|
||||||
"go.problem.item3": "مصمم لأكبر عدد ممكن من المبرمجين",
|
"go.problem.item3": "مصمم لأكبر عدد ممكن من المبرمجين",
|
||||||
"go.problem.item4": "يتضمن GLM-5 وKimi K2.5 وMiniMax M2.5",
|
"go.problem.item4": "يتضمن GLM-5 وKimi K2.5 وMiniMax M2.5 وMiniMax M2.7",
|
||||||
"go.how.title": "كيف يعمل Go",
|
"go.how.title": "كيف يعمل Go",
|
||||||
"go.how.body": "يبدأ Go من $5 للشهر الأول، ثم $10/شهر. يمكنك استخدامه مع OpenCode أو أي وكيل.",
|
"go.how.body": "يبدأ Go من $5 للشهر الأول، ثم $10/شهر. يمكنك استخدامه مع OpenCode أو أي وكيل.",
|
||||||
"go.how.step1.title": "أنشئ حسابًا",
|
"go.how.step1.title": "أنشئ حسابًا",
|
||||||
@@ -318,10 +318,10 @@ export const dict = {
|
|||||||
"go.faq.q1": "ما هو OpenCode Go؟",
|
"go.faq.q1": "ما هو OpenCode Go؟",
|
||||||
"go.faq.a1": "Go هو اشتراك منخفض التكلفة يمنحك وصولًا موثوقًا إلى نماذج مفتوحة المصدر قادرة على البرمجة الوكيلة.",
|
"go.faq.a1": "Go هو اشتراك منخفض التكلفة يمنحك وصولًا موثوقًا إلى نماذج مفتوحة المصدر قادرة على البرمجة الوكيلة.",
|
||||||
"go.faq.q2": "ما النماذج التي يتضمنها Go؟",
|
"go.faq.q2": "ما النماذج التي يتضمنها Go؟",
|
||||||
"go.faq.a2": "يتضمن Go نماذج GLM-5 وKimi K2.5 وMiniMax M2.5، مع حدود سخية ووصول موثوق.",
|
"go.faq.a2": "يتضمن Go نماذج GLM-5 وKimi K2.5 وMiniMax M2.5 وMiniMax M2.7، مع حدود سخية ووصول موثوق.",
|
||||||
"go.faq.q3": "هل Go هو نفسه Zen؟",
|
"go.faq.q3": "هل Go هو نفسه Zen؟",
|
||||||
"go.faq.a3":
|
"go.faq.a3":
|
||||||
"لا. Zen هو الدفع حسب الاستخدام، بينما يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى نماذج المصدر المفتوح GLM-5 و Kimi K2.5 و MiniMax M2.5.",
|
"لا. Zen هو الدفع حسب الاستخدام، بينما يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى نماذج المصدر المفتوح GLM-5 و Kimi K2.5 و MiniMax M2.5 وMiniMax M2.7.",
|
||||||
"go.faq.q4": "كم تكلفة Go؟",
|
"go.faq.q4": "كم تكلفة Go؟",
|
||||||
"go.faq.a4.p1.beforePricing": "تكلفة Go",
|
"go.faq.a4.p1.beforePricing": "تكلفة Go",
|
||||||
"go.faq.a4.p1.pricingLink": "$5 للشهر الأول",
|
"go.faq.a4.p1.pricingLink": "$5 للشهر الأول",
|
||||||
@@ -345,7 +345,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.faq.q9": "ما الفرق بين النماذج المجانية وGo؟",
|
"go.faq.q9": "ما الفرق بين النماذج المجانية وGo؟",
|
||||||
"go.faq.a9":
|
"go.faq.a9":
|
||||||
"تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة 200 طلب/يوم. يتضمن Go نماذج GLM-5 وKimi K2.5 وMiniMax M2.5 مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، أسبوعيًا، وشهريًا)، تعادل تقريبًا 12 دولارًا كل 5 ساعات، و30 دولارًا في الأسبوع، و60 دولارًا في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).",
|
"تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة 200 طلب/يوم. يتضمن Go نماذج GLM-5 وKimi K2.5 وMiniMax M2.5 وMiniMax M2.7 مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، أسبوعيًا، وشهريًا)، تعادل تقريبًا 12 دولارًا كل 5 ساعات، و30 دولارًا في الأسبوع، و60 دولارًا في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).",
|
||||||
|
|
||||||
"zen.api.error.rateLimitExceeded": "تم تجاوز حد الطلبات. يرجى المحاولة مرة أخرى لاحقًا.",
|
"zen.api.error.rateLimitExceeded": "تم تجاوز حد الطلبات. يرجى المحاولة مرة أخرى لاحقًا.",
|
||||||
"zen.api.error.modelNotSupported": "النموذج {{model}} غير مدعوم",
|
"zen.api.error.modelNotSupported": "النموذج {{model}} غير مدعوم",
|
||||||
@@ -644,6 +644,8 @@ export const dict = {
|
|||||||
"تم تصميم الخطة بشكل أساسي للمستخدمين الدوليين، مع استضافة النماذج في الولايات المتحدة والاتحاد الأوروبي وسنغافورة للحصول على وصول عالمي مستقر. قد تتغير الأسعار وحدود الاستخدام بناءً على تعلمنا من الاستخدام المبكر والملاحظات.",
|
"تم تصميم الخطة بشكل أساسي للمستخدمين الدوليين، مع استضافة النماذج في الولايات المتحدة والاتحاد الأوروبي وسنغافورة للحصول على وصول عالمي مستقر. قد تتغير الأسعار وحدود الاستخدام بناءً على تعلمنا من الاستخدام المبكر والملاحظات.",
|
||||||
"workspace.lite.promo.subscribe": "الاشتراك في Go",
|
"workspace.lite.promo.subscribe": "الاشتراك في Go",
|
||||||
"workspace.lite.promo.subscribing": "جارٍ إعادة التوجيه...",
|
"workspace.lite.promo.subscribing": "جارٍ إعادة التوجيه...",
|
||||||
|
"workspace.lite.promo.otherMethods": "طرق دفع أخرى",
|
||||||
|
"workspace.lite.promo.selectMethod": "اختر طريقة الدفع",
|
||||||
|
|
||||||
"download.title": "OpenCode | تنزيل",
|
"download.title": "OpenCode | تنزيل",
|
||||||
"download.meta.description": "نزّل OpenCode لـ macOS، Windows، وLinux",
|
"download.meta.description": "نزّل OpenCode لـ macOS، Windows، وLinux",
|
||||||
@@ -688,8 +690,12 @@ export const dict = {
|
|||||||
"enterprise.form.name.placeholder": "جيف بيزوس",
|
"enterprise.form.name.placeholder": "جيف بيزوس",
|
||||||
"enterprise.form.role.label": "المنصب",
|
"enterprise.form.role.label": "المنصب",
|
||||||
"enterprise.form.role.placeholder": "رئيس مجلس الإدارة التنفيذي",
|
"enterprise.form.role.placeholder": "رئيس مجلس الإدارة التنفيذي",
|
||||||
|
"enterprise.form.company.label": "الشركة",
|
||||||
|
"enterprise.form.company.placeholder": "Acme Inc",
|
||||||
"enterprise.form.email.label": "البريد الإلكتروني للشركة",
|
"enterprise.form.email.label": "البريد الإلكتروني للشركة",
|
||||||
"enterprise.form.email.placeholder": "jeff@amazon.com",
|
"enterprise.form.email.placeholder": "jeff@amazon.com",
|
||||||
|
"enterprise.form.phone.label": "رقم الهاتف",
|
||||||
|
"enterprise.form.phone.placeholder": "+1 234 567 8900",
|
||||||
"enterprise.form.message.label": "ما المشكلة التي تحاول حلها؟",
|
"enterprise.form.message.label": "ما المشكلة التي تحاول حلها؟",
|
||||||
"enterprise.form.message.placeholder": "نحتاج مساعدة في...",
|
"enterprise.form.message.placeholder": "نحتاج مساعدة في...",
|
||||||
"enterprise.form.send": "إرسال",
|
"enterprise.form.send": "إرسال",
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ export const dict = {
|
|||||||
|
|
||||||
"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.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, Kimi K2.5 e MiniMax M2.5.",
|
"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, Kimi K2.5, MiniMax M2.5 e MiniMax M2.7.",
|
||||||
"go.hero.title": "Modelos de codificação de baixo custo para todos",
|
"go.hero.title": "Modelos de codificação de baixo custo para todos",
|
||||||
"go.hero.body":
|
"go.hero.body":
|
||||||
"O Go traz a codificação com agentes para programadores em todo o mundo. Oferecendo limites generosos e acesso confiável aos modelos de código aberto mais capazes, para que você possa construir com agentes poderosos sem se preocupar com custos ou disponibilidade.",
|
"O Go traz a codificação com agentes para programadores em todo o mundo. Oferecendo limites generosos e acesso confiável aos modelos de código aberto mais capazes, para que você possa construir com agentes poderosos sem se preocupar com custos ou disponibilidade.",
|
||||||
@@ -302,7 +302,7 @@ export const dict = {
|
|||||||
"go.problem.item1": "Preço de assinatura de baixo custo",
|
"go.problem.item1": "Preço de assinatura de baixo custo",
|
||||||
"go.problem.item2": "Limites generosos e acesso confiável",
|
"go.problem.item2": "Limites generosos e acesso confiável",
|
||||||
"go.problem.item3": "Feito para o maior número possível de programadores",
|
"go.problem.item3": "Feito para o maior número possível de programadores",
|
||||||
"go.problem.item4": "Inclui GLM-5, Kimi K2.5 e MiniMax M2.5",
|
"go.problem.item4": "Inclui GLM-5, Kimi K2.5, MiniMax M2.5 e MiniMax M2.7",
|
||||||
"go.how.title": "Como o Go funciona",
|
"go.how.title": "Como o Go funciona",
|
||||||
"go.how.body":
|
"go.how.body":
|
||||||
"O Go começa em $5 no primeiro mês, depois $10/mês. Você pode usá-lo com o OpenCode ou qualquer agente.",
|
"O Go começa em $5 no primeiro mês, depois $10/mês. Você pode usá-lo com o OpenCode ou qualquer agente.",
|
||||||
@@ -325,10 +325,10 @@ export const dict = {
|
|||||||
"go.faq.a1":
|
"go.faq.a1":
|
||||||
"Go é uma assinatura de baixo custo que oferece acesso confiável a modelos de código aberto capazes para codificação com agentes.",
|
"Go é uma assinatura de baixo custo que oferece acesso confiável a modelos de código aberto capazes para codificação com agentes.",
|
||||||
"go.faq.q2": "Quais modelos o Go inclui?",
|
"go.faq.q2": "Quais modelos o Go inclui?",
|
||||||
"go.faq.a2": "Go inclui GLM-5, Kimi K2.5 e MiniMax M2.5, com limites generosos e acesso confiável.",
|
"go.faq.a2": "Go inclui GLM-5, Kimi K2.5, MiniMax M2.5 e MiniMax M2.7, com limites generosos e acesso confiável.",
|
||||||
"go.faq.q3": "O Go é o mesmo que o Zen?",
|
"go.faq.q3": "O Go é o mesmo que o Zen?",
|
||||||
"go.faq.a3":
|
"go.faq.a3":
|
||||||
"Não. Zen é pay-as-you-go, enquanto o Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos e acesso confiável aos modelos open source GLM-5, Kimi K2.5 e MiniMax M2.5.",
|
"Não. Zen é pay-as-you-go, enquanto o Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos e acesso confiável aos modelos open source GLM-5, Kimi K2.5, MiniMax M2.5 e MiniMax M2.7.",
|
||||||
"go.faq.q4": "Quanto custa o Go?",
|
"go.faq.q4": "Quanto custa o Go?",
|
||||||
"go.faq.a4.p1.beforePricing": "O Go custa",
|
"go.faq.a4.p1.beforePricing": "O Go custa",
|
||||||
"go.faq.a4.p1.pricingLink": "$5 no primeiro mês",
|
"go.faq.a4.p1.pricingLink": "$5 no primeiro mês",
|
||||||
@@ -353,7 +353,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.faq.q9": "Qual a diferença entre os modelos gratuitos e o Go?",
|
"go.faq.q9": "Qual a diferença entre os modelos gratuitos e o Go?",
|
||||||
"go.faq.a9":
|
"go.faq.a9":
|
||||||
"Os modelos gratuitos incluem Big Pickle e modelos promocionais disponíveis no momento, com uma cota de 200 requisições/dia. O Go inclui GLM-5, Kimi K2.5 e MiniMax M2.5 com cotas de requisição mais altas aplicadas em janelas móveis (5 horas, semanal e mensal), aproximadamente equivalentes a $12 por 5 horas, $30 por semana e $60 por mês (as contagens reais de requisições variam de acordo com o modelo e o uso).",
|
"Os modelos gratuitos incluem Big Pickle e modelos promocionais disponíveis no momento, com uma cota de 200 requisições/dia. O Go inclui GLM-5, Kimi K2.5, MiniMax M2.5 e MiniMax M2.7 com cotas de requisição mais altas aplicadas em janelas móveis (5 horas, semanal e mensal), aproximadamente equivalentes a $12 por 5 horas, $30 por semana e $60 por mês (as contagens reais de requisições variam de acordo com o modelo e o uso).",
|
||||||
|
|
||||||
"zen.api.error.rateLimitExceeded": "Limite de taxa excedido. Por favor, tente novamente mais tarde.",
|
"zen.api.error.rateLimitExceeded": "Limite de taxa excedido. Por favor, tente novamente mais tarde.",
|
||||||
"zen.api.error.modelNotSupported": "Modelo {{model}} não suportado",
|
"zen.api.error.modelNotSupported": "Modelo {{model}} não suportado",
|
||||||
@@ -654,6 +654,8 @@ export const dict = {
|
|||||||
"O plano é projetado principalmente para usuários internacionais, com modelos hospedados nos EUA, UE e Singapura para acesso global estável. Preços e limites de uso podem mudar conforme aprendemos com o uso inicial e feedback.",
|
"O plano é projetado principalmente para usuários internacionais, com modelos hospedados nos EUA, UE e Singapura para acesso global estável. Preços e limites de uso podem mudar conforme aprendemos com o uso inicial e feedback.",
|
||||||
"workspace.lite.promo.subscribe": "Assinar Go",
|
"workspace.lite.promo.subscribe": "Assinar Go",
|
||||||
"workspace.lite.promo.subscribing": "Redirecionando...",
|
"workspace.lite.promo.subscribing": "Redirecionando...",
|
||||||
|
"workspace.lite.promo.otherMethods": "Outros métodos de pagamento",
|
||||||
|
"workspace.lite.promo.selectMethod": "Selecionar método de pagamento",
|
||||||
|
|
||||||
"download.title": "OpenCode | Baixar",
|
"download.title": "OpenCode | Baixar",
|
||||||
"download.meta.description": "Baixe o OpenCode para macOS, Windows e Linux",
|
"download.meta.description": "Baixe o OpenCode para macOS, Windows e Linux",
|
||||||
@@ -700,8 +702,12 @@ export const dict = {
|
|||||||
"enterprise.form.name.placeholder": "Jeff Bezos",
|
"enterprise.form.name.placeholder": "Jeff Bezos",
|
||||||
"enterprise.form.role.label": "Cargo",
|
"enterprise.form.role.label": "Cargo",
|
||||||
"enterprise.form.role.placeholder": "Presidente Executivo",
|
"enterprise.form.role.placeholder": "Presidente Executivo",
|
||||||
|
"enterprise.form.company.label": "Empresa",
|
||||||
|
"enterprise.form.company.placeholder": "Acme Inc",
|
||||||
"enterprise.form.email.label": "E-mail corporativo",
|
"enterprise.form.email.label": "E-mail corporativo",
|
||||||
"enterprise.form.email.placeholder": "jeff@amazon.com",
|
"enterprise.form.email.placeholder": "jeff@amazon.com",
|
||||||
|
"enterprise.form.phone.label": "Telefone",
|
||||||
|
"enterprise.form.phone.placeholder": "+1 234 567 8900",
|
||||||
"enterprise.form.message.label": "Qual problema você está tentando resolver?",
|
"enterprise.form.message.label": "Qual problema você está tentando resolver?",
|
||||||
"enterprise.form.message.placeholder": "Precisamos de ajuda com...",
|
"enterprise.form.message.placeholder": "Precisamos de ajuda com...",
|
||||||
"enterprise.form.send": "Enviar",
|
"enterprise.form.send": "Enviar",
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle",
|
"go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle",
|
||||||
"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, Kimi K2.5 og MiniMax M2.5.",
|
"Go starter ved $5 for den første måned, derefter $10/måned, med generøse 5-timers anmodningsgrænser for GLM-5, Kimi K2.5, MiniMax M2.5 og MiniMax M2.7.",
|
||||||
"go.hero.title": "Kodningsmodeller til lav pris for alle",
|
"go.hero.title": "Kodningsmodeller til lav pris for alle",
|
||||||
"go.hero.body":
|
"go.hero.body":
|
||||||
"Go bringer agentisk kodning til programmører over hele verden. Med generøse grænser og pålidelig adgang til de mest kapable open source-modeller, så du kan bygge med kraftfulde agenter uden at bekymre dig om omkostninger eller tilgængelighed.",
|
"Go bringer agentisk kodning til programmører over hele verden. Med generøse grænser og pålidelig adgang til de mest kapable open source-modeller, så du kan bygge med kraftfulde agenter uden at bekymre dig om omkostninger eller tilgængelighed.",
|
||||||
@@ -299,7 +299,7 @@ export const dict = {
|
|||||||
"go.problem.item1": "Lavpris abonnementspriser",
|
"go.problem.item1": "Lavpris abonnementspriser",
|
||||||
"go.problem.item2": "Generøse grænser og pålidelig adgang",
|
"go.problem.item2": "Generøse grænser og pålidelig adgang",
|
||||||
"go.problem.item3": "Bygget til så mange programmører som muligt",
|
"go.problem.item3": "Bygget til så mange programmører som muligt",
|
||||||
"go.problem.item4": "Inkluderer GLM-5, Kimi K2.5 og MiniMax M2.5",
|
"go.problem.item4": "Inkluderer GLM-5, Kimi K2.5, MiniMax M2.5 og MiniMax M2.7",
|
||||||
"go.how.title": "Hvordan Go virker",
|
"go.how.title": "Hvordan Go virker",
|
||||||
"go.how.body":
|
"go.how.body":
|
||||||
"Go starter ved $5 for den første måned, derefter $10/måned. Du kan bruge det med OpenCode eller enhver agent.",
|
"Go starter ved $5 for den første måned, derefter $10/måned. Du kan bruge det med OpenCode eller enhver agent.",
|
||||||
@@ -322,10 +322,11 @@ export const dict = {
|
|||||||
"go.faq.a1":
|
"go.faq.a1":
|
||||||
"Go er et lavprisabonnement, der giver dig pålidelig adgang til kapable open source-modeller til agentisk kodning.",
|
"Go er et lavprisabonnement, der giver dig pålidelig adgang til kapable open source-modeller til agentisk kodning.",
|
||||||
"go.faq.q2": "Hvilke modeller inkluderer Go?",
|
"go.faq.q2": "Hvilke modeller inkluderer Go?",
|
||||||
"go.faq.a2": "Go inkluderer GLM-5, Kimi K2.5 og MiniMax M2.5, med generøse grænser og pålidelig adgang.",
|
"go.faq.a2":
|
||||||
|
"Go inkluderer GLM-5, Kimi K2.5, MiniMax M2.5 og MiniMax M2.7, med generøse grænser og pålidelig adgang.",
|
||||||
"go.faq.q3": "Er Go det samme som Zen?",
|
"go.faq.q3": "Er Go det samme som Zen?",
|
||||||
"go.faq.a3":
|
"go.faq.a3":
|
||||||
"Nej. Zen er pay-as-you-go, mens Go starter ved $5 for den første måned, derefter $10/måned, med generøse grænser og pålidelig adgang til open source-modellerne GLM-5, Kimi K2.5 og MiniMax M2.5.",
|
"Nej. Zen er pay-as-you-go, mens Go starter ved $5 for den første måned, derefter $10/måned, med generøse grænser og pålidelig adgang til open source-modellerne GLM-5, Kimi K2.5, MiniMax M2.5 og MiniMax M2.7.",
|
||||||
"go.faq.q4": "Hvad koster Go?",
|
"go.faq.q4": "Hvad koster Go?",
|
||||||
"go.faq.a4.p1.beforePricing": "Go koster",
|
"go.faq.a4.p1.beforePricing": "Go koster",
|
||||||
"go.faq.a4.p1.pricingLink": "$5 første måned",
|
"go.faq.a4.p1.pricingLink": "$5 første måned",
|
||||||
@@ -349,7 +350,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.faq.q9": "Hvad er forskellen på gratis modeller og Go?",
|
"go.faq.q9": "Hvad er forskellen på gratis modeller og Go?",
|
||||||
"go.faq.a9":
|
"go.faq.a9":
|
||||||
"Gratis modeller inkluderer Big Pickle plus salgsfremmende modeller tilgængelige på det tidspunkt, med en kvote på 200 forespørgsler/dag. Go inkluderer GLM-5, Kimi K2.5 og MiniMax M2.5 med højere anmodningskvoter håndhævet over rullende vinduer (5-timers, ugentlig og månedlig), nogenlunde svarende til $12 pr. 5 timer, $30 pr. uge og $60 pr. måned (faktiske anmodningstal varierer efter model og brug).",
|
"Gratis modeller inkluderer Big Pickle plus salgsfremmende modeller tilgængelige på det tidspunkt, med en kvote på 200 forespørgsler/dag. Go inkluderer GLM-5, Kimi K2.5, MiniMax M2.5 og MiniMax M2.7 med højere anmodningskvoter håndhævet over rullende vinduer (5-timers, ugentlig og månedlig), nogenlunde svarende til $12 pr. 5 timer, $30 pr. uge og $60 pr. måned (faktiske anmodningstal varierer efter model og brug).",
|
||||||
|
|
||||||
"zen.api.error.rateLimitExceeded": "Hastighedsgrænse overskredet. Prøv venligst igen senere.",
|
"zen.api.error.rateLimitExceeded": "Hastighedsgrænse overskredet. Prøv venligst igen senere.",
|
||||||
"zen.api.error.modelNotSupported": "Model {{model}} understøttes ikke",
|
"zen.api.error.modelNotSupported": "Model {{model}} understøttes ikke",
|
||||||
@@ -650,6 +651,8 @@ export const dict = {
|
|||||||
"Planen er primært designet til internationale brugere, med modeller hostet i USA, EU og Singapore for stabil global adgang. Priser og forbrugsgrænser kan ændre sig, efterhånden som vi lærer af tidlig brug og feedback.",
|
"Planen er primært designet til internationale brugere, med modeller hostet i USA, EU og Singapore for stabil global adgang. Priser og forbrugsgrænser kan ændre sig, efterhånden som vi lærer af tidlig brug og feedback.",
|
||||||
"workspace.lite.promo.subscribe": "Abonner på Go",
|
"workspace.lite.promo.subscribe": "Abonner på Go",
|
||||||
"workspace.lite.promo.subscribing": "Omdirigerer...",
|
"workspace.lite.promo.subscribing": "Omdirigerer...",
|
||||||
|
"workspace.lite.promo.otherMethods": "Andre betalingsmetoder",
|
||||||
|
"workspace.lite.promo.selectMethod": "Vælg betalingsmetode",
|
||||||
|
|
||||||
"download.title": "OpenCode | Download",
|
"download.title": "OpenCode | Download",
|
||||||
"download.meta.description": "Download OpenCode til macOS, Windows og Linux",
|
"download.meta.description": "Download OpenCode til macOS, Windows og Linux",
|
||||||
@@ -694,8 +697,12 @@ export const dict = {
|
|||||||
"enterprise.form.name.placeholder": "Jeff Bezos",
|
"enterprise.form.name.placeholder": "Jeff Bezos",
|
||||||
"enterprise.form.role.label": "Rolle",
|
"enterprise.form.role.label": "Rolle",
|
||||||
"enterprise.form.role.placeholder": "Bestyrelsesformand",
|
"enterprise.form.role.placeholder": "Bestyrelsesformand",
|
||||||
|
"enterprise.form.company.label": "Virksomhed",
|
||||||
|
"enterprise.form.company.placeholder": "Acme Inc",
|
||||||
"enterprise.form.email.label": "Firma-e-mail",
|
"enterprise.form.email.label": "Firma-e-mail",
|
||||||
"enterprise.form.email.placeholder": "jeff@amazon.com",
|
"enterprise.form.email.placeholder": "jeff@amazon.com",
|
||||||
|
"enterprise.form.phone.label": "Telefonnummer",
|
||||||
|
"enterprise.form.phone.placeholder": "+1 234 567 8900",
|
||||||
"enterprise.form.message.label": "Hvilket problem prøver du at løse?",
|
"enterprise.form.message.label": "Hvilket problem prøver du at løse?",
|
||||||
"enterprise.form.message.placeholder": "Vi har brug for hjælp med...",
|
"enterprise.form.message.placeholder": "Vi har brug for hjælp med...",
|
||||||
"enterprise.form.send": "Send",
|
"enterprise.form.send": "Send",
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle",
|
"go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle",
|
||||||
"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, Kimi K2.5 und MiniMax M2.5.",
|
"Go beginnt bei $5 für den ersten Monat, danach $10/Monat, mit großzügigen 5-Stunden-Anfragelimits für GLM-5, Kimi K2.5, MiniMax M2.5 und MiniMax M2.7.",
|
||||||
"go.hero.title": "Kostengünstige Coding-Modelle für alle",
|
"go.hero.title": "Kostengünstige Coding-Modelle für alle",
|
||||||
"go.hero.body":
|
"go.hero.body":
|
||||||
"Go bringt Agentic Coding zu Programmierern auf der ganzen Welt. Mit großzügigen Limits und zuverlässigem Zugang zu den leistungsfähigsten Open-Source-Modellen, damit du mit leistungsstarken Agenten entwickeln kannst, ohne dir Gedanken über Kosten oder Verfügbarkeit zu machen.",
|
"Go bringt Agentic Coding zu Programmierern auf der ganzen Welt. Mit großzügigen Limits und zuverlässigem Zugang zu den leistungsfähigsten Open-Source-Modellen, damit du mit leistungsstarken Agenten entwickeln kannst, ohne dir Gedanken über Kosten oder Verfügbarkeit zu machen.",
|
||||||
@@ -301,7 +301,7 @@ export const dict = {
|
|||||||
"go.problem.item1": "Kostengünstiges Abonnement",
|
"go.problem.item1": "Kostengünstiges Abonnement",
|
||||||
"go.problem.item2": "Großzügige Limits und zuverlässiger Zugang",
|
"go.problem.item2": "Großzügige Limits und zuverlässiger Zugang",
|
||||||
"go.problem.item3": "Für so viele Programmierer wie möglich gebaut",
|
"go.problem.item3": "Für so viele Programmierer wie möglich gebaut",
|
||||||
"go.problem.item4": "Beinhaltet GLM-5, Kimi K2.5 und MiniMax M2.5",
|
"go.problem.item4": "Beinhaltet GLM-5, Kimi K2.5, MiniMax M2.5 und MiniMax M2.7",
|
||||||
"go.how.title": "Wie Go funktioniert",
|
"go.how.title": "Wie Go funktioniert",
|
||||||
"go.how.body":
|
"go.how.body":
|
||||||
"Go beginnt bei $5 für den ersten Monat, danach $10/Monat. Du kannst es mit OpenCode oder jedem Agenten nutzen.",
|
"Go beginnt bei $5 für den ersten Monat, danach $10/Monat. Du kannst es mit OpenCode oder jedem Agenten nutzen.",
|
||||||
@@ -324,10 +324,11 @@ export const dict = {
|
|||||||
"go.faq.a1":
|
"go.faq.a1":
|
||||||
"Go ist ein kostengünstiges Abonnement, das dir zuverlässigen Zugang zu leistungsfähigen Open-Source-Modellen für Agentic Coding bietet.",
|
"Go ist ein kostengünstiges Abonnement, das dir zuverlässigen Zugang zu leistungsfähigen Open-Source-Modellen für Agentic Coding bietet.",
|
||||||
"go.faq.q2": "Welche Modelle beinhaltet Go?",
|
"go.faq.q2": "Welche Modelle beinhaltet Go?",
|
||||||
"go.faq.a2": "Go beinhaltet GLM-5, Kimi K2.5 und MiniMax M2.5, mit großzügigen Limits und zuverlässigem Zugang.",
|
"go.faq.a2":
|
||||||
|
"Go beinhaltet GLM-5, Kimi K2.5, MiniMax M2.5 und MiniMax M2.7, mit großzügigen Limits und zuverlässigem Zugang.",
|
||||||
"go.faq.q3": "Ist Go dasselbe wie Zen?",
|
"go.faq.q3": "Ist Go dasselbe wie Zen?",
|
||||||
"go.faq.a3":
|
"go.faq.a3":
|
||||||
"Nein. Zen ist Pay-as-you-go, während Go bei $5 für den ersten Monat beginnt, danach $10/Monat, mit großzügigen Limits und zuverlässigem Zugang zu den Open-Source-Modellen GLM-5, Kimi K2.5 und MiniMax M2.5.",
|
"Nein. Zen ist Pay-as-you-go, während Go bei $5 für den ersten Monat beginnt, danach $10/Monat, mit großzügigen Limits und zuverlässigem Zugang zu den Open-Source-Modellen GLM-5, Kimi K2.5, MiniMax M2.5 und MiniMax M2.7.",
|
||||||
"go.faq.q4": "Wie viel kostet Go?",
|
"go.faq.q4": "Wie viel kostet Go?",
|
||||||
"go.faq.a4.p1.beforePricing": "Go kostet",
|
"go.faq.a4.p1.beforePricing": "Go kostet",
|
||||||
"go.faq.a4.p1.pricingLink": "$5 im ersten Monat",
|
"go.faq.a4.p1.pricingLink": "$5 im ersten Monat",
|
||||||
@@ -352,7 +353,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.faq.q9": "Was ist der Unterschied zwischen kostenlosen Modellen und Go?",
|
"go.faq.q9": "Was ist der Unterschied zwischen kostenlosen Modellen und Go?",
|
||||||
"go.faq.a9":
|
"go.faq.a9":
|
||||||
"Kostenlose Modelle beinhalten Big Pickle sowie Werbemodelle, die zum jeweiligen Zeitpunkt verfügbar sind, mit einem Kontingent von 200 Anfragen/Tag. Go beinhaltet GLM-5, Kimi K2.5 und MiniMax M2.5 mit höheren Anfragekontingenten, die über rollierende Zeitfenster (5 Stunden, wöchentlich und monatlich) durchgesetzt werden, grob äquivalent zu $12 pro 5 Stunden, $30 pro Woche und $60 pro Monat (tatsächliche Anfragezahlen variieren je nach Modell und Nutzung).",
|
"Kostenlose Modelle beinhalten Big Pickle sowie Werbemodelle, die zum jeweiligen Zeitpunkt verfügbar sind, mit einem Kontingent von 200 Anfragen/Tag. Go beinhaltet GLM-5, Kimi K2.5, MiniMax M2.5 und MiniMax M2.7 mit höheren Anfragekontingenten, die über rollierende Zeitfenster (5 Stunden, wöchentlich und monatlich) durchgesetzt werden, grob äquivalent zu $12 pro 5 Stunden, $30 pro Woche und $60 pro Monat (tatsächliche Anfragezahlen variieren je nach Modell und Nutzung).",
|
||||||
|
|
||||||
"zen.api.error.rateLimitExceeded": "Ratenlimit überschritten. Bitte versuche es später erneut.",
|
"zen.api.error.rateLimitExceeded": "Ratenlimit überschritten. Bitte versuche es später erneut.",
|
||||||
"zen.api.error.modelNotSupported": "Modell {{model}} wird nicht unterstützt",
|
"zen.api.error.modelNotSupported": "Modell {{model}} wird nicht unterstützt",
|
||||||
@@ -653,6 +654,8 @@ export const dict = {
|
|||||||
"Der Plan wurde hauptsächlich für internationale Nutzer entwickelt, wobei die Modelle in den USA, der EU und Singapur gehostet werden, um einen stabilen weltweiten Zugriff zu gewährleisten. Preise und Nutzungslimits können sich ändern, während wir aus der frühen Nutzung und dem Feedback lernen.",
|
"Der Plan wurde hauptsächlich für internationale Nutzer entwickelt, wobei die Modelle in den USA, der EU und Singapur gehostet werden, um einen stabilen weltweiten Zugriff zu gewährleisten. Preise und Nutzungslimits können sich ändern, während wir aus der frühen Nutzung und dem Feedback lernen.",
|
||||||
"workspace.lite.promo.subscribe": "Go abonnieren",
|
"workspace.lite.promo.subscribe": "Go abonnieren",
|
||||||
"workspace.lite.promo.subscribing": "Leite weiter...",
|
"workspace.lite.promo.subscribing": "Leite weiter...",
|
||||||
|
"workspace.lite.promo.otherMethods": "Andere Zahlungsmethoden",
|
||||||
|
"workspace.lite.promo.selectMethod": "Zahlungsmethode auswählen",
|
||||||
|
|
||||||
"download.title": "OpenCode | Download",
|
"download.title": "OpenCode | Download",
|
||||||
"download.meta.description": "Lade OpenCode für macOS, Windows und Linux herunter",
|
"download.meta.description": "Lade OpenCode für macOS, Windows und Linux herunter",
|
||||||
@@ -699,8 +702,12 @@ export const dict = {
|
|||||||
"enterprise.form.name.placeholder": "Jeff Bezos",
|
"enterprise.form.name.placeholder": "Jeff Bezos",
|
||||||
"enterprise.form.role.label": "Rolle",
|
"enterprise.form.role.label": "Rolle",
|
||||||
"enterprise.form.role.placeholder": "Executive Chairman",
|
"enterprise.form.role.placeholder": "Executive Chairman",
|
||||||
|
"enterprise.form.company.label": "Unternehmen",
|
||||||
|
"enterprise.form.company.placeholder": "Acme Inc",
|
||||||
"enterprise.form.email.label": "Firmen-E-Mail",
|
"enterprise.form.email.label": "Firmen-E-Mail",
|
||||||
"enterprise.form.email.placeholder": "jeff@amazon.com",
|
"enterprise.form.email.placeholder": "jeff@amazon.com",
|
||||||
|
"enterprise.form.phone.label": "Telefonnummer",
|
||||||
|
"enterprise.form.phone.placeholder": "+1 234 567 8900",
|
||||||
"enterprise.form.message.label": "Welches Problem versuchen Sie zu lösen?",
|
"enterprise.form.message.label": "Welches Problem versuchen Sie zu lösen?",
|
||||||
"enterprise.form.message.placeholder": "Wir brauchen Hilfe bei...",
|
"enterprise.form.message.placeholder": "Wir brauchen Hilfe bei...",
|
||||||
"enterprise.form.send": "Senden",
|
"enterprise.form.send": "Senden",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user