mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-15 07:48:24 -04:00
Compare commits
107 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2beeccb39a | |||
| 77b3b46788 | |||
| 36dfe1646b | |||
| 6926dc26d1 | |||
| eb74e4a6d2 | |||
| 85d8e143bf | |||
| 8e1b53b32c | |||
| 0a7dfc03ee | |||
| 4c27e7fc64 | |||
| 0f5626d2e4 | |||
| 5ea95451dd | |||
| 9239d877b9 | |||
| fc68c24433 | |||
| db9619dad6 | |||
| 84d9b38873 | |||
| 8035c3435b | |||
| 71e7603d71 | |||
| 40e49c5b49 | |||
| afe9b97274 | |||
| 3b3549902d | |||
| e9a9c75c1f | |||
| 2b171828b0 | |||
| 8dd817023a | |||
| 0d6c601365 | |||
| 5460bf9989 | |||
| eb3bfffad4 | |||
| e2d03ce38c | |||
| 32f9dc6383 | |||
| c529529f84 | |||
| 13bac9c91a | |||
| fe53af4819 | |||
| e82c5a9a28 | |||
| 3236f228fb | |||
| 0e0e7a4a4b | |||
| 10a3d6c54e | |||
| 832b8e252e | |||
| 040f551c57 | |||
| cc818f8032 | |||
| d5337b41f4 | |||
| 9f7a76d6c0 | |||
| 6a16db4b92 | |||
| 9ad6588f3e | |||
| fb6bf0b35e | |||
| f80343b875 | |||
| 9b805e1cc4 | |||
| 2e0d5d2308 | |||
| 38e0dc9ccd | |||
| 40aeaa120d | |||
| 6a64177589 | |||
| 5dc47905a9 | |||
| dc0044882c | |||
| 45ae7dc653 | |||
| 129fe1e350 | |||
| 214a6c6cf1 | |||
| 3f249aba6d | |||
| 5c6ec1caac | |||
| 24f9df5463 | |||
| 12b8e1c2be | |||
| d70099b059 | |||
| ce845a0b1b | |||
| 05d3e65f76 | |||
| 51618e9cef | |||
| e78944e9a4 | |||
| bfdc38e421 | |||
| 83023e4f0f | |||
| d0a57305ef | |||
| 27a70ad70f | |||
| 0bbf26a1ce | |||
| 83cdb4de64 | |||
| 4989632245 | |||
| d460614cd7 | |||
| 7866dbcfcc | |||
| e71a21e0a8 | |||
| 1071aca91f | |||
| b3d0446d13 | |||
| 949191ab74 | |||
| 92cd908fb5 | |||
| 6fcc970def | |||
| 52a7a04ad8 | |||
| 37b8662a9d | |||
| ddcb32ae0b | |||
| 2c056c90da | |||
| 812d1bb32a | |||
| 9a58c43ef4 | |||
| 63585db6a7 | |||
| bd44489ada | |||
| a6ef9e9206 | |||
| 6e09a1d904 | |||
| 4f21757e0d | |||
| 2dbcd79fd2 | |||
| 48a7f0fd93 | |||
| d69962b0f7 | |||
| a6f23cb08e | |||
| 0540751897 | |||
| baa204193c | |||
| aeece6166b | |||
| 0d7e62a532 | |||
| 41aa254db4 | |||
| d178d8249f | |||
| e6f5214779 | |||
| 84f60d97a0 | |||
| cbf4b68fee | |||
| bd4527b4f2 | |||
| f4a9fe29a3 | |||
| 5a0bfa7061 | |||
| 1ac1a0287c | |||
| 8e09e8c612 |
+4
-1
@@ -10,6 +10,9 @@
|
|||||||
adamdotdevin
|
adamdotdevin
|
||||||
-agusbasari29 AI PR slop
|
-agusbasari29 AI PR slop
|
||||||
ariane-emory
|
ariane-emory
|
||||||
|
-atharvau AI review spamming literally every PR
|
||||||
|
-danieljoshuanazareth
|
||||||
|
-danieljoshuanazareth
|
||||||
edemaine
|
edemaine
|
||||||
-florianleibert
|
-florianleibert
|
||||||
fwang
|
fwang
|
||||||
@@ -17,8 +20,8 @@ iamdavidhill
|
|||||||
jayair
|
jayair
|
||||||
kitlangton
|
kitlangton
|
||||||
kommander
|
kommander
|
||||||
|
-opencode2026
|
||||||
r44vc0rp
|
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
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -33,6 +33,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
issue-id: ${{ github.event.issue.number }}
|
issue-id: ${{ github.event.issue.number }}
|
||||||
comment-id: ${{ github.event.comment.id }}
|
comment-id: ${{ github.event.comment.id }}
|
||||||
roles: admin,maintain
|
roles: admin,maintain,write
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
|
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
plans/
|
node_modules
|
||||||
bun.lock
|
plans
|
||||||
package.json
|
package.json
|
||||||
package-lock.json
|
bun.lock
|
||||||
|
.gitignore
|
||||||
|
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.1-pro
|
model: opencode/gpt-5.4
|
||||||
---
|
---
|
||||||
|
|
||||||
You are a professional translator and localization specialist.
|
You are a professional translator and localization specialist.
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
go through each PR merged since the last tag
|
||||||
|
|
||||||
|
for each PR spawn a subagent to summarize what the PR was about. focus on user facing changes. if it was entirely internal or code related you can ignore it. also skip docs updates. each subagent should append its summary to UPCOMING_CHANGELOG.md
|
||||||
|
|
||||||
|
once that is done, read UPCOMING_CHANGELOG.md and group it into sections for better readability. make sure all PR references are preserved
|
||||||
@@ -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.27",
|
"version": "1.3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -41,9 +41,11 @@
|
|||||||
"@solid-primitives/resize-observer": "2.1.3",
|
"@solid-primitives/resize-observer": "2.1.3",
|
||||||
"@solid-primitives/scroll": "2.1.3",
|
"@solid-primitives/scroll": "2.1.3",
|
||||||
"@solid-primitives/storage": "catalog:",
|
"@solid-primitives/storage": "catalog:",
|
||||||
|
"@solid-primitives/timer": "1.4.4",
|
||||||
"@solid-primitives/websocket": "1.3.1",
|
"@solid-primitives/websocket": "1.3.1",
|
||||||
"@solidjs/meta": "catalog:",
|
"@solidjs/meta": "catalog:",
|
||||||
"@solidjs/router": "catalog:",
|
"@solidjs/router": "catalog:",
|
||||||
|
"@tanstack/solid-query": "5.91.4",
|
||||||
"@thisbeyond/solid-dnd": "0.7.5",
|
"@thisbeyond/solid-dnd": "0.7.5",
|
||||||
"diff": "catalog:",
|
"diff": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -77,7 +79,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.2.27",
|
"version": "1.3.0",
|
||||||
"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 +113,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.2.27",
|
"version": "1.3.0",
|
||||||
"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",
|
||||||
@@ -128,7 +130,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/workers-types": "catalog:",
|
"@cloudflare/workers-types": "catalog:",
|
||||||
"@tsconfig/node22": "22.0.2",
|
"@tsconfig/node22": "22.0.2",
|
||||||
"@types/bun": "1.3.0",
|
"@types/bun": "catalog:",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
"drizzle-kit": "catalog:",
|
"drizzle-kit": "catalog:",
|
||||||
@@ -138,7 +140,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.2.27",
|
"version": "1.3.0",
|
||||||
"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 +164,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.2.27",
|
"version": "1.3.0",
|
||||||
"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 +188,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.2.27",
|
"version": "1.3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/app": "workspace:*",
|
"@opencode-ai/app": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
@@ -219,7 +221,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop-electron": {
|
"packages/desktop-electron": {
|
||||||
"name": "@opencode-ai/desktop-electron",
|
"name": "@opencode-ai/desktop-electron",
|
||||||
"version": "1.2.27",
|
"version": "1.3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/app": "workspace:*",
|
"@opencode-ai/app": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
@@ -250,7 +252,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.2.27",
|
"version": "1.3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
@@ -279,7 +281,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.2.27",
|
"version": "1.3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -295,7 +297,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.2.27",
|
"version": "1.3.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -325,8 +327,6 @@
|
|||||||
"@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",
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
"@gitlab/gitlab-ai-provider": "3.6.0",
|
|
||||||
"@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",
|
||||||
@@ -338,8 +338,8 @@
|
|||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
"@openrouter/ai-sdk-provider": "1.5.4",
|
"@openrouter/ai-sdk-provider": "1.5.4",
|
||||||
"@opentui/core": "0.1.87",
|
"@opentui/core": "0.1.90",
|
||||||
"@opentui/solid": "0.1.87",
|
"@opentui/solid": "0.1.90",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
@@ -355,9 +355,10 @@
|
|||||||
"cross-spawn": "^7.0.6",
|
"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",
|
||||||
@@ -368,6 +369,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:",
|
||||||
@@ -409,8 +411,8 @@
|
|||||||
"@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",
|
||||||
@@ -419,7 +421,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.2.27",
|
"version": "1.3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
@@ -443,7 +445,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.2.27",
|
"version": "1.3.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hey-api/openapi-ts": "0.90.10",
|
"@hey-api/openapi-ts": "0.90.10",
|
||||||
"@tsconfig/node22": "catalog:",
|
"@tsconfig/node22": "catalog:",
|
||||||
@@ -454,7 +456,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.2.27",
|
"version": "1.3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -489,7 +491,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.2.27",
|
"version": "1.3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -535,7 +537,7 @@
|
|||||||
},
|
},
|
||||||
"packages/util": {
|
"packages/util": {
|
||||||
"name": "@opencode-ai/util",
|
"name": "@opencode-ai/util",
|
||||||
"version": "1.2.27",
|
"version": "1.3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -546,7 +548,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.2.27",
|
"version": "1.3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
@@ -586,6 +588,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": {
|
||||||
@@ -608,7 +612,7 @@
|
|||||||
"@tailwindcss/vite": "4.1.11",
|
"@tailwindcss/vite": "4.1.11",
|
||||||
"@tsconfig/bun": "1.0.9",
|
"@tsconfig/bun": "1.0.9",
|
||||||
"@tsconfig/node22": "22.0.2",
|
"@tsconfig/node22": "22.0.2",
|
||||||
"@types/bun": "1.3.9",
|
"@types/bun": "1.3.11",
|
||||||
"@types/luxon": "3.7.1",
|
"@types/luxon": "3.7.1",
|
||||||
"@types/node": "22.13.9",
|
"@types/node": "22.13.9",
|
||||||
"@types/semver": "7.7.1",
|
"@types/semver": "7.7.1",
|
||||||
@@ -616,8 +620,8 @@
|
|||||||
"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.35",
|
"effect": "4.0.0-beta.35",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
"hono": "4.10.7",
|
"hono": "4.10.7",
|
||||||
@@ -1108,10 +1112,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=="],
|
||||||
@@ -1448,21 +1448,21 @@
|
|||||||
|
|
||||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||||
|
|
||||||
"@opentui/core": ["@opentui/core@0.1.87", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.87", "@opentui/core-darwin-x64": "0.1.87", "@opentui/core-linux-arm64": "0.1.87", "@opentui/core-linux-x64": "0.1.87", "@opentui/core-win32-arm64": "0.1.87", "@opentui/core-win32-x64": "0.1.87", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-dhsmMv0IqKftwG7J/pBrLBj2armsYIg5R3LBvciRQI/6X89GufP4l1u0+QTACAx6iR4SYJJNVNQ2tdX8LM9rMw=="],
|
"@opentui/core": ["@opentui/core@0.1.90", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.90", "@opentui/core-darwin-x64": "0.1.90", "@opentui/core-linux-arm64": "0.1.90", "@opentui/core-linux-x64": "0.1.90", "@opentui/core-win32-arm64": "0.1.90", "@opentui/core-win32-x64": "0.1.90", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-Os2dviqWVETU3kaK36lbSvdcI93GAWhw0xb9ng/d0DWYuM9scRmAhLHiOayp61saWv/BR8OJXeuQYHvrp5rd6A=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.87", "", { "os": "darwin", "cpu": "arm64" }, "sha512-G8oq85diOfkU6n0T1CxCle7oDmpKxwhcdhZ9khBMU5IrfLx9ZDuCM3F6MsiRQWdvPPCq2oomNbd64bYkPamYgw=="],
|
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.90", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XFrm2zCg1SlHPQ5A2HX/I4dCrmTjYaCJIIpo3QuPIvZBGH3aBMdWDJh2tXw7AB5Mmh8X1K4hDkP5nlK9x0Ewow=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.87", "", { "os": "darwin", "cpu": "x64" }, "sha512-MYTFQfOHm6qO7YaY4GHK9u/oJlXY6djaaxl5I+k4p2mk3vvuFIl/AP1ypITwBFjyV5gyp7PRWFp4nGfY9oN8bw=="],
|
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.90", "", { "os": "darwin", "cpu": "x64" }, "sha512-vbDpUsnlZ+0CeVKyBBXE+l2+X1XoVncMxMOhXTiMtud2/Cwu+Vfs/g3LC/6Zv08yaytA+9g7Z8sdf0QCqFyQ4w=="],
|
||||||
|
|
||||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.87", "", { "os": "linux", "cpu": "arm64" }, "sha512-he8o1h5M6oskRJ7wE+xKJgmWnv5ZwN6gB3M/Z+SeHtOMPa5cZmi3TefTjG54llEgFfx0F9RcqHof7TJ/GNxRkw=="],
|
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.90", "", { "os": "linux", "cpu": "arm64" }, "sha512-OTbvBTP5mVQ4uwKyuz6b59ElG+D0i1Ln+q6cVhNkLgeRLySIn1uXEzUFQGlnVgb8lFDANsn3yQmdv+R+Cpw0og=="],
|
||||||
|
|
||||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.87", "", { "os": "linux", "cpu": "x64" }, "sha512-aiUwjPlH4yDcB8/6YDKSmMkaoGAAltL0Xo0AzXyAtJXWK5tkCSaYjEVwzJ/rYRkr4Magnad+Mjth4AQUWdR2AA=="],
|
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.90", "", { "os": "linux", "cpu": "x64" }, "sha512-2PJi/LLlO7tGk9Ful/n+6iBdg1RFrA9ibU7wVneE6Z1P0LCYeu7bpwMzea1TXL0eAQWPHsjTs9aPlqPxln0EJw=="],
|
||||||
|
|
||||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.87", "", { "os": "win32", "cpu": "arm64" }, "sha512-cmP0pOyREjWGniHqbDmaMY7U+1AyagrD8VseJbU0cGpNgVpG2/gbrJUGdfdLB0SNb+mzLdx6SOjdxtrElwRCQA=="],
|
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.90", "", { "os": "win32", "cpu": "arm64" }, "sha512-+sTRaOb7gCMZ6iLuuG4y9kzyweJzBDcIJN0Xh49ikFWTwVECDXEVtXahNGlw57avm2yYUoNzmpBjK/LV7zBj9A=="],
|
||||||
|
|
||||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.87", "", { "os": "win32", "cpu": "x64" }, "sha512-N2GErAAP8iODf2RPp86pilPaVKiD6G4pkpZL5nLGbKsl0bndrVTpSqZcn8+/nQwFZDPD/AsiRTYNOfWOblhzOw=="],
|
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.90", "", { "os": "win32", "cpu": "x64" }, "sha512-aVFyErckWp4oW9NJ/ZDKBUAlTlfVUiRXGP63JXFOoeqI7EYaM8uBt6rgZAJuUdFWCN2Q66WRS8Y2mk+0BJwVBg=="],
|
||||||
|
|
||||||
"@opentui/solid": ["@opentui/solid@0.1.87", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.87", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-lRT9t30l8+FtgOjjWJcdb2MT6hP8/RKqwGgYwTI7fXrOqdhxxwdP2SM+rH2l3suHeASheiTdlvPAo230iUcsvg=="],
|
"@opentui/solid": ["@opentui/solid@0.1.90", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.90", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-zEHDpJOTGS707ts5j4diqoWuFLSqV6yARKl1H0FJkwWOotu+rxCyksL+C0gX0jJUonAw2cjlZ2NNtZY8g78zkg=="],
|
||||||
|
|
||||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||||
|
|
||||||
@@ -1890,6 +1890,8 @@
|
|||||||
|
|
||||||
"@solid-primitives/storage": ["@solid-primitives/storage@4.3.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "@tauri-apps/plugin-store": "*", "solid-js": "^1.6.12" }, "optionalPeers": ["@tauri-apps/plugin-store"] }, "sha512-ACbNwMZ1s8VAvld6EUXkDkX/US3IhtlPLxg6+B2s9MwNUugwdd51I98LPEaHrdLpqPmyzqgoJe0TxEFlf3Dqrw=="],
|
"@solid-primitives/storage": ["@solid-primitives/storage@4.3.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "@tauri-apps/plugin-store": "*", "solid-js": "^1.6.12" }, "optionalPeers": ["@tauri-apps/plugin-store"] }, "sha512-ACbNwMZ1s8VAvld6EUXkDkX/US3IhtlPLxg6+B2s9MwNUugwdd51I98LPEaHrdLpqPmyzqgoJe0TxEFlf3Dqrw=="],
|
||||||
|
|
||||||
|
"@solid-primitives/timer": ["@solid-primitives/timer@1.4.4", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Ayjyb3+v1hyU92vuLUN0tVHq2mmTCPGxSDLGJMsDydRqx9ZfJIc9xj6cxK4XvdY3pif3ps2mIv52pjgToybEpQ=="],
|
||||||
|
|
||||||
"@solid-primitives/trigger": ["@solid-primitives/trigger@1.2.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Za2JebEiDyfamjmDwRaESYqBBYOlgYGzB8kHYH0QrkXyLf2qNADlKdGN+z3vWSLCTDcKxChS43Kssjuc0OZhng=="],
|
"@solid-primitives/trigger": ["@solid-primitives/trigger@1.2.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Za2JebEiDyfamjmDwRaESYqBBYOlgYGzB8kHYH0QrkXyLf2qNADlKdGN+z3vWSLCTDcKxChS43Kssjuc0OZhng=="],
|
||||||
|
|
||||||
"@solid-primitives/utils": ["@solid-primitives/utils@6.4.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-AeGTBg8Wtkh/0s+evyLtP8piQoS4wyqqQaAFs2HJcFMMjYAtUgo+ZPduRXLjPlqKVc2ejeR544oeqpbn8Egn8A=="],
|
"@solid-primitives/utils": ["@solid-primitives/utils@6.4.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-AeGTBg8Wtkh/0s+evyLtP8piQoS4wyqqQaAFs2HJcFMMjYAtUgo+ZPduRXLjPlqKVc2ejeR544oeqpbn8Egn8A=="],
|
||||||
@@ -1968,10 +1970,14 @@
|
|||||||
|
|
||||||
"@tanstack/directive-functions-plugin": ["@tanstack/directive-functions-plugin@1.134.5", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.27.7", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@tanstack/router-utils": "1.133.19", "babel-dead-code-elimination": "^1.0.10", "pathe": "^2.0.3", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "vite": ">=6.0.0 || >=7.0.0" } }, "sha512-J3oawV8uBRBbPoLgMdyHt+LxzTNuWRKNJJuCLWsm/yq6v0IQSvIVCgfD2+liIiSnDPxGZ8ExduPXy8IzS70eXw=="],
|
"@tanstack/directive-functions-plugin": ["@tanstack/directive-functions-plugin@1.134.5", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.27.7", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@tanstack/router-utils": "1.133.19", "babel-dead-code-elimination": "^1.0.10", "pathe": "^2.0.3", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "vite": ">=6.0.0 || >=7.0.0" } }, "sha512-J3oawV8uBRBbPoLgMdyHt+LxzTNuWRKNJJuCLWsm/yq6v0IQSvIVCgfD2+liIiSnDPxGZ8ExduPXy8IzS70eXw=="],
|
||||||
|
|
||||||
|
"@tanstack/query-core": ["@tanstack/query-core@5.91.2", "", {}, "sha512-Uz2pTgPC1mhqrrSGg18RKCWT/pkduAYtxbcyIyKBhw7dTWjXZIzqmpzO2lBkyWr4hlImQgpu1m1pei3UnkFRWw=="],
|
||||||
|
|
||||||
"@tanstack/router-utils": ["@tanstack/router-utils@1.133.19", "", { "dependencies": { "@babel/core": "^7.27.4", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.5", "@babel/preset-typescript": "^7.27.1", "ansis": "^4.1.0", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-WEp5D2gPxvlLDRXwD/fV7RXjYtqaqJNXKB/L6OyZEbT+9BG/Ib2d7oG9GSUZNNMGPGYAlhBUOi3xutySsk6rxA=="],
|
"@tanstack/router-utils": ["@tanstack/router-utils@1.133.19", "", { "dependencies": { "@babel/core": "^7.27.4", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.5", "@babel/preset-typescript": "^7.27.1", "ansis": "^4.1.0", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-WEp5D2gPxvlLDRXwD/fV7RXjYtqaqJNXKB/L6OyZEbT+9BG/Ib2d7oG9GSUZNNMGPGYAlhBUOi3xutySsk6rxA=="],
|
||||||
|
|
||||||
"@tanstack/server-functions-plugin": ["@tanstack/server-functions-plugin@1.134.5", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.27.7", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@tanstack/directive-functions-plugin": "1.134.5", "babel-dead-code-elimination": "^1.0.9", "tiny-invariant": "^1.3.3" } }, "sha512-2sWxq70T+dOEUlE3sHlXjEPhaFZfdPYlWTSkHchWXrFGw2YOAa+hzD6L9wHMjGDQezYd03ue8tQlHG+9Jzbzgw=="],
|
"@tanstack/server-functions-plugin": ["@tanstack/server-functions-plugin@1.134.5", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.27.7", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@tanstack/directive-functions-plugin": "1.134.5", "babel-dead-code-elimination": "^1.0.9", "tiny-invariant": "^1.3.3" } }, "sha512-2sWxq70T+dOEUlE3sHlXjEPhaFZfdPYlWTSkHchWXrFGw2YOAa+hzD6L9wHMjGDQezYd03ue8tQlHG+9Jzbzgw=="],
|
||||||
|
|
||||||
|
"@tanstack/solid-query": ["@tanstack/solid-query@5.91.4", "", { "dependencies": { "@tanstack/query-core": "5.91.2" }, "peerDependencies": { "solid-js": "^1.6.0" } }, "sha512-oCEgn8iT7WnF/7ISd7usBpUK1C9EdvQfg8ZUpKNKZ4edVClICZrCX6f3/Bp8ZlwQnL21KLc2rp+CejEuehlRxg=="],
|
||||||
|
|
||||||
"@tauri-apps/api": ["@tauri-apps/api@2.10.1", "", {}, "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw=="],
|
"@tauri-apps/api": ["@tauri-apps/api@2.10.1", "", {}, "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw=="],
|
||||||
|
|
||||||
"@tauri-apps/cli": ["@tauri-apps/cli@2.10.1", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.10.1", "@tauri-apps/cli-darwin-x64": "2.10.1", "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1", "@tauri-apps/cli-linux-arm64-gnu": "2.10.1", "@tauri-apps/cli-linux-arm64-musl": "2.10.1", "@tauri-apps/cli-linux-riscv64-gnu": "2.10.1", "@tauri-apps/cli-linux-x64-gnu": "2.10.1", "@tauri-apps/cli-linux-x64-musl": "2.10.1", "@tauri-apps/cli-win32-arm64-msvc": "2.10.1", "@tauri-apps/cli-win32-ia32-msvc": "2.10.1", "@tauri-apps/cli-win32-x64-msvc": "2.10.1" }, "bin": { "tauri": "tauri.js" } }, "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g=="],
|
"@tauri-apps/cli": ["@tauri-apps/cli@2.10.1", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.10.1", "@tauri-apps/cli-darwin-x64": "2.10.1", "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1", "@tauri-apps/cli-linux-arm64-gnu": "2.10.1", "@tauri-apps/cli-linux-arm64-musl": "2.10.1", "@tauri-apps/cli-linux-riscv64-gnu": "2.10.1", "@tauri-apps/cli-linux-x64-gnu": "2.10.1", "@tauri-apps/cli-linux-x64-musl": "2.10.1", "@tauri-apps/cli-win32-arm64-msvc": "2.10.1", "@tauri-apps/cli-win32-ia32-msvc": "2.10.1", "@tauri-apps/cli-win32-x64-msvc": "2.10.1" }, "bin": { "tauri": "tauri.js" } }, "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g=="],
|
||||||
@@ -2054,7 +2060,7 @@
|
|||||||
|
|
||||||
"@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
|
"@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
|
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
||||||
|
|
||||||
"@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="],
|
"@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="],
|
||||||
|
|
||||||
@@ -2450,7 +2456,7 @@
|
|||||||
|
|
||||||
"bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="],
|
"bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
|
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
||||||
|
|
||||||
"bun-webgpu": ["bun-webgpu@0.1.5", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.5", "bun-webgpu-darwin-x64": "^0.1.5", "bun-webgpu-linux-x64": "^0.1.5", "bun-webgpu-win32-x64": "^0.1.5" } }, "sha512-91/K6S5whZKX7CWAm9AylhyKrLGRz6BUiiPiM/kXadSnD4rffljCD/q9cNFftm5YXhx4MvLqw33yEilxogJvwA=="],
|
"bun-webgpu": ["bun-webgpu@0.1.5", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.5", "bun-webgpu-darwin-x64": "^0.1.5", "bun-webgpu-linux-x64": "^0.1.5", "bun-webgpu-win32-x64": "^0.1.5" } }, "sha512-91/K6S5whZKX7CWAm9AylhyKrLGRz6BUiiPiM/kXadSnD4rffljCD/q9cNFftm5YXhx4MvLqw33yEilxogJvwA=="],
|
||||||
|
|
||||||
@@ -2736,9 +2742,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=="],
|
||||||
|
|
||||||
@@ -3020,6 +3026,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=="],
|
||||||
@@ -3028,6 +3036,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=="],
|
||||||
@@ -3780,6 +3790,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=="],
|
||||||
@@ -4108,6 +4120,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=="],
|
||||||
@@ -4240,7 +4254,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=="],
|
||||||
|
|
||||||
@@ -5054,10 +5068,6 @@
|
|||||||
|
|
||||||
"@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=="],
|
||||||
@@ -5196,8 +5206,6 @@
|
|||||||
|
|
||||||
"@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
"@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
||||||
|
|
||||||
"@opentui/solid/babel-preset-solid": ["babel-preset-solid@1.9.9", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.1" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.8" }, "optionalPeers": ["solid-js"] }, "sha512-pCnxWrciluXCeli/dj5PIEHgbNzim3evtTn12snjqqg8QZWJNMjH1AWIp4iG/tbVjqQ72aBEymMSagvmgxubXw=="],
|
|
||||||
|
|
||||||
"@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="],
|
"@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="],
|
||||||
|
|
||||||
"@pierre/diffs/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="],
|
"@pierre/diffs/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="],
|
||||||
@@ -5386,6 +5394,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=="],
|
||||||
@@ -5452,6 +5462,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=="],
|
||||||
@@ -5528,6 +5542,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=="],
|
||||||
@@ -6278,6 +6294,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,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-yfA50QKqylmaioxi+6d++W8Xv4Wix1hl3hEF6Zz7Ue0=",
|
"x86_64-linux": "sha256-E5neEbBiwQDhIQ5QVhijpHCCP9hcxm319S9WrDKngSw=",
|
||||||
"aarch64-linux": "sha256-b5sO7V+/zzJClHHKjkSz+9AUBYC8cb7S3m5ab1kpAyk=",
|
"aarch64-linux": "sha256-lnwaGSEirl9izskDooB/xQ0ZdirW0t3/S+OoOnfYaoQ=",
|
||||||
"aarch64-darwin": "sha256-V66nmRX6kAjrc41ARVeuTElWK7KD8qG/DVk9K7Fu+J8=",
|
"aarch64-darwin": "sha256-RDxxW9NMlGMIdIxTsbOYVqxunflkILv2dA7JqjnJgm4=",
|
||||||
"x86_64-darwin": "sha256-cFyh60WESiqZ5XWZi1+g3F/beSDL1+UPG8KhRivhK8w="
|
"x86_64-darwin": "sha256-1tvvktu2NRg6N6ASuKzqzcEmMrzH3/LFey0Vxr4E8zg="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-5
@@ -4,7 +4,7 @@
|
|||||||
"description": "AI-powered development tool",
|
"description": "AI-powered development tool",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "bun@1.3.10",
|
"packageManager": "bun@1.3.11",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||||
"dev:desktop": "bun --cwd packages/desktop tauri dev",
|
"dev:desktop": "bun --cwd packages/desktop tauri dev",
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
],
|
],
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"@effect/platform-node": "4.0.0-beta.35",
|
"@effect/platform-node": "4.0.0-beta.35",
|
||||||
"@types/bun": "1.3.9",
|
"@types/bun": "1.3.11",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
"ulid": "3.0.1",
|
"ulid": "3.0.1",
|
||||||
@@ -43,8 +43,8 @@
|
|||||||
"@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.35",
|
"effect": "4.0.0-beta.35",
|
||||||
"ai": "5.0.124",
|
"ai": "5.0.124",
|
||||||
"hono": "4.10.7",
|
"hono": "4.10.7",
|
||||||
@@ -112,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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+172
-9
@@ -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,
|
||||||
@@ -22,6 +24,16 @@ import {
|
|||||||
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(() => {
|
||||||
@@ -163,9 +175,9 @@ export async function runTerminal(page: Page, input: { cmd: string; token: strin
|
|||||||
await expect.poll(() => terminalHas(page, { term, token: input.token }), { timeout }).toBe(true)
|
await expect.poll(() => terminalHas(page, { term, token: input.token }), { timeout }).toBe(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openPalette(page: Page) {
|
export async function openPalette(page: Page, key = "K") {
|
||||||
await defocus(page)
|
await defocus(page)
|
||||||
await page.keyboard.press(`${modKey}+P`)
|
await page.keyboard.press(`${modKey}+${key}`)
|
||||||
|
|
||||||
const dialog = page.getByRole("dialog")
|
const dialog = page.getByRole("dialog")
|
||||||
await expect(dialog).toBeVisible()
|
await expect(dialog).toBeVisible()
|
||||||
@@ -195,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`)
|
||||||
@@ -208,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)
|
||||||
@@ -225,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)
|
||||||
@@ -240,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")
|
||||||
@@ -252,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
|
||||||
@@ -313,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', {
|
||||||
@@ -338,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 ""
|
||||||
@@ -361,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]
|
||||||
@@ -772,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)
|
||||||
@@ -782,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)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { openPalette } from "../actions"
|
import { closeDialog, openPalette } from "../actions"
|
||||||
|
|
||||||
test("search palette opens and closes", async ({ page, gotoSession }) => {
|
test("search palette opens and closes", async ({ page, gotoSession }) => {
|
||||||
await gotoSession()
|
await gotoSession()
|
||||||
@@ -9,3 +9,12 @@ test("search palette opens and closes", async ({ page, gotoSession }) => {
|
|||||||
await page.keyboard.press("Escape")
|
await page.keyboard.press("Escape")
|
||||||
await expect(dialog).toHaveCount(0)
|
await expect(dialog).toHaveCount(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("search palette also opens with cmd+p", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const dialog = await openPalette(page, "P")
|
||||||
|
|
||||||
|
await closeDialog(page, dialog)
|
||||||
|
await expect(dialog).toHaveCount(0)
|
||||||
|
})
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 }) {
|
||||||
|
return `${workspaceItemSelector(space.slug)}, ${workspaceItemSelector(space.raw)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function button(space: { slug: string; raw: string }) {
|
||||||
|
return `${workspaceNewSessionSelector(space.slug)}, ${workspaceNewSessionSelector(space.raw)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitWorkspaceReady(page: Page, space: { slug: string; raw: string }) {
|
||||||
await openSidebar(page)
|
await openSidebar(page)
|
||||||
await expect
|
await expect(page.locator(item(space)).first()).toBeVisible({ timeout: 60_000 })
|
||||||
.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 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,7 +108,10 @@ test("prompt history restores unsent draft with arrow navigation", async ({ page
|
|||||||
await page.keyboard.type(draft)
|
await page.keyboard.type(draft)
|
||||||
await wait(page, draft)
|
await wait(page, draft)
|
||||||
|
|
||||||
await edge(page, "start")
|
// Clear the draft before navigating history (ArrowUp only works when prompt is empty)
|
||||||
|
await prompt.fill("")
|
||||||
|
await wait(page, "")
|
||||||
|
|
||||||
await page.keyboard.press("ArrowUp")
|
await page.keyboard.press("ArrowUp")
|
||||||
await wait(page, second)
|
await wait(page, second)
|
||||||
|
|
||||||
@@ -119,7 +122,7 @@ test("prompt history restores unsent draft with arrow navigation", async ({ page
|
|||||||
await wait(page, second)
|
await wait(page, second)
|
||||||
|
|
||||||
await page.keyboard.press("ArrowDown")
|
await page.keyboard.press("ArrowDown")
|
||||||
await wait(page, draft)
|
await wait(page, "")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import { base64Decode } from "@opencode-ai/util/encode"
|
|
||||||
import type { Locator, Page } from "@playwright/test"
|
import type { Locator, Page } from "@playwright/test"
|
||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { openSidebar, sessionIDFromUrl, setWorkspacesEnabled, waitSessionIdle, waitSlug } from "../actions"
|
import {
|
||||||
|
openSidebar,
|
||||||
|
resolveSlug,
|
||||||
|
sessionIDFromUrl,
|
||||||
|
setWorkspacesEnabled,
|
||||||
|
waitSession,
|
||||||
|
waitSessionIdle,
|
||||||
|
waitSlug,
|
||||||
|
} from "../actions"
|
||||||
import {
|
import {
|
||||||
promptAgentSelector,
|
promptAgentSelector,
|
||||||
promptModelSelector,
|
promptModelSelector,
|
||||||
@@ -30,8 +37,6 @@ const text = async (locator: Locator) => ((await locator.textContent()) ?? "").t
|
|||||||
|
|
||||||
const modelKey = (state: Probe | null) => (state?.model ? `${state.model.providerID}:${state.model.modelID}` : null)
|
const modelKey = (state: Probe | null) => (state?.model ? `${state.model.providerID}:${state.model.modelID}` : null)
|
||||||
|
|
||||||
const dirKey = (state: Probe | null) => state?.dir ?? ""
|
|
||||||
|
|
||||||
async function probe(page: Page): Promise<Probe | null> {
|
async function probe(page: Page): Promise<Probe | null> {
|
||||||
return page.evaluate(() => {
|
return page.evaluate(() => {
|
||||||
const win = window as Window & {
|
const win = window as Window & {
|
||||||
@@ -45,21 +50,6 @@ async function probe(page: Page): Promise<Probe | null> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function currentDir(page: Page) {
|
|
||||||
let hit = ""
|
|
||||||
await expect
|
|
||||||
.poll(
|
|
||||||
async () => {
|
|
||||||
const next = dirKey(await probe(page))
|
|
||||||
if (next) hit = next
|
|
||||||
return next
|
|
||||||
},
|
|
||||||
{ timeout: 30_000 },
|
|
||||||
)
|
|
||||||
.not.toBe("")
|
|
||||||
return hit
|
|
||||||
}
|
|
||||||
|
|
||||||
async function read(page: Page): Promise<Footer> {
|
async function read(page: Page): Promise<Footer> {
|
||||||
return {
|
return {
|
||||||
agent: await text(page.locator(`${promptAgentSelector} [data-slot="select-select-trigger-value"]`).first()),
|
agent: await text(page.locator(`${promptAgentSelector} [data-slot="select-select-trigger-value"]`).first()),
|
||||||
@@ -188,8 +178,7 @@ async function chooseOtherModel(page: Page): Promise<Footer> {
|
|||||||
|
|
||||||
async function goto(page: Page, directory: string, sessionID?: string) {
|
async function goto(page: Page, directory: string, 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 expect.poll(async () => dirKey(await probe(page)), { timeout: 30_000 }).toBe(directory)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submit(page: Page, value: string) {
|
async function submit(page: Page, value: string) {
|
||||||
@@ -224,10 +213,9 @@ 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 waitSession(page, { directory: next.directory })
|
||||||
if (!directory) throw new Error(`Failed to decode workspace slug: ${slug}`)
|
return next
|
||||||
return { slug, directory }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitWorkspace(page: Page, slug: string) {
|
async function waitWorkspace(page: Page, slug: string) {
|
||||||
@@ -257,10 +245,8 @@ async function newWorkspaceSession(page: Page, slug: string) {
|
|||||||
await expect(button).toBeVisible()
|
await expect(button).toBeVisible()
|
||||||
await button.click({ force: true })
|
await button.click({ force: true })
|
||||||
|
|
||||||
const next = await waitSlug(page)
|
const next = await resolveSlug(await waitSlug(page))
|
||||||
await expect(page).toHaveURL(new RegExp(`/${next}/session(?:[/?#]|$)`))
|
return waitSession(page, { directory: next.directory }).then((item) => item.directory)
|
||||||
await expect(page.locator(promptSelector)).toBeVisible()
|
|
||||||
return currentDir(page)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test("session model and variant restore per session without leaking into new sessions", async ({
|
test("session model and variant restore per session without leaking into new sessions", async ({
|
||||||
@@ -279,7 +265,7 @@ test("session model and variant restore per session without leaking into new ses
|
|||||||
await waitUser(directory, first)
|
await waitUser(directory, first)
|
||||||
|
|
||||||
await page.reload()
|
await page.reload()
|
||||||
await expect(page.locator(promptSelector)).toBeVisible()
|
await waitSession(page, { directory, sessionID: first })
|
||||||
await waitFooter(page, firstState)
|
await waitFooter(page, firstState)
|
||||||
|
|
||||||
await gotoSession()
|
await gotoSession()
|
||||||
|
|||||||
@@ -169,6 +169,70 @@ async function overflow(page: Parameters<typeof test>[0]["page"], file: string)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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("review applies inline comment clicks without horizontal overflow", async ({ page, withProject }) => {
|
||||||
test.setTimeout(180_000)
|
test.setTimeout(180_000)
|
||||||
|
|
||||||
@@ -218,6 +282,56 @@ test("review applies inline comment clicks without horizontal overflow", async (
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ test("changing file open keybind works", async ({ page, gotoSession }) => {
|
|||||||
await expect(keybindButton).toBeVisible()
|
await expect(keybindButton).toBeVisible()
|
||||||
|
|
||||||
const initialKeybind = await keybindButton.textContent()
|
const initialKeybind = await keybindButton.textContent()
|
||||||
expect(initialKeybind).toContain("P")
|
expect(initialKeybind).toContain("K")
|
||||||
|
|
||||||
await keybindButton.click()
|
await keybindButton.click()
|
||||||
await expect(keybindButton).toHaveText(/press/i)
|
await expect(keybindButton).toHaveText(/press/i)
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { cleanupSession, closeSidebar, hoverSessionItem } from "../actions"
|
import {
|
||||||
|
defocus,
|
||||||
|
cleanupSession,
|
||||||
|
cleanupTestProject,
|
||||||
|
closeSidebar,
|
||||||
|
createTestProject,
|
||||||
|
hoverSessionItem,
|
||||||
|
openSidebar,
|
||||||
|
waitSession,
|
||||||
|
} from "../actions"
|
||||||
import { projectSwitchSelector } from "../selectors"
|
import { projectSwitchSelector } from "../selectors"
|
||||||
|
import { dirSlug } from "../utils"
|
||||||
|
|
||||||
test("collapsed sidebar popover stays open when archiving a session", async ({ page, slug, sdk, gotoSession }) => {
|
test("collapsed sidebar popover stays open when archiving a session", async ({ page, slug, sdk, gotoSession }) => {
|
||||||
const stamp = Date.now()
|
const stamp = Date.now()
|
||||||
@@ -37,3 +47,72 @@ test("collapsed sidebar popover stays open when archiving a session", async ({ p
|
|||||||
await cleanupSession({ sdk, sessionID: two.id })
|
await cleanupSession({ sdk, sessionID: two.id })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("open sidebar project popover stays closed after clicking avatar", async ({ page, withProject }) => {
|
||||||
|
await page.setViewportSize({ width: 1400, height: 800 })
|
||||||
|
|
||||||
|
const other = await createTestProject()
|
||||||
|
const slug = dirSlug(other)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await withProject(
|
||||||
|
async () => {
|
||||||
|
await openSidebar(page)
|
||||||
|
|
||||||
|
const project = page.locator(projectSwitchSelector(slug)).first()
|
||||||
|
const card = page.locator('[data-component="hover-card-content"]')
|
||||||
|
|
||||||
|
await expect(project).toBeVisible()
|
||||||
|
await project.hover()
|
||||||
|
await expect(card.getByText(/recent sessions/i)).toBeVisible()
|
||||||
|
|
||||||
|
await page.mouse.down()
|
||||||
|
await expect(card).toHaveCount(0)
|
||||||
|
await page.mouse.up()
|
||||||
|
|
||||||
|
await waitSession(page, { directory: other })
|
||||||
|
await expect(card).toHaveCount(0)
|
||||||
|
},
|
||||||
|
{ extra: [other] },
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
await cleanupTestProject(other)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("open sidebar project switch activates on first tabbed enter", async ({ page, withProject }) => {
|
||||||
|
await page.setViewportSize({ width: 1400, height: 800 })
|
||||||
|
|
||||||
|
const other = await createTestProject()
|
||||||
|
const slug = dirSlug(other)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await withProject(
|
||||||
|
async () => {
|
||||||
|
await openSidebar(page)
|
||||||
|
await defocus(page)
|
||||||
|
|
||||||
|
const project = page.locator(projectSwitchSelector(slug)).first()
|
||||||
|
|
||||||
|
await expect(project).toBeVisible()
|
||||||
|
|
||||||
|
let hit = false
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
hit = await project.evaluate((el) => {
|
||||||
|
return el.matches(":focus") || !!el.parentElement?.matches(":focus")
|
||||||
|
})
|
||||||
|
if (hit) break
|
||||||
|
await page.keyboard.press("Tab")
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(hit).toBe(true)
|
||||||
|
|
||||||
|
await page.keyboard.press("Enter")
|
||||||
|
await waitSession(page, { directory: other })
|
||||||
|
},
|
||||||
|
{ extra: [other] },
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
await cleanupTestProject(other)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Page } from "@playwright/test"
|
import type { Page } from "@playwright/test"
|
||||||
import { runTerminal, waitTerminalReady } from "../actions"
|
import { runTerminal, waitTerminalReady } from "../actions"
|
||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { terminalSelector } from "../selectors"
|
import { dropdownMenuContentSelector, terminalSelector } from "../selectors"
|
||||||
import { terminalToggleKey, workspacePersistKey } from "../utils"
|
import { terminalToggleKey, workspacePersistKey } from "../utils"
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
@@ -130,3 +130,39 @@ test("closing the active terminal tab falls back to the previous tab", async ({
|
|||||||
.toEqual({ count: 1, first: true })
|
.toEqual({ count: 1, first: true })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("terminal tab can be renamed from the context menu", async ({ page, withProject }) => {
|
||||||
|
await withProject(async ({ directory, gotoSession }) => {
|
||||||
|
const key = workspacePersistKey(directory, "terminal")
|
||||||
|
const rename = `E2E term ${Date.now()}`
|
||||||
|
const tab = page.locator('#terminal-panel [data-slot="tabs-trigger"]').first()
|
||||||
|
|
||||||
|
await gotoSession()
|
||||||
|
await open(page)
|
||||||
|
|
||||||
|
await expect(tab).toContainText(/Terminal 1/)
|
||||||
|
await tab.click({ button: "right" })
|
||||||
|
|
||||||
|
const menu = page.locator(dropdownMenuContentSelector).first()
|
||||||
|
await expect(menu).toBeVisible()
|
||||||
|
await menu.getByRole("menuitem", { name: /^Rename$/i }).click()
|
||||||
|
await expect(menu).toHaveCount(0)
|
||||||
|
|
||||||
|
const input = page.locator('#terminal-panel input[type="text"]').first()
|
||||||
|
await expect(input).toBeVisible()
|
||||||
|
await input.fill(rename)
|
||||||
|
await input.press("Enter")
|
||||||
|
|
||||||
|
await expect(input).toHaveCount(0)
|
||||||
|
await expect(tab).toContainText(rename)
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const state = await store(page, key)
|
||||||
|
return state?.all[0]?.title
|
||||||
|
},
|
||||||
|
{ timeout: 5_000 },
|
||||||
|
)
|
||||||
|
.toBe(rename)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.2.27",
|
"version": "1.3.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -51,9 +51,11 @@
|
|||||||
"@solid-primitives/resize-observer": "2.1.3",
|
"@solid-primitives/resize-observer": "2.1.3",
|
||||||
"@solid-primitives/scroll": "2.1.3",
|
"@solid-primitives/scroll": "2.1.3",
|
||||||
"@solid-primitives/storage": "catalog:",
|
"@solid-primitives/storage": "catalog:",
|
||||||
|
"@solid-primitives/timer": "1.4.4",
|
||||||
"@solid-primitives/websocket": "1.3.1",
|
"@solid-primitives/websocket": "1.3.1",
|
||||||
"@solidjs/meta": "catalog:",
|
"@solidjs/meta": "catalog:",
|
||||||
"@solidjs/router": "catalog:",
|
"@solidjs/router": "catalog:",
|
||||||
|
"@tanstack/solid-query": "5.91.4",
|
||||||
"@thisbeyond/solid-dnd": "0.7.5",
|
"@thisbeyond/solid-dnd": "0.7.5",
|
||||||
"diff": "catalog:",
|
"diff": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
|
|||||||
+44
-31
@@ -9,6 +9,7 @@ import { Splash } from "@opencode-ai/ui/logo"
|
|||||||
import { ThemeProvider } from "@opencode-ai/ui/theme"
|
import { ThemeProvider } from "@opencode-ai/ui/theme"
|
||||||
import { MetaProvider } from "@solidjs/meta"
|
import { MetaProvider } from "@solidjs/meta"
|
||||||
import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router"
|
import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router"
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
|
||||||
import { type Duration, Effect } from "effect"
|
import { type Duration, Effect } from "effect"
|
||||||
import {
|
import {
|
||||||
type Component,
|
type Component,
|
||||||
@@ -46,21 +47,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>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -89,6 +82,11 @@ function MarkedProviderWithNativeParser(props: ParentProps) {
|
|||||||
return <MarkedProvider nativeParser={platform.parseMarkdown}>{props.children}</MarkedProvider>
|
return <MarkedProvider nativeParser={platform.parseMarkdown}>{props.children}</MarkedProvider>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function QueryProvider(props: ParentProps) {
|
||||||
|
const client = new QueryClient()
|
||||||
|
return <QueryClientProvider client={client}>{props.children}</QueryClientProvider>
|
||||||
|
}
|
||||||
|
|
||||||
function AppShellProviders(props: ParentProps) {
|
function AppShellProviders(props: ParentProps) {
|
||||||
return (
|
return (
|
||||||
<SettingsProvider>
|
<SettingsProvider>
|
||||||
@@ -124,8 +122,10 @@ function SessionProviders(props: ParentProps) {
|
|||||||
function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
|
function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
|
||||||
return (
|
return (
|
||||||
<AppShellProviders>
|
<AppShellProviders>
|
||||||
{props.appChildren}
|
<Suspense fallback={<Loading />}>
|
||||||
{props.children}
|
{props.appChildren}
|
||||||
|
{props.children}
|
||||||
|
</Suspense>
|
||||||
</AppShellProviders>
|
</AppShellProviders>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -142,11 +142,13 @@ export function AppBaseProviders(props: ParentProps) {
|
|||||||
<LanguageProvider>
|
<LanguageProvider>
|
||||||
<UiI18nBridge>
|
<UiI18nBridge>
|
||||||
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
|
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
|
||||||
<DialogProvider>
|
<QueryProvider>
|
||||||
<MarkedProviderWithNativeParser>
|
<DialogProvider>
|
||||||
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
|
<MarkedProviderWithNativeParser>
|
||||||
</MarkedProviderWithNativeParser>
|
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
|
||||||
</DialogProvider>
|
</MarkedProviderWithNativeParser>
|
||||||
|
</DialogProvider>
|
||||||
|
</QueryProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</UiI18nBridge>
|
</UiI18nBridge>
|
||||||
</LanguageProvider>
|
</LanguageProvider>
|
||||||
@@ -265,6 +267,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,20 +286,22 @@ 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}>
|
||||||
<GlobalSDKProvider>
|
<ServerKey>
|
||||||
<GlobalSyncProvider>
|
<GlobalSDKProvider>
|
||||||
<Dynamic
|
<GlobalSyncProvider>
|
||||||
component={props.router ?? Router}
|
<Dynamic
|
||||||
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
|
component={props.router ?? Router}
|
||||||
>
|
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
|
||||||
<Route path="/" component={HomeRoute} />
|
>
|
||||||
<Route path="/:dir" component={DirectoryLayout}>
|
<Route path="/" component={HomeRoute} />
|
||||||
<Route path="/" component={SessionIndexRoute} />
|
<Route path="/:dir" component={DirectoryLayout}>
|
||||||
<Route path="/session/:id?" component={SessionRoute} />
|
<Route path="/" component={SessionIndexRoute} />
|
||||||
</Route>
|
<Route path="/session/:id?" component={SessionRoute} />
|
||||||
</Dynamic>
|
</Route>
|
||||||
</GlobalSyncProvider>
|
</Dynamic>
|
||||||
</GlobalSDKProvider>
|
</GlobalSyncProvider>
|
||||||
|
</GlobalSDKProvider>
|
||||||
|
</ServerKey>
|
||||||
</ConnectionGate>
|
</ConnectionGate>
|
||||||
</ServerProvider>
|
</ServerProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,18 +12,15 @@ import { showToast } from "@opencode-ai/ui/toast"
|
|||||||
import { createMemo, Match, onCleanup, onMount, Switch } from "solid-js"
|
import { createMemo, Match, onCleanup, onMount, Switch } from "solid-js"
|
||||||
import { createStore, produce } from "solid-js/store"
|
import { createStore, produce } from "solid-js/store"
|
||||||
import { Link } from "@/components/link"
|
import { Link } from "@/components/link"
|
||||||
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 { useLanguage } from "@/context/language"
|
||||||
import { DialogSelectModel } from "./dialog-select-model"
|
|
||||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||||
|
|
||||||
export function DialogConnectProvider(props: { provider: string }) {
|
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 +46,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 +75,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 +123,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 +133,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 +144,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
{
|
{
|
||||||
providerID: props.provider,
|
providerID: props.provider,
|
||||||
method: index,
|
method: index,
|
||||||
|
inputs,
|
||||||
},
|
},
|
||||||
{ throwOnError: true },
|
{ throwOnError: true },
|
||||||
)
|
)
|
||||||
@@ -163,6 +171,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 +425,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 +438,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 +486,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 +504,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 +584,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">
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ export type FormState = {
|
|||||||
apiKey: string
|
apiKey: string
|
||||||
models: ModelRow[]
|
models: ModelRow[]
|
||||||
headers: HeaderRow[]
|
headers: HeaderRow[]
|
||||||
saving: boolean
|
|
||||||
err: {
|
err: {
|
||||||
providerID?: string
|
providerID?: string
|
||||||
name?: string
|
name?: string
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ describe("validateCustomProvider", () => {
|
|||||||
{ row: "h0", key: " X-Test ", value: " enabled ", err: {} },
|
{ row: "h0", key: " X-Test ", value: " enabled ", err: {} },
|
||||||
{ row: "h1", key: "", value: "", err: {} },
|
{ row: "h1", key: "", value: "", err: {} },
|
||||||
],
|
],
|
||||||
saving: false,
|
|
||||||
err: {},
|
err: {},
|
||||||
},
|
},
|
||||||
t,
|
t,
|
||||||
@@ -60,7 +59,6 @@ describe("validateCustomProvider", () => {
|
|||||||
{ row: "h0", key: "Authorization", value: "one", err: {} },
|
{ row: "h0", key: "Authorization", value: "one", err: {} },
|
||||||
{ row: "h1", key: "authorization", value: "two", err: {} },
|
{ row: "h1", key: "authorization", value: "two", err: {} },
|
||||||
],
|
],
|
||||||
saving: false,
|
|
||||||
err: {},
|
err: {},
|
||||||
},
|
},
|
||||||
t,
|
t,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
|
import { useMutation } from "@tanstack/solid-query"
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { batch, For } from "solid-js"
|
import { batch, For } from "solid-js"
|
||||||
@@ -31,7 +32,6 @@ export function DialogCustomProvider(props: Props) {
|
|||||||
apiKey: "",
|
apiKey: "",
|
||||||
models: [modelRow()],
|
models: [modelRow()],
|
||||||
headers: [headerRow()],
|
headers: [headerRow()],
|
||||||
saving: false,
|
|
||||||
err: {},
|
err: {},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -116,48 +116,49 @@ export function DialogCustomProvider(props: Props) {
|
|||||||
return output.result
|
return output.result
|
||||||
}
|
}
|
||||||
|
|
||||||
const save = async (e: SubmitEvent) => {
|
const saveMutation = useMutation(() => ({
|
||||||
e.preventDefault()
|
mutationFn: async (result: NonNullable<ReturnType<typeof validate>>) => {
|
||||||
if (form.saving) return
|
const disabledProviders = globalSync.data.config.disabled_providers ?? []
|
||||||
|
const nextDisabled = disabledProviders.filter((id) => id !== result.providerID)
|
||||||
|
|
||||||
const result = validate()
|
if (result.key) {
|
||||||
if (!result) return
|
await globalSDK.client.auth.set({
|
||||||
|
|
||||||
setForm("saving", true)
|
|
||||||
|
|
||||||
const disabledProviders = globalSync.data.config.disabled_providers ?? []
|
|
||||||
const nextDisabled = disabledProviders.filter((id) => id !== result.providerID)
|
|
||||||
|
|
||||||
const auth = result.key
|
|
||||||
? globalSDK.client.auth.set({
|
|
||||||
providerID: result.providerID,
|
providerID: result.providerID,
|
||||||
auth: {
|
auth: {
|
||||||
type: "api",
|
type: "api",
|
||||||
key: result.key,
|
key: result.key,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
: Promise.resolve()
|
}
|
||||||
|
|
||||||
auth
|
await globalSync.updateConfig({
|
||||||
.then(() =>
|
provider: { [result.providerID]: result.config },
|
||||||
globalSync.updateConfig({ provider: { [result.providerID]: result.config }, disabled_providers: nextDisabled }),
|
disabled_providers: nextDisabled,
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
dialog.close()
|
|
||||||
showToast({
|
|
||||||
variant: "success",
|
|
||||||
icon: "circle-check",
|
|
||||||
title: language.t("provider.connect.toast.connected.title", { provider: result.name }),
|
|
||||||
description: language.t("provider.connect.toast.connected.description", { provider: result.name }),
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.catch((err: unknown) => {
|
return result
|
||||||
const message = err instanceof Error ? err.message : String(err)
|
},
|
||||||
showToast({ title: language.t("common.requestFailed"), description: message })
|
onSuccess: (result) => {
|
||||||
})
|
dialog.close()
|
||||||
.finally(() => {
|
showToast({
|
||||||
setForm("saving", false)
|
variant: "success",
|
||||||
|
icon: "circle-check",
|
||||||
|
title: language.t("provider.connect.toast.connected.title", { provider: result.name }),
|
||||||
|
description: language.t("provider.connect.toast.connected.description", { provider: result.name }),
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
const message = err instanceof Error ? err.message : String(err)
|
||||||
|
showToast({ title: language.t("common.requestFailed"), description: message })
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const save = (e: SubmitEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (saveMutation.isPending) return
|
||||||
|
|
||||||
|
const result = validate()
|
||||||
|
if (!result) return
|
||||||
|
saveMutation.mutate(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -312,8 +313,14 @@ export function DialogCustomProvider(props: Props) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button class="w-auto self-start" type="submit" size="large" variant="primary" disabled={form.saving}>
|
<Button
|
||||||
{form.saving ? language.t("common.saving") : language.t("common.submit")}
|
class="w-auto self-start"
|
||||||
|
type="submit"
|
||||||
|
size="large"
|
||||||
|
variant="primary"
|
||||||
|
disabled={saveMutation.isPending}
|
||||||
|
>
|
||||||
|
{saveMutation.isPending ? language.t("common.saving") : language.t("common.submit")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Button } from "@opencode-ai/ui/button"
|
|||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
|
import { useMutation } from "@tanstack/solid-query"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { createMemo, For, Show } from "solid-js"
|
import { createMemo, For, Show } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
@@ -28,7 +29,6 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
color: props.project.icon?.color || "pink",
|
color: props.project.icon?.color || "pink",
|
||||||
iconUrl: props.project.icon?.override || "",
|
iconUrl: props.project.icon?.override || "",
|
||||||
startup: props.project.commands?.start ?? "",
|
startup: props.project.commands?.start ?? "",
|
||||||
saving: false,
|
|
||||||
dragOver: false,
|
dragOver: false,
|
||||||
iconHover: false,
|
iconHover: false,
|
||||||
})
|
})
|
||||||
@@ -71,38 +71,37 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
setStore("iconUrl", "")
|
setStore("iconUrl", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSubmit(e: SubmitEvent) {
|
const saveMutation = useMutation(() => ({
|
||||||
e.preventDefault()
|
mutationFn: async () => {
|
||||||
|
const name = store.name.trim() === folderName() ? "" : store.name.trim()
|
||||||
|
const start = store.startup.trim()
|
||||||
|
|
||||||
await Promise.resolve()
|
if (props.project.id && props.project.id !== "global") {
|
||||||
.then(async () => {
|
await globalSDK.client.project.update({
|
||||||
setStore("saving", true)
|
projectID: props.project.id,
|
||||||
const name = store.name.trim() === folderName() ? "" : store.name.trim()
|
directory: props.project.worktree,
|
||||||
const start = store.startup.trim()
|
|
||||||
|
|
||||||
if (props.project.id && props.project.id !== "global") {
|
|
||||||
await globalSDK.client.project.update({
|
|
||||||
projectID: props.project.id,
|
|
||||||
directory: props.project.worktree,
|
|
||||||
name,
|
|
||||||
icon: { color: store.color, override: store.iconUrl },
|
|
||||||
commands: { start },
|
|
||||||
})
|
|
||||||
globalSync.project.icon(props.project.worktree, store.iconUrl || undefined)
|
|
||||||
dialog.close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
globalSync.project.meta(props.project.worktree, {
|
|
||||||
name,
|
name,
|
||||||
icon: { color: store.color, override: store.iconUrl || undefined },
|
icon: { color: store.color, override: store.iconUrl },
|
||||||
commands: { start: start || undefined },
|
commands: { start },
|
||||||
})
|
})
|
||||||
|
globalSync.project.icon(props.project.worktree, store.iconUrl || undefined)
|
||||||
dialog.close()
|
dialog.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
globalSync.project.meta(props.project.worktree, {
|
||||||
|
name,
|
||||||
|
icon: { color: store.color, override: store.iconUrl || undefined },
|
||||||
|
commands: { start: start || undefined },
|
||||||
})
|
})
|
||||||
.finally(() => {
|
dialog.close()
|
||||||
setStore("saving", false)
|
},
|
||||||
})
|
}))
|
||||||
|
|
||||||
|
function handleSubmit(e: SubmitEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
if (saveMutation.isPending) return
|
||||||
|
saveMutation.mutate()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -246,8 +245,8 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
<Button type="button" variant="ghost" size="large" onClick={() => dialog.close()}>
|
<Button type="button" variant="ghost" size="large" onClick={() => dialog.close()}>
|
||||||
{language.t("common.cancel")}
|
{language.t("common.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" variant="primary" size="large" disabled={store.saving}>
|
<Button type="submit" variant="primary" size="large" disabled={saveMutation.isPending}>
|
||||||
{store.saving ? language.t("common.saving") : language.t("common.save")}
|
{saveMutation.isPending ? language.t("common.saving") : language.t("common.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Component, createMemo, createSignal, Show } from "solid-js"
|
import { useMutation } from "@tanstack/solid-query"
|
||||||
|
import { Component, createMemo, Show } from "solid-js"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
@@ -17,7 +18,6 @@ export const DialogSelectMcp: Component = () => {
|
|||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const [loading, setLoading] = createSignal<string | null>(null)
|
|
||||||
|
|
||||||
const items = createMemo(() =>
|
const items = createMemo(() =>
|
||||||
Object.entries(sync.data.mcp ?? {})
|
Object.entries(sync.data.mcp ?? {})
|
||||||
@@ -25,10 +25,8 @@ export const DialogSelectMcp: Component = () => {
|
|||||||
.sort((a, b) => a.name.localeCompare(b.name)),
|
.sort((a, b) => a.name.localeCompare(b.name)),
|
||||||
)
|
)
|
||||||
|
|
||||||
const toggle = async (name: string) => {
|
const toggle = useMutation(() => ({
|
||||||
if (loading()) return
|
mutationFn: async (name: string) => {
|
||||||
setLoading(name)
|
|
||||||
try {
|
|
||||||
const status = sync.data.mcp[name]
|
const status = sync.data.mcp[name]
|
||||||
if (status?.status === "connected") {
|
if (status?.status === "connected") {
|
||||||
await sdk.client.mcp.disconnect({ name })
|
await sdk.client.mcp.disconnect({ name })
|
||||||
@@ -38,10 +36,8 @@ export const DialogSelectMcp: Component = () => {
|
|||||||
|
|
||||||
const result = await sdk.client.mcp.status()
|
const result = await sdk.client.mcp.status()
|
||||||
if (result.data) sync.set("mcp", result.data)
|
if (result.data) sync.set("mcp", result.data)
|
||||||
} finally {
|
},
|
||||||
setLoading(null)
|
}))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length)
|
const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length)
|
||||||
const totalCount = createMemo(() => items().length)
|
const totalCount = createMemo(() => items().length)
|
||||||
@@ -59,7 +55,8 @@ export const DialogSelectMcp: Component = () => {
|
|||||||
filterKeys={["name", "status"]}
|
filterKeys={["name", "status"]}
|
||||||
sortBy={(a, b) => a.name.localeCompare(b.name)}
|
sortBy={(a, b) => a.name.localeCompare(b.name)}
|
||||||
onSelect={(x) => {
|
onSelect={(x) => {
|
||||||
if (x) toggle(x.name)
|
if (!x || toggle.isPending) return
|
||||||
|
toggle.mutate(x.name)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(i) => {
|
{(i) => {
|
||||||
@@ -83,7 +80,7 @@ export const DialogSelectMcp: Component = () => {
|
|||||||
<Show when={statusLabel()}>
|
<Show when={statusLabel()}>
|
||||||
<span class="text-11-regular text-text-weaker">{statusLabel()}</span>
|
<span class="text-11-regular text-text-weaker">{statusLabel()}</span>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={loading() === i.name}>
|
<Show when={toggle.isPending && toggle.variables === i.name}>
|
||||||
<span class="text-11-regular text-text-weak">{language.t("common.loading.ellipsis")}</span>
|
<span class="text-11-regular text-text-weak">{language.t("common.loading.ellipsis")}</span>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,7 +89,14 @@ export const DialogSelectMcp: Component = () => {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<div onClick={(e) => e.stopPropagation()}>
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
<Switch checked={enabled()} disabled={loading() === i.name} onChange={() => toggle(i.name)} />
|
<Switch
|
||||||
|
checked={enabled()}
|
||||||
|
disabled={toggle.isPending && toggle.variables === i.name}
|
||||||
|
onChange={() => {
|
||||||
|
if (toggle.isPending) return
|
||||||
|
toggle.mutate(i.name)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Icon } from "@opencode-ai/ui/icon"
|
|||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
|
import { useMutation } from "@tanstack/solid-query"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import { createEffect, createMemo, createResource, onCleanup, Show } from "solid-js"
|
import { createEffect, createMemo, createResource, onCleanup, Show } from "solid-js"
|
||||||
@@ -186,7 +187,6 @@ export function DialogSelectServer() {
|
|||||||
name: "",
|
name: "",
|
||||||
username: DEFAULT_USERNAME,
|
username: DEFAULT_USERNAME,
|
||||||
password: "",
|
password: "",
|
||||||
adding: false,
|
|
||||||
error: "",
|
error: "",
|
||||||
showForm: false,
|
showForm: false,
|
||||||
status: undefined as boolean | undefined,
|
status: undefined as boolean | undefined,
|
||||||
@@ -198,7 +198,6 @@ export function DialogSelectServer() {
|
|||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
error: "",
|
error: "",
|
||||||
busy: false,
|
|
||||||
status: undefined as boolean | undefined,
|
status: undefined as boolean | undefined,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -209,7 +208,6 @@ export function DialogSelectServer() {
|
|||||||
name: "",
|
name: "",
|
||||||
username: DEFAULT_USERNAME,
|
username: DEFAULT_USERNAME,
|
||||||
password: "",
|
password: "",
|
||||||
adding: false,
|
|
||||||
error: "",
|
error: "",
|
||||||
showForm: false,
|
showForm: false,
|
||||||
status: undefined,
|
status: undefined,
|
||||||
@@ -224,10 +222,78 @@ export function DialogSelectServer() {
|
|||||||
password: "",
|
password: "",
|
||||||
error: "",
|
error: "",
|
||||||
status: undefined,
|
status: undefined,
|
||||||
busy: false,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addMutation = useMutation(() => ({
|
||||||
|
mutationFn: async (value: string) => {
|
||||||
|
const normalized = normalizeServerUrl(value)
|
||||||
|
if (!normalized) {
|
||||||
|
resetAdd()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const conn: ServerConnection.Http = {
|
||||||
|
type: "http",
|
||||||
|
http: { url: normalized },
|
||||||
|
}
|
||||||
|
if (store.addServer.name.trim()) conn.displayName = store.addServer.name.trim()
|
||||||
|
if (store.addServer.password) conn.http.password = store.addServer.password
|
||||||
|
if (store.addServer.password && store.addServer.username) conn.http.username = store.addServer.username
|
||||||
|
const result = await checkServerHealth(conn.http)
|
||||||
|
if (!result.healthy) {
|
||||||
|
setStore("addServer", { error: language.t("dialog.server.add.error") })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resetAdd()
|
||||||
|
await select(conn, true)
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const editMutation = useMutation(() => ({
|
||||||
|
mutationFn: async (input: { original: ServerConnection.Any; value: string }) => {
|
||||||
|
if (input.original.type !== "http") return
|
||||||
|
const normalized = normalizeServerUrl(input.value)
|
||||||
|
if (!normalized) {
|
||||||
|
resetEdit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = store.editServer.name.trim() || undefined
|
||||||
|
const username = store.editServer.username || undefined
|
||||||
|
const password = store.editServer.password || undefined
|
||||||
|
const existingName = input.original.displayName
|
||||||
|
if (
|
||||||
|
normalized === input.original.http.url &&
|
||||||
|
name === existingName &&
|
||||||
|
username === input.original.http.username &&
|
||||||
|
password === input.original.http.password
|
||||||
|
) {
|
||||||
|
resetEdit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const conn: ServerConnection.Http = {
|
||||||
|
type: "http",
|
||||||
|
displayName: name,
|
||||||
|
http: { url: normalized, username, password },
|
||||||
|
}
|
||||||
|
const result = await checkServerHealth(conn.http)
|
||||||
|
if (!result.healthy) {
|
||||||
|
setStore("editServer", { error: language.t("dialog.server.add.error") })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (normalized === input.original.http.url) {
|
||||||
|
server.add(conn)
|
||||||
|
} else {
|
||||||
|
replaceServer(input.original, conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
resetEdit()
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
const replaceServer = (original: ServerConnection.Http, next: ServerConnection.Http) => {
|
const replaceServer = (original: ServerConnection.Http, next: ServerConnection.Http) => {
|
||||||
const active = server.key
|
const active = server.key
|
||||||
const newConn = server.add(next)
|
const newConn = server.add(next)
|
||||||
@@ -291,12 +357,12 @@ 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) => {
|
||||||
if (store.addServer.adding) return
|
if (addMutation.isPending) return
|
||||||
setStore("addServer", { url: value, error: "" })
|
setStore("addServer", { url: value, error: "" })
|
||||||
void previewStatus(value, store.addServer.username, store.addServer.password, (next) =>
|
void previewStatus(value, store.addServer.username, store.addServer.password, (next) =>
|
||||||
setStore("addServer", { status: next }),
|
setStore("addServer", { status: next }),
|
||||||
@@ -304,12 +370,12 @@ export function DialogSelectServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleAddNameChange = (value: string) => {
|
const handleAddNameChange = (value: string) => {
|
||||||
if (store.addServer.adding) return
|
if (addMutation.isPending) return
|
||||||
setStore("addServer", { name: value, error: "" })
|
setStore("addServer", { name: value, error: "" })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAddUsernameChange = (value: string) => {
|
const handleAddUsernameChange = (value: string) => {
|
||||||
if (store.addServer.adding) return
|
if (addMutation.isPending) return
|
||||||
setStore("addServer", { username: value, error: "" })
|
setStore("addServer", { username: value, error: "" })
|
||||||
void previewStatus(store.addServer.url, value, store.addServer.password, (next) =>
|
void previewStatus(store.addServer.url, value, store.addServer.password, (next) =>
|
||||||
setStore("addServer", { status: next }),
|
setStore("addServer", { status: next }),
|
||||||
@@ -317,7 +383,7 @@ export function DialogSelectServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleAddPasswordChange = (value: string) => {
|
const handleAddPasswordChange = (value: string) => {
|
||||||
if (store.addServer.adding) return
|
if (addMutation.isPending) return
|
||||||
setStore("addServer", { password: value, error: "" })
|
setStore("addServer", { password: value, error: "" })
|
||||||
void previewStatus(store.addServer.url, store.addServer.username, value, (next) =>
|
void previewStatus(store.addServer.url, store.addServer.username, value, (next) =>
|
||||||
setStore("addServer", { status: next }),
|
setStore("addServer", { status: next }),
|
||||||
@@ -325,7 +391,7 @@ export function DialogSelectServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleEditChange = (value: string) => {
|
const handleEditChange = (value: string) => {
|
||||||
if (store.editServer.busy) return
|
if (editMutation.isPending) return
|
||||||
setStore("editServer", { value, error: "" })
|
setStore("editServer", { value, error: "" })
|
||||||
void previewStatus(value, store.editServer.username, store.editServer.password, (next) =>
|
void previewStatus(value, store.editServer.username, store.editServer.password, (next) =>
|
||||||
setStore("editServer", { status: next }),
|
setStore("editServer", { status: next }),
|
||||||
@@ -333,12 +399,12 @@ export function DialogSelectServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleEditNameChange = (value: string) => {
|
const handleEditNameChange = (value: string) => {
|
||||||
if (store.editServer.busy) return
|
if (editMutation.isPending) return
|
||||||
setStore("editServer", { name: value, error: "" })
|
setStore("editServer", { name: value, error: "" })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEditUsernameChange = (value: string) => {
|
const handleEditUsernameChange = (value: string) => {
|
||||||
if (store.editServer.busy) return
|
if (editMutation.isPending) return
|
||||||
setStore("editServer", { username: value, error: "" })
|
setStore("editServer", { username: value, error: "" })
|
||||||
void previewStatus(store.editServer.value, value, store.editServer.password, (next) =>
|
void previewStatus(store.editServer.value, value, store.editServer.password, (next) =>
|
||||||
setStore("editServer", { status: next }),
|
setStore("editServer", { status: next }),
|
||||||
@@ -346,85 +412,13 @@ export function DialogSelectServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleEditPasswordChange = (value: string) => {
|
const handleEditPasswordChange = (value: string) => {
|
||||||
if (store.editServer.busy) return
|
if (editMutation.isPending) return
|
||||||
setStore("editServer", { password: value, error: "" })
|
setStore("editServer", { password: value, error: "" })
|
||||||
void previewStatus(store.editServer.value, store.editServer.username, value, (next) =>
|
void previewStatus(store.editServer.value, store.editServer.username, value, (next) =>
|
||||||
setStore("editServer", { status: next }),
|
setStore("editServer", { status: next }),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleAdd(value: string) {
|
|
||||||
if (store.addServer.adding) return
|
|
||||||
const normalized = normalizeServerUrl(value)
|
|
||||||
if (!normalized) {
|
|
||||||
resetAdd()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setStore("addServer", { adding: true, error: "" })
|
|
||||||
|
|
||||||
const conn: ServerConnection.Http = {
|
|
||||||
type: "http",
|
|
||||||
http: { url: normalized },
|
|
||||||
}
|
|
||||||
if (store.addServer.name.trim()) conn.displayName = store.addServer.name.trim()
|
|
||||||
if (store.addServer.password) conn.http.password = store.addServer.password
|
|
||||||
if (store.addServer.password && store.addServer.username) conn.http.username = store.addServer.username
|
|
||||||
const result = await checkServerHealth(conn.http)
|
|
||||||
setStore("addServer", { adding: false })
|
|
||||||
if (!result.healthy) {
|
|
||||||
setStore("addServer", { error: language.t("dialog.server.add.error") })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resetAdd()
|
|
||||||
await select(conn, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleEdit(original: ServerConnection.Any, value: string) {
|
|
||||||
if (store.editServer.busy || original.type !== "http") return
|
|
||||||
const normalized = normalizeServerUrl(value)
|
|
||||||
if (!normalized) {
|
|
||||||
resetEdit()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = store.editServer.name.trim() || undefined
|
|
||||||
const username = store.editServer.username || undefined
|
|
||||||
const password = store.editServer.password || undefined
|
|
||||||
const existingName = original.displayName
|
|
||||||
if (
|
|
||||||
normalized === original.http.url &&
|
|
||||||
name === existingName &&
|
|
||||||
username === original.http.username &&
|
|
||||||
password === original.http.password
|
|
||||||
) {
|
|
||||||
resetEdit()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setStore("editServer", { busy: true, error: "" })
|
|
||||||
|
|
||||||
const conn: ServerConnection.Http = {
|
|
||||||
type: "http",
|
|
||||||
displayName: name,
|
|
||||||
http: { url: normalized, username, password },
|
|
||||||
}
|
|
||||||
const result = await checkServerHealth(conn.http)
|
|
||||||
setStore("editServer", { busy: false })
|
|
||||||
if (!result.healthy) {
|
|
||||||
setStore("editServer", { error: language.t("dialog.server.add.error") })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (normalized === original.http.url) {
|
|
||||||
server.add(conn)
|
|
||||||
} else {
|
|
||||||
replaceServer(original, conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
resetEdit()
|
|
||||||
}
|
|
||||||
|
|
||||||
const mode = createMemo<"list" | "add" | "edit">(() => {
|
const mode = createMemo<"list" | "add" | "edit">(() => {
|
||||||
if (store.editServer.id) return "edit"
|
if (store.editServer.id) return "edit"
|
||||||
if (store.addServer.showForm) return "add"
|
if (store.addServer.showForm) return "add"
|
||||||
@@ -464,23 +458,26 @@ export function DialogSelectServer() {
|
|||||||
password: conn.http.password ?? "",
|
password: conn.http.password ?? "",
|
||||||
error: "",
|
error: "",
|
||||||
status: store.status[ServerConnection.key(conn)]?.healthy,
|
status: store.status[ServerConnection.key(conn)]?.healthy,
|
||||||
busy: false,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const submitForm = () => {
|
const submitForm = () => {
|
||||||
if (mode() === "add") {
|
if (mode() === "add") {
|
||||||
void handleAdd(store.addServer.url)
|
if (addMutation.isPending) return
|
||||||
|
setStore("addServer", { error: "" })
|
||||||
|
addMutation.mutate(store.addServer.url)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const original = editing()
|
const original = editing()
|
||||||
if (!original) return
|
if (!original) return
|
||||||
void handleEdit(original, store.editServer.value)
|
if (editMutation.isPending) return
|
||||||
|
setStore("editServer", { error: "" })
|
||||||
|
editMutation.mutate({ original, value: store.editServer.value })
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFormMode = createMemo(() => mode() !== "list")
|
const isFormMode = createMemo(() => mode() !== "list")
|
||||||
const isAddMode = createMemo(() => mode() === "add")
|
const isAddMode = createMemo(() => mode() === "add")
|
||||||
const formBusy = createMemo(() => (isAddMode() ? store.addServer.adding : store.editServer.busy))
|
const formBusy = createMemo(() => (isAddMode() ? addMutation.isPending : editMutation.isPending))
|
||||||
|
|
||||||
const formTitle = createMemo(() => {
|
const formTitle = createMemo(() => {
|
||||||
if (!isFormMode()) return language.t("dialog.server.title")
|
if (!isFormMode()) return language.t("dialog.server.title")
|
||||||
|
|||||||
@@ -1043,7 +1043,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const { addAttachment, removeAttachment, handlePaste } = createPromptAttachments({
|
const { addAttachments, removeAttachment, handlePaste } = createPromptAttachments({
|
||||||
editor: () => editorRef,
|
editor: () => editorRef,
|
||||||
isDialogActive: () => !!dialog.active,
|
isDialogActive: () => !!dialog.active,
|
||||||
setDraggingType: (type) => setStore("draggingType", type),
|
setDraggingType: (type) => setStore("draggingType", type),
|
||||||
@@ -1241,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1369,11 +1383,12 @@ 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) void addAttachments(Array.from(list))
|
||||||
e.currentTarget.value = ""
|
e.currentTarget.value = ""
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -71,6 +71,18 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
|||||||
|
|
||||||
const addAttachment = (file: File) => add(file)
|
const addAttachment = (file: File) => add(file)
|
||||||
|
|
||||||
|
const addAttachments = async (files: File[], toast = true) => {
|
||||||
|
let found = false
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const ok = await add(file, false)
|
||||||
|
if (ok) found = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found && files.length > 0 && toast) warn()
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
const removeAttachment = (id: string) => {
|
const removeAttachment = (id: string) => {
|
||||||
const current = prompt.current()
|
const current = prompt.current()
|
||||||
const next = current.filter((part) => part.type !== "image" || part.id !== id)
|
const next = current.filter((part) => part.type !== "image" || part.id !== id)
|
||||||
@@ -84,18 +96,14 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
|||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
const items = Array.from(clipboardData.items)
|
const files = Array.from(clipboardData.items).flatMap((item) => {
|
||||||
const fileItems = items.filter((item) => item.kind === "file")
|
if (item.kind !== "file") return []
|
||||||
|
const file = item.getAsFile()
|
||||||
|
return file ? [file] : []
|
||||||
|
})
|
||||||
|
|
||||||
if (fileItems.length > 0) {
|
if (files.length > 0) {
|
||||||
let found = false
|
await addAttachments(files)
|
||||||
for (const item of fileItems) {
|
|
||||||
const file = item.getAsFile()
|
|
||||||
if (!file) continue
|
|
||||||
const ok = await add(file, false)
|
|
||||||
if (ok) found = true
|
|
||||||
}
|
|
||||||
if (!found) warn()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,12 +177,7 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
|||||||
const dropped = event.dataTransfer?.files
|
const dropped = event.dataTransfer?.files
|
||||||
if (!dropped) return
|
if (!dropped) return
|
||||||
|
|
||||||
let found = false
|
await addAttachments(Array.from(dropped))
|
||||||
for (const file of Array.from(dropped)) {
|
|
||||||
const ok = await add(file, false)
|
|
||||||
if (ok) found = true
|
|
||||||
}
|
|
||||||
if (!found && dropped.length > 0) warn()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -191,6 +194,7 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
addAttachment,
|
addAttachment,
|
||||||
|
addAttachments,
|
||||||
removeAttachment,
|
removeAttachment,
|
||||||
handlePaste,
|
handlePaste,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,32 @@ describe("buildRequestParts", () => {
|
|||||||
expect(result.optimisticParts.every((part) => part.sessionID === "ses_1" && part.messageID === "msg_1")).toBe(true)
|
expect(result.optimisticParts.every((part) => part.sessionID === "ses_1" && part.messageID === "msg_1")).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("keeps multiple uploaded attachments in order", () => {
|
||||||
|
const result = buildRequestParts({
|
||||||
|
prompt: [{ type: "text", content: "check these", start: 0, end: 11 }],
|
||||||
|
context: [],
|
||||||
|
images: [
|
||||||
|
{ type: "image", id: "img_1", filename: "a.png", mime: "image/png", dataUrl: "data:image/png;base64,AAA" },
|
||||||
|
{
|
||||||
|
type: "image",
|
||||||
|
id: "img_2",
|
||||||
|
filename: "b.pdf",
|
||||||
|
mime: "application/pdf",
|
||||||
|
dataUrl: "data:application/pdf;base64,BBB",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
text: "check these",
|
||||||
|
messageID: "msg_multi",
|
||||||
|
sessionID: "ses_multi",
|
||||||
|
sessionDirectory: "/repo",
|
||||||
|
})
|
||||||
|
|
||||||
|
const files = result.requestParts.filter((part) => part.type === "file" && part.url.startsWith("data:"))
|
||||||
|
|
||||||
|
expect(files).toHaveLength(2)
|
||||||
|
expect(files.map((part) => (part.type === "file" ? part.filename : ""))).toEqual(["a.png", "b.pdf"])
|
||||||
|
})
|
||||||
|
|
||||||
test("deduplicates context files when prompt already includes same path", () => {
|
test("deduplicates context files when prompt already includes same path", () => {
|
||||||
const prompt: Prompt = [{ type: "file", path: "src/foo.ts", content: "@src/foo.ts", start: 0, end: 11 }]
|
const prompt: Prompt = [{ type: "file", path: "src/foo.ts", content: "@src/foo.ts", start: 0, end: 11 }]
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ describe("prompt-input history", () => {
|
|||||||
test("canNavigateHistoryAtCursor only allows prompt boundaries", () => {
|
test("canNavigateHistoryAtCursor only allows prompt boundaries", () => {
|
||||||
const value = "a\nb\nc"
|
const value = "a\nb\nc"
|
||||||
|
|
||||||
expect(canNavigateHistoryAtCursor("up", value, 0)).toBe(true)
|
expect(canNavigateHistoryAtCursor("up", value, 0)).toBe(false)
|
||||||
expect(canNavigateHistoryAtCursor("down", value, 0)).toBe(false)
|
expect(canNavigateHistoryAtCursor("down", value, 0)).toBe(false)
|
||||||
|
|
||||||
expect(canNavigateHistoryAtCursor("up", value, 2)).toBe(false)
|
expect(canNavigateHistoryAtCursor("up", value, 2)).toBe(false)
|
||||||
@@ -135,11 +135,14 @@ describe("prompt-input history", () => {
|
|||||||
expect(canNavigateHistoryAtCursor("up", value, 5)).toBe(false)
|
expect(canNavigateHistoryAtCursor("up", value, 5)).toBe(false)
|
||||||
expect(canNavigateHistoryAtCursor("down", value, 5)).toBe(true)
|
expect(canNavigateHistoryAtCursor("down", value, 5)).toBe(true)
|
||||||
|
|
||||||
expect(canNavigateHistoryAtCursor("up", "abc", 0)).toBe(true)
|
expect(canNavigateHistoryAtCursor("up", "abc", 0)).toBe(false)
|
||||||
expect(canNavigateHistoryAtCursor("down", "abc", 3)).toBe(true)
|
expect(canNavigateHistoryAtCursor("down", "abc", 3)).toBe(true)
|
||||||
expect(canNavigateHistoryAtCursor("up", "abc", 1)).toBe(false)
|
expect(canNavigateHistoryAtCursor("up", "abc", 1)).toBe(false)
|
||||||
expect(canNavigateHistoryAtCursor("down", "abc", 1)).toBe(false)
|
expect(canNavigateHistoryAtCursor("down", "abc", 1)).toBe(false)
|
||||||
|
|
||||||
|
expect(canNavigateHistoryAtCursor("up", "", 0)).toBe(true)
|
||||||
|
expect(canNavigateHistoryAtCursor("down", "", 0)).toBe(true)
|
||||||
|
|
||||||
expect(canNavigateHistoryAtCursor("up", "abc", 0, true)).toBe(true)
|
expect(canNavigateHistoryAtCursor("up", "abc", 0, true)).toBe(true)
|
||||||
expect(canNavigateHistoryAtCursor("up", "abc", 3, true)).toBe(true)
|
expect(canNavigateHistoryAtCursor("up", "abc", 3, true)).toBe(true)
|
||||||
expect(canNavigateHistoryAtCursor("down", "abc", 0, true)).toBe(true)
|
expect(canNavigateHistoryAtCursor("down", "abc", 0, true)).toBe(true)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function canNavigateHistoryAtCursor(direction: "up" | "down", text: strin
|
|||||||
const atStart = position === 0
|
const atStart = position === 0
|
||||||
const atEnd = position === text.length
|
const atEnd = position === text.length
|
||||||
if (inHistory) return atStart || atEnd
|
if (inHistory) return atStart || atEnd
|
||||||
if (direction === "up") return position === 0
|
if (direction === "up") return position === 0 && text.length === 0
|
||||||
return position === text.length
|
return position === text.length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -267,14 +267,14 @@ export function SessionContextTab() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
class="@container h-full pb-10"
|
class="@container h-full"
|
||||||
viewportRef={(el) => {
|
viewportRef={(el) => {
|
||||||
scroll = el
|
scroll = el
|
||||||
restoreScroll()
|
restoreScroll()
|
||||||
}}
|
}}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
>
|
>
|
||||||
<div class="px-6 pt-4 flex flex-col gap-10">
|
<div class="px-6 pt-4 pb-10 flex flex-col gap-10">
|
||||||
<div class="grid grid-cols-1 @[32rem]:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 @[32rem]:grid-cols-2 gap-4">
|
||||||
<For each={stats}>
|
<For each={stats}>
|
||||||
{(stat) => <Stat label={language.t(stat.label as Parameters<typeof language.t>[0])} value={stat.value()} />}
|
{(stat) => <Stat label={language.t(stat.label as Parameters<typeof language.t>[0])} value={stat.value()} />}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
|||||||
})
|
})
|
||||||
let input: HTMLInputElement | undefined
|
let input: HTMLInputElement | undefined
|
||||||
let blurFrame: number | undefined
|
let blurFrame: number | undefined
|
||||||
|
let editRequested = false
|
||||||
|
|
||||||
const isDefaultTitle = () => {
|
const isDefaultTitle = () => {
|
||||||
const number = props.terminal.titleNumber
|
const number = props.terminal.titleNumber
|
||||||
@@ -168,8 +169,14 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
|||||||
left: `${store.menuPosition.x}px`,
|
left: `${store.menuPosition.x}px`,
|
||||||
top: `${store.menuPosition.y}px`,
|
top: `${store.menuPosition.y}px`,
|
||||||
}}
|
}}
|
||||||
|
onCloseAutoFocus={(e) => {
|
||||||
|
if (!editRequested) return
|
||||||
|
e.preventDefault()
|
||||||
|
editRequested = false
|
||||||
|
requestAnimationFrame(() => edit())
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Item onSelect={edit}>
|
<DropdownMenu.Item onSelect={() => (editRequested = true)}>
|
||||||
<Icon name="edit" class="w-4 h-4 mr-2" />
|
<Icon name="edit" class="w-4 h-4 mr-2" />
|
||||||
{language.t("common.rename")}
|
{language.t("common.rename")}
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Icon } from "@opencode-ai/ui/icon"
|
|||||||
import { Popover } from "@opencode-ai/ui/popover"
|
import { Popover } from "@opencode-ai/ui/popover"
|
||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { Switch } from "@opencode-ai/ui/switch"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
|
import { useMutation } from "@tanstack/solid-query"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import { type Accessor, createEffect, createMemo, createSignal, For, type JSXElement, onCleanup, Show } from "solid-js"
|
import { type Accessor, createEffect, createMemo, createSignal, For, type JSXElement, onCleanup, Show } from "solid-js"
|
||||||
@@ -130,41 +131,30 @@ const useDefaultServerKey = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const useMcpToggle = (input: {
|
const useMcpToggleMutation = () => {
|
||||||
sync: ReturnType<typeof useSync>
|
const sync = useSync()
|
||||||
sdk: ReturnType<typeof useSDK>
|
const sdk = useSDK()
|
||||||
language: ReturnType<typeof useLanguage>
|
const language = useLanguage()
|
||||||
}) => {
|
|
||||||
const [loading, setLoading] = createSignal<string | null>(null)
|
|
||||||
|
|
||||||
const toggle = async (name: string) => {
|
return useMutation(() => ({
|
||||||
if (loading()) return
|
mutationFn: async (name: string) => {
|
||||||
setLoading(name)
|
const status = sync.data.mcp[name]
|
||||||
|
await (status?.status === "connected" ? sdk.client.mcp.disconnect({ name }) : sdk.client.mcp.connect({ name }))
|
||||||
try {
|
const result = await sdk.client.mcp.status()
|
||||||
const status = input.sync.data.mcp[name]
|
if (result.data) sync.set("mcp", result.data)
|
||||||
await (status?.status === "connected"
|
},
|
||||||
? input.sdk.client.mcp.disconnect({ name })
|
onError: (err) => {
|
||||||
: input.sdk.client.mcp.connect({ name }))
|
|
||||||
const result = await input.sdk.client.mcp.status()
|
|
||||||
if (result.data) input.sync.set("mcp", result.data)
|
|
||||||
} catch (err) {
|
|
||||||
showToast({
|
showToast({
|
||||||
variant: "error",
|
variant: "error",
|
||||||
title: input.language.t("common.requestFailed"),
|
title: language.t("common.requestFailed"),
|
||||||
description: err instanceof Error ? err.message : String(err),
|
description: err instanceof Error ? err.message : String(err),
|
||||||
})
|
})
|
||||||
} finally {
|
},
|
||||||
setLoading(null)
|
}))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { loading, toggle }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StatusPopover() {
|
export function StatusPopover() {
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const sdk = useSDK()
|
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
@@ -181,7 +171,7 @@ export function StatusPopover() {
|
|||||||
})
|
})
|
||||||
const health = useServerHealth(servers)
|
const health = useServerHealth(servers)
|
||||||
const sortedServers = createMemo(() => listServersByHealth(servers(), server.key, health))
|
const sortedServers = createMemo(() => listServersByHealth(servers(), server.key, health))
|
||||||
const mcp = useMcpToggle({ sync, sdk, language })
|
const toggleMcp = useMcpToggleMutation()
|
||||||
const defaultServer = useDefaultServerKey(platform.getDefaultServer)
|
const defaultServer = useDefaultServerKey(platform.getDefaultServer)
|
||||||
const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b)))
|
const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b)))
|
||||||
const mcpStatus = (name: string) => sync.data.mcp?.[name]?.status
|
const mcpStatus = (name: string) => sync.data.mcp?.[name]?.status
|
||||||
@@ -277,8 +267,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]} />
|
||||||
@@ -337,8 +327,11 @@ export function StatusPopover() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex items-center gap-2 w-full h-8 pl-3 pr-2 py-1 rounded-md hover:bg-surface-raised-base-hover transition-colors text-left"
|
class="flex items-center gap-2 w-full h-8 pl-3 pr-2 py-1 rounded-md hover:bg-surface-raised-base-hover transition-colors text-left"
|
||||||
onClick={() => mcp.toggle(name)}
|
onClick={() => {
|
||||||
disabled={mcp.loading() === name}
|
if (toggleMcp.isPending) return
|
||||||
|
toggleMcp.mutate(name)
|
||||||
|
}}
|
||||||
|
disabled={toggleMcp.isPending && toggleMcp.variables === name}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
@@ -354,8 +347,11 @@ export function StatusPopover() {
|
|||||||
<div onClick={(event) => event.stopPropagation()}>
|
<div onClick={(event) => event.stopPropagation()}>
|
||||||
<Switch
|
<Switch
|
||||||
checked={enabled()}
|
checked={enabled()}
|
||||||
disabled={mcp.loading() === name}
|
disabled={toggleMcp.isPending && toggleMcp.variables === name}
|
||||||
onChange={() => mcp.toggle(name)}
|
onChange={() => {
|
||||||
|
if (toggleMcp.isPending) return
|
||||||
|
toggleMcp.mutate(name)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -165,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
|
||||||
@@ -215,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 },
|
||||||
@@ -474,7 +480,7 @@ 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) => {
|
||||||
@@ -506,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
|
||||||
|
|
||||||
|
|||||||
@@ -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 }]
|
||||||
|
}
|
||||||
@@ -40,4 +40,11 @@ describe("command keybind helpers", () => {
|
|||||||
expect(display.includes("Alt") || display.includes("⌥")).toBe(true)
|
expect(display.includes("Alt") || display.includes("⌥")).toBe(true)
|
||||||
expect(formatKeybind("none")).toBe("")
|
expect(formatKeybind("none")).toBe("")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("formatKeybind prefers the first combo", () => {
|
||||||
|
const display = formatKeybind("mod+k,mod+p")
|
||||||
|
|
||||||
|
expect(display.includes("K") || display.includes("k")).toBe(true)
|
||||||
|
expect(display.includes("P") || display.includes("p")).toBe(false)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
const index = store.all.findIndex((x) => x.id === id)
|
await clone(sdk.client, id)
|
||||||
const pty = store.all[index]
|
},
|
||||||
if (!pty) return
|
bind() {
|
||||||
const clone = await sdk.client.pty
|
const client = sdk.client
|
||||||
.create({
|
return {
|
||||||
title: pty.title,
|
trim(id: string) {
|
||||||
})
|
const index = store.all.findIndex((x) => x.id === id)
|
||||||
.catch((error: unknown) => {
|
if (index === -1) return
|
||||||
console.error("Failed to clone terminal", error)
|
setStore("all", index, (pty) => trimTerminal(pty))
|
||||||
return undefined
|
},
|
||||||
})
|
update(pty: Partial<LocalPTY> & { id: string }) {
|
||||||
if (!clone?.data) return
|
update(client, pty)
|
||||||
|
},
|
||||||
const active = store.active === pty.id
|
async clone(id: string) {
|
||||||
|
await clone(client, 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...",
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ export const dict = {
|
|||||||
|
|
||||||
"command.sidebar.toggle": "Toggle sidebar",
|
"command.sidebar.toggle": "Toggle sidebar",
|
||||||
"command.project.open": "Open project",
|
"command.project.open": "Open project",
|
||||||
|
"command.project.previous": "Previous project",
|
||||||
|
"command.project.next": "Next project",
|
||||||
"command.provider.connect": "Connect provider",
|
"command.provider.connect": "Connect provider",
|
||||||
"command.server.switch": "Switch server",
|
"command.server.switch": "Switch server",
|
||||||
"command.settings.open": "Open settings",
|
"command.settings.open": "Open settings",
|
||||||
@@ -221,6 +223,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...",
|
||||||
@@ -273,7 +276,7 @@ export const dict = {
|
|||||||
"prompt.context.includeActiveFile": "Include active file",
|
"prompt.context.includeActiveFile": "Include active file",
|
||||||
"prompt.context.removeActiveFile": "Remove active file from context",
|
"prompt.context.removeActiveFile": "Remove active file from context",
|
||||||
"prompt.context.removeFile": "Remove file from context",
|
"prompt.context.removeFile": "Remove file from context",
|
||||||
"prompt.action.attachFile": "Add file",
|
"prompt.action.attachFile": "Add files",
|
||||||
"prompt.attachment.remove": "Remove attachment",
|
"prompt.attachment.remove": "Remove attachment",
|
||||||
"prompt.action.send": "Send",
|
"prompt.action.send": "Send",
|
||||||
"prompt.action.stop": "Stop",
|
"prompt.action.stop": "Stop",
|
||||||
|
|||||||
@@ -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,57 +29,53 @@ 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)
|
||||||
showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: language.t("common.requestFailed"),
|
|
||||||
description: language.t("directory.error.invalidUrl"),
|
|
||||||
})
|
|
||||||
navigate("/", { replace: true })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const current = params.dir
|
if (!directory) {
|
||||||
globalSDK
|
if (invalid === params.dir) return
|
||||||
.createClient({
|
invalid = b64Dir
|
||||||
directory: raw,
|
showToast({
|
||||||
throwOnError: true,
|
variant: "error",
|
||||||
})
|
title: language.t("common.requestFailed"),
|
||||||
.path.get()
|
description: language.t("directory.error.invalidUrl"),
|
||||||
.then((x) => {
|
|
||||||
if (params.dir !== current) return
|
|
||||||
const next = x.data?.directory ?? raw
|
|
||||||
batch(() => {
|
|
||||||
setState("invalid", "")
|
|
||||||
setState("resolved", next)
|
|
||||||
})
|
})
|
||||||
if (next === raw) return
|
navigate("/", { replace: true })
|
||||||
const path = location.pathname.slice(current.length + 1)
|
return
|
||||||
navigate(`/${base64Encode(next)}${path}${location.search}${location.hash}`, { replace: true })
|
}
|
||||||
})
|
|
||||||
.catch(() => {
|
return await globalSDK
|
||||||
if (params.dir !== current) return
|
.createClient({
|
||||||
batch(() => {
|
directory,
|
||||||
setState("invalid", "")
|
throwOnError: true,
|
||||||
setState("resolved", raw)
|
|
||||||
})
|
})
|
||||||
})
|
.path.get()
|
||||||
})
|
.then((x) => {
|
||||||
|
const next = x.data?.directory ?? directory
|
||||||
|
invalid = ""
|
||||||
|
if (next === directory) return next
|
||||||
|
const path = pathname.slice(b64Dir.length + 1)
|
||||||
|
navigate(`/${base64Encode(next)}${path}${location.search}${location.hash}`, { replace: true })
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
invalid = ""
|
||||||
|
return directory
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={state.resolved} keyed>
|
<Show when={resolved()} keyed>
|
||||||
{(resolved) => (
|
{(resolved) => (
|
||||||
<SDKProvider directory={() => resolved}>
|
<SDKProvider directory={() => resolved}>
|
||||||
<SyncProvider>
|
<SyncProvider>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -129,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"> = {
|
||||||
@@ -137,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,
|
||||||
@@ -201,13 +211,22 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const stop = () => setState("sizing", false)
|
const stop = () => setState("sizing", false)
|
||||||
|
const blur = () => reset()
|
||||||
|
const hide = () => {
|
||||||
|
if (document.visibilityState !== "hidden") return
|
||||||
|
reset()
|
||||||
|
}
|
||||||
window.addEventListener("pointerup", stop)
|
window.addEventListener("pointerup", stop)
|
||||||
window.addEventListener("pointercancel", stop)
|
window.addEventListener("pointercancel", stop)
|
||||||
window.addEventListener("blur", stop)
|
window.addEventListener("blur", stop)
|
||||||
|
window.addEventListener("blur", blur)
|
||||||
|
document.addEventListener("visibilitychange", hide)
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
window.removeEventListener("pointerup", stop)
|
window.removeEventListener("pointerup", stop)
|
||||||
window.removeEventListener("pointercancel", stop)
|
window.removeEventListener("pointercancel", stop)
|
||||||
window.removeEventListener("blur", stop)
|
window.removeEventListener("blur", stop)
|
||||||
|
window.removeEventListener("blur", blur)
|
||||||
|
document.removeEventListener("visibilitychange", hide)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -227,6 +246,12 @@ export default function Layout(props: ParentProps) {
|
|||||||
navLeave.current = undefined
|
navLeave.current = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
disarm()
|
||||||
|
setState("hoverSession", undefined)
|
||||||
|
setHoverProject(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
const arm = () => {
|
const arm = () => {
|
||||||
if (layout.sidebar.opened()) return
|
if (layout.sidebar.opened()) return
|
||||||
if (state.hoverProject === undefined) return
|
if (state.hoverProject === undefined) return
|
||||||
@@ -295,8 +320,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
const clearSidebarHoverState = () => {
|
const clearSidebarHoverState = () => {
|
||||||
if (layout.sidebar.opened()) return
|
if (layout.sidebar.opened()) return
|
||||||
setState("hoverSession", undefined)
|
reset()
|
||||||
setHoverProject(undefined)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const navigateWithSidebarReset = (href: string) => {
|
const navigateWithSidebarReset = (href: string) => {
|
||||||
@@ -484,8 +508,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)
|
||||||
|
|
||||||
@@ -543,13 +567,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 })
|
||||||
@@ -619,7 +644,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
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -630,7 +655,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)
|
||||||
@@ -682,7 +711,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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -695,7 +724,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
params.dir
|
route()
|
||||||
globalSDK.url
|
globalSDK.url
|
||||||
|
|
||||||
prefetchToken.value += 1
|
prefetchToken.value += 1
|
||||||
@@ -921,6 +950,26 @@ export default function Layout(props: ParentProps) {
|
|||||||
navigateToSession(session)
|
navigateToSession(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function navigateProjectByOffset(offset: number) {
|
||||||
|
const projects = layout.projects.list()
|
||||||
|
if (projects.length === 0) return
|
||||||
|
|
||||||
|
const current = currentProject()?.worktree
|
||||||
|
const fallback = currentDir() ? projectRoot(currentDir()) : undefined
|
||||||
|
const active = current ?? fallback
|
||||||
|
const index = active ? projects.findIndex((project) => project.worktree === active) : -1
|
||||||
|
|
||||||
|
const target =
|
||||||
|
index === -1
|
||||||
|
? offset > 0
|
||||||
|
? projects[0]
|
||||||
|
: projects[projects.length - 1]
|
||||||
|
: projects[(index + offset + projects.length) % projects.length]
|
||||||
|
if (!target) return
|
||||||
|
|
||||||
|
openProject(target.worktree)
|
||||||
|
}
|
||||||
|
|
||||||
function navigateSessionByUnseen(offset: number) {
|
function navigateSessionByUnseen(offset: number) {
|
||||||
const sessions = currentSessions()
|
const sessions = currentSessions()
|
||||||
if (sessions.length === 0) return
|
if (sessions.length === 0) return
|
||||||
@@ -987,6 +1036,20 @@ export default function Layout(props: ParentProps) {
|
|||||||
keybind: "mod+o",
|
keybind: "mod+o",
|
||||||
onSelect: () => chooseProject(),
|
onSelect: () => chooseProject(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "project.previous",
|
||||||
|
title: language.t("command.project.previous"),
|
||||||
|
category: language.t("command.category.project"),
|
||||||
|
keybind: "mod+alt+arrowup",
|
||||||
|
onSelect: () => navigateProjectByOffset(-1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "project.next",
|
||||||
|
title: language.t("command.project.next"),
|
||||||
|
category: language.t("command.category.project"),
|
||||||
|
keybind: "mod+alt+arrowdown",
|
||||||
|
onSelect: () => navigateProjectByOffset(1),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "provider.connect",
|
id: "provider.connect",
|
||||||
title: language.t("command.provider.connect"),
|
title: language.t("command.provider.connect"),
|
||||||
@@ -1155,13 +1218,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]
|
||||||
|
|
||||||
@@ -1177,13 +1244,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
|
||||||
@@ -1347,8 +1407,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]
|
||||||
|
|
||||||
@@ -1683,38 +1744,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)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -1778,8 +1852,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])
|
||||||
@@ -1902,6 +1981,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,
|
||||||
@@ -1909,6 +1989,10 @@ export default function Layout(props: ParentProps) {
|
|||||||
onProjectMouseEnter: (worktree, event) => aim.enter(worktree, event),
|
onProjectMouseEnter: (worktree, event) => aim.enter(worktree, event),
|
||||||
onProjectMouseLeave: (worktree) => aim.leave(worktree),
|
onProjectMouseLeave: (worktree) => aim.leave(worktree),
|
||||||
onProjectFocus: (worktree) => aim.activate(worktree),
|
onProjectFocus: (worktree) => aim.activate(worktree),
|
||||||
|
onHoverOpenChanged: (worktree, hoverOpen) => {
|
||||||
|
if (!hoverOpen && state.hoverProject && state.hoverProject !== worktree) return
|
||||||
|
setState("hoverProject", hoverOpen ? worktree : undefined)
|
||||||
|
},
|
||||||
navigateToProject,
|
navigateToProject,
|
||||||
openSidebar: () => layout.sidebar.open(),
|
openSidebar: () => layout.sidebar.open(),
|
||||||
closeProject,
|
closeProject,
|
||||||
@@ -2302,14 +2386,12 @@ export default function Layout(props: ParentProps) {
|
|||||||
size={layout.sidebar.width()}
|
size={layout.sidebar.width()}
|
||||||
min={244}
|
min={244}
|
||||||
max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64}
|
max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64}
|
||||||
collapseThreshold={244}
|
|
||||||
onResize={(w) => {
|
onResize={(w) => {
|
||||||
setState("sizing", true)
|
setState("sizing", true)
|
||||||
if (sizet !== undefined) clearTimeout(sizet)
|
if (sizet !== undefined) clearTimeout(sizet)
|
||||||
sizet = window.setTimeout(() => setState("sizing", false), 120)
|
sizet = window.setTimeout(() => setState("sizing", false), 120)
|
||||||
layout.sidebar.resize(w)
|
layout.sidebar.resize(w)
|
||||||
}}
|
}}
|
||||||
onCollapse={layout.sidebar.close}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -157,34 +157,45 @@ const SessionHoverPreview = (props: {
|
|||||||
messageLabel: (message: Message) => string | undefined
|
messageLabel: (message: Message) => string | undefined
|
||||||
onMessageSelect: (message: Message) => void
|
onMessageSelect: (message: Message) => void
|
||||||
trigger: JSX.Element
|
trigger: JSX.Element
|
||||||
}): JSX.Element => (
|
}): JSX.Element => {
|
||||||
<HoverCard
|
let ref: HTMLDivElement | undefined
|
||||||
openDelay={1000}
|
|
||||||
closeDelay={props.sidebarHovering() ? 600 : 0}
|
return (
|
||||||
placement="right-start"
|
<HoverCard
|
||||||
gutter={16}
|
openDelay={1000}
|
||||||
shift={-2}
|
closeDelay={props.sidebarHovering() ? 600 : 0}
|
||||||
trigger={props.trigger}
|
placement="right-start"
|
||||||
open={props.hoverSession() === props.session.id}
|
gutter={16}
|
||||||
onOpenChange={(open) => props.setHoverSession(open ? props.session.id : undefined)}
|
shift={-2}
|
||||||
>
|
trigger={<div ref={ref}>{props.trigger}</div>}
|
||||||
<Show
|
open={props.hoverSession() === props.session.id}
|
||||||
when={props.hoverReady()}
|
onOpenChange={(open) => {
|
||||||
fallback={<div class="text-12-regular text-text-weak">{props.language.t("session.messages.loading")}</div>}
|
if (!open) {
|
||||||
|
props.setHoverSession(undefined)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!ref?.matches(":hover")) return
|
||||||
|
props.setHoverSession(props.session.id)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div class="overflow-y-auto overflow-x-hidden max-h-72 h-full">
|
<Show
|
||||||
<MessageNav
|
when={props.hoverReady()}
|
||||||
messages={props.hoverMessages() ?? []}
|
fallback={<div class="text-12-regular text-text-weak">{props.language.t("session.messages.loading")}</div>}
|
||||||
current={undefined}
|
>
|
||||||
getLabel={props.messageLabel}
|
<div class="overflow-y-auto overflow-x-hidden max-h-72 h-full">
|
||||||
onMessageSelect={props.onMessageSelect}
|
<MessageNav
|
||||||
size="normal"
|
messages={props.hoverMessages() ?? []}
|
||||||
class="w-60"
|
current={undefined}
|
||||||
/>
|
getLabel={props.messageLabel}
|
||||||
</div>
|
onMessageSelect={props.onMessageSelect}
|
||||||
</Show>
|
size="normal"
|
||||||
</HoverCard>
|
class="w-60"
|
||||||
)
|
/>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</HoverCard>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -22,6 +23,7 @@ export type ProjectSidebarContext = {
|
|||||||
onProjectMouseEnter: (worktree: string, event: MouseEvent) => void
|
onProjectMouseEnter: (worktree: string, event: MouseEvent) => void
|
||||||
onProjectMouseLeave: (worktree: string) => void
|
onProjectMouseLeave: (worktree: string) => void
|
||||||
onProjectFocus: (worktree: string) => void
|
onProjectFocus: (worktree: string) => void
|
||||||
|
onHoverOpenChanged: (worktree: string, hovered: boolean) => void
|
||||||
navigateToProject: (directory: string) => void
|
navigateToProject: (directory: string) => void
|
||||||
openSidebar: () => void
|
openSidebar: () => void
|
||||||
closeProject: (directory: string) => void
|
closeProject: (directory: string) => void
|
||||||
@@ -108,8 +110,14 @@ const ProjectTile = (props: {
|
|||||||
"bg-surface-base-hover border border-border-weak-base": !props.selected() && props.active(),
|
"bg-surface-base-hover border border-border-weak-base": !props.selected() && props.active(),
|
||||||
}}
|
}}
|
||||||
onPointerDown={(event) => {
|
onPointerDown={(event) => {
|
||||||
|
if (event.button === 0 && !event.ctrlKey) {
|
||||||
|
props.setOpen(false)
|
||||||
|
props.setSuppressHover(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!props.overlay()) return
|
if (!props.overlay()) return
|
||||||
if (event.button !== 2 && !(event.button === 0 && event.ctrlKey)) return
|
if (event.button !== 2 && !(event.button === 0 && event.ctrlKey)) return
|
||||||
|
props.setOpen(false)
|
||||||
props.setSuppressHover(true)
|
props.setSuppressHover(true)
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}}
|
}}
|
||||||
@@ -129,12 +137,11 @@ const ProjectTile = (props: {
|
|||||||
props.onProjectFocus(props.project.worktree)
|
props.onProjectFocus(props.project.worktree)
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
props.setOpen(false)
|
||||||
if (props.selected()) {
|
if (props.selected()) {
|
||||||
props.setSuppressHover(true)
|
|
||||||
layout.sidebar.toggle()
|
layout.sidebar.toggle()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
props.setSuppressHover(false)
|
|
||||||
props.navigateToProject(props.project.worktree)
|
props.navigateToProject(props.project.worktree)
|
||||||
}}
|
}}
|
||||||
onBlur={() => props.setOpen(false)}
|
onBlur={() => props.setOpen(false)}
|
||||||
@@ -191,7 +198,6 @@ const ProjectPreviewPanel = (props: {
|
|||||||
projectChildren: Accessor<Map<string, string[]>>
|
projectChildren: Accessor<Map<string, string[]>>
|
||||||
workspaceSessions: (directory: string) => ReturnType<typeof sortedRootSessions>
|
workspaceSessions: (directory: string) => ReturnType<typeof sortedRootSessions>
|
||||||
workspaceChildren: (directory: string) => Map<string, string[]>
|
workspaceChildren: (directory: string) => Map<string, string[]>
|
||||||
setOpen: (value: boolean) => void
|
|
||||||
ctx: ProjectSidebarContext
|
ctx: ProjectSidebarContext
|
||||||
language: ReturnType<typeof useLanguage>
|
language: ReturnType<typeof useLanguage>
|
||||||
}): JSX.Element => (
|
}): JSX.Element => (
|
||||||
@@ -258,7 +264,7 @@ const ProjectPreviewPanel = (props: {
|
|||||||
class="flex w-full text-left justify-start text-text-base px-2 hover:bg-transparent active:bg-transparent"
|
class="flex w-full text-left justify-start text-text-base px-2 hover:bg-transparent active:bg-transparent"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.ctx.openSidebar()
|
props.ctx.openSidebar()
|
||||||
props.setOpen(false)
|
props.ctx.onHoverOpenChanged(props.project.worktree, false)
|
||||||
if (props.selected()) return
|
if (props.selected()) return
|
||||||
props.ctx.navigateToProject(props.project.worktree)
|
props.ctx.navigateToProject(props.project.worktree)
|
||||||
}}
|
}}
|
||||||
@@ -278,37 +284,21 @@ 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))
|
||||||
const [state, setState] = createStore({
|
const [state, setState] = createStore({
|
||||||
open: false,
|
|
||||||
menu: false,
|
menu: false,
|
||||||
suppressHover: false,
|
suppressHover: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isHoverProject = () => props.ctx.hoverProject() === props.project.worktree
|
||||||
const preview = createMemo(() => !props.mobile && props.ctx.sidebarOpened())
|
const preview = createMemo(() => !props.mobile && props.ctx.sidebarOpened())
|
||||||
const overlay = createMemo(() => !props.mobile && !props.ctx.sidebarOpened())
|
const overlay = createMemo(() => !props.mobile && !props.ctx.sidebarOpened())
|
||||||
const active = createMemo(
|
const active = createMemo(() => state.menu || (preview() ? isHoverProject() : overlay() && isHoverProject()))
|
||||||
() => state.menu || (preview() ? state.open : overlay() && props.ctx.hoverProject() === props.project.worktree),
|
|
||||||
)
|
|
||||||
|
|
||||||
createEffect(() => {
|
const hoverOpen = () => isHoverProject() && preview() && !selected() && !state.menu
|
||||||
if (preview()) return
|
|
||||||
if (!state.open) return
|
|
||||||
setState("open", false)
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (!selected()) return
|
|
||||||
if (!state.open) return
|
|
||||||
setState("open", false)
|
|
||||||
})
|
|
||||||
|
|
||||||
const label = (directory: string) => {
|
const label = (directory: string) => {
|
||||||
const [data] = globalSync.child(directory, { bootstrap: false })
|
const [data] = globalSync.child(directory, { bootstrap: false })
|
||||||
@@ -349,7 +339,7 @@ export const SortableProject = (props: {
|
|||||||
workspacesEnabled={props.ctx.workspacesEnabled}
|
workspacesEnabled={props.ctx.workspacesEnabled}
|
||||||
closeProject={props.ctx.closeProject}
|
closeProject={props.ctx.closeProject}
|
||||||
setMenu={(value) => setState("menu", value)}
|
setMenu={(value) => setState("menu", value)}
|
||||||
setOpen={(value) => setState("open", value)}
|
setOpen={(value) => props.ctx.onHoverOpenChanged(props.project.worktree, value)}
|
||||||
setSuppressHover={(value) => setState("suppressHover", value)}
|
setSuppressHover={(value) => setState("suppressHover", value)}
|
||||||
language={language}
|
language={language}
|
||||||
/>
|
/>
|
||||||
@@ -360,7 +350,7 @@ export const SortableProject = (props: {
|
|||||||
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
|
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
|
||||||
<Show when={preview() && !selected()} fallback={tile()}>
|
<Show when={preview() && !selected()} fallback={tile()}>
|
||||||
<HoverCard
|
<HoverCard
|
||||||
open={!state.suppressHover && state.open && !state.menu}
|
open={!state.suppressHover && hoverOpen() && !state.menu}
|
||||||
openDelay={0}
|
openDelay={0}
|
||||||
closeDelay={0}
|
closeDelay={0}
|
||||||
placement="right-start"
|
placement="right-start"
|
||||||
@@ -369,7 +359,7 @@ export const SortableProject = (props: {
|
|||||||
onOpenChange={(value) => {
|
onOpenChange={(value) => {
|
||||||
if (state.menu) return
|
if (state.menu) return
|
||||||
if (value && state.suppressHover) return
|
if (value && state.suppressHover) return
|
||||||
setState("open", value)
|
props.ctx.onHoverOpenChanged(props.project.worktree, value)
|
||||||
if (value) props.ctx.setHoverSession(undefined)
|
if (value) props.ctx.setHoverSession(undefined)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -384,7 +374,6 @@ export const SortableProject = (props: {
|
|||||||
projectChildren={projectChildren}
|
projectChildren={projectChildren}
|
||||||
workspaceSessions={workspaceSessions}
|
workspaceSessions={workspaceSessions}
|
||||||
workspaceChildren={workspaceChildren}
|
workspaceChildren={workspaceChildren}
|
||||||
setOpen={(value) => setState("open", value)}
|
|
||||||
ctx={props.ctx}
|
ctx={props.ctx}
|
||||||
language={language}
|
language={language}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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)
|
||||||
|
|||||||
+129
-121
@@ -1,5 +1,6 @@
|
|||||||
import type { Project, UserMessage } from "@opencode-ai/sdk/v2"
|
import type { Project, UserMessage } from "@opencode-ai/sdk/v2"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
|
import { useMutation } from "@tanstack/solid-query"
|
||||||
import {
|
import {
|
||||||
batch,
|
batch,
|
||||||
onCleanup,
|
onCleanup,
|
||||||
@@ -327,10 +328,7 @@ export default function Page() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const [ui, setUi] = createStore({
|
const [ui, setUi] = createStore({
|
||||||
git: false,
|
|
||||||
pendingMessage: undefined as string | undefined,
|
pendingMessage: undefined as string | undefined,
|
||||||
restoring: undefined as string | undefined,
|
|
||||||
reverting: false,
|
|
||||||
reviewSnap: false,
|
reviewSnap: false,
|
||||||
scrollGesture: 0,
|
scrollGesture: 0,
|
||||||
scroll: {
|
scroll: {
|
||||||
@@ -506,7 +504,6 @@ export default function Page() {
|
|||||||
|
|
||||||
const [followup, setFollowup] = createStore({
|
const [followup, setFollowup] = createStore({
|
||||||
items: {} as Record<string, (FollowupDraft & { id: string })[] | undefined>,
|
items: {} as Record<string, (FollowupDraft & { id: string })[] | undefined>,
|
||||||
sending: {} as Record<string, string | undefined>,
|
|
||||||
failed: {} as Record<string, string | undefined>,
|
failed: {} as Record<string, string | undefined>,
|
||||||
paused: {} as Record<string, boolean | undefined>,
|
paused: {} as Record<string, boolean | undefined>,
|
||||||
edit: {} as Record<
|
edit: {} as Record<
|
||||||
@@ -644,25 +641,24 @@ export default function Page() {
|
|||||||
globalSync.set("project", [...list, next])
|
globalSync.set("project", [...list, next])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const gitMutation = useMutation(() => ({
|
||||||
|
mutationFn: () => sdk.client.project.initGit(),
|
||||||
|
onSuccess: (x) => {
|
||||||
|
if (!x.data) return
|
||||||
|
upsert(x.data)
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: language.t("common.requestFailed"),
|
||||||
|
description: formatServerError(err, language.t),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
function initGit() {
|
function initGit() {
|
||||||
if (ui.git) return
|
if (gitMutation.isPending) return
|
||||||
setUi("git", true)
|
gitMutation.mutate()
|
||||||
void sdk.client.project
|
|
||||||
.initGit()
|
|
||||||
.then((x) => {
|
|
||||||
if (!x.data) return
|
|
||||||
upsert(x.data)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: language.t("common.requestFailed"),
|
|
||||||
description: formatServerError(err, language.t),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setUi("git", false)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let inputRef!: HTMLDivElement
|
let inputRef!: HTMLDivElement
|
||||||
@@ -961,8 +957,8 @@ export default function Page() {
|
|||||||
{language.t("session.review.noVcs.createGit.description")}
|
{language.t("session.review.noVcs.createGit.description")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button size="large" disabled={ui.git} onClick={initGit}>
|
<Button size="large" disabled={gitMutation.isPending} onClick={initGit}>
|
||||||
{ui.git
|
{gitMutation.isPending
|
||||||
? language.t("session.review.noVcs.createGit.actionLoading")
|
? language.t("session.review.noVcs.createGit.actionLoading")
|
||||||
: language.t("session.review.noVcs.createGit.action")}
|
: language.t("session.review.noVcs.createGit.action")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1379,10 +1375,40 @@ export default function Page() {
|
|||||||
return followup.edit[id]
|
return followup.edit[id]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const followupMutation = useMutation(() => ({
|
||||||
|
mutationFn: async (input: { sessionID: string; id: string; manual?: boolean }) => {
|
||||||
|
const item = (followup.items[input.sessionID] ?? []).find((entry) => entry.id === input.id)
|
||||||
|
if (!item) return
|
||||||
|
|
||||||
|
if (input.manual) setFollowup("paused", input.sessionID, undefined)
|
||||||
|
setFollowup("failed", input.sessionID, undefined)
|
||||||
|
|
||||||
|
const ok = await sendFollowupDraft({
|
||||||
|
client: sdk.client,
|
||||||
|
sync,
|
||||||
|
globalSync,
|
||||||
|
draft: item,
|
||||||
|
optimisticBusy: item.sessionDirectory === sdk.directory,
|
||||||
|
}).catch((err) => {
|
||||||
|
setFollowup("failed", input.sessionID, input.id)
|
||||||
|
fail(err)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
if (!ok) return
|
||||||
|
|
||||||
|
setFollowup("items", input.sessionID, (items) => (items ?? []).filter((entry) => entry.id !== input.id))
|
||||||
|
if (input.manual) resumeScroll()
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const followupBusy = (sessionID: string) =>
|
||||||
|
followupMutation.isPending && followupMutation.variables?.sessionID === sessionID
|
||||||
|
|
||||||
const sendingFollowup = createMemo(() => {
|
const sendingFollowup = createMemo(() => {
|
||||||
const id = params.id
|
const id = params.id
|
||||||
if (!id) return
|
if (!id) return
|
||||||
return followup.sending[id]
|
if (!followupBusy(id)) return
|
||||||
|
return followupMutation.variables?.id
|
||||||
})
|
})
|
||||||
|
|
||||||
const queueEnabled = createMemo(() => {
|
const queueEnabled = createMemo(() => {
|
||||||
@@ -1422,37 +1448,15 @@ export default function Page() {
|
|||||||
const sendFollowup = (sessionID: string, id: string, opts?: { manual?: boolean }) => {
|
const sendFollowup = (sessionID: string, id: string, opts?: { manual?: boolean }) => {
|
||||||
const item = (followup.items[sessionID] ?? []).find((entry) => entry.id === id)
|
const item = (followup.items[sessionID] ?? []).find((entry) => entry.id === id)
|
||||||
if (!item) return Promise.resolve()
|
if (!item) return Promise.resolve()
|
||||||
if (followup.sending[sessionID]) return Promise.resolve()
|
if (followupBusy(sessionID)) return Promise.resolve()
|
||||||
|
|
||||||
if (opts?.manual) setFollowup("paused", sessionID, undefined)
|
return followupMutation.mutateAsync({ sessionID, id, manual: opts?.manual })
|
||||||
setFollowup("sending", sessionID, id)
|
|
||||||
setFollowup("failed", sessionID, undefined)
|
|
||||||
|
|
||||||
return sendFollowupDraft({
|
|
||||||
client: sdk.client,
|
|
||||||
sync,
|
|
||||||
globalSync,
|
|
||||||
draft: item,
|
|
||||||
optimisticBusy: item.sessionDirectory === sdk.directory,
|
|
||||||
})
|
|
||||||
.then((ok) => {
|
|
||||||
if (ok === false) return
|
|
||||||
setFollowup("items", sessionID, (items) => (items ?? []).filter((entry) => entry.id !== id))
|
|
||||||
if (opts?.manual) resumeScroll()
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setFollowup("failed", sessionID, id)
|
|
||||||
fail(err)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setFollowup("sending", sessionID, (value) => (value === id ? undefined : value))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const editFollowup = (id: string) => {
|
const editFollowup = (id: string) => {
|
||||||
const sessionID = params.id
|
const sessionID = params.id
|
||||||
if (!sessionID) return
|
if (!sessionID) return
|
||||||
if (followup.sending[sessionID]) return
|
if (followupBusy(sessionID)) return
|
||||||
|
|
||||||
const item = queuedFollowups().find((entry) => entry.id === id)
|
const item = queuedFollowups().find((entry) => entry.id === id)
|
||||||
if (!item) return
|
if (!item) return
|
||||||
@@ -1475,6 +1479,74 @@ export default function Page() {
|
|||||||
const halt = (sessionID: string) =>
|
const halt = (sessionID: string) =>
|
||||||
busy(sessionID) ? sdk.client.session.abort({ sessionID }).catch(() => {}) : Promise.resolve()
|
busy(sessionID) ? sdk.client.session.abort({ sessionID }).catch(() => {}) : Promise.resolve()
|
||||||
|
|
||||||
|
const revertMutation = useMutation(() => ({
|
||||||
|
mutationFn: async (input: { sessionID: string; messageID: string }) => {
|
||||||
|
const prev = prompt.current().slice()
|
||||||
|
const last = info()?.revert
|
||||||
|
const value = draft(input.messageID)
|
||||||
|
batch(() => {
|
||||||
|
roll(input.sessionID, { messageID: input.messageID })
|
||||||
|
prompt.set(value)
|
||||||
|
})
|
||||||
|
await halt(input.sessionID)
|
||||||
|
.then(() => sdk.client.session.revert(input))
|
||||||
|
.then((result) => {
|
||||||
|
if (result.data) merge(result.data)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
batch(() => {
|
||||||
|
roll(input.sessionID, last)
|
||||||
|
prompt.set(prev)
|
||||||
|
})
|
||||||
|
fail(err)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const restoreMutation = useMutation(() => ({
|
||||||
|
mutationFn: async (id: string) => {
|
||||||
|
const sessionID = params.id
|
||||||
|
if (!sessionID) return
|
||||||
|
|
||||||
|
const next = userMessages().find((item) => item.id > id)
|
||||||
|
const prev = prompt.current().slice()
|
||||||
|
const last = info()?.revert
|
||||||
|
|
||||||
|
batch(() => {
|
||||||
|
roll(sessionID, next ? { messageID: next.id } : undefined)
|
||||||
|
if (next) {
|
||||||
|
prompt.set(draft(next.id))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
prompt.reset()
|
||||||
|
})
|
||||||
|
|
||||||
|
const task = !next
|
||||||
|
? halt(sessionID).then(() => sdk.client.session.unrevert({ sessionID }))
|
||||||
|
: halt(sessionID).then(() =>
|
||||||
|
sdk.client.session.revert({
|
||||||
|
sessionID,
|
||||||
|
messageID: next.id,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await task
|
||||||
|
.then((result) => {
|
||||||
|
if (result.data) merge(result.data)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
batch(() => {
|
||||||
|
roll(sessionID, last)
|
||||||
|
prompt.set(prev)
|
||||||
|
})
|
||||||
|
fail(err)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const reverting = createMemo(() => revertMutation.isPending || restoreMutation.isPending)
|
||||||
|
const restoring = createMemo(() => (restoreMutation.isPending ? restoreMutation.variables : undefined))
|
||||||
|
|
||||||
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)
|
const dir = base64Encode(sdk.directory)
|
||||||
@@ -1496,77 +1568,13 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const revert = (input: { sessionID: string; messageID: string }) => {
|
const revert = (input: { sessionID: string; messageID: string }) => {
|
||||||
if (ui.reverting || ui.restoring) return
|
if (reverting()) return
|
||||||
const prev = prompt.current().slice()
|
return revertMutation.mutateAsync(input)
|
||||||
const last = info()?.revert
|
|
||||||
const value = draft(input.messageID)
|
|
||||||
batch(() => {
|
|
||||||
setUi("reverting", true)
|
|
||||||
roll(input.sessionID, { messageID: input.messageID })
|
|
||||||
prompt.set(value)
|
|
||||||
})
|
|
||||||
return halt(input.sessionID)
|
|
||||||
.then(() => sdk.client.session.revert(input))
|
|
||||||
.then((result) => {
|
|
||||||
if (result.data) merge(result.data)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
batch(() => {
|
|
||||||
roll(input.sessionID, last)
|
|
||||||
prompt.set(prev)
|
|
||||||
})
|
|
||||||
fail(err)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setUi("reverting", false)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const restore = (id: string) => {
|
const restore = (id: string) => {
|
||||||
const sessionID = params.id
|
if (!params.id || reverting()) return
|
||||||
if (!sessionID || ui.restoring || ui.reverting) return
|
return restoreMutation.mutateAsync(id)
|
||||||
|
|
||||||
const next = userMessages().find((item) => item.id > id)
|
|
||||||
const prev = prompt.current().slice()
|
|
||||||
const last = info()?.revert
|
|
||||||
|
|
||||||
batch(() => {
|
|
||||||
setUi("restoring", id)
|
|
||||||
setUi("reverting", true)
|
|
||||||
roll(sessionID, next ? { messageID: next.id } : undefined)
|
|
||||||
if (next) {
|
|
||||||
prompt.set(draft(next.id))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
prompt.reset()
|
|
||||||
})
|
|
||||||
|
|
||||||
const task = !next
|
|
||||||
? halt(sessionID).then(() => sdk.client.session.unrevert({ sessionID }))
|
|
||||||
: halt(sessionID).then(() =>
|
|
||||||
sdk.client.session.revert({
|
|
||||||
sessionID,
|
|
||||||
messageID: next.id,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
return task
|
|
||||||
.then((result) => {
|
|
||||||
if (result.data) merge(result.data)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
batch(() => {
|
|
||||||
roll(sessionID, last)
|
|
||||||
prompt.set(prev)
|
|
||||||
})
|
|
||||||
fail(err)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
batch(() => {
|
|
||||||
setUi("restoring", (value) => (value === id ? undefined : value))
|
|
||||||
setUi("reverting", false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const rolled = createMemo(() => {
|
const rolled = createMemo(() => {
|
||||||
@@ -1585,7 +1593,7 @@ export default function Page() {
|
|||||||
|
|
||||||
const item = queuedFollowups()[0]
|
const item = queuedFollowups()[0]
|
||||||
if (!item) return
|
if (!item) return
|
||||||
if (followup.sending[sessionID]) return
|
if (followupBusy(sessionID)) return
|
||||||
if (followup.failed[sessionID] === item.id) return
|
if (followup.failed[sessionID] === item.id) return
|
||||||
if (followup.paused[sessionID]) return
|
if (followup.paused[sessionID]) return
|
||||||
if (composer.blocked()) return
|
if (composer.blocked()) return
|
||||||
@@ -1780,8 +1788,8 @@ export default function Page() {
|
|||||||
rolled().length > 0
|
rolled().length > 0
|
||||||
? {
|
? {
|
||||||
items: rolled(),
|
items: rolled(),
|
||||||
restoring: ui.restoring,
|
restoring: restoring(),
|
||||||
disabled: ui.reverting,
|
disabled: reverting(),
|
||||||
onRestore: restore,
|
onRestore: restore,
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { For, Show, createMemo, onCleanup, onMount, type Component } from "solid-js"
|
import { For, Show, createMemo, onCleanup, onMount, type Component } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
|
import { useMutation } from "@tanstack/solid-query"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { DockPrompt } from "@opencode-ai/ui/dock-prompt"
|
import { DockPrompt } from "@opencode-ai/ui/dock-prompt"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
@@ -24,7 +25,6 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
custom: cached?.custom ?? ([] as string[]),
|
custom: cached?.custom ?? ([] as string[]),
|
||||||
customOn: cached?.customOn ?? ([] as boolean[]),
|
customOn: cached?.customOn ?? ([] as boolean[]),
|
||||||
editing: false,
|
editing: false,
|
||||||
sending: false,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
let root: HTMLDivElement | undefined
|
let root: HTMLDivElement | undefined
|
||||||
@@ -126,36 +126,40 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
showToast({ title: language.t("common.requestFailed"), description: message })
|
showToast({ title: language.t("common.requestFailed"), description: message })
|
||||||
}
|
}
|
||||||
|
|
||||||
const reply = async (answers: QuestionAnswer[]) => {
|
const replyMutation = useMutation(() => ({
|
||||||
if (store.sending) return
|
mutationFn: (answers: QuestionAnswer[]) => sdk.client.question.reply({ requestID: props.request.id, answers }),
|
||||||
|
onMutate: () => {
|
||||||
props.onSubmit()
|
props.onSubmit()
|
||||||
setStore("sending", true)
|
},
|
||||||
try {
|
onSuccess: () => {
|
||||||
await sdk.client.question.reply({ requestID: props.request.id, answers })
|
|
||||||
replied = true
|
replied = true
|
||||||
cache.delete(props.request.id)
|
cache.delete(props.request.id)
|
||||||
} catch (err) {
|
},
|
||||||
fail(err)
|
onError: fail,
|
||||||
} finally {
|
}))
|
||||||
setStore("sending", false)
|
|
||||||
}
|
const rejectMutation = useMutation(() => ({
|
||||||
|
mutationFn: () => sdk.client.question.reject({ requestID: props.request.id }),
|
||||||
|
onMutate: () => {
|
||||||
|
props.onSubmit()
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
replied = true
|
||||||
|
cache.delete(props.request.id)
|
||||||
|
},
|
||||||
|
onError: fail,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const sending = createMemo(() => replyMutation.isPending || rejectMutation.isPending)
|
||||||
|
|
||||||
|
const reply = async (answers: QuestionAnswer[]) => {
|
||||||
|
if (sending()) return
|
||||||
|
await replyMutation.mutateAsync(answers)
|
||||||
}
|
}
|
||||||
|
|
||||||
const reject = async () => {
|
const reject = async () => {
|
||||||
if (store.sending) return
|
if (sending()) return
|
||||||
|
await rejectMutation.mutateAsync()
|
||||||
props.onSubmit()
|
|
||||||
setStore("sending", true)
|
|
||||||
try {
|
|
||||||
await sdk.client.question.reject({ requestID: props.request.id })
|
|
||||||
replied = true
|
|
||||||
cache.delete(props.request.id)
|
|
||||||
} catch (err) {
|
|
||||||
fail(err)
|
|
||||||
} finally {
|
|
||||||
setStore("sending", false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const submit = () => void reply(questions().map((_, i) => store.answers[i] ?? []))
|
const submit = () => void reply(questions().map((_, i) => store.answers[i] ?? []))
|
||||||
@@ -175,7 +179,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
}
|
}
|
||||||
|
|
||||||
const customToggle = () => {
|
const customToggle = () => {
|
||||||
if (store.sending) return
|
if (sending()) return
|
||||||
|
|
||||||
if (!multi()) {
|
if (!multi()) {
|
||||||
setStore("customOn", store.tab, true)
|
setStore("customOn", store.tab, true)
|
||||||
@@ -198,14 +202,14 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
}
|
}
|
||||||
|
|
||||||
const customOpen = () => {
|
const customOpen = () => {
|
||||||
if (store.sending) return
|
if (sending()) return
|
||||||
if (!on()) setStore("customOn", store.tab, true)
|
if (!on()) setStore("customOn", store.tab, true)
|
||||||
setStore("editing", true)
|
setStore("editing", true)
|
||||||
customUpdate(input(), true)
|
customUpdate(input(), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectOption = (optIndex: number) => {
|
const selectOption = (optIndex: number) => {
|
||||||
if (store.sending) return
|
if (sending()) return
|
||||||
|
|
||||||
if (optIndex === options().length) {
|
if (optIndex === options().length) {
|
||||||
customOpen()
|
customOpen()
|
||||||
@@ -227,7 +231,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
}
|
}
|
||||||
|
|
||||||
const next = () => {
|
const next = () => {
|
||||||
if (store.sending) return
|
if (sending()) return
|
||||||
if (store.editing) commitCustom()
|
if (store.editing) commitCustom()
|
||||||
|
|
||||||
if (store.tab >= total() - 1) {
|
if (store.tab >= total() - 1) {
|
||||||
@@ -240,14 +244,14 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
}
|
}
|
||||||
|
|
||||||
const back = () => {
|
const back = () => {
|
||||||
if (store.sending) return
|
if (sending()) return
|
||||||
if (store.tab <= 0) return
|
if (store.tab <= 0) return
|
||||||
setStore("tab", store.tab - 1)
|
setStore("tab", store.tab - 1)
|
||||||
setStore("editing", false)
|
setStore("editing", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const jump = (tab: number) => {
|
const jump = (tab: number) => {
|
||||||
if (store.sending) return
|
if (sending()) return
|
||||||
setStore("tab", tab)
|
setStore("tab", tab)
|
||||||
setStore("editing", false)
|
setStore("editing", false)
|
||||||
}
|
}
|
||||||
@@ -270,7 +274,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
(store.answers[i()]?.length ?? 0) > 0 ||
|
(store.answers[i()]?.length ?? 0) > 0 ||
|
||||||
(store.customOn[i()] === true && (store.custom[i()] ?? "").trim().length > 0)
|
(store.customOn[i()] === true && (store.custom[i()] ?? "").trim().length > 0)
|
||||||
}
|
}
|
||||||
disabled={store.sending}
|
disabled={sending()}
|
||||||
onClick={() => jump(i())}
|
onClick={() => jump(i())}
|
||||||
aria-label={`${language.t("ui.tool.questions")} ${i() + 1}`}
|
aria-label={`${language.t("ui.tool.questions")} ${i() + 1}`}
|
||||||
/>
|
/>
|
||||||
@@ -281,16 +285,16 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
}
|
}
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
<Button variant="ghost" size="large" disabled={store.sending} onClick={reject}>
|
<Button variant="ghost" size="large" disabled={sending()} onClick={reject}>
|
||||||
{language.t("ui.common.dismiss")}
|
{language.t("ui.common.dismiss")}
|
||||||
</Button>
|
</Button>
|
||||||
<div data-slot="question-footer-actions">
|
<div data-slot="question-footer-actions">
|
||||||
<Show when={store.tab > 0}>
|
<Show when={store.tab > 0}>
|
||||||
<Button variant="secondary" size="large" disabled={store.sending} onClick={back}>
|
<Button variant="secondary" size="large" disabled={sending()} onClick={back}>
|
||||||
{language.t("ui.common.back")}
|
{language.t("ui.common.back")}
|
||||||
</Button>
|
</Button>
|
||||||
</Show>
|
</Show>
|
||||||
<Button variant={last() ? "primary" : "secondary"} size="large" disabled={store.sending} onClick={next}>
|
<Button variant={last() ? "primary" : "secondary"} size="large" disabled={sending()} onClick={next}>
|
||||||
{last() ? language.t("ui.common.submit") : language.t("ui.common.next")}
|
{last() ? language.t("ui.common.submit") : language.t("ui.common.next")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -311,7 +315,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
data-picked={picked()}
|
data-picked={picked()}
|
||||||
role={multi() ? "checkbox" : "radio"}
|
role={multi() ? "checkbox" : "radio"}
|
||||||
aria-checked={picked()}
|
aria-checked={picked()}
|
||||||
disabled={store.sending}
|
disabled={sending()}
|
||||||
onClick={() => selectOption(i())}
|
onClick={() => selectOption(i())}
|
||||||
>
|
>
|
||||||
<span data-slot="question-option-check" aria-hidden="true">
|
<span data-slot="question-option-check" aria-hidden="true">
|
||||||
@@ -345,7 +349,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
data-picked={on()}
|
data-picked={on()}
|
||||||
role={multi() ? "checkbox" : "radio"}
|
role={multi() ? "checkbox" : "radio"}
|
||||||
aria-checked={on()}
|
aria-checked={on()}
|
||||||
disabled={store.sending}
|
disabled={sending()}
|
||||||
onClick={customOpen}
|
onClick={customOpen}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -377,7 +381,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
role={multi() ? "checkbox" : "radio"}
|
role={multi() ? "checkbox" : "radio"}
|
||||||
aria-checked={on()}
|
aria-checked={on()}
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
if (store.sending) {
|
if (sending()) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -419,7 +423,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
placeholder={language.t("ui.question.custom.placeholder")}
|
placeholder={language.t("ui.question.custom.placeholder")}
|
||||||
value={input()}
|
value={input()}
|
||||||
rows={1}
|
rows={1}
|
||||||
disabled={store.sending}
|
disabled={sending()}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { For, createEffect, createMemo, on, onCleanup, Show, Index, type JSX } from "solid-js"
|
import { For, createEffect, createMemo, on, onCleanup, Show, Index, type JSX, createSignal } from "solid-js"
|
||||||
import { createStore, produce } from "solid-js/store"
|
import { createStore, produce } from "solid-js/store"
|
||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate } from "@solidjs/router"
|
||||||
|
import { useMutation } from "@tanstack/solid-query"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
@@ -29,6 +30,7 @@ import { useSDK } from "@/context/sdk"
|
|||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { messageAgentColor } from "@/utils/agent"
|
import { messageAgentColor } from "@/utils/agent"
|
||||||
import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note"
|
import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note"
|
||||||
|
import { makeTimer } from "@solid-primitives/timer"
|
||||||
|
|
||||||
type MessageComment = {
|
type MessageComment = {
|
||||||
path: string
|
path: string
|
||||||
@@ -249,38 +251,21 @@ export function MessageTimeline(props: {
|
|||||||
const working = createMemo(() => !!pending() || sessionStatus().type !== "idle")
|
const working = createMemo(() => !!pending() || sessionStatus().type !== "idle")
|
||||||
const tint = createMemo(() => messageAgentColor(sessionMessages(), sync.data.agent))
|
const tint = createMemo(() => messageAgentColor(sessionMessages(), sync.data.agent))
|
||||||
|
|
||||||
const [slot, setSlot] = createStore({
|
const [timeoutDone, setTimeoutDone] = createSignal(true)
|
||||||
open: false,
|
|
||||||
show: false,
|
const workingStatus = createMemo<"hidden" | "showing" | "hiding">((prev) => {
|
||||||
fade: false,
|
if (working()) return "showing"
|
||||||
|
if (prev === "showing" || !timeoutDone()) return "hiding"
|
||||||
|
return "hidden"
|
||||||
})
|
})
|
||||||
|
|
||||||
let f: number | undefined
|
createEffect(() => {
|
||||||
const clear = () => {
|
if (workingStatus() !== "hiding") return
|
||||||
if (f !== undefined) window.clearTimeout(f)
|
|
||||||
f = undefined
|
setTimeoutDone(false)
|
||||||
}
|
makeTimer(() => setTimeoutDone(true), 260, setTimeout)
|
||||||
|
})
|
||||||
|
|
||||||
onCleanup(clear)
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
working,
|
|
||||||
(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 },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
const activeMessageID = createMemo(() => {
|
const activeMessageID = createMemo(() => {
|
||||||
const parentID = pending()?.parentID
|
const parentID = pending()?.parentID
|
||||||
if (parentID) {
|
if (parentID) {
|
||||||
@@ -321,7 +306,6 @@ export function MessageTimeline(props: {
|
|||||||
const [title, setTitle] = createStore({
|
const [title, setTitle] = createStore({
|
||||||
draft: "",
|
draft: "",
|
||||||
editing: false,
|
editing: false,
|
||||||
saving: false,
|
|
||||||
menuOpen: false,
|
menuOpen: false,
|
||||||
pendingRename: false,
|
pendingRename: false,
|
||||||
pendingShare: false,
|
pendingShare: false,
|
||||||
@@ -335,38 +319,6 @@ export function MessageTimeline(props: {
|
|||||||
|
|
||||||
let more: HTMLButtonElement | undefined
|
let more: HTMLButtonElement | undefined
|
||||||
|
|
||||||
const [req, setReq] = createStore({ share: false, unshare: false })
|
|
||||||
|
|
||||||
const shareSession = () => {
|
|
||||||
const id = sessionID()
|
|
||||||
if (!id || req.share) return
|
|
||||||
if (!shareEnabled()) return
|
|
||||||
setReq("share", true)
|
|
||||||
globalSDK.client.session
|
|
||||||
.share({ sessionID: id, directory: sdk.directory })
|
|
||||||
.catch((err: unknown) => {
|
|
||||||
console.error("Failed to share session", err)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setReq("share", false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const unshareSession = () => {
|
|
||||||
const id = sessionID()
|
|
||||||
if (!id || req.unshare) return
|
|
||||||
if (!shareEnabled()) return
|
|
||||||
setReq("unshare", true)
|
|
||||||
globalSDK.client.session
|
|
||||||
.unshare({ sessionID: id, directory: sdk.directory })
|
|
||||||
.catch((err: unknown) => {
|
|
||||||
console.error("Failed to unshare session", err)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setReq("unshare", false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const viewShare = () => {
|
const viewShare = () => {
|
||||||
const url = shareUrl()
|
const url = shareUrl()
|
||||||
if (!url) return
|
if (!url) return
|
||||||
@@ -382,6 +334,54 @@ export function MessageTimeline(props: {
|
|||||||
return language.t("common.requestFailed")
|
return language.t("common.requestFailed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const shareMutation = useMutation(() => ({
|
||||||
|
mutationFn: (id: string) => globalSDK.client.session.share({ sessionID: id, directory: sdk.directory }),
|
||||||
|
onError: (err) => {
|
||||||
|
console.error("Failed to share session", err)
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const unshareMutation = useMutation(() => ({
|
||||||
|
mutationFn: (id: string) => globalSDK.client.session.unshare({ sessionID: id, directory: sdk.directory }),
|
||||||
|
onError: (err) => {
|
||||||
|
console.error("Failed to unshare session", err)
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const titleMutation = useMutation(() => ({
|
||||||
|
mutationFn: (input: { id: string; title: string }) =>
|
||||||
|
sdk.client.session.update({ sessionID: input.id, title: input.title }),
|
||||||
|
onSuccess: (_, input) => {
|
||||||
|
sync.set(
|
||||||
|
produce((draft) => {
|
||||||
|
const index = draft.session.findIndex((s) => s.id === input.id)
|
||||||
|
if (index !== -1) draft.session[index].title = input.title
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
setTitle("editing", false)
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
showToast({
|
||||||
|
title: language.t("common.requestFailed"),
|
||||||
|
description: errorMessage(err),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const shareSession = () => {
|
||||||
|
const id = sessionID()
|
||||||
|
if (!id || shareMutation.isPending) return
|
||||||
|
if (!shareEnabled()) return
|
||||||
|
shareMutation.mutate(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const unshareSession = () => {
|
||||||
|
const id = sessionID()
|
||||||
|
if (!id || unshareMutation.isPending) return
|
||||||
|
if (!shareEnabled()) return
|
||||||
|
unshareMutation.mutate(id)
|
||||||
|
}
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
sessionKey,
|
sessionKey,
|
||||||
@@ -389,7 +389,6 @@ export function MessageTimeline(props: {
|
|||||||
setTitle({
|
setTitle({
|
||||||
draft: "",
|
draft: "",
|
||||||
editing: false,
|
editing: false,
|
||||||
saving: false,
|
|
||||||
menuOpen: false,
|
menuOpen: false,
|
||||||
pendingRename: false,
|
pendingRename: false,
|
||||||
pendingShare: false,
|
pendingShare: false,
|
||||||
@@ -408,40 +407,22 @@ export function MessageTimeline(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const closeTitleEditor = () => {
|
const closeTitleEditor = () => {
|
||||||
if (title.saving) return
|
if (titleMutation.isPending) return
|
||||||
setTitle({ editing: false, saving: false })
|
setTitle("editing", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveTitleEditor = async () => {
|
const saveTitleEditor = () => {
|
||||||
const id = sessionID()
|
const id = sessionID()
|
||||||
if (!id) return
|
if (!id) return
|
||||||
if (title.saving) return
|
if (titleMutation.isPending) return
|
||||||
|
|
||||||
const next = title.draft.trim()
|
const next = title.draft.trim()
|
||||||
if (!next || next === (titleValue() ?? "")) {
|
if (!next || next === (titleValue() ?? "")) {
|
||||||
setTitle({ editing: false, saving: false })
|
setTitle("editing", false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setTitle("saving", true)
|
titleMutation.mutate({ id, title: next })
|
||||||
await sdk.client.session
|
|
||||||
.update({ sessionID: id, title: next })
|
|
||||||
.then(() => {
|
|
||||||
sync.set(
|
|
||||||
produce((draft) => {
|
|
||||||
const index = draft.session.findIndex((s) => s.id === id)
|
|
||||||
if (index !== -1) draft.session[index].title = next
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
setTitle({ editing: false, saving: false })
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setTitle("saving", false)
|
|
||||||
showToast({
|
|
||||||
title: language.t("common.requestFailed"),
|
|
||||||
description: errorMessage(err),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const navigateAfterSessionRemoval = (sessionID: string, parentID?: string, nextSessionID?: string) => {
|
const navigateAfterSessionRemoval = (sessionID: string, parentID?: string, nextSessionID?: string) => {
|
||||||
@@ -679,17 +660,15 @@ export function MessageTimeline(props: {
|
|||||||
<div
|
<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)]"
|
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={{
|
style={{
|
||||||
width: slot.open ? "16px" : "0px",
|
width: working() ? "16px" : "0px",
|
||||||
"margin-right": slot.open ? "8px" : "0px",
|
"margin-right": working() ? "8px" : "0px",
|
||||||
}}
|
}}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<Show when={slot.show}>
|
<Show when={workingStatus() !== "hidden"}>
|
||||||
<div
|
<div
|
||||||
class="transition-opacity duration-200 ease-out"
|
class="transition-opacity duration-200 ease-out"
|
||||||
classList={{
|
classList={{ "opacity-0": workingStatus() === "hiding" }}
|
||||||
"opacity-0": slot.fade,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Spinner class="size-4" style={{ color: tint() ?? "var(--icon-interactive-base)" }} />
|
<Spinner class="size-4" style={{ color: tint() ?? "var(--icon-interactive-base)" }} />
|
||||||
</div>
|
</div>
|
||||||
@@ -712,7 +691,7 @@ export function MessageTimeline(props: {
|
|||||||
titleRef = el
|
titleRef = el
|
||||||
}}
|
}}
|
||||||
value={title.draft}
|
value={title.draft}
|
||||||
disabled={title.saving}
|
disabled={titleMutation.isPending}
|
||||||
class="text-14-medium text-text-strong grow-1 min-w-0 rounded-[6px]"
|
class="text-14-medium text-text-strong grow-1 min-w-0 rounded-[6px]"
|
||||||
style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }}
|
style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }}
|
||||||
onInput={(event) => setTitle("draft", event.currentTarget.value)}
|
onInput={(event) => setTitle("draft", event.currentTarget.value)}
|
||||||
@@ -863,9 +842,9 @@ export function MessageTimeline(props: {
|
|||||||
variant="primary"
|
variant="primary"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
onClick={shareSession}
|
onClick={shareSession}
|
||||||
disabled={req.share}
|
disabled={shareMutation.isPending}
|
||||||
>
|
>
|
||||||
{req.share
|
{shareMutation.isPending
|
||||||
? language.t("session.share.action.publishing")
|
? language.t("session.share.action.publishing")
|
||||||
: language.t("session.share.action.publish")}
|
: language.t("session.share.action.publish")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -886,9 +865,9 @@ export function MessageTimeline(props: {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
class="w-full shadow-none border border-border-weak-base"
|
class="w-full shadow-none border border-border-weak-base"
|
||||||
onClick={unshareSession}
|
onClick={unshareSession}
|
||||||
disabled={req.unshare}
|
disabled={unshareMutation.isPending}
|
||||||
>
|
>
|
||||||
{req.unshare
|
{unshareMutation.isPending
|
||||||
? language.t("session.share.action.unpublishing")
|
? language.t("session.share.action.unpublishing")
|
||||||
: language.t("session.share.action.unpublish")}
|
: language.t("session.share.action.unpublish")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -897,7 +876,7 @@ export function MessageTimeline(props: {
|
|||||||
variant="primary"
|
variant="primary"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
onClick={viewShare}
|
onClick={viewShare}
|
||||||
disabled={req.unshare}
|
disabled={unshareMutation.isPending}
|
||||||
>
|
>
|
||||||
{language.t("session.share.action.view")}
|
{language.t("session.share.action.view")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -915,7 +894,6 @@ export function MessageTimeline(props: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
role="log"
|
role="log"
|
||||||
class="flex flex-col gap-12 items-start justify-start pb-16 transition-[margin]"
|
class="flex flex-col gap-12 items-start justify-start pb-16 transition-[margin]"
|
||||||
|
|||||||
@@ -438,12 +438,10 @@ export function SessionSidePanel(props: {
|
|||||||
size={layout.fileTree.width()}
|
size={layout.fileTree.width()}
|
||||||
min={200}
|
min={200}
|
||||||
max={480}
|
max={480}
|
||||||
collapseThreshold={160}
|
|
||||||
onResize={(width) => {
|
onResize={(width) => {
|
||||||
props.size.touch()
|
props.size.touch()
|
||||||
layout.fileTree.resize(width)
|
layout.fileTree.resize(width)
|
||||||
}}
|
}}
|
||||||
onCollapse={layout.fileTree.close}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -280,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) => {
|
||||||
<Show when={all().find((pty) => pty.id === id)}>
|
const ops = terminal.bind()
|
||||||
{(pty) => (
|
return (
|
||||||
<div id={`terminal-wrapper-${id}`} class="absolute inset-0">
|
<Show when={all().find((pty) => pty.id === id)}>
|
||||||
<Terminal
|
{(pty) => (
|
||||||
pty={pty()}
|
<div id={`terminal-wrapper-${id}`} class="absolute inset-0">
|
||||||
autoFocus={opened()}
|
<Terminal
|
||||||
onConnect={() => terminal.trim(id)}
|
pty={pty()}
|
||||||
onCleanup={terminal.update}
|
autoFocus={opened()}
|
||||||
onConnectError={() => terminal.clone(id)}
|
onConnect={() => ops.trim(id)}
|
||||||
/>
|
onCleanup={ops.update}
|
||||||
</div>
|
onConnectError={() => ops.clone(id)}
|
||||||
)}
|
/>
|
||||||
</Show>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}}
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
id: "file.open",
|
id: "file.open",
|
||||||
title: language.t("command.file.open"),
|
title: language.t("command.file.open"),
|
||||||
description: language.t("palette.search.placeholder"),
|
description: language.t("palette.search.placeholder"),
|
||||||
keybind: "mod+p",
|
keybind: "mod+k,mod+p",
|
||||||
slash: "open",
|
slash: "open",
|
||||||
onSelect: () => dialog.show(() => <DialogSelectFile onOpenFile={showAllFiles} />),
|
onSelect: () => dialog.show(() => <DialogSelectFile onOpenFile={showAllFiles} />),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import type { Part } from "@opencode-ai/sdk/v2"
|
||||||
|
import { extractPromptFromParts } from "./prompt"
|
||||||
|
|
||||||
|
describe("extractPromptFromParts", () => {
|
||||||
|
test("restores multiple uploaded attachments", () => {
|
||||||
|
const parts = [
|
||||||
|
{
|
||||||
|
id: "text_1",
|
||||||
|
type: "text",
|
||||||
|
text: "check these",
|
||||||
|
sessionID: "ses_1",
|
||||||
|
messageID: "msg_1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "file_1",
|
||||||
|
type: "file",
|
||||||
|
mime: "image/png",
|
||||||
|
url: "data:image/png;base64,AAA",
|
||||||
|
filename: "a.png",
|
||||||
|
sessionID: "ses_1",
|
||||||
|
messageID: "msg_1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "file_2",
|
||||||
|
type: "file",
|
||||||
|
mime: "application/pdf",
|
||||||
|
url: "data:application/pdf;base64,BBB",
|
||||||
|
filename: "b.pdf",
|
||||||
|
sessionID: "ses_1",
|
||||||
|
messageID: "msg_1",
|
||||||
|
},
|
||||||
|
] satisfies Part[]
|
||||||
|
|
||||||
|
const result = extractPromptFromParts(parts)
|
||||||
|
|
||||||
|
expect(result).toHaveLength(3)
|
||||||
|
expect(result[0]).toMatchObject({ type: "text", content: "check these" })
|
||||||
|
expect(result.slice(1)).toMatchObject([
|
||||||
|
{ type: "image", filename: "a.png", mime: "image/png", dataUrl: "data:image/png;base64,AAA" },
|
||||||
|
{ type: "image", filename: "b.pdf", mime: "application/pdf", dataUrl: "data:application/pdf;base64,BBB" },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.2.27",
|
"version": "1.3.0",
|
||||||
"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">
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -651,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",
|
||||||
|
|||||||
@@ -654,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",
|
||||||
|
|||||||
@@ -646,6 +646,8 @@ export const dict = {
|
|||||||
"The plan is designed primarily for international users, with models hosted in the US, EU, and Singapore for stable global access. Pricing and usage limits may change as we learn from early usage and feedback.",
|
"The plan is designed primarily for international users, with models hosted in the US, EU, and Singapore for stable global access. Pricing and usage limits may change as we learn from early usage and feedback.",
|
||||||
"workspace.lite.promo.subscribe": "Subscribe to Go",
|
"workspace.lite.promo.subscribe": "Subscribe to Go",
|
||||||
"workspace.lite.promo.subscribing": "Redirecting...",
|
"workspace.lite.promo.subscribing": "Redirecting...",
|
||||||
|
"workspace.lite.promo.otherMethods": "Other payment methods",
|
||||||
|
"workspace.lite.promo.selectMethod": "Select payment method",
|
||||||
|
|
||||||
"download.title": "OpenCode | Download",
|
"download.title": "OpenCode | Download",
|
||||||
"download.meta.description": "Download OpenCode for macOS, Windows, and Linux",
|
"download.meta.description": "Download OpenCode for macOS, Windows, and Linux",
|
||||||
|
|||||||
@@ -654,6 +654,8 @@ export const dict = {
|
|||||||
"El plan está diseñado principalmente para usuarios internacionales, con modelos alojados en EE. UU., la UE y Singapur para un acceso global estable. Los precios y los límites de uso pueden cambiar a medida que aprendemos del uso inicial y los comentarios.",
|
"El plan está diseñado principalmente para usuarios internacionales, con modelos alojados en EE. UU., la UE y Singapur para un acceso global estable. Los precios y los límites de uso pueden cambiar a medida que aprendemos del uso inicial y los comentarios.",
|
||||||
"workspace.lite.promo.subscribe": "Suscribirse a Go",
|
"workspace.lite.promo.subscribe": "Suscribirse a Go",
|
||||||
"workspace.lite.promo.subscribing": "Redirigiendo...",
|
"workspace.lite.promo.subscribing": "Redirigiendo...",
|
||||||
|
"workspace.lite.promo.otherMethods": "Otros métodos de pago",
|
||||||
|
"workspace.lite.promo.selectMethod": "Seleccionar método de pago",
|
||||||
|
|
||||||
"download.title": "OpenCode | Descargar",
|
"download.title": "OpenCode | Descargar",
|
||||||
"download.meta.description": "Descarga OpenCode para macOS, Windows y Linux",
|
"download.meta.description": "Descarga OpenCode para macOS, Windows y Linux",
|
||||||
|
|||||||
@@ -661,6 +661,8 @@ export const dict = {
|
|||||||
"Le plan est conçu principalement pour les utilisateurs internationaux, avec des modèles hébergés aux États-Unis, dans l'UE et à Singapour pour un accès mondial stable. Les tarifs et les limites d'utilisation peuvent changer à mesure que nous apprenons des premières utilisations et des commentaires.",
|
"Le plan est conçu principalement pour les utilisateurs internationaux, avec des modèles hébergés aux États-Unis, dans l'UE et à Singapour pour un accès mondial stable. Les tarifs et les limites d'utilisation peuvent changer à mesure que nous apprenons des premières utilisations et des commentaires.",
|
||||||
"workspace.lite.promo.subscribe": "S'abonner à Go",
|
"workspace.lite.promo.subscribe": "S'abonner à Go",
|
||||||
"workspace.lite.promo.subscribing": "Redirection...",
|
"workspace.lite.promo.subscribing": "Redirection...",
|
||||||
|
"workspace.lite.promo.otherMethods": "Autres méthodes de paiement",
|
||||||
|
"workspace.lite.promo.selectMethod": "Sélectionner la méthode de paiement",
|
||||||
|
|
||||||
"download.title": "OpenCode | Téléchargement",
|
"download.title": "OpenCode | Téléchargement",
|
||||||
"download.meta.description": "Téléchargez OpenCode pour macOS, Windows et Linux",
|
"download.meta.description": "Téléchargez OpenCode pour macOS, Windows et Linux",
|
||||||
|
|||||||
@@ -652,6 +652,8 @@ export const dict = {
|
|||||||
"Il piano è progettato principalmente per gli utenti internazionali, con modelli ospitati in US, EU e Singapore per un accesso globale stabile. I prezzi e i limiti di utilizzo potrebbero cambiare man mano che impariamo dall'utilizzo iniziale e dal feedback.",
|
"Il piano è progettato principalmente per gli utenti internazionali, con modelli ospitati in US, EU e Singapore per un accesso globale stabile. I prezzi e i limiti di utilizzo potrebbero cambiare man mano che impariamo dall'utilizzo iniziale e dal feedback.",
|
||||||
"workspace.lite.promo.subscribe": "Abbonati a Go",
|
"workspace.lite.promo.subscribe": "Abbonati a Go",
|
||||||
"workspace.lite.promo.subscribing": "Reindirizzamento...",
|
"workspace.lite.promo.subscribing": "Reindirizzamento...",
|
||||||
|
"workspace.lite.promo.otherMethods": "Altri metodi di pagamento",
|
||||||
|
"workspace.lite.promo.selectMethod": "Seleziona metodo di pagamento",
|
||||||
|
|
||||||
"download.title": "OpenCode | Download",
|
"download.title": "OpenCode | Download",
|
||||||
"download.meta.description": "Scarica OpenCode per macOS, Windows e Linux",
|
"download.meta.description": "Scarica OpenCode per macOS, Windows e Linux",
|
||||||
|
|||||||
@@ -653,6 +653,8 @@ export const dict = {
|
|||||||
"このプランは主にグローバルユーザー向けに設計されており、米国、EU、シンガポールでホストされたモデルにより安定したグローバルアクセスを提供します。料金と利用制限は、初期の利用状況やフィードバックに基づいて変更される可能性があります。",
|
"このプランは主にグローバルユーザー向けに設計されており、米国、EU、シンガポールでホストされたモデルにより安定したグローバルアクセスを提供します。料金と利用制限は、初期の利用状況やフィードバックに基づいて変更される可能性があります。",
|
||||||
"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 向けにダウンロード",
|
||||||
|
|||||||
@@ -645,6 +645,8 @@ export const dict = {
|
|||||||
"이 플랜은 주로 글로벌 사용자를 위해 설계되었으며, 안정적인 글로벌 액세스를 위해 미국, EU 및 싱가포르에 모델이 호스팅되어 있습니다. 가격 및 사용 한도는 초기 사용을 통해 학습하고 피드백을 수집함에 따라 변경될 수 있습니다.",
|
"이 플랜은 주로 글로벌 사용자를 위해 설계되었으며, 안정적인 글로벌 액세스를 위해 미국, EU 및 싱가포르에 모델이 호스팅되어 있습니다. 가격 및 사용 한도는 초기 사용을 통해 학습하고 피드백을 수집함에 따라 변경될 수 있습니다.",
|
||||||
"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": "macOS, Windows, Linux용 OpenCode 다운로드",
|
"download.meta.description": "macOS, Windows, Linux용 OpenCode 다운로드",
|
||||||
|
|||||||
@@ -651,6 +651,8 @@ export const dict = {
|
|||||||
"Planen er primært designet for internasjonale brukere, med modeller driftet i USA, EU og Singapore for stabil global tilgang. Priser og bruksgrenser kan endres etter hvert som vi lærer fra tidlig bruk og tilbakemeldinger.",
|
"Planen er primært designet for internasjonale brukere, med modeller driftet i USA, EU og Singapore for stabil global tilgang. Priser og bruksgrenser kan endres etter hvert som vi lærer fra tidlig bruk og tilbakemeldinger.",
|
||||||
"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": "Velg betalingsmetode",
|
||||||
|
|
||||||
"download.title": "OpenCode | Last ned",
|
"download.title": "OpenCode | Last ned",
|
||||||
"download.meta.description": "Last ned OpenCode for macOS, Windows og Linux",
|
"download.meta.description": "Last ned OpenCode for macOS, Windows og Linux",
|
||||||
|
|||||||
@@ -652,6 +652,8 @@ export const dict = {
|
|||||||
"Plan został zaprojektowany głównie dla użytkowników międzynarodowych, z modelami hostowanymi w USA, UE i Singapurze, aby zapewnić stabilny globalny dostęp. Ceny i limity użycia mogą ulec zmianie w miarę analizy wczesnego użycia i zbierania opinii.",
|
"Plan został zaprojektowany głównie dla użytkowników międzynarodowych, z modelami hostowanymi w USA, UE i Singapurze, aby zapewnić stabilny globalny dostęp. Ceny i limity użycia mogą ulec zmianie w miarę analizy wczesnego użycia i zbierania opinii.",
|
||||||
"workspace.lite.promo.subscribe": "Subskrybuj Go",
|
"workspace.lite.promo.subscribe": "Subskrybuj Go",
|
||||||
"workspace.lite.promo.subscribing": "Przekierowywanie...",
|
"workspace.lite.promo.subscribing": "Przekierowywanie...",
|
||||||
|
"workspace.lite.promo.otherMethods": "Inne metody płatności",
|
||||||
|
"workspace.lite.promo.selectMethod": "Wybierz metodę płatności",
|
||||||
|
|
||||||
"download.title": "OpenCode | Pobierz",
|
"download.title": "OpenCode | Pobierz",
|
||||||
"download.meta.description": "Pobierz OpenCode na macOS, Windows i Linux",
|
"download.meta.description": "Pobierz OpenCode na macOS, Windows i Linux",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user