Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4a86db544 | ||
|
|
665b251ee1 |
+1
-4
@@ -10,9 +10,6 @@
|
|||||||
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
|
||||||
@@ -20,8 +17,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
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
name: close-issues
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 2 * * *" # Daily at 2:00 AM
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
close:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
issues: write
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: oven-sh/setup-bun@v2
|
|
||||||
with:
|
|
||||||
bun-version: latest
|
|
||||||
|
|
||||||
- name: Close stale issues
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
|
||||||
run: bun script/github/close-issues.ts
|
|
||||||
@@ -11,7 +11,7 @@ concurrency: ${{ github.workflow }}-${{ github.ref }}
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
nix build ".#packages.${SYSTEM}.node_modules_updater" --no-link 2>&1 | tee "$BUILD_LOG" || true
|
nix build ".#packages.${SYSTEM}.node_modules_updater" --no-link 2>&1 | tee "$BUILD_LOG" || true
|
||||||
|
|
||||||
# Extract hash from build log with portability
|
# Extract hash from build log with portability
|
||||||
HASH="$(nix run --inputs-from . nixpkgs#gnugrep -- -oP 'got:\s*\Ksha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | tail -n1 || true)"
|
HASH="$(grep -oE 'sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | tail -n1 || true)"
|
||||||
|
|
||||||
if [ -z "$HASH" ]; then
|
if [ -z "$HASH" ]; then
|
||||||
echo "::error::Failed to compute hash for ${SYSTEM}"
|
echo "::error::Failed to compute hash for ${SYSTEM}"
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
name: stale-issues
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "30 1 * * *" # Daily at 1:30 AM
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
DAYS_BEFORE_STALE: 90
|
||||||
|
DAYS_BEFORE_CLOSE: 7
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v10
|
||||||
|
with:
|
||||||
|
days-before-stale: ${{ env.DAYS_BEFORE_STALE }}
|
||||||
|
days-before-close: ${{ env.DAYS_BEFORE_CLOSE }}
|
||||||
|
stale-issue-label: "stale"
|
||||||
|
close-issue-message: |
|
||||||
|
[automated] Closing due to ${{ env.DAYS_BEFORE_STALE }}+ days of inactivity.
|
||||||
|
|
||||||
|
Feel free to reopen if you still need this!
|
||||||
|
stale-issue-message: |
|
||||||
|
[automated] This issue has had no activity for ${{ env.DAYS_BEFORE_STALE }} days.
|
||||||
|
|
||||||
|
It will be closed in ${{ env.DAYS_BEFORE_CLOSE }} days if there's no new activity.
|
||||||
|
remove-stale-when-updated: true
|
||||||
|
exempt-issue-labels: "pinned,security,feature-request,on-hold"
|
||||||
|
start-date: "2025-12-27"
|
||||||
@@ -50,17 +50,20 @@ 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: ${{ github.workspace }}/.playwright-browsers
|
PLAYWRIGHT_BROWSERS_PATH: 0
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -73,28 +76,9 @@ jobs:
|
|||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: ./.github/actions/setup-bun
|
uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
- 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
|
|
||||||
run: bunx playwright install-deps chromium
|
|
||||||
|
|
||||||
- name: Install Playwright browsers
|
- name: Install Playwright browsers
|
||||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
|
||||||
working-directory: packages/app
|
working-directory: packages/app
|
||||||
run: bunx playwright install chromium
|
run: ${{ matrix.settings.playwright }}
|
||||||
|
|
||||||
- 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,write
|
roles: admin,maintain
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
|
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
node_modules
|
plans/
|
||||||
plans
|
|
||||||
package.json
|
|
||||||
bun.lock
|
bun.lock
|
||||||
.gitignore
|
package.json
|
||||||
package-lock.json
|
package-lock.json
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
description: Translate content for a specified locale while preserving technical terms
|
description: Translate content for a specified locale while preserving technical terms
|
||||||
mode: subagent
|
mode: subagent
|
||||||
model: opencode/gpt-5.4
|
model: opencode/gemini-3.1-pro
|
||||||
---
|
---
|
||||||
|
|
||||||
You are a professional translator and localization specialist.
|
You are a professional translator and localization specialist.
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
---
|
|
||||||
model: opencode/kimi-k2.5
|
|
||||||
---
|
|
||||||
|
|
||||||
create UPCOMING_CHANGELOG.md
|
|
||||||
|
|
||||||
it should have sections
|
|
||||||
|
|
||||||
```
|
|
||||||
# TUI
|
|
||||||
|
|
||||||
# Desktop
|
|
||||||
|
|
||||||
# Core
|
|
||||||
|
|
||||||
# Misc
|
|
||||||
```
|
|
||||||
|
|
||||||
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 into the appropriate section.
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
/// <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,
|
||||||
@@ -22,16 +24,7 @@ interface PR {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default tool({
|
export default tool({
|
||||||
description: `Use this tool to search GitHub pull requests by title and description.
|
description: 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),
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
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,5 +1,7 @@
|
|||||||
/// <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"],
|
||||||
@@ -38,12 +40,7 @@ async function githubFetch(endpoint: string, options: RequestInit = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default tool({
|
export default tool({
|
||||||
description: `Use this tool to assign and/or label a GitHub issue.
|
description: DESCRIPTION,
|
||||||
|
|
||||||
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[]])
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
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.
|
||||||
+1
-1
@@ -137,4 +137,4 @@ OpenCode 内置两种 Agent,可用 `Tab` 键快速切换:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**加入我们的社区** [飞书](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=738j8655-cd59-4633-a30a-1124e0096789&qr_code=true) | [X.com](https://x.com/opencode)
|
**加入我们的社区** [飞书](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=de8k6664-1b5e-43f2-8efd-21d6772647b5&qr_code=true) | [X.com](https://x.com/opencode)
|
||||||
|
|||||||
+1
-1
@@ -137,4 +137,4 @@ OpenCode 內建了兩種 Agent,您可以使用 `Tab` 鍵快速切換。
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**加入我們的社群** [飞书](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=738j8655-cd59-4633-a30a-1124e0096789&qr_code=true) | [X.com](https://x.com/opencode)
|
**加入我們的社群** [飞书](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=de8k6664-1b5e-43f2-8efd-21d6772647b5&qr_code=true) | [X.com](https://x.com/opencode)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.3.2",
|
"version": "1.2.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -41,11 +41,9 @@
|
|||||||
"@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:",
|
||||||
@@ -79,7 +77,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.3.2",
|
"version": "1.2.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
@@ -113,7 +111,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.3.2",
|
"version": "1.2.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-sts": "3.782.0",
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
"@jsx-email/render": "1.1.1",
|
"@jsx-email/render": "1.1.1",
|
||||||
@@ -130,7 +128,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/workers-types": "catalog:",
|
"@cloudflare/workers-types": "catalog:",
|
||||||
"@tsconfig/node22": "22.0.2",
|
"@tsconfig/node22": "22.0.2",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "1.3.0",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
"drizzle-kit": "catalog:",
|
"drizzle-kit": "catalog:",
|
||||||
@@ -140,7 +138,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.3.2",
|
"version": "1.2.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "2.0.0",
|
"@ai-sdk/anthropic": "2.0.0",
|
||||||
"@ai-sdk/openai": "2.0.2",
|
"@ai-sdk/openai": "2.0.2",
|
||||||
@@ -164,7 +162,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.3.2",
|
"version": "1.2.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
@@ -188,7 +186,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.3.2",
|
"version": "1.2.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/app": "workspace:*",
|
"@opencode-ai/app": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
@@ -221,7 +219,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop-electron": {
|
"packages/desktop-electron": {
|
||||||
"name": "@opencode-ai/desktop-electron",
|
"name": "@opencode-ai/desktop-electron",
|
||||||
"version": "1.3.2",
|
"version": "1.2.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/app": "workspace:*",
|
"@opencode-ai/app": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
@@ -252,7 +250,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.3.2",
|
"version": "1.2.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
@@ -281,7 +279,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.3.2",
|
"version": "1.2.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -297,7 +295,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.3.2",
|
"version": "1.2.27",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -327,9 +325,11 @@
|
|||||||
"@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.27.1",
|
"@modelcontextprotocol/sdk": "1.25.2",
|
||||||
"@octokit/graphql": "9.0.2",
|
"@octokit/graphql": "9.0.2",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
"@openauthjs/openauth": "catalog:",
|
"@openauthjs/openauth": "catalog:",
|
||||||
@@ -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.90",
|
"@opentui/core": "0.1.87",
|
||||||
"@opentui/solid": "0.1.90",
|
"@opentui/solid": "0.1.87",
|
||||||
"@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,10 +355,9 @@
|
|||||||
"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": "catalog:",
|
"drizzle-orm": "1.0.0-beta.16-ea816b6",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
"gitlab-ai-provider": "5.3.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",
|
||||||
@@ -369,8 +368,6 @@
|
|||||||
"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",
|
|
||||||
"opencode-poe-auth": "0.0.1",
|
|
||||||
"opentui-spinner": "0.0.6",
|
"opentui-spinner": "0.0.6",
|
||||||
"partial-json": "0.1.7",
|
"partial-json": "0.1.7",
|
||||||
"remeda": "catalog:",
|
"remeda": "catalog:",
|
||||||
@@ -412,8 +409,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": "catalog:",
|
"drizzle-kit": "1.0.0-beta.16-ea816b6",
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "1.0.0-beta.16-ea816b6",
|
||||||
"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",
|
||||||
@@ -422,7 +419,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.3.2",
|
"version": "1.2.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
@@ -446,7 +443,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.3.2",
|
"version": "1.2.27",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hey-api/openapi-ts": "0.90.10",
|
"@hey-api/openapi-ts": "0.90.10",
|
||||||
"@tsconfig/node22": "catalog:",
|
"@tsconfig/node22": "catalog:",
|
||||||
@@ -457,7 +454,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.3.2",
|
"version": "1.2.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -492,7 +489,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.3.2",
|
"version": "1.2.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -538,7 +535,7 @@
|
|||||||
},
|
},
|
||||||
"packages/util": {
|
"packages/util": {
|
||||||
"name": "@opencode-ai/util",
|
"name": "@opencode-ai/util",
|
||||||
"version": "1.3.2",
|
"version": "1.2.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -549,7 +546,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.3.2",
|
"version": "1.2.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
@@ -589,8 +586,6 @@
|
|||||||
],
|
],
|
||||||
"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": {
|
||||||
@@ -613,7 +608,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.11",
|
"@types/bun": "1.3.9",
|
||||||
"@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",
|
||||||
@@ -621,8 +616,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.19-d95b7a4",
|
"drizzle-kit": "1.0.0-beta.16-ea816b6",
|
||||||
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
"drizzle-orm": "1.0.0-beta.16-ea816b6",
|
||||||
"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",
|
||||||
@@ -1113,6 +1108,10 @@
|
|||||||
|
|
||||||
"@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=="],
|
||||||
@@ -1325,7 +1324,7 @@
|
|||||||
|
|
||||||
"@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
|
"@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
|
||||||
|
|
||||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="],
|
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.2", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww=="],
|
||||||
|
|
||||||
"@motionone/animation": ["@motionone/animation@10.18.0", "", { "dependencies": { "@motionone/easing": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw=="],
|
"@motionone/animation": ["@motionone/animation@10.18.0", "", { "dependencies": { "@motionone/easing": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw=="],
|
||||||
|
|
||||||
@@ -1449,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.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": ["@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-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.90", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XFrm2zCg1SlHPQ5A2HX/I4dCrmTjYaCJIIpo3QuPIvZBGH3aBMdWDJh2tXw7AB5Mmh8X1K4hDkP5nlK9x0Ewow=="],
|
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.87", "", { "os": "darwin", "cpu": "arm64" }, "sha512-G8oq85diOfkU6n0T1CxCle7oDmpKxwhcdhZ9khBMU5IrfLx9ZDuCM3F6MsiRQWdvPPCq2oomNbd64bYkPamYgw=="],
|
||||||
|
|
||||||
"@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-darwin-x64": ["@opentui/core-darwin-x64@0.1.87", "", { "os": "darwin", "cpu": "x64" }, "sha512-MYTFQfOHm6qO7YaY4GHK9u/oJlXY6djaaxl5I+k4p2mk3vvuFIl/AP1ypITwBFjyV5gyp7PRWFp4nGfY9oN8bw=="],
|
||||||
|
|
||||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.90", "", { "os": "linux", "cpu": "arm64" }, "sha512-OTbvBTP5mVQ4uwKyuz6b59ElG+D0i1Ln+q6cVhNkLgeRLySIn1uXEzUFQGlnVgb8lFDANsn3yQmdv+R+Cpw0og=="],
|
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.87", "", { "os": "linux", "cpu": "arm64" }, "sha512-he8o1h5M6oskRJ7wE+xKJgmWnv5ZwN6gB3M/Z+SeHtOMPa5cZmi3TefTjG54llEgFfx0F9RcqHof7TJ/GNxRkw=="],
|
||||||
|
|
||||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.90", "", { "os": "linux", "cpu": "x64" }, "sha512-2PJi/LLlO7tGk9Ful/n+6iBdg1RFrA9ibU7wVneE6Z1P0LCYeu7bpwMzea1TXL0eAQWPHsjTs9aPlqPxln0EJw=="],
|
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.87", "", { "os": "linux", "cpu": "x64" }, "sha512-aiUwjPlH4yDcB8/6YDKSmMkaoGAAltL0Xo0AzXyAtJXWK5tkCSaYjEVwzJ/rYRkr4Magnad+Mjth4AQUWdR2AA=="],
|
||||||
|
|
||||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.90", "", { "os": "win32", "cpu": "arm64" }, "sha512-+sTRaOb7gCMZ6iLuuG4y9kzyweJzBDcIJN0Xh49ikFWTwVECDXEVtXahNGlw57avm2yYUoNzmpBjK/LV7zBj9A=="],
|
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.87", "", { "os": "win32", "cpu": "arm64" }, "sha512-cmP0pOyREjWGniHqbDmaMY7U+1AyagrD8VseJbU0cGpNgVpG2/gbrJUGdfdLB0SNb+mzLdx6SOjdxtrElwRCQA=="],
|
||||||
|
|
||||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.90", "", { "os": "win32", "cpu": "x64" }, "sha512-aVFyErckWp4oW9NJ/ZDKBUAlTlfVUiRXGP63JXFOoeqI7EYaM8uBt6rgZAJuUdFWCN2Q66WRS8Y2mk+0BJwVBg=="],
|
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.87", "", { "os": "win32", "cpu": "x64" }, "sha512-N2GErAAP8iODf2RPp86pilPaVKiD6G4pkpZL5nLGbKsl0bndrVTpSqZcn8+/nQwFZDPD/AsiRTYNOfWOblhzOw=="],
|
||||||
|
|
||||||
"@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=="],
|
"@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=="],
|
||||||
|
|
||||||
"@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=="],
|
||||||
|
|
||||||
@@ -1891,8 +1890,6 @@
|
|||||||
|
|
||||||
"@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=="],
|
||||||
@@ -1971,14 +1968,10 @@
|
|||||||
|
|
||||||
"@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=="],
|
||||||
@@ -2061,7 +2054,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.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
"@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
|
||||||
|
|
||||||
"@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=="],
|
||||||
|
|
||||||
@@ -2457,7 +2450,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.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
"bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
@@ -2743,9 +2736,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.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-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-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=="],
|
"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=="],
|
||||||
|
|
||||||
"dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="],
|
"dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="],
|
||||||
|
|
||||||
@@ -2889,7 +2882,7 @@
|
|||||||
|
|
||||||
"express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="],
|
"express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="],
|
||||||
|
|
||||||
"express-rate-limit": ["express-rate-limit@8.3.1", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw=="],
|
"express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="],
|
||||||
|
|
||||||
"expressive-code": ["expressive-code@0.41.7", "", { "dependencies": { "@expressive-code/core": "^0.41.7", "@expressive-code/plugin-frames": "^0.41.7", "@expressive-code/plugin-shiki": "^0.41.7", "@expressive-code/plugin-text-markers": "^0.41.7" } }, "sha512-2wZjC8OQ3TaVEMcBtYY4Va3lo6J+Ai9jf3d4dbhURMJcU4Pbqe6EcHe424MIZI0VHUA1bR6xdpoHYi3yxokWqA=="],
|
"expressive-code": ["expressive-code@0.41.7", "", { "dependencies": { "@expressive-code/core": "^0.41.7", "@expressive-code/plugin-frames": "^0.41.7", "@expressive-code/plugin-shiki": "^0.41.7", "@expressive-code/plugin-text-markers": "^0.41.7" } }, "sha512-2wZjC8OQ3TaVEMcBtYY4Va3lo6J+Ai9jf3d4dbhURMJcU4Pbqe6EcHe424MIZI0VHUA1bR6xdpoHYi3yxokWqA=="],
|
||||||
|
|
||||||
@@ -3027,8 +3020,6 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -3037,8 +3028,6 @@
|
|||||||
|
|
||||||
"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.3.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-EiAipDMa4Ngsxp4MMaua5YHWsHhc9kGXKmBxulJg1Gueb+5IZmMwxaVtgWTGWZITxC3tzKEeRt/3U4McE2vTIA=="],
|
|
||||||
|
|
||||||
"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=="],
|
||||||
@@ -3791,10 +3780,6 @@
|
|||||||
|
|
||||||
"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=="],
|
|
||||||
|
|
||||||
"opencode-poe-auth": ["opencode-poe-auth@0.0.1", "", { "dependencies": { "open": "^10.0.0", "poe-oauth": "*" }, "peerDependencies": { "@opencode-ai/plugin": "*" } }, "sha512-cXqTlS6AXHzo1oBdosnxbT47ZJEZ9WXn050X8Re6wZ1vaNnTpB/l2fMQt90evT7RBK0fB8UjXQUDMKyd7bbiqg=="],
|
|
||||||
|
|
||||||
"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=="],
|
||||||
@@ -3927,8 +3912,6 @@
|
|||||||
|
|
||||||
"pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="],
|
"pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="],
|
||||||
|
|
||||||
"poe-oauth": ["poe-oauth@0.0.3", "", {}, "sha512-KgxDylcuq/mov8URSplrBGjrIjkQwjN/Ml8BhqaGsAvHzYN3yhuROdv1sDRfwqncg7TT8XzJvMeJAWmv/4NDLw=="],
|
|
||||||
|
|
||||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||||
|
|
||||||
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
||||||
@@ -4125,8 +4108,6 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -4259,7 +4240,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.6", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg=="],
|
"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=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
@@ -5073,6 +5054,10 @@
|
|||||||
|
|
||||||
"@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=="],
|
||||||
@@ -5129,8 +5114,6 @@
|
|||||||
|
|
||||||
"@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
"@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||||
|
|
||||||
"@modelcontextprotocol/sdk/hono": ["hono@4.12.9", "", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="],
|
|
||||||
|
|
||||||
"@modelcontextprotocol/sdk/jose": ["jose@6.2.1", "", {}, "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw=="],
|
"@modelcontextprotocol/sdk/jose": ["jose@6.2.1", "", {}, "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw=="],
|
||||||
|
|
||||||
"@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
"@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||||
@@ -5213,6 +5196,8 @@
|
|||||||
|
|
||||||
"@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=="],
|
||||||
@@ -5401,8 +5386,6 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -5469,10 +5452,6 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -5549,10 +5528,6 @@
|
|||||||
|
|
||||||
"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=="],
|
|
||||||
|
|
||||||
"opencode-poe-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=="],
|
||||||
@@ -6303,18 +6278,12 @@
|
|||||||
|
|
||||||
"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-poe-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=="],
|
||||||
|
|
||||||
"opencontrol/@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
"opencontrol/@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||||
|
|
||||||
"opencontrol/@modelcontextprotocol/sdk/express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="],
|
|
||||||
|
|
||||||
"opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="],
|
"opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="],
|
||||||
|
|
||||||
"opencontrol/@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
"opencontrol/@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||||
|
|||||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1773909469,
|
"lastModified": 1772091128,
|
||||||
"narHash": "sha256-vglVrLfHjFIzIdV9A27Ugul6rh3I1qHbbitGW7dk420=",
|
"narHash": "sha256-TnrYykX8Mf/Ugtkix6V+PjW7miU2yClA6uqWl/v6KWM=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "7149c06513f335be57f26fcbbbe34afda923882b",
|
"rev": "3f0336406035444b4a24b942788334af5f906259",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -496,6 +496,7 @@ async function subscribeSessionEvents() {
|
|||||||
|
|
||||||
const TOOL: Record<string, [string, string]> = {
|
const TOOL: Record<string, [string, string]> = {
|
||||||
todowrite: ["Todo", "\x1b[33m\x1b[1m"],
|
todowrite: ["Todo", "\x1b[33m\x1b[1m"],
|
||||||
|
todoread: ["Todo", "\x1b[33m\x1b[1m"],
|
||||||
bash: ["Bash", "\x1b[31m\x1b[1m"],
|
bash: ["Bash", "\x1b[31m\x1b[1m"],
|
||||||
edit: ["Edit", "\x1b[32m\x1b[1m"],
|
edit: ["Edit", "\x1b[32m\x1b[1m"],
|
||||||
glob: ["Glob", "\x1b[34m\x1b[1m"],
|
glob: ["Glob", "\x1b[34m\x1b[1m"],
|
||||||
|
|||||||
@@ -122,7 +122,6 @@ 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-l3k/1fRIAQkj7zdVj2Ad3QZWeTOf1CuIM6vgMHRaK1s=",
|
"x86_64-linux": "sha256-yfA50QKqylmaioxi+6d++W8Xv4Wix1hl3hEF6Zz7Ue0=",
|
||||||
"aarch64-linux": "sha256-iN3YtrKAUTK1GIwVMoVYkMXhtDZOiP7sSJ+Z8v4B5xw=",
|
"aarch64-linux": "sha256-b5sO7V+/zzJClHHKjkSz+9AUBYC8cb7S3m5ab1kpAyk=",
|
||||||
"aarch64-darwin": "sha256-FFedoiPyfHGdzQnITz1SRV7xv2XoT9vzxIDp4EcVdkU=",
|
"aarch64-darwin": "sha256-V66nmRX6kAjrc41ARVeuTElWK7KD8qG/DVk9K7Fu+J8=",
|
||||||
"x86_64-darwin": "sha256-0HiHkhJiN73UixUq5CC6YP6DkZzLar8lKnEL1aoiiHg="
|
"x86_64-darwin": "sha256-cFyh60WESiqZ5XWZi1+g3F/beSDL1+UPG8KhRivhK8w="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-7
@@ -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.11",
|
"packageManager": "bun@1.3.10",
|
||||||
"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.11",
|
"@types/bun": "1.3.9",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
"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.19-d95b7a4",
|
"drizzle-kit": "1.0.0-beta.16-ea816b6",
|
||||||
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
"drizzle-orm": "1.0.0-beta.16-ea816b6",
|
||||||
"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,8 +112,6 @@
|
|||||||
},
|
},
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-147
@@ -9,7 +9,6 @@ import { createSdk, modKey, resolveDirectory, serverUrl } from "./utils"
|
|||||||
import {
|
import {
|
||||||
dropdownMenuTriggerSelector,
|
dropdownMenuTriggerSelector,
|
||||||
dropdownMenuContentSelector,
|
dropdownMenuContentSelector,
|
||||||
projectSwitchSelector,
|
|
||||||
projectMenuTriggerSelector,
|
projectMenuTriggerSelector,
|
||||||
projectCloseMenuSelector,
|
projectCloseMenuSelector,
|
||||||
projectWorkspacesToggleSelector,
|
projectWorkspacesToggleSelector,
|
||||||
@@ -24,16 +23,6 @@ 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(() => {
|
||||||
@@ -175,9 +164,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, key = "K") {
|
export async function openPalette(page: Page) {
|
||||||
await defocus(page)
|
await defocus(page)
|
||||||
await page.keyboard.press(`${modKey}+${key}`)
|
await page.keyboard.press(`${modKey}+P`)
|
||||||
|
|
||||||
const dialog = page.getByRole("dialog")
|
const dialog = page.getByRole("dialog")
|
||||||
await expect(dialog).toBeVisible()
|
await expect(dialog).toBeVisible()
|
||||||
@@ -207,49 +196,9 @@ export async function closeDialog(page: Page, dialog: Locator) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function isSidebarClosed(page: Page) {
|
export async function isSidebarClosed(page: Page) {
|
||||||
const button = await waitSidebarButton(page, "isSidebarClosed")
|
|
||||||
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 button = page.getByRole("button", { name: /toggle sidebar/i }).first()
|
||||||
const boundary = page.getByRole("heading", { name: /something went wrong/i }).first()
|
await expect(button).toBeVisible()
|
||||||
await button.or(boundary).first().waitFor({ state: "visible", timeout: 10_000 })
|
return (await button.getAttribute("aria-expanded")) !== "true"
|
||||||
await assertHealthy(page, context)
|
|
||||||
return button
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function toggleSidebar(page: Page) {
|
export async function toggleSidebar(page: Page) {
|
||||||
@@ -260,7 +209,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 = await waitSidebarButton(page, "openSidebar")
|
const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
|
||||||
await button.click()
|
await button.click()
|
||||||
|
|
||||||
const opened = await expect(button)
|
const opened = await expect(button)
|
||||||
@@ -277,7 +226,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 = await waitSidebarButton(page, "closeSidebar")
|
const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
|
||||||
await button.click()
|
await button.click()
|
||||||
|
|
||||||
const closed = await expect(button)
|
const closed = await expect(button)
|
||||||
@@ -292,7 +241,6 @@ 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")
|
||||||
@@ -305,8 +253,6 @@ 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
|
||||||
@@ -368,12 +314,10 @@ 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\n${id}\n`)
|
await fs.writeFile(path.join(root, "README.md"), "# e2e\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', {
|
||||||
@@ -395,24 +339,12 @@ 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 ""
|
||||||
@@ -442,7 +374,6 @@ export async function waitDir(page: Page, directory: string) {
|
|||||||
await expect
|
await expect
|
||||||
.poll(
|
.poll(
|
||||||
async () => {
|
async () => {
|
||||||
await assertHealthy(page, "waitDir")
|
|
||||||
const slug = slugFromUrl(page.url())
|
const slug = slugFromUrl(page.url())
|
||||||
if (!slug) return ""
|
if (!slug) return ""
|
||||||
return resolveSlug(slug)
|
return resolveSlug(slug)
|
||||||
@@ -455,69 +386,6 @@ export async function waitDir(page: Page, directory: string) {
|
|||||||
return { directory: target, slug: base64Encode(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]
|
||||||
@@ -929,14 +797,8 @@ 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)
|
||||||
@@ -945,7 +807,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({ force: true, timeout: 1500 })
|
.click({ 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 { closeDialog, openPalette } from "../actions"
|
import { 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,12 +9,3 @@ 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,16 +1,7 @@
|
|||||||
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 {
|
import { cleanupSession, cleanupTestProject, createTestProject, seedProjects, sessionIDFromUrl } from "./actions"
|
||||||
healthPhase,
|
import { promptSelector } from "./selectors"
|
||||||
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"
|
||||||
@@ -36,29 +27,6 @@ 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()
|
||||||
@@ -80,20 +48,21 @@ 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 waitSession(page, { directory, sessionID })
|
await expect(page.locator(promptSelector)).toBeVisible()
|
||||||
}
|
}
|
||||||
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 waitSession(page, { directory: root, sessionID })
|
await expect(page.locator(promptSelector)).toBeVisible()
|
||||||
const current = sessionIDFromUrl(page.url())
|
const current = sessionIDFromUrl(page.url())
|
||||||
if (current) trackSession(current)
|
if (current) trackSession(current)
|
||||||
}
|
}
|
||||||
@@ -108,16 +77,13 @@ 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,4 +1,5 @@
|
|||||||
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 {
|
import {
|
||||||
defocus,
|
defocus,
|
||||||
@@ -6,14 +7,43 @@ import {
|
|||||||
cleanupTestProject,
|
cleanupTestProject,
|
||||||
openSidebar,
|
openSidebar,
|
||||||
sessionIDFromUrl,
|
sessionIDFromUrl,
|
||||||
setWorkspacesEnabled,
|
waitDir,
|
||||||
waitSession,
|
|
||||||
waitSessionSaved,
|
|
||||||
waitSlug,
|
waitSlug,
|
||||||
} from "../actions"
|
} 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 })
|
||||||
|
|
||||||
@@ -54,7 +84,9 @@ 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 setWorkspacesEnabled(page, slug, true)
|
await workspaces(page, directory, 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()
|
||||||
|
|
||||||
@@ -76,7 +108,8 @@ 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 })
|
||||||
|
|
||||||
await waitSession(page, { directory: space })
|
await waitSlug(page)
|
||||||
|
await waitDir(page, space)
|
||||||
|
|
||||||
// Create a session by sending a prompt
|
// Create a session by sending a prompt
|
||||||
const prompt = page.locator(promptSelector)
|
const prompt = page.locator(promptSelector)
|
||||||
@@ -90,7 +123,6 @@ 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}(?:[/?#]|$)`))
|
||||||
|
|
||||||
@@ -98,14 +130,15 @@ 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({ force: true })
|
await otherButton.click()
|
||||||
await waitSession(page, { directory: other })
|
await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`))
|
||||||
|
|
||||||
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({ force: true })
|
await rootButton.click()
|
||||||
|
|
||||||
await waitSession(page, { directory: space, sessionID: created })
|
await waitDir(page, space)
|
||||||
|
await expect.poll(() => sessionIDFromUrl(page.url()) ?? "").toBe(created)
|
||||||
await expect(page).toHaveURL(new RegExp(`/session/${created}(?:[/?#]|$)`))
|
await expect(page).toHaveURL(new RegExp(`/session/${created}(?:[/?#]|$)`))
|
||||||
},
|
},
|
||||||
{ extra: [other] },
|
{ extra: [other] },
|
||||||
|
|||||||
@@ -1,17 +1,8 @@
|
|||||||
import type { Page } from "@playwright/test"
|
import type { Page } from "@playwright/test"
|
||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import {
|
import { openSidebar, resolveSlug, sessionIDFromUrl, setWorkspacesEnabled, waitDir, waitSlug } from "../actions"
|
||||||
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, dirDecode, dirSlug, resolveDirectory } from "../utils"
|
||||||
|
|
||||||
function item(space: { slug: string; raw: string }) {
|
function item(space: { slug: string; raw: string }) {
|
||||||
return `${workspaceItemSelector(space.slug)}, ${workspaceItemSelector(space.raw)}`
|
return `${workspaceItemSelector(space.slug)}, ${workspaceItemSelector(space.raw)}`
|
||||||
@@ -23,7 +14,20 @@ function button(space: { slug: string; raw: string }) {
|
|||||||
|
|
||||||
async function waitWorkspaceReady(page: Page, space: { slug: string; raw: string }) {
|
async function waitWorkspaceReady(page: Page, space: { slug: string; raw: string }) {
|
||||||
await openSidebar(page)
|
await openSidebar(page)
|
||||||
await expect(page.locator(item(space)).first()).toBeVisible({ timeout: 60_000 })
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const row = page.locator(item(space)).first()
|
||||||
|
try {
|
||||||
|
await row.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[]) {
|
||||||
@@ -45,8 +49,7 @@ async function openWorkspaceNewSession(page: Page, space: { slug: string; raw: s
|
|||||||
await expect(next).toBeVisible()
|
await expect(next).toBeVisible()
|
||||||
await next.click({ force: true })
|
await next.click({ force: true })
|
||||||
|
|
||||||
await waitSession(page, { directory: space.directory })
|
return waitDir(page, space.directory)
|
||||||
await expect.poll(() => sessionIDFromUrl(page.url()) ?? "").toBe("")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createSessionFromWorkspace(
|
async function createSessionFromWorkspace(
|
||||||
@@ -54,28 +57,39 @@ async function createSessionFromWorkspace(
|
|||||||
space: { slug: string; raw: string; directory: string },
|
space: { slug: string; raw: string; directory: string },
|
||||||
text: string,
|
text: string,
|
||||||
) {
|
) {
|
||||||
await openWorkspaceNewSession(page, space)
|
const next = 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 page.keyboard.press("Enter")
|
await expect.poll(async () => ((await prompt.textContent()) ?? "").trim()).toContain(text)
|
||||||
|
await prompt.press("Enter")
|
||||||
|
|
||||||
|
await waitDir(page, next.directory)
|
||||||
|
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(`/session/${sessionID}(?:[/?#]|$)`))
|
||||||
|
return { sessionID, slug: next.slug }
|
||||||
|
}
|
||||||
|
|
||||||
await waitSessionSaved(space.directory, sessionID)
|
async function sessionDirectory(directory: string, sessionID: string) {
|
||||||
await createSdk(space.directory)
|
const info = await createSdk(directory)
|
||||||
.session.abort({ sessionID })
|
.session.get({ sessionID })
|
||||||
|
.then((x) => x.data)
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
return sessionID
|
if (!info) return ""
|
||||||
|
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 ({ slug: root, trackDirectory, trackSession }) => {
|
await withProject(async ({ directory, slug: root, trackSession, trackDirectory }) => {
|
||||||
await openSidebar(page)
|
await openSidebar(page)
|
||||||
await setWorkspacesEnabled(page, root, true)
|
await setWorkspacesEnabled(page, root, true)
|
||||||
|
|
||||||
@@ -87,8 +101,17 @@ test("new sessions from sidebar workspace actions stay in selected workspace", a
|
|||||||
trackDirectory(second.directory)
|
trackDirectory(second.directory)
|
||||||
await waitWorkspaceReady(page, second)
|
await waitWorkspaceReady(page, second)
|
||||||
|
|
||||||
trackSession(await createSessionFromWorkspace(page, first, `workspace one ${Date.now()}`), first.directory)
|
const firstSession = await createSessionFromWorkspace(page, first, `workspace one ${Date.now()}`)
|
||||||
trackSession(await createSessionFromWorkspace(page, second, `workspace two ${Date.now()}`), second.directory)
|
trackSession(firstSession.sessionID, first.directory)
|
||||||
trackSession(await createSessionFromWorkspace(page, first, `workspace one again ${Date.now()}`), first.directory)
|
|
||||||
|
const secondSession = await createSessionFromWorkspace(page, second, `workspace two ${Date.now()}`)
|
||||||
|
trackSession(secondSession.sessionID, second.directory)
|
||||||
|
|
||||||
|
const thirdSession = await createSessionFromWorkspace(page, first, `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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
waitSlug,
|
waitSlug,
|
||||||
} from "../actions"
|
} from "../actions"
|
||||||
import { dropdownMenuContentSelector, inlineInputSelector, workspaceItemSelector } from "../selectors"
|
import { dropdownMenuContentSelector, inlineInputSelector, workspaceItemSelector } from "../selectors"
|
||||||
import { createSdk, dirSlug } from "../utils"
|
import { createSdk, dirDecode, dirSlug } from "../utils"
|
||||||
|
|
||||||
async function setupWorkspaceTest(page: Page, project: { slug: string }) {
|
async function setupWorkspaceTest(page: Page, project: { slug: string }) {
|
||||||
const rootSlug = project.slug
|
const rootSlug = project.slug
|
||||||
@@ -258,7 +258,7 @@ test("can delete a workspace", async ({ page, withProject }) => {
|
|||||||
await clickMenuItem(menu, /^Delete$/i, { force: true })
|
await clickMenuItem(menu, /^Delete$/i, { force: true })
|
||||||
await confirmDialog(page, /^Delete workspace$/i)
|
await confirmDialog(page, /^Delete workspace$/i)
|
||||||
|
|
||||||
await expect.poll(() => base64Decode(slugFromUrl(page.url()))).toBe(project.directory)
|
await expect.poll(() => dirDecode(slugFromUrl(page.url()))).toBe(project.directory)
|
||||||
|
|
||||||
await expect
|
await expect
|
||||||
.poll(
|
.poll(
|
||||||
|
|||||||
@@ -108,10 +108,7 @@ 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)
|
||||||
|
|
||||||
// Clear the draft before navigating history (ArrowUp only works when prompt is empty)
|
await edge(page, "start")
|
||||||
await prompt.fill("")
|
|
||||||
await wait(page, "")
|
|
||||||
|
|
||||||
await page.keyboard.press("ArrowUp")
|
await page.keyboard.press("ArrowUp")
|
||||||
await wait(page, second)
|
await wait(page, second)
|
||||||
|
|
||||||
@@ -122,7 +119,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, "")
|
await wait(page, draft)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
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 {
|
import { openSidebar, resolveSlug, sessionIDFromUrl, setWorkspacesEnabled, waitSessionIdle, waitSlug } from "../actions"
|
||||||
openSidebar,
|
|
||||||
resolveSlug,
|
|
||||||
sessionIDFromUrl,
|
|
||||||
setWorkspacesEnabled,
|
|
||||||
waitSession,
|
|
||||||
waitSessionIdle,
|
|
||||||
waitSlug,
|
|
||||||
} from "../actions"
|
|
||||||
import {
|
import {
|
||||||
promptAgentSelector,
|
promptAgentSelector,
|
||||||
promptModelSelector,
|
promptModelSelector,
|
||||||
@@ -37,6 +29,8 @@ 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 & {
|
||||||
@@ -50,6 +44,21 @@ 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()),
|
||||||
@@ -178,7 +187,8 @@ 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 waitSession(page, { directory, sessionID })
|
await expect(page.locator(promptSelector)).toBeVisible()
|
||||||
|
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) {
|
||||||
@@ -214,7 +224,7 @@ async function createWorkspace(page: Page, root: string, seen: string[]) {
|
|||||||
await page.getByRole("button", { name: "New workspace" }).first().click()
|
await page.getByRole("button", { name: "New workspace" }).first().click()
|
||||||
|
|
||||||
const next = await resolveSlug(await waitSlug(page, [root, ...seen]))
|
const next = await resolveSlug(await waitSlug(page, [root, ...seen]))
|
||||||
await waitSession(page, { directory: next.directory })
|
await expect(page).toHaveURL(new RegExp(`/${next.slug}/session(?:[/?#]|$)`))
|
||||||
return next
|
return next
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,7 +256,9 @@ async function newWorkspaceSession(page: Page, slug: string) {
|
|||||||
await button.click({ force: true })
|
await button.click({ force: true })
|
||||||
|
|
||||||
const next = await resolveSlug(await waitSlug(page))
|
const next = await resolveSlug(await waitSlug(page))
|
||||||
return waitSession(page, { directory: next.directory }).then((item) => item.directory)
|
await expect(page).toHaveURL(new RegExp(`/${next.slug}/session(?:[/?#]|$)`))
|
||||||
|
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 ({
|
||||||
@@ -265,7 +277,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 waitSession(page, { directory, sessionID: first })
|
await expect(page.locator(promptSelector)).toBeVisible()
|
||||||
await waitFooter(page, firstState)
|
await waitFooter(page, firstState)
|
||||||
|
|
||||||
await gotoSession()
|
await gotoSession()
|
||||||
|
|||||||
@@ -169,70 +169,6 @@ 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)
|
||||||
|
|
||||||
@@ -282,56 +218,6 @@ 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("K")
|
expect(initialKeybind).toContain("P")
|
||||||
|
|
||||||
await keybindButton.click()
|
await keybindButton.click()
|
||||||
await expect(keybindButton).toHaveText(/press/i)
|
await expect(keybindButton).toHaveText(/press/i)
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import {
|
import { cleanupSession, closeSidebar, hoverSessionItem } from "../actions"
|
||||||
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()
|
||||||
@@ -47,72 +37,3 @@ 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 { dropdownMenuContentSelector, terminalSelector } from "../selectors"
|
import { terminalSelector } from "../selectors"
|
||||||
import { terminalToggleKey, workspacePersistKey } from "../utils"
|
import { terminalToggleKey, workspacePersistKey } from "../utils"
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
@@ -130,39 +130,3 @@ 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,5 +1,5 @@
|
|||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||||
import { base64Encode, checksum } from "@opencode-ai/util/encode"
|
import { base64Decode, base64Encode, checksum } from "@opencode-ai/util/encode"
|
||||||
|
|
||||||
export const serverHost = process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"
|
export const serverHost = process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"
|
||||||
export const serverPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"
|
export const serverPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"
|
||||||
@@ -48,6 +48,10 @@ export function dirSlug(directory: string) {
|
|||||||
return base64Encode(directory)
|
return base64Encode(directory)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function dirDecode(slug: string) {
|
||||||
|
return base64Decode(slug)
|
||||||
|
}
|
||||||
|
|
||||||
export function dirPath(directory: string) {
|
export function dirPath(directory: string) {
|
||||||
return `/${dirSlug(directory)}`
|
return `/${dirSlug(directory)}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.3.2",
|
"version": "1.2.27",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -51,11 +51,9 @@
|
|||||||
"@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:",
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ import { MarkedProvider } from "@opencode-ai/ui/context/marked"
|
|||||||
import { File } from "@opencode-ai/ui/file"
|
import { File } from "@opencode-ai/ui/file"
|
||||||
import { Font } from "@opencode-ai/ui/font"
|
import { Font } from "@opencode-ai/ui/font"
|
||||||
import { Splash } from "@opencode-ai/ui/logo"
|
import { Splash } from "@opencode-ai/ui/logo"
|
||||||
import { ThemeProvider } from "@opencode-ai/ui/theme/context"
|
import { ThemeProvider } from "@opencode-ai/ui/theme"
|
||||||
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,
|
||||||
@@ -32,7 +31,7 @@ import { FileProvider } from "@/context/file"
|
|||||||
import { GlobalSDKProvider } from "@/context/global-sdk"
|
import { GlobalSDKProvider } from "@/context/global-sdk"
|
||||||
import { GlobalSyncProvider } from "@/context/global-sync"
|
import { GlobalSyncProvider } from "@/context/global-sync"
|
||||||
import { HighlightsProvider } from "@/context/highlights"
|
import { HighlightsProvider } from "@/context/highlights"
|
||||||
import { LanguageProvider, type Locale, useLanguage } from "@/context/language"
|
import { LanguageProvider, useLanguage } from "@/context/language"
|
||||||
import { LayoutProvider } from "@/context/layout"
|
import { LayoutProvider } from "@/context/layout"
|
||||||
import { ModelsProvider } from "@/context/models"
|
import { ModelsProvider } from "@/context/models"
|
||||||
import { NotificationProvider } from "@/context/notification"
|
import { NotificationProvider } from "@/context/notification"
|
||||||
@@ -82,11 +81,6 @@ 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>
|
||||||
@@ -130,7 +124,7 @@ function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
|
export function AppBaseProviders(props: ParentProps) {
|
||||||
return (
|
return (
|
||||||
<MetaProvider>
|
<MetaProvider>
|
||||||
<Font />
|
<Font />
|
||||||
@@ -139,16 +133,14 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
|
|||||||
void window.api?.setTitlebar?.({ mode })
|
void window.api?.setTitlebar?.({ mode })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LanguageProvider locale={props.locale}>
|
<LanguageProvider>
|
||||||
<UiI18nBridge>
|
<UiI18nBridge>
|
||||||
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
|
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
|
||||||
<QueryProvider>
|
<DialogProvider>
|
||||||
<DialogProvider>
|
<MarkedProviderWithNativeParser>
|
||||||
<MarkedProviderWithNativeParser>
|
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
|
||||||
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
|
</MarkedProviderWithNativeParser>
|
||||||
</MarkedProviderWithNativeParser>
|
</DialogProvider>
|
||||||
</DialogProvider>
|
|
||||||
</QueryProvider>
|
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</UiI18nBridge>
|
</UiI18nBridge>
|
||||||
</LanguageProvider>
|
</LanguageProvider>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ function Cell(props: { bad?: boolean; dim?: boolean; label: string; tip: string;
|
|||||||
<Tooltip value={props.tip} placement="top">
|
<Tooltip value={props.tip} placement="top">
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
"flex min-h-[42px] w-full min-w-0 flex-col items-center justify-center rounded-[8px] px-0.5 py-1 text-center": true,
|
"flex min-h-[42px] w-full min-w-0 flex-col items-center justify-center rounded-[8px] bg-white/5 px-0.5 py-1 text-center": true,
|
||||||
"col-span-2": !!props.wide,
|
"col-span-2": !!props.wide,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -363,7 +363,11 @@ export function DebugBar() {
|
|||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
aria-label={language.t("debugBar.ariaLabel")}
|
aria-label={language.t("debugBar.ariaLabel")}
|
||||||
class="pointer-events-auto fixed bottom-3 right-3 z-50 w-[308px] max-w-[calc(100vw-1.5rem)] overflow-hidden rounded-xl border border-border-base bg-surface-raised-stronger-non-alpha p-0.5 text-text-strong shadow-[var(--shadow-lg-border-base)] sm:bottom-4 sm:right-4 sm:w-[324px]"
|
class="pointer-events-auto fixed bottom-3 right-3 z-50 w-[308px] max-w-[calc(100vw-1.5rem)] overflow-hidden rounded-xl border p-0.5 text-text-on-interactive-base shadow-[var(--shadow-lg-border-base)] sm:bottom-4 sm:right-4 sm:w-[324px]"
|
||||||
|
style={{
|
||||||
|
"background-color": "color-mix(in srgb, var(--icon-interactive-base) 42%, black)",
|
||||||
|
"border-color": "color-mix(in srgb, white 14%, transparent)",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div class="grid grid-cols-5 gap-px font-mono">
|
<div class="grid grid-cols-5 gap-px font-mono">
|
||||||
<Cell
|
<Cell
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ProviderAuthAuthorization, ProviderAuthMethod } from "@opencode-ai/sdk/v2/client"
|
import type { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
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"
|
||||||
@@ -9,12 +9,13 @@ import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
|||||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||||
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 { createEffect, createMemo, createResource, 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 { 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 }) {
|
||||||
@@ -34,25 +35,15 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === props.provider)!)
|
const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === props.provider)!)
|
||||||
const fallback = createMemo<ProviderAuthMethod[]>(() => [
|
const methods = createMemo(
|
||||||
{
|
() =>
|
||||||
type: "api" as const,
|
globalSync.data.provider_auth[props.provider] ?? [
|
||||||
label: language.t("provider.connect.method.apiKey"),
|
{
|
||||||
},
|
type: "api",
|
||||||
])
|
label: language.t("provider.connect.method.apiKey"),
|
||||||
const [auth] = createResource(
|
},
|
||||||
() => props.provider,
|
],
|
||||||
async () => {
|
|
||||||
const cached = globalSync.data.provider_auth[props.provider]
|
|
||||||
if (cached) return cached
|
|
||||||
const res = await globalSDK.client.provider.auth()
|
|
||||||
if (!alive.value) return fallback()
|
|
||||||
globalSync.set("provider_auth", res.data ?? {})
|
|
||||||
return res.data?.[props.provider] ?? fallback()
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
const loading = createMemo(() => auth.loading && !globalSync.data.provider_auth[props.provider])
|
|
||||||
const methods = createMemo(() => auth.latest ?? globalSync.data.provider_auth[props.provider] ?? fallback())
|
|
||||||
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,
|
||||||
@@ -187,11 +178,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
index: 0,
|
index: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
const prompts = createMemo<NonNullable<ProviderAuthMethod["prompts"]>>(() => {
|
const prompts = createMemo(() => method()?.prompts ?? [])
|
||||||
const value = method()
|
|
||||||
if (value?.type !== "oauth") return []
|
|
||||||
return value.prompts ?? []
|
|
||||||
})
|
|
||||||
const matches = (prompt: NonNullable<ReturnType<typeof prompts>[number]>, value: Record<string, string>) => {
|
const matches = (prompt: NonNullable<ReturnType<typeof prompts>[number]>, value: Record<string, string>) => {
|
||||||
if (!prompt.when) return true
|
if (!prompt.when) return true
|
||||||
const actual = value[prompt.when.key]
|
const actual = value[prompt.when.key]
|
||||||
@@ -310,12 +297,8 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
listRef?.onKeyDown(e)
|
listRef?.onKeyDown(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
let auto = false
|
onMount(() => {
|
||||||
createEffect(() => {
|
|
||||||
if (auto) return
|
|
||||||
if (loading()) return
|
|
||||||
if (methods().length === 1) {
|
if (methods().length === 1) {
|
||||||
auto = true
|
|
||||||
selectMethod(0)
|
selectMethod(0)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -591,14 +574,6 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
<div class="px-2.5 pb-10 flex flex-col gap-6">
|
<div class="px-2.5 pb-10 flex flex-col gap-6">
|
||||||
<div onKeyDown={handleKey} tabIndex={0} autofocus={store.methodIndex === undefined ? true : undefined}>
|
<div onKeyDown={handleKey} tabIndex={0} autofocus={store.methodIndex === undefined ? true : undefined}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={loading()}>
|
|
||||||
<div class="text-14-regular text-text-base">
|
|
||||||
<div class="flex items-center gap-x-2">
|
|
||||||
<Spinner />
|
|
||||||
<span>{language.t("provider.connect.status.inProgress")}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Match>
|
|
||||||
<Match when={store.methodIndex === undefined}>
|
<Match when={store.methodIndex === undefined}>
|
||||||
<MethodSelection />
|
<MethodSelection />
|
||||||
</Match>
|
</Match>
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ 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,6 +16,7 @@ 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,
|
||||||
@@ -59,6 +60,7 @@ 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,7 +3,6 @@ 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"
|
||||||
@@ -32,6 +31,7 @@ export function DialogCustomProvider(props: Props) {
|
|||||||
apiKey: "",
|
apiKey: "",
|
||||||
models: [modelRow()],
|
models: [modelRow()],
|
||||||
headers: [headerRow()],
|
headers: [headerRow()],
|
||||||
|
saving: false,
|
||||||
err: {},
|
err: {},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -116,49 +116,48 @@ export function DialogCustomProvider(props: Props) {
|
|||||||
return output.result
|
return output.result
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveMutation = useMutation(() => ({
|
const save = async (e: SubmitEvent) => {
|
||||||
mutationFn: async (result: NonNullable<ReturnType<typeof validate>>) => {
|
e.preventDefault()
|
||||||
const disabledProviders = globalSync.data.config.disabled_providers ?? []
|
if (form.saving) return
|
||||||
const nextDisabled = disabledProviders.filter((id) => id !== result.providerID)
|
|
||||||
|
|
||||||
if (result.key) {
|
const result = validate()
|
||||||
await globalSDK.client.auth.set({
|
if (!result) return
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
await globalSync.updateConfig({
|
auth
|
||||||
provider: { [result.providerID]: result.config },
|
.then(() =>
|
||||||
disabled_providers: nextDisabled,
|
globalSync.updateConfig({ provider: { [result.providerID]: result.config }, 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 }),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
return result
|
.catch((err: unknown) => {
|
||||||
},
|
const message = err instanceof Error ? err.message : String(err)
|
||||||
onSuccess: (result) => {
|
showToast({ title: language.t("common.requestFailed"), description: message })
|
||||||
dialog.close()
|
})
|
||||||
showToast({
|
.finally(() => {
|
||||||
variant: "success",
|
setForm("saving", false)
|
||||||
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 (
|
||||||
@@ -313,14 +312,8 @@ export function DialogCustomProvider(props: Props) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button class="w-auto self-start" type="submit" size="large" variant="primary" disabled={form.saving}>
|
||||||
class="w-auto self-start"
|
{form.saving ? language.t("common.saving") : language.t("common.submit")}
|
||||||
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,7 +2,6 @@ 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"
|
||||||
@@ -29,6 +28,7 @@ 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,37 +71,38 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
setStore("iconUrl", "")
|
setStore("iconUrl", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveMutation = useMutation(() => ({
|
async function handleSubmit(e: SubmitEvent) {
|
||||||
mutationFn: async () => {
|
|
||||||
const name = store.name.trim() === folderName() ? "" : store.name.trim()
|
|
||||||
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,
|
|
||||||
icon: { color: store.color, override: store.iconUrl || undefined },
|
|
||||||
commands: { start: start || undefined },
|
|
||||||
})
|
|
||||||
dialog.close()
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
function handleSubmit(e: SubmitEvent) {
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (saveMutation.isPending) return
|
|
||||||
saveMutation.mutate()
|
await Promise.resolve()
|
||||||
|
.then(async () => {
|
||||||
|
setStore("saving", true)
|
||||||
|
const name = store.name.trim() === folderName() ? "" : store.name.trim()
|
||||||
|
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,
|
||||||
|
icon: { color: store.color, override: store.iconUrl || undefined },
|
||||||
|
commands: { start: start || undefined },
|
||||||
|
})
|
||||||
|
dialog.close()
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setStore("saving", false)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -245,8 +246,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={saveMutation.isPending}>
|
<Button type="submit" variant="primary" size="large" disabled={store.saving}>
|
||||||
{saveMutation.isPending ? language.t("common.saving") : language.t("common.save")}
|
{store.saving ? language.t("common.saving") : language.t("common.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useMutation } from "@tanstack/solid-query"
|
import { Component, createMemo, createSignal, Show } from "solid-js"
|
||||||
import { Component, createMemo, Show } from "solid-js"
|
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
@@ -18,6 +17,7 @@ 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,8 +25,10 @@ export const DialogSelectMcp: Component = () => {
|
|||||||
.sort((a, b) => a.name.localeCompare(b.name)),
|
.sort((a, b) => a.name.localeCompare(b.name)),
|
||||||
)
|
)
|
||||||
|
|
||||||
const toggle = useMutation(() => ({
|
const toggle = async (name: string) => {
|
||||||
mutationFn: async (name: string) => {
|
if (loading()) return
|
||||||
|
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 })
|
||||||
@@ -36,8 +38,10 @@ 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)
|
||||||
@@ -55,8 +59,7 @@ 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.isPending) return
|
if (x) toggle(x.name)
|
||||||
toggle.mutate(x.name)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(i) => {
|
{(i) => {
|
||||||
@@ -80,7 +83,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={toggle.isPending && toggle.variables === i.name}>
|
<Show when={loading() === 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>
|
||||||
@@ -89,14 +92,7 @@ export const DialogSelectMcp: Component = () => {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<div onClick={(e) => e.stopPropagation()}>
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
<Switch
|
<Switch checked={enabled()} disabled={loading() === i.name} onChange={() => toggle(i.name)} />
|
||||||
checked={enabled()}
|
|
||||||
disabled={toggle.isPending && toggle.variables === i.name}
|
|
||||||
onChange={() => {
|
|
||||||
if (toggle.isPending) return
|
|
||||||
toggle.mutate(i.name)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ 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"
|
||||||
@@ -187,6 +186,7 @@ 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,6 +198,7 @@ export function DialogSelectServer() {
|
|||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
error: "",
|
error: "",
|
||||||
|
busy: false,
|
||||||
status: undefined as boolean | undefined,
|
status: undefined as boolean | undefined,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -208,6 +209,7 @@ 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,
|
||||||
@@ -222,78 +224,10 @@ 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)
|
||||||
@@ -362,7 +296,7 @@ export function DialogSelectServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleAddChange = (value: string) => {
|
const handleAddChange = (value: string) => {
|
||||||
if (addMutation.isPending) return
|
if (store.addServer.adding) 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 }),
|
||||||
@@ -370,12 +304,12 @@ export function DialogSelectServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleAddNameChange = (value: string) => {
|
const handleAddNameChange = (value: string) => {
|
||||||
if (addMutation.isPending) return
|
if (store.addServer.adding) return
|
||||||
setStore("addServer", { name: value, error: "" })
|
setStore("addServer", { name: value, error: "" })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAddUsernameChange = (value: string) => {
|
const handleAddUsernameChange = (value: string) => {
|
||||||
if (addMutation.isPending) return
|
if (store.addServer.adding) 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 }),
|
||||||
@@ -383,7 +317,7 @@ export function DialogSelectServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleAddPasswordChange = (value: string) => {
|
const handleAddPasswordChange = (value: string) => {
|
||||||
if (addMutation.isPending) return
|
if (store.addServer.adding) 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 }),
|
||||||
@@ -391,7 +325,7 @@ export function DialogSelectServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleEditChange = (value: string) => {
|
const handleEditChange = (value: string) => {
|
||||||
if (editMutation.isPending) return
|
if (store.editServer.busy) 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 }),
|
||||||
@@ -399,12 +333,12 @@ export function DialogSelectServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleEditNameChange = (value: string) => {
|
const handleEditNameChange = (value: string) => {
|
||||||
if (editMutation.isPending) return
|
if (store.editServer.busy) return
|
||||||
setStore("editServer", { name: value, error: "" })
|
setStore("editServer", { name: value, error: "" })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEditUsernameChange = (value: string) => {
|
const handleEditUsernameChange = (value: string) => {
|
||||||
if (editMutation.isPending) return
|
if (store.editServer.busy) 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 }),
|
||||||
@@ -412,13 +346,85 @@ export function DialogSelectServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleEditPasswordChange = (value: string) => {
|
const handleEditPasswordChange = (value: string) => {
|
||||||
if (editMutation.isPending) return
|
if (store.editServer.busy) 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"
|
||||||
@@ -458,26 +464,23 @@ 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") {
|
||||||
if (addMutation.isPending) return
|
void handleAdd(store.addServer.url)
|
||||||
setStore("addServer", { error: "" })
|
|
||||||
addMutation.mutate(store.addServer.url)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const original = editing()
|
const original = editing()
|
||||||
if (!original) return
|
if (!original) return
|
||||||
if (editMutation.isPending) return
|
void handleEdit(original, store.editServer.value)
|
||||||
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() ? addMutation.isPending : editMutation.isPending))
|
const formBusy = createMemo(() => (isAddMode() ? store.addServer.adding : store.editServer.busy))
|
||||||
|
|
||||||
const formTitle = createMemo(() => {
|
const formTitle = createMemo(() => {
|
||||||
if (!isFormMode()) return language.t("dialog.server.title")
|
if (!isFormMode()) return language.t("dialog.server.title")
|
||||||
|
|||||||
@@ -572,7 +572,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const open = recent()
|
const open = recent()
|
||||||
const seen = new Set(open)
|
const seen = new Set(open)
|
||||||
const pinned: AtOption[] = open.map((path) => ({ type: "file", path, display: path, recent: true }))
|
const pinned: AtOption[] = open.map((path) => ({ type: "file", path, display: path, recent: true }))
|
||||||
if (!query.trim()) return [...agents, ...pinned]
|
|
||||||
const paths = await files.searchFilesAndDirectories(query)
|
const paths = await files.searchFilesAndDirectories(query)
|
||||||
const fileOptions: AtOption[] = paths
|
const fileOptions: AtOption[] = paths
|
||||||
.filter((path) => !seen.has(path))
|
.filter((path) => !seen.has(path))
|
||||||
@@ -1044,7 +1043,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const { addAttachments, removeAttachment, handlePaste } = createPromptAttachments({
|
const { addAttachment, removeAttachment, handlePaste } = createPromptAttachments({
|
||||||
editor: () => editorRef,
|
editor: () => editorRef,
|
||||||
isDialogActive: () => !!dialog.active,
|
isDialogActive: () => !!dialog.active,
|
||||||
setDraggingType: (type) => setStore("draggingType", type),
|
setDraggingType: (type) => setStore("draggingType", type),
|
||||||
@@ -1384,12 +1383,11 @@ 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 list = e.currentTarget.files
|
const file = e.currentTarget.files?.[0]
|
||||||
if (list) void addAttachments(Array.from(list))
|
if (file) void addAttachment(file)
|
||||||
e.currentTarget.value = ""
|
e.currentTarget.value = ""
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -1498,7 +1496,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<Show when={local.model.current()?.provider?.id}>
|
<Show when={local.model.current()?.provider?.id}>
|
||||||
<ProviderIcon
|
<ProviderIcon
|
||||||
id={local.model.current()?.provider?.id ?? ""}
|
id={local.model.current()!.provider.id}
|
||||||
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
||||||
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
||||||
/>
|
/>
|
||||||
@@ -1530,7 +1528,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<Show when={local.model.current()?.provider?.id}>
|
<Show when={local.model.current()?.provider?.id}>
|
||||||
<ProviderIcon
|
<ProviderIcon
|
||||||
id={local.model.current()?.provider?.id ?? ""}
|
id={local.model.current()!.provider.id}
|
||||||
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
||||||
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -71,18 +71,6 @@ 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)
|
||||||
@@ -96,14 +84,18 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
|||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
const files = Array.from(clipboardData.items).flatMap((item) => {
|
const items = Array.from(clipboardData.items)
|
||||||
if (item.kind !== "file") return []
|
const fileItems = items.filter((item) => item.kind === "file")
|
||||||
const file = item.getAsFile()
|
|
||||||
return file ? [file] : []
|
|
||||||
})
|
|
||||||
|
|
||||||
if (files.length > 0) {
|
if (fileItems.length > 0) {
|
||||||
await addAttachments(files)
|
let found = false
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +169,12 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
|||||||
const dropped = event.dataTransfer?.files
|
const dropped = event.dataTransfer?.files
|
||||||
if (!dropped) return
|
if (!dropped) return
|
||||||
|
|
||||||
await addAttachments(Array.from(dropped))
|
let found = false
|
||||||
|
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(() => {
|
||||||
@@ -194,7 +191,6 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
addAttachment,
|
addAttachment,
|
||||||
addAttachments,
|
|
||||||
removeAttachment,
|
removeAttachment,
|
||||||
handlePaste,
|
handlePaste,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,32 +49,6 @@ 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,6 +1,4 @@
|
|||||||
import { ACCEPTED_FILE_TYPES, ACCEPTED_IMAGE_TYPES } from "@/constants/file-picker"
|
export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
|
||||||
|
|
||||||
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([
|
||||||
@@ -20,6 +18,61 @@ 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(false)
|
expect(canNavigateHistoryAtCursor("up", value, 0)).toBe(true)
|
||||||
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,14 +135,11 @@ 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(false)
|
expect(canNavigateHistoryAtCursor("up", "abc", 0)).toBe(true)
|
||||||
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 && text.length === 0
|
if (direction === "up") return position === 0
|
||||||
return position === text.length
|
return position === text.length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -267,14 +267,14 @@ export function SessionContextTab() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
class="@container h-full"
|
class="@container h-full pb-10"
|
||||||
viewportRef={(el) => {
|
viewportRef={(el) => {
|
||||||
scroll = el
|
scroll = el
|
||||||
restoreScroll()
|
restoreScroll()
|
||||||
}}
|
}}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
>
|
>
|
||||||
<div class="px-6 pt-4 pb-10 flex flex-col gap-10">
|
<div class="px-6 pt-4 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,7 +24,6 @@ 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
|
||||||
@@ -169,14 +168,8 @@ 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={() => (editRequested = true)}>
|
<DropdownMenu.Item onSelect={edit}>
|
||||||
<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>
|
||||||
|
|||||||
@@ -1,41 +1,27 @@
|
|||||||
import { Component, Show, createMemo, createResource, onMount, type JSX } from "solid-js"
|
import { Component, Show, createMemo, createResource, type JSX } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Select } from "@opencode-ai/ui/select"
|
import { Select } from "@opencode-ai/ui/select"
|
||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { Switch } from "@opencode-ai/ui/switch"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context"
|
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useSettings, monoFontFamily } from "@/context/settings"
|
import { useSettings, monoFontFamily } from "@/context/settings"
|
||||||
import { playSoundById, SOUND_OPTIONS } from "@/utils/sound"
|
import { playSound, SOUND_OPTIONS } from "@/utils/sound"
|
||||||
import { Link } from "./link"
|
import { Link } from "./link"
|
||||||
import { SettingsList } from "./settings-list"
|
import { SettingsList } from "./settings-list"
|
||||||
|
|
||||||
let demoSoundState = {
|
let demoSoundState = {
|
||||||
cleanup: undefined as (() => void) | undefined,
|
cleanup: undefined as (() => void) | undefined,
|
||||||
timeout: undefined as NodeJS.Timeout | undefined,
|
timeout: undefined as NodeJS.Timeout | undefined,
|
||||||
run: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
type ThemeOption = {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
let font: Promise<typeof import("@opencode-ai/ui/font-loader")> | undefined
|
|
||||||
|
|
||||||
function loadFont() {
|
|
||||||
font ??= import("@opencode-ai/ui/font-loader")
|
|
||||||
return font
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// To prevent audio from overlapping/playing very quickly when navigating the settings menus,
|
// To prevent audio from overlapping/playing very quickly when navigating the settings menus,
|
||||||
// delay the playback by 100ms during quick selection changes and pause existing sounds.
|
// delay the playback by 100ms during quick selection changes and pause existing sounds.
|
||||||
const stopDemoSound = () => {
|
const stopDemoSound = () => {
|
||||||
demoSoundState.run += 1
|
|
||||||
if (demoSoundState.cleanup) {
|
if (demoSoundState.cleanup) {
|
||||||
demoSoundState.cleanup()
|
demoSoundState.cleanup()
|
||||||
}
|
}
|
||||||
@@ -43,19 +29,12 @@ const stopDemoSound = () => {
|
|||||||
demoSoundState.cleanup = undefined
|
demoSoundState.cleanup = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const playDemoSound = (id: string | undefined) => {
|
const playDemoSound = (src: string | undefined) => {
|
||||||
stopDemoSound()
|
stopDemoSound()
|
||||||
if (!id) return
|
if (!src) return
|
||||||
|
|
||||||
const run = ++demoSoundState.run
|
|
||||||
demoSoundState.timeout = setTimeout(() => {
|
demoSoundState.timeout = setTimeout(() => {
|
||||||
void playSoundById(id).then((cleanup) => {
|
demoSoundState.cleanup = playSound(src)
|
||||||
if (demoSoundState.run !== run) {
|
|
||||||
cleanup?.()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
demoSoundState.cleanup = cleanup
|
|
||||||
})
|
|
||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,10 +44,6 @@ export const SettingsGeneral: Component = () => {
|
|||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
void theme.loadThemes()
|
|
||||||
})
|
|
||||||
|
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
checking: false,
|
checking: false,
|
||||||
})
|
})
|
||||||
@@ -129,7 +104,9 @@ export const SettingsGeneral: Component = () => {
|
|||||||
.finally(() => setStore("checking", false))
|
.finally(() => setStore("checking", false))
|
||||||
}
|
}
|
||||||
|
|
||||||
const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
|
const themeOptions = createMemo(() =>
|
||||||
|
Object.entries(theme.themes()).map(([id, def]) => ({ id, name: def.name ?? id })),
|
||||||
|
)
|
||||||
|
|
||||||
const colorSchemeOptions = createMemo((): { value: ColorScheme; label: string }[] => [
|
const colorSchemeOptions = createMemo((): { value: ColorScheme; label: string }[] => [
|
||||||
{ value: "system", label: language.t("theme.scheme.system") },
|
{ value: "system", label: language.t("theme.scheme.system") },
|
||||||
@@ -166,7 +143,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
] as const
|
] as const
|
||||||
const fontOptionsList = [...fontOptions]
|
const fontOptionsList = [...fontOptions]
|
||||||
|
|
||||||
const noneSound = { id: "none", label: "sound.option.none" } as const
|
const noneSound = { id: "none", label: "sound.option.none", src: undefined } as const
|
||||||
const soundOptions = [noneSound, ...SOUND_OPTIONS]
|
const soundOptions = [noneSound, ...SOUND_OPTIONS]
|
||||||
|
|
||||||
const soundSelectProps = (
|
const soundSelectProps = (
|
||||||
@@ -181,7 +158,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
label: (o: (typeof soundOptions)[number]) => language.t(o.label),
|
label: (o: (typeof soundOptions)[number]) => language.t(o.label),
|
||||||
onHighlight: (option: (typeof soundOptions)[number] | undefined) => {
|
onHighlight: (option: (typeof soundOptions)[number] | undefined) => {
|
||||||
if (!option) return
|
if (!option) return
|
||||||
playDemoSound(option.id === "none" ? undefined : option.id)
|
playDemoSound(option.src)
|
||||||
},
|
},
|
||||||
onSelect: (option: (typeof soundOptions)[number] | undefined) => {
|
onSelect: (option: (typeof soundOptions)[number] | undefined) => {
|
||||||
if (!option) return
|
if (!option) return
|
||||||
@@ -192,7 +169,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
}
|
}
|
||||||
setEnabled(true)
|
setEnabled(true)
|
||||||
set(option.id)
|
set(option.id)
|
||||||
playDemoSound(option.id)
|
playDemoSound(option.src)
|
||||||
},
|
},
|
||||||
variant: "secondary" as const,
|
variant: "secondary" as const,
|
||||||
size: "small" as const,
|
size: "small" as const,
|
||||||
@@ -344,9 +321,6 @@ export const SettingsGeneral: Component = () => {
|
|||||||
current={fontOptionsList.find((o) => o.value === settings.appearance.font())}
|
current={fontOptionsList.find((o) => o.value === settings.appearance.font())}
|
||||||
value={(o) => o.value}
|
value={(o) => o.value}
|
||||||
label={(o) => language.t(o.label)}
|
label={(o) => language.t(o.label)}
|
||||||
onHighlight={(option) => {
|
|
||||||
void loadFont().then((x) => x.ensureMonoFont(option?.value))
|
|
||||||
}}
|
|
||||||
onSelect={(option) => option && settings.appearance.setFont(option.value)}
|
onSelect={(option) => option && settings.appearance.setFont(option.value)}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ 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"
|
||||||
@@ -16,6 +15,7 @@ import { useSDK } from "@/context/sdk"
|
|||||||
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
|
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health"
|
import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health"
|
||||||
|
import { DialogSelectServer } from "./dialog-select-server"
|
||||||
|
|
||||||
const pollMs = 10_000
|
const pollMs = 10_000
|
||||||
|
|
||||||
@@ -53,15 +53,11 @@ const listServersByHealth = (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const useServerHealth = (servers: Accessor<ServerConnection.Any[]>, enabled: Accessor<boolean>) => {
|
const useServerHealth = (servers: Accessor<ServerConnection.Any[]>) => {
|
||||||
const checkServerHealth = useCheckServerHealth()
|
const checkServerHealth = useCheckServerHealth()
|
||||||
const [status, setStatus] = createStore({} as Record<ServerConnection.Key, ServerHealth | undefined>)
|
const [status, setStatus] = createStore({} as Record<ServerConnection.Key, ServerHealth | undefined>)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!enabled()) {
|
|
||||||
setStatus(reconcile({}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const list = servers()
|
const list = servers()
|
||||||
let dead = false
|
let dead = false
|
||||||
|
|
||||||
@@ -134,30 +130,41 @@ const useDefaultServerKey = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const useMcpToggleMutation = () => {
|
const useMcpToggle = (input: {
|
||||||
const sync = useSync()
|
sync: ReturnType<typeof useSync>
|
||||||
const sdk = useSDK()
|
sdk: ReturnType<typeof useSDK>
|
||||||
const language = useLanguage()
|
language: ReturnType<typeof useLanguage>
|
||||||
|
}) => {
|
||||||
|
const [loading, setLoading] = createSignal<string | null>(null)
|
||||||
|
|
||||||
return useMutation(() => ({
|
const toggle = async (name: string) => {
|
||||||
mutationFn: async (name: string) => {
|
if (loading()) return
|
||||||
const status = sync.data.mcp[name]
|
setLoading(name)
|
||||||
await (status?.status === "connected" ? sdk.client.mcp.disconnect({ name }) : sdk.client.mcp.connect({ name }))
|
|
||||||
const result = await sdk.client.mcp.status()
|
try {
|
||||||
if (result.data) sync.set("mcp", result.data)
|
const status = input.sync.data.mcp[name]
|
||||||
},
|
await (status?.status === "connected"
|
||||||
onError: (err) => {
|
? input.sdk.client.mcp.disconnect({ name })
|
||||||
|
: 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: language.t("common.requestFailed"),
|
title: input.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()
|
||||||
@@ -165,12 +172,6 @@ export function StatusPopover() {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const [shown, setShown] = createSignal(false)
|
const [shown, setShown] = createSignal(false)
|
||||||
let dialogRun = 0
|
|
||||||
let dialogDead = false
|
|
||||||
onCleanup(() => {
|
|
||||||
dialogDead = true
|
|
||||||
dialogRun += 1
|
|
||||||
})
|
|
||||||
const servers = createMemo(() => {
|
const servers = createMemo(() => {
|
||||||
const current = server.current
|
const current = server.current
|
||||||
const list = server.list
|
const list = server.list
|
||||||
@@ -178,9 +179,9 @@ export function StatusPopover() {
|
|||||||
if (list.every((item) => ServerConnection.key(item) !== ServerConnection.key(current))) return [current, ...list]
|
if (list.every((item) => ServerConnection.key(item) !== ServerConnection.key(current))) return [current, ...list]
|
||||||
return [current, ...list.filter((item) => ServerConnection.key(item) !== ServerConnection.key(current))]
|
return [current, ...list.filter((item) => ServerConnection.key(item) !== ServerConnection.key(current))]
|
||||||
})
|
})
|
||||||
const health = useServerHealth(servers, shown)
|
const health = useServerHealth(servers)
|
||||||
const sortedServers = createMemo(() => listServersByHealth(servers(), server.key, health))
|
const sortedServers = createMemo(() => listServersByHealth(servers(), server.key, health))
|
||||||
const toggleMcp = useMcpToggleMutation()
|
const mcp = useMcpToggle({ sync, sdk, language })
|
||||||
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
|
||||||
@@ -309,13 +310,7 @@ export function StatusPopover() {
|
|||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
class="mt-3 self-start h-8 px-3 py-1.5"
|
class="mt-3 self-start h-8 px-3 py-1.5"
|
||||||
onClick={() => {
|
onClick={() => dialog.show(() => <DialogSelectServer />, defaultServer.refresh)}
|
||||||
const run = ++dialogRun
|
|
||||||
void import("./dialog-select-server").then((x) => {
|
|
||||||
if (dialogDead || dialogRun !== run) return
|
|
||||||
dialog.show(() => <x.DialogSelectServer />, defaultServer.refresh)
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{language.t("status.popover.action.manageServers")}
|
{language.t("status.popover.action.manageServers")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -342,11 +337,8 @@ 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={() => {
|
onClick={() => mcp.toggle(name)}
|
||||||
if (toggleMcp.isPending) return
|
disabled={mcp.loading() === name}
|
||||||
toggleMcp.mutate(name)
|
|
||||||
}}
|
|
||||||
disabled={toggleMcp.isPending && toggleMcp.variables === name}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
@@ -362,11 +354,8 @@ export function StatusPopover() {
|
|||||||
<div onClick={(event) => event.stopPropagation()}>
|
<div onClick={(event) => event.stopPropagation()}>
|
||||||
<Switch
|
<Switch
|
||||||
checked={enabled()}
|
checked={enabled()}
|
||||||
disabled={toggleMcp.isPending && toggleMcp.variables === name}
|
disabled={mcp.loading() === name}
|
||||||
onChange={() => {
|
onChange={() => mcp.toggle(name)}
|
||||||
if (toggleMcp.isPending) return
|
|
||||||
toggleMcp.mutate(name)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import { withAlpha } from "@opencode-ai/ui/theme/color"
|
import { type HexColor, resolveThemeVariant, useTheme, withAlpha } from "@opencode-ai/ui/theme"
|
||||||
import { useTheme } from "@opencode-ai/ui/theme/context"
|
|
||||||
import { resolveThemeVariant } from "@opencode-ai/ui/theme/resolve"
|
|
||||||
import type { HexColor } from "@opencode-ai/ui/theme/types"
|
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import type { FitAddon, Ghostty, Terminal as Term } from "ghostty-web"
|
import type { FitAddon, Ghostty, Terminal as Term } from "ghostty-web"
|
||||||
import { type ComponentProps, createEffect, createMemo, onCleanup, onMount, splitProps } from "solid-js"
|
import { type ComponentProps, createEffect, createMemo, onCleanup, onMount, splitProps } from "solid-js"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
|
|||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
import { useTheme } from "@opencode-ai/ui/theme/context"
|
import { useTheme } from "@opencode-ai/ui/theme"
|
||||||
|
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
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 }]
|
|
||||||
}
|
|
||||||
@@ -32,25 +32,6 @@ describe("command keybind helpers", () => {
|
|||||||
expect(matchKeybind(keybinds, new KeyboardEvent("keydown", { key: ",", ctrlKey: true, altKey: true }))).toBe(false)
|
expect(matchKeybind(keybinds, new KeyboardEvent("keydown", { key: ",", ctrlKey: true, altKey: true }))).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("matchKeybind supports bracket keys", () => {
|
|
||||||
const keybinds = parseKeybind("mod+alt+[, mod+alt+]")
|
|
||||||
const prev = keybinds[0]
|
|
||||||
const next = keybinds[1]
|
|
||||||
|
|
||||||
expect(
|
|
||||||
matchKeybind(
|
|
||||||
keybinds,
|
|
||||||
new KeyboardEvent("keydown", { key: "[", ctrlKey: prev?.ctrl, metaKey: prev?.meta, altKey: true }),
|
|
||||||
),
|
|
||||||
).toBe(true)
|
|
||||||
expect(
|
|
||||||
matchKeybind(
|
|
||||||
keybinds,
|
|
||||||
new KeyboardEvent("keydown", { key: "]", ctrlKey: next?.ctrl, metaKey: next?.meta, altKey: true }),
|
|
||||||
),
|
|
||||||
).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("formatKeybind returns human readable output", () => {
|
test("formatKeybind returns human readable output", () => {
|
||||||
const display = formatKeybind("ctrl+alt+arrowup")
|
const display = formatKeybind("ctrl+alt+arrowup")
|
||||||
|
|
||||||
@@ -59,11 +40,4 @@ 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)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,7 +9,17 @@ import type {
|
|||||||
} from "@opencode-ai/sdk/v2/client"
|
} from "@opencode-ai/sdk/v2/client"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { getFilename } from "@opencode-ai/util/path"
|
import { getFilename } from "@opencode-ai/util/path"
|
||||||
import { createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js"
|
import {
|
||||||
|
createContext,
|
||||||
|
getOwner,
|
||||||
|
Match,
|
||||||
|
onCleanup,
|
||||||
|
onMount,
|
||||||
|
type ParentProps,
|
||||||
|
Switch,
|
||||||
|
untrack,
|
||||||
|
useContext,
|
||||||
|
} from "solid-js"
|
||||||
import { createStore, produce, reconcile } from "solid-js/store"
|
import { createStore, produce, reconcile } from "solid-js/store"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
@@ -70,8 +80,6 @@ function createGlobalSync() {
|
|||||||
|
|
||||||
let active = true
|
let active = true
|
||||||
let projectWritten = false
|
let projectWritten = false
|
||||||
let bootedAt = 0
|
|
||||||
let bootingRoot = false
|
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
active = false
|
active = false
|
||||||
@@ -250,11 +258,6 @@ function createGlobalSync() {
|
|||||||
const sdk = sdkFor(directory)
|
const sdk = sdkFor(directory)
|
||||||
await bootstrapDirectory({
|
await bootstrapDirectory({
|
||||||
directory,
|
directory,
|
||||||
global: {
|
|
||||||
config: globalStore.config,
|
|
||||||
project: globalStore.project,
|
|
||||||
provider: globalStore.provider,
|
|
||||||
},
|
|
||||||
sdk,
|
sdk,
|
||||||
store: child[0],
|
store: child[0],
|
||||||
setStore: child[1],
|
setStore: child[1],
|
||||||
@@ -275,20 +278,15 @@ function createGlobalSync() {
|
|||||||
const unsub = globalSDK.event.listen((e) => {
|
const unsub = globalSDK.event.listen((e) => {
|
||||||
const directory = e.name
|
const directory = e.name
|
||||||
const event = e.details
|
const event = e.details
|
||||||
const recent = bootingRoot || Date.now() - bootedAt < 1500
|
|
||||||
|
|
||||||
if (directory === "global") {
|
if (directory === "global") {
|
||||||
applyGlobalEvent({
|
applyGlobalEvent({
|
||||||
event,
|
event,
|
||||||
project: globalStore.project,
|
project: globalStore.project,
|
||||||
refresh: () => {
|
refresh: queue.refresh,
|
||||||
if (recent) return
|
|
||||||
queue.refresh()
|
|
||||||
},
|
|
||||||
setGlobalProject: setProjects,
|
setGlobalProject: setProjects,
|
||||||
})
|
})
|
||||||
if (event.type === "server.connected" || event.type === "global.disposed") {
|
if (event.type === "server.connected" || event.type === "global.disposed") {
|
||||||
if (recent) return
|
|
||||||
for (const directory of Object.keys(children.children)) {
|
for (const directory of Object.keys(children.children)) {
|
||||||
queue.push(directory)
|
queue.push(directory)
|
||||||
}
|
}
|
||||||
@@ -327,19 +325,17 @@ function createGlobalSync() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
bootingRoot = true
|
await bootstrapGlobal({
|
||||||
try {
|
globalSDK: globalSDK.client,
|
||||||
await bootstrapGlobal({
|
connectErrorTitle: language.t("dialog.server.add.error"),
|
||||||
globalSDK: globalSDK.client,
|
connectErrorDescription: language.t("error.globalSync.connectFailed", {
|
||||||
requestFailedTitle: language.t("common.requestFailed"),
|
url: globalSDK.url,
|
||||||
translate: language.t,
|
}),
|
||||||
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
|
requestFailedTitle: language.t("common.requestFailed"),
|
||||||
setGlobalStore: setBootStore,
|
translate: language.t,
|
||||||
})
|
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
|
||||||
bootedAt = Date.now()
|
setGlobalStore: setBootStore,
|
||||||
} finally {
|
})
|
||||||
bootingRoot = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -382,7 +378,6 @@ 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,
|
||||||
@@ -396,7 +391,13 @@ const GlobalSyncContext = createContext<ReturnType<typeof createGlobalSync>>()
|
|||||||
|
|
||||||
export function GlobalSyncProvider(props: ParentProps) {
|
export function GlobalSyncProvider(props: ParentProps) {
|
||||||
const value = createGlobalSync()
|
const value = createGlobalSync()
|
||||||
return <GlobalSyncContext.Provider value={value}>{props.children}</GlobalSyncContext.Provider>
|
return (
|
||||||
|
<Switch>
|
||||||
|
<Match when={value.ready}>
|
||||||
|
<GlobalSyncContext.Provider value={value}>{props.children}</GlobalSyncContext.Provider>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGlobalSync() {
|
export function useGlobalSync() {
|
||||||
|
|||||||
@@ -31,102 +31,73 @@ type GlobalStore = {
|
|||||||
reload: undefined | "pending" | "complete"
|
reload: undefined | "pending" | "complete"
|
||||||
}
|
}
|
||||||
|
|
||||||
function waitForPaint() {
|
|
||||||
return new Promise<void>((resolve) => {
|
|
||||||
let done = false
|
|
||||||
const finish = () => {
|
|
||||||
if (done) return
|
|
||||||
done = true
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
const timer = setTimeout(finish, 50)
|
|
||||||
if (typeof requestAnimationFrame !== "function") return
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
clearTimeout(timer)
|
|
||||||
finish()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function errors(list: PromiseSettledResult<unknown>[]) {
|
|
||||||
return list.filter((item): item is PromiseRejectedResult => item.status === "rejected").map((item) => item.reason)
|
|
||||||
}
|
|
||||||
|
|
||||||
function runAll(list: Array<() => Promise<unknown>>) {
|
|
||||||
return Promise.allSettled(list.map((item) => item()))
|
|
||||||
}
|
|
||||||
|
|
||||||
function showErrors(input: {
|
|
||||||
errors: unknown[]
|
|
||||||
title: string
|
|
||||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
|
||||||
formatMoreCount: (count: number) => string
|
|
||||||
}) {
|
|
||||||
if (input.errors.length === 0) return
|
|
||||||
const message = formatServerError(input.errors[0], input.translate)
|
|
||||||
const more = input.errors.length > 1 ? input.formatMoreCount(input.errors.length - 1) : ""
|
|
||||||
showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: input.title,
|
|
||||||
description: message + more,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function bootstrapGlobal(input: {
|
export async function bootstrapGlobal(input: {
|
||||||
globalSDK: OpencodeClient
|
globalSDK: OpencodeClient
|
||||||
|
connectErrorTitle: string
|
||||||
|
connectErrorDescription: string
|
||||||
requestFailedTitle: string
|
requestFailedTitle: string
|
||||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||||
formatMoreCount: (count: number) => string
|
formatMoreCount: (count: number) => string
|
||||||
setGlobalStore: SetStoreFunction<GlobalStore>
|
setGlobalStore: SetStoreFunction<GlobalStore>
|
||||||
}) {
|
}) {
|
||||||
const fast = [
|
const health = await input.globalSDK.global
|
||||||
() =>
|
.health()
|
||||||
retry(() =>
|
.then((x) => x.data)
|
||||||
input.globalSDK.path.get().then((x) => {
|
.catch(() => undefined)
|
||||||
input.setGlobalStore("path", x.data!)
|
if (!health?.healthy) {
|
||||||
}),
|
showToast({
|
||||||
),
|
variant: "error",
|
||||||
() =>
|
title: input.connectErrorTitle,
|
||||||
retry(() =>
|
description: input.connectErrorDescription,
|
||||||
input.globalSDK.global.config.get().then((x) => {
|
})
|
||||||
input.setGlobalStore("config", x.data!)
|
input.setGlobalStore("ready", true)
|
||||||
}),
|
return
|
||||||
),
|
}
|
||||||
() =>
|
|
||||||
retry(() =>
|
const tasks = [
|
||||||
input.globalSDK.provider.list().then((x) => {
|
retry(() =>
|
||||||
input.setGlobalStore("provider", normalizeProviderList(x.data!))
|
input.globalSDK.path.get().then((x) => {
|
||||||
}),
|
input.setGlobalStore("path", x.data!)
|
||||||
),
|
}),
|
||||||
|
),
|
||||||
|
retry(() =>
|
||||||
|
input.globalSDK.global.config.get().then((x) => {
|
||||||
|
input.setGlobalStore("config", x.data!)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
retry(() =>
|
||||||
|
input.globalSDK.project.list().then((x) => {
|
||||||
|
const projects = (x.data ?? [])
|
||||||
|
.filter((p) => !!p?.id)
|
||||||
|
.filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => cmp(a.id, b.id))
|
||||||
|
input.setGlobalStore("project", projects)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
retry(() =>
|
||||||
|
input.globalSDK.provider.list().then((x) => {
|
||||||
|
input.setGlobalStore("provider", normalizeProviderList(x.data!))
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
retry(() =>
|
||||||
|
input.globalSDK.provider.auth().then((x) => {
|
||||||
|
input.setGlobalStore("provider_auth", x.data ?? {})
|
||||||
|
}),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
const slow = [
|
const results = await Promise.allSettled(tasks)
|
||||||
() =>
|
const errors = results.filter((r): r is PromiseRejectedResult => r.status === "rejected").map((r) => r.reason)
|
||||||
retry(() =>
|
if (errors.length) {
|
||||||
input.globalSDK.project.list().then((x) => {
|
const message = formatServerError(errors[0], input.translate)
|
||||||
const projects = (x.data ?? [])
|
const more = errors.length > 1 ? input.formatMoreCount(errors.length - 1) : ""
|
||||||
.filter((p) => !!p?.id)
|
showToast({
|
||||||
.filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
|
variant: "error",
|
||||||
.slice()
|
title: input.requestFailedTitle,
|
||||||
.sort((a, b) => cmp(a.id, b.id))
|
description: message + more,
|
||||||
input.setGlobalStore("project", projects)
|
})
|
||||||
}),
|
}
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
showErrors({
|
|
||||||
errors: errors(await runAll(fast)),
|
|
||||||
title: input.requestFailedTitle,
|
|
||||||
translate: input.translate,
|
|
||||||
formatMoreCount: input.formatMoreCount,
|
|
||||||
})
|
|
||||||
await waitForPaint()
|
|
||||||
showErrors({
|
|
||||||
errors: errors(await runAll(slow)),
|
|
||||||
title: input.requestFailedTitle,
|
|
||||||
translate: input.translate,
|
|
||||||
formatMoreCount: input.formatMoreCount,
|
|
||||||
})
|
|
||||||
input.setGlobalStore("ready", true)
|
input.setGlobalStore("ready", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,10 +111,6 @@ function groupBySession<T extends { id: string; sessionID: string }>(input: T[])
|
|||||||
}, {})
|
}, {})
|
||||||
}
|
}
|
||||||
|
|
||||||
function projectID(directory: string, projects: Project[]) {
|
|
||||||
return projects.find((project) => project.worktree === directory || project.sandboxes?.includes(directory))?.id
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function bootstrapDirectory(input: {
|
export async function bootstrapDirectory(input: {
|
||||||
directory: string
|
directory: string
|
||||||
sdk: OpencodeClient
|
sdk: OpencodeClient
|
||||||
@@ -152,130 +119,88 @@ export async function bootstrapDirectory(input: {
|
|||||||
vcsCache: VcsCache
|
vcsCache: VcsCache
|
||||||
loadSessions: (directory: string) => Promise<void> | void
|
loadSessions: (directory: string) => Promise<void> | void
|
||||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||||
global: {
|
|
||||||
config: Config
|
|
||||||
project: Project[]
|
|
||||||
provider: ProviderListResponse
|
|
||||||
}
|
|
||||||
}) {
|
}) {
|
||||||
const loading = input.store.status !== "complete"
|
if (input.store.status !== "complete") input.setStore("status", "loading")
|
||||||
const seededProject = projectID(input.directory, input.global.project)
|
|
||||||
if (seededProject) input.setStore("project", seededProject)
|
|
||||||
if (input.store.provider.all.length === 0 && input.global.provider.all.length > 0) {
|
|
||||||
input.setStore("provider", input.global.provider)
|
|
||||||
}
|
|
||||||
if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) {
|
|
||||||
input.setStore("config", input.global.config)
|
|
||||||
}
|
|
||||||
if (loading) input.setStore("status", "partial")
|
|
||||||
|
|
||||||
const fast = [
|
const blockingRequests = {
|
||||||
() =>
|
project: () => input.sdk.project.current().then((x) => input.setStore("project", x.data!.id)),
|
||||||
seededProject
|
provider: () =>
|
||||||
? Promise.resolve()
|
input.sdk.provider.list().then((x) => {
|
||||||
: retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)),
|
input.setStore("provider", normalizeProviderList(x.data!))
|
||||||
() => retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", x.data ?? []))),
|
}),
|
||||||
() => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
|
agent: () => input.sdk.app.agents().then((x) => input.setStore("agent", x.data ?? [])),
|
||||||
() =>
|
config: () => input.sdk.config.get().then((x) => input.setStore("config", x.data!)),
|
||||||
retry(() =>
|
}
|
||||||
input.sdk.path.get().then((x) => {
|
|
||||||
input.setStore("path", x.data!)
|
try {
|
||||||
const next = projectID(x.data?.directory ?? input.directory, input.global.project)
|
await Promise.all(Object.values(blockingRequests).map((p) => retry(p)))
|
||||||
if (next) input.setStore("project", next)
|
} catch (err) {
|
||||||
}),
|
console.error("Failed to bootstrap instance", err)
|
||||||
),
|
const project = getFilename(input.directory)
|
||||||
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
|
showToast({
|
||||||
() =>
|
variant: "error",
|
||||||
retry(() =>
|
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||||
input.sdk.vcs.get().then((x) => {
|
description: formatServerError(err, input.translate),
|
||||||
const next = x.data ?? input.store.vcs
|
})
|
||||||
input.setStore("vcs", next)
|
input.setStore("status", "partial")
|
||||||
if (next) input.vcsCache.setStore("value", next)
|
return
|
||||||
}),
|
}
|
||||||
),
|
|
||||||
() => retry(() => input.sdk.command.list().then((x) => input.setStore("command", x.data ?? []))),
|
if (input.store.status !== "complete") input.setStore("status", "partial")
|
||||||
() =>
|
|
||||||
retry(() =>
|
Promise.all([
|
||||||
input.sdk.permission.list().then((x) => {
|
input.sdk.path.get().then((x) => input.setStore("path", x.data!)),
|
||||||
const grouped = groupBySession(
|
input.sdk.command.list().then((x) => input.setStore("command", x.data ?? [])),
|
||||||
(x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID),
|
input.sdk.session.status().then((x) => input.setStore("session_status", x.data!)),
|
||||||
|
input.loadSessions(input.directory),
|
||||||
|
input.sdk.mcp.status().then((x) => input.setStore("mcp", x.data!)),
|
||||||
|
input.sdk.lsp.status().then((x) => input.setStore("lsp", x.data!)),
|
||||||
|
input.sdk.vcs.get().then((x) => {
|
||||||
|
const next = x.data ?? input.store.vcs
|
||||||
|
input.setStore("vcs", next)
|
||||||
|
if (next?.branch) input.vcsCache.setStore("value", next)
|
||||||
|
}),
|
||||||
|
input.sdk.permission.list().then((x) => {
|
||||||
|
const grouped = groupBySession(
|
||||||
|
(x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID),
|
||||||
|
)
|
||||||
|
batch(() => {
|
||||||
|
for (const sessionID of Object.keys(input.store.permission)) {
|
||||||
|
if (grouped[sessionID]) continue
|
||||||
|
input.setStore("permission", sessionID, [])
|
||||||
|
}
|
||||||
|
for (const [sessionID, permissions] of Object.entries(grouped)) {
|
||||||
|
input.setStore(
|
||||||
|
"permission",
|
||||||
|
sessionID,
|
||||||
|
reconcile(
|
||||||
|
permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
|
||||||
|
{ key: "id" },
|
||||||
|
),
|
||||||
)
|
)
|
||||||
batch(() => {
|
}
|
||||||
for (const sessionID of Object.keys(input.store.permission)) {
|
})
|
||||||
if (grouped[sessionID]) continue
|
}),
|
||||||
input.setStore("permission", sessionID, [])
|
input.sdk.question.list().then((x) => {
|
||||||
}
|
const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID))
|
||||||
for (const [sessionID, permissions] of Object.entries(grouped)) {
|
batch(() => {
|
||||||
input.setStore(
|
for (const sessionID of Object.keys(input.store.question)) {
|
||||||
"permission",
|
if (grouped[sessionID]) continue
|
||||||
sessionID,
|
input.setStore("question", sessionID, [])
|
||||||
reconcile(
|
}
|
||||||
permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
|
for (const [sessionID, questions] of Object.entries(grouped)) {
|
||||||
{ key: "id" },
|
input.setStore(
|
||||||
),
|
"question",
|
||||||
)
|
sessionID,
|
||||||
}
|
reconcile(
|
||||||
})
|
questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
|
||||||
}),
|
{ key: "id" },
|
||||||
),
|
),
|
||||||
() =>
|
)
|
||||||
retry(() =>
|
}
|
||||||
input.sdk.question.list().then((x) => {
|
})
|
||||||
const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID))
|
}),
|
||||||
batch(() => {
|
]).then(() => {
|
||||||
for (const sessionID of Object.keys(input.store.question)) {
|
input.setStore("status", "complete")
|
||||||
if (grouped[sessionID]) continue
|
})
|
||||||
input.setStore("question", sessionID, [])
|
|
||||||
}
|
|
||||||
for (const [sessionID, questions] of Object.entries(grouped)) {
|
|
||||||
input.setStore(
|
|
||||||
"question",
|
|
||||||
sessionID,
|
|
||||||
reconcile(
|
|
||||||
questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
|
|
||||||
{ key: "id" },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
const slow = [
|
|
||||||
() =>
|
|
||||||
retry(() =>
|
|
||||||
input.sdk.provider.list().then((x) => {
|
|
||||||
input.setStore("provider", normalizeProviderList(x.data!))
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
() => Promise.resolve(input.loadSessions(input.directory)),
|
|
||||||
() => retry(() => input.sdk.mcp.status().then((x) => input.setStore("mcp", x.data!))),
|
|
||||||
() => retry(() => input.sdk.lsp.status().then((x) => input.setStore("lsp", x.data!))),
|
|
||||||
]
|
|
||||||
|
|
||||||
const errs = errors(await runAll(fast))
|
|
||||||
if (errs.length > 0) {
|
|
||||||
console.error("Failed to bootstrap instance", errs[0])
|
|
||||||
const project = getFilename(input.directory)
|
|
||||||
showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: input.translate("toast.project.reloadFailed.title", { project }),
|
|
||||||
description: formatServerError(errs[0], input.translate),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
await waitForPaint()
|
|
||||||
const slowErrs = errors(await runAll(slow))
|
|
||||||
if (slowErrs.length > 0) {
|
|
||||||
console.error("Failed to finish bootstrap instance", slowErrs[0])
|
|
||||||
const project = getFilename(input.directory)
|
|
||||||
showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: input.translate("toast.project.reloadFailed.title", { project }),
|
|
||||||
description: formatServerError(slowErrs[0], input.translate),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -226,15 +226,6 @@ 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)
|
||||||
@@ -265,7 +256,6 @@ export function createChildStoreManager(input: {
|
|||||||
children,
|
children,
|
||||||
ensureChild,
|
ensureChild,
|
||||||
child,
|
child,
|
||||||
peek,
|
|
||||||
projectMeta,
|
projectMeta,
|
||||||
projectIcon,
|
projectIcon,
|
||||||
mark,
|
mark,
|
||||||
|
|||||||
@@ -494,10 +494,8 @@ describe("applyDirectoryEvent", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("updates vcs branch in store and cache", () => {
|
test("updates vcs branch in store and cache", () => {
|
||||||
const [store, setStore] = createStore(baseState({ vcs: { branch: "main", default_branch: "main" } }))
|
const [store, setStore] = createStore(baseState())
|
||||||
const [cacheStore, setCacheStore] = createStore({
|
const [cacheStore, setCacheStore] = createStore({ value: undefined as State["vcs"] })
|
||||||
value: { branch: "main", default_branch: "main" } as State["vcs"],
|
|
||||||
})
|
|
||||||
|
|
||||||
applyDirectoryEvent({
|
applyDirectoryEvent({
|
||||||
event: { type: "vcs.branch.updated", properties: { branch: "feature/test" } },
|
event: { type: "vcs.branch.updated", properties: { branch: "feature/test" } },
|
||||||
@@ -513,8 +511,8 @@ describe("applyDirectoryEvent", () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(store.vcs).toEqual({ branch: "feature/test", default_branch: "main" })
|
expect(store.vcs).toEqual({ branch: "feature/test" })
|
||||||
expect(cacheStore.value).toEqual({ branch: "feature/test", default_branch: "main" })
|
expect(cacheStore.value).toEqual({ branch: "feature/test" })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("routes disposal and lsp events to side-effect handlers", () => {
|
test("routes disposal and lsp events to side-effect handlers", () => {
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ import type { State, VcsCache } from "./types"
|
|||||||
import { trimSessions } from "./session-trim"
|
import { trimSessions } from "./session-trim"
|
||||||
import { dropSessionCaches } from "./session-cache"
|
import { dropSessionCaches } from "./session-cache"
|
||||||
|
|
||||||
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
|
||||||
|
|
||||||
export function applyGlobalEvent(input: {
|
export function applyGlobalEvent(input: {
|
||||||
event: { type: string; properties?: unknown }
|
event: { type: string; properties?: unknown }
|
||||||
project: Project[]
|
project: Project[]
|
||||||
@@ -213,7 +211,6 @@ export function applyDirectoryEvent(input: {
|
|||||||
}
|
}
|
||||||
case "message.part.updated": {
|
case "message.part.updated": {
|
||||||
const part = (event.properties as { part: Part }).part
|
const part = (event.properties as { part: Part }).part
|
||||||
if (SKIP_PARTS.has(part.type)) break
|
|
||||||
const parts = input.store.part[part.messageID]
|
const parts = input.store.part[part.messageID]
|
||||||
if (!parts) {
|
if (!parts) {
|
||||||
input.setStore("part", part.messageID, [part])
|
input.setStore("part", part.messageID, [part])
|
||||||
@@ -271,9 +268,9 @@ export function applyDirectoryEvent(input: {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "vcs.branch.updated": {
|
case "vcs.branch.updated": {
|
||||||
const props = event.properties as { branch?: string }
|
const props = event.properties as { branch: string }
|
||||||
if (input.store.vcs?.branch === props.branch) break
|
if (input.store.vcs?.branch === props.branch) break
|
||||||
const next = { ...input.store.vcs, branch: props.branch }
|
const next = { branch: props.branch }
|
||||||
input.setStore("vcs", next)
|
input.setStore("vcs", next)
|
||||||
if (input.vcsCache) input.vcsCache.setStore("value", next)
|
if (input.vcsCache) input.vcsCache.setStore("value", next)
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -1,10 +1,42 @@
|
|||||||
import * as i18n from "@solid-primitives/i18n"
|
import * as i18n from "@solid-primitives/i18n"
|
||||||
import { createEffect, createMemo, createResource } from "solid-js"
|
import { createEffect, createMemo } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { dict as en } from "@/i18n/en"
|
import { dict as en } from "@/i18n/en"
|
||||||
|
import { dict as zh } from "@/i18n/zh"
|
||||||
|
import { dict as zht } from "@/i18n/zht"
|
||||||
|
import { dict as ko } from "@/i18n/ko"
|
||||||
|
import { dict as de } from "@/i18n/de"
|
||||||
|
import { dict as es } from "@/i18n/es"
|
||||||
|
import { dict as fr } from "@/i18n/fr"
|
||||||
|
import { dict as da } from "@/i18n/da"
|
||||||
|
import { dict as ja } from "@/i18n/ja"
|
||||||
|
import { dict as pl } from "@/i18n/pl"
|
||||||
|
import { dict as ru } from "@/i18n/ru"
|
||||||
|
import { dict as ar } from "@/i18n/ar"
|
||||||
|
import { dict as no } from "@/i18n/no"
|
||||||
|
import { dict as br } from "@/i18n/br"
|
||||||
|
import { dict as th } from "@/i18n/th"
|
||||||
|
import { dict as bs } from "@/i18n/bs"
|
||||||
|
import { dict as tr } from "@/i18n/tr"
|
||||||
import { dict as uiEn } from "@opencode-ai/ui/i18n/en"
|
import { dict as uiEn } from "@opencode-ai/ui/i18n/en"
|
||||||
|
import { dict as uiZh } from "@opencode-ai/ui/i18n/zh"
|
||||||
|
import { dict as uiZht } from "@opencode-ai/ui/i18n/zht"
|
||||||
|
import { dict as uiKo } from "@opencode-ai/ui/i18n/ko"
|
||||||
|
import { dict as uiDe } from "@opencode-ai/ui/i18n/de"
|
||||||
|
import { dict as uiEs } from "@opencode-ai/ui/i18n/es"
|
||||||
|
import { dict as uiFr } from "@opencode-ai/ui/i18n/fr"
|
||||||
|
import { dict as uiDa } from "@opencode-ai/ui/i18n/da"
|
||||||
|
import { dict as uiJa } from "@opencode-ai/ui/i18n/ja"
|
||||||
|
import { dict as uiPl } from "@opencode-ai/ui/i18n/pl"
|
||||||
|
import { dict as uiRu } from "@opencode-ai/ui/i18n/ru"
|
||||||
|
import { dict as uiAr } from "@opencode-ai/ui/i18n/ar"
|
||||||
|
import { dict as uiNo } from "@opencode-ai/ui/i18n/no"
|
||||||
|
import { dict as uiBr } from "@opencode-ai/ui/i18n/br"
|
||||||
|
import { dict as uiTh } from "@opencode-ai/ui/i18n/th"
|
||||||
|
import { dict as uiBs } from "@opencode-ai/ui/i18n/bs"
|
||||||
|
import { dict as uiTr } from "@opencode-ai/ui/i18n/tr"
|
||||||
|
|
||||||
export type Locale =
|
export type Locale =
|
||||||
| "en"
|
| "en"
|
||||||
@@ -27,7 +59,6 @@ export type Locale =
|
|||||||
|
|
||||||
type RawDictionary = typeof en & typeof uiEn
|
type RawDictionary = typeof en & typeof uiEn
|
||||||
type Dictionary = i18n.Flatten<RawDictionary>
|
type Dictionary = i18n.Flatten<RawDictionary>
|
||||||
type Source = { dict: Record<string, string> }
|
|
||||||
|
|
||||||
function cookie(locale: Locale) {
|
function cookie(locale: Locale) {
|
||||||
return `oc_locale=${encodeURIComponent(locale)}; Path=/; Max-Age=31536000; SameSite=Lax`
|
return `oc_locale=${encodeURIComponent(locale)}; Path=/; Max-Age=31536000; SameSite=Lax`
|
||||||
@@ -94,43 +125,24 @@ const LABEL_KEY: Record<Locale, keyof Dictionary> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const base = i18n.flatten({ ...en, ...uiEn })
|
const base = i18n.flatten({ ...en, ...uiEn })
|
||||||
const dicts = new Map<Locale, Dictionary>([["en", base]])
|
const DICT: Record<Locale, Dictionary> = {
|
||||||
|
en: base,
|
||||||
const merge = (app: Promise<Source>, ui: Promise<Source>) =>
|
zh: { ...base, ...i18n.flatten({ ...zh, ...uiZh }) },
|
||||||
Promise.all([app, ui]).then(([a, b]) => ({ ...base, ...i18n.flatten({ ...a.dict, ...b.dict }) }) as Dictionary)
|
zht: { ...base, ...i18n.flatten({ ...zht, ...uiZht }) },
|
||||||
|
ko: { ...base, ...i18n.flatten({ ...ko, ...uiKo }) },
|
||||||
const loaders: Record<Exclude<Locale, "en">, () => Promise<Dictionary>> = {
|
de: { ...base, ...i18n.flatten({ ...de, ...uiDe }) },
|
||||||
zh: () => merge(import("@/i18n/zh"), import("@opencode-ai/ui/i18n/zh")),
|
es: { ...base, ...i18n.flatten({ ...es, ...uiEs }) },
|
||||||
zht: () => merge(import("@/i18n/zht"), import("@opencode-ai/ui/i18n/zht")),
|
fr: { ...base, ...i18n.flatten({ ...fr, ...uiFr }) },
|
||||||
ko: () => merge(import("@/i18n/ko"), import("@opencode-ai/ui/i18n/ko")),
|
da: { ...base, ...i18n.flatten({ ...da, ...uiDa }) },
|
||||||
de: () => merge(import("@/i18n/de"), import("@opencode-ai/ui/i18n/de")),
|
ja: { ...base, ...i18n.flatten({ ...ja, ...uiJa }) },
|
||||||
es: () => merge(import("@/i18n/es"), import("@opencode-ai/ui/i18n/es")),
|
pl: { ...base, ...i18n.flatten({ ...pl, ...uiPl }) },
|
||||||
fr: () => merge(import("@/i18n/fr"), import("@opencode-ai/ui/i18n/fr")),
|
ru: { ...base, ...i18n.flatten({ ...ru, ...uiRu }) },
|
||||||
da: () => merge(import("@/i18n/da"), import("@opencode-ai/ui/i18n/da")),
|
ar: { ...base, ...i18n.flatten({ ...ar, ...uiAr }) },
|
||||||
ja: () => merge(import("@/i18n/ja"), import("@opencode-ai/ui/i18n/ja")),
|
no: { ...base, ...i18n.flatten({ ...no, ...uiNo }) },
|
||||||
pl: () => merge(import("@/i18n/pl"), import("@opencode-ai/ui/i18n/pl")),
|
br: { ...base, ...i18n.flatten({ ...br, ...uiBr }) },
|
||||||
ru: () => merge(import("@/i18n/ru"), import("@opencode-ai/ui/i18n/ru")),
|
th: { ...base, ...i18n.flatten({ ...th, ...uiTh }) },
|
||||||
ar: () => merge(import("@/i18n/ar"), import("@opencode-ai/ui/i18n/ar")),
|
bs: { ...base, ...i18n.flatten({ ...bs, ...uiBs }) },
|
||||||
no: () => merge(import("@/i18n/no"), import("@opencode-ai/ui/i18n/no")),
|
tr: { ...base, ...i18n.flatten({ ...tr, ...uiTr }) },
|
||||||
br: () => merge(import("@/i18n/br"), import("@opencode-ai/ui/i18n/br")),
|
|
||||||
th: () => merge(import("@/i18n/th"), import("@opencode-ai/ui/i18n/th")),
|
|
||||||
bs: () => merge(import("@/i18n/bs"), import("@opencode-ai/ui/i18n/bs")),
|
|
||||||
tr: () => merge(import("@/i18n/tr"), import("@opencode-ai/ui/i18n/tr")),
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadDict(locale: Locale) {
|
|
||||||
const hit = dicts.get(locale)
|
|
||||||
if (hit) return Promise.resolve(hit)
|
|
||||||
if (locale === "en") return Promise.resolve(base)
|
|
||||||
const load = loaders[locale]
|
|
||||||
return load().then((next: Dictionary) => {
|
|
||||||
dicts.set(locale, next)
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function loadLocaleDict(locale: Locale) {
|
|
||||||
return loadDict(locale).then(() => undefined)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const localeMatchers: Array<{ locale: Locale; match: (language: string) => boolean }> = [
|
const localeMatchers: Array<{ locale: Locale; match: (language: string) => boolean }> = [
|
||||||
@@ -156,6 +168,27 @@ const localeMatchers: Array<{ locale: Locale; match: (language: string) => boole
|
|||||||
{ locale: "tr", match: (language) => language.startsWith("tr") },
|
{ locale: "tr", match: (language) => language.startsWith("tr") },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
type ParityKey = "command.session.previous.unseen" | "command.session.next.unseen"
|
||||||
|
const PARITY_CHECK: Record<Exclude<Locale, "en">, Record<ParityKey, string>> = {
|
||||||
|
zh,
|
||||||
|
zht,
|
||||||
|
ko,
|
||||||
|
de,
|
||||||
|
es,
|
||||||
|
fr,
|
||||||
|
da,
|
||||||
|
ja,
|
||||||
|
pl,
|
||||||
|
ru,
|
||||||
|
ar,
|
||||||
|
no,
|
||||||
|
br,
|
||||||
|
th,
|
||||||
|
bs,
|
||||||
|
tr,
|
||||||
|
}
|
||||||
|
void PARITY_CHECK
|
||||||
|
|
||||||
function detectLocale(): Locale {
|
function detectLocale(): Locale {
|
||||||
if (typeof navigator !== "object") return "en"
|
if (typeof navigator !== "object") return "en"
|
||||||
|
|
||||||
@@ -170,48 +203,27 @@ function detectLocale(): Locale {
|
|||||||
return "en"
|
return "en"
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeLocale(value: string): Locale {
|
function normalizeLocale(value: string): Locale {
|
||||||
return LOCALES.includes(value as Locale) ? (value as Locale) : "en"
|
return LOCALES.includes(value as Locale) ? (value as Locale) : "en"
|
||||||
}
|
}
|
||||||
|
|
||||||
function readStoredLocale() {
|
|
||||||
if (typeof localStorage !== "object") return
|
|
||||||
try {
|
|
||||||
const raw = localStorage.getItem("opencode.global.dat:language")
|
|
||||||
if (!raw) return
|
|
||||||
const next = JSON.parse(raw) as { locale?: string }
|
|
||||||
if (typeof next?.locale !== "string") return
|
|
||||||
return normalizeLocale(next.locale)
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const warm = readStoredLocale() ?? detectLocale()
|
|
||||||
if (warm !== "en") void loadDict(warm)
|
|
||||||
|
|
||||||
export const { use: useLanguage, provider: LanguageProvider } = createSimpleContext({
|
export const { use: useLanguage, provider: LanguageProvider } = createSimpleContext({
|
||||||
name: "Language",
|
name: "Language",
|
||||||
init: (props: { locale?: Locale }) => {
|
init: () => {
|
||||||
const initial = props.locale ?? readStoredLocale() ?? detectLocale()
|
|
||||||
const [store, setStore, _, ready] = persisted(
|
const [store, setStore, _, ready] = persisted(
|
||||||
Persist.global("language", ["language.v1"]),
|
Persist.global("language", ["language.v1"]),
|
||||||
createStore({
|
createStore({
|
||||||
locale: initial,
|
locale: detectLocale() as Locale,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const locale = createMemo<Locale>(() => normalizeLocale(store.locale))
|
const locale = createMemo<Locale>(() => normalizeLocale(store.locale))
|
||||||
|
console.log("locale", locale())
|
||||||
const intl = createMemo(() => INTL[locale()])
|
const intl = createMemo(() => INTL[locale()])
|
||||||
|
|
||||||
const [dict] = createResource(locale, loadDict, {
|
const dict = createMemo<Dictionary>(() => DICT[locale()])
|
||||||
initialValue: dicts.get(initial) ?? base,
|
|
||||||
})
|
|
||||||
|
|
||||||
const t = i18n.translator(() => dict() ?? base, i18n.resolveTemplate) as (
|
const t = i18n.translator(dict, i18n.resolveTemplate)
|
||||||
key: keyof Dictionary,
|
|
||||||
params?: Record<string, string | number | boolean>,
|
|
||||||
) => string
|
|
||||||
|
|
||||||
const label = (value: Locale) => t(LABEL_KEY[value])
|
const label = (value: Locale) => t(LABEL_KEY[value])
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { base64Encode } from "@opencode-ai/util/encode"
|
|||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
import { EventSessionError } from "@opencode-ai/sdk/v2"
|
import { EventSessionError } from "@opencode-ai/sdk/v2"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { playSoundById } from "@/utils/sound"
|
import { playSound, soundSrc } from "@/utils/sound"
|
||||||
|
|
||||||
type NotificationBase = {
|
type NotificationBase = {
|
||||||
directory?: string
|
directory?: string
|
||||||
@@ -234,7 +234,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
|||||||
if (session.parentID) return
|
if (session.parentID) return
|
||||||
|
|
||||||
if (settings.sounds.agentEnabled()) {
|
if (settings.sounds.agentEnabled()) {
|
||||||
void playSoundById(settings.sounds.agent())
|
playSound(soundSrc(settings.sounds.agent()))
|
||||||
}
|
}
|
||||||
|
|
||||||
append({
|
append({
|
||||||
@@ -263,7 +263,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
|||||||
if (session?.parentID) return
|
if (session?.parentID) return
|
||||||
|
|
||||||
if (settings.sounds.errorsEnabled()) {
|
if (settings.sounds.errorsEnabled()) {
|
||||||
void playSoundById(settings.sounds.errors())
|
playSound(soundSrc(settings.sounds.errors()))
|
||||||
}
|
}
|
||||||
|
|
||||||
const error = "error" in event.properties ? event.properties.error : undefined
|
const error = "error" in event.properties ? event.properties.error : undefined
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { ServerConnection } from "./server"
|
|||||||
|
|
||||||
type PickerPaths = string | string[] | null
|
type PickerPaths = string | string[] | null
|
||||||
type OpenDirectoryPickerOptions = { title?: string; multiple?: boolean }
|
type OpenDirectoryPickerOptions = { title?: string; multiple?: boolean }
|
||||||
type OpenFilePickerOptions = { title?: string; multiple?: boolean; accept?: string[]; extensions?: string[] }
|
type OpenFilePickerOptions = { title?: string; multiple?: boolean }
|
||||||
type SaveFilePickerOptions = { title?: string; defaultPath?: string }
|
type SaveFilePickerOptions = { title?: string; defaultPath?: string }
|
||||||
type UpdateInfo = { updateAvailable: boolean; version?: string }
|
type UpdateInfo = { updateAvailable: boolean; version?: string }
|
||||||
|
|
||||||
|
|||||||
@@ -104,13 +104,6 @@ function withFallback<T>(read: () => T | undefined, fallback: T) {
|
|||||||
return createMemo(() => read() ?? fallback)
|
return createMemo(() => read() ?? fallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
let font: Promise<typeof import("@opencode-ai/ui/font-loader")> | undefined
|
|
||||||
|
|
||||||
function loadFont() {
|
|
||||||
font ??= import("@opencode-ai/ui/font-loader")
|
|
||||||
return font
|
|
||||||
}
|
|
||||||
|
|
||||||
export const { use: useSettings, provider: SettingsProvider } = createSimpleContext({
|
export const { use: useSettings, provider: SettingsProvider } = createSimpleContext({
|
||||||
name: "Settings",
|
name: "Settings",
|
||||||
init: () => {
|
init: () => {
|
||||||
@@ -118,11 +111,7 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
|||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (typeof document === "undefined") return
|
if (typeof document === "undefined") return
|
||||||
const id = store.appearance?.font ?? defaultSettings.appearance.font
|
document.documentElement.style.setProperty("--font-family-mono", monoFontFamily(store.appearance?.font))
|
||||||
if (id !== defaultSettings.appearance.font) {
|
|
||||||
void loadFont().then((x) => x.ensureMonoFont(id))
|
|
||||||
}
|
|
||||||
document.documentElement.style.setProperty("--font-family-mono", monoFontFamily(id))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ import { useSDK } from "./sdk"
|
|||||||
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
||||||
import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache"
|
import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache"
|
||||||
|
|
||||||
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
|
||||||
|
|
||||||
function sortParts(parts: Part[]) {
|
function sortParts(parts: Part[]) {
|
||||||
return parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id))
|
return parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id))
|
||||||
}
|
}
|
||||||
@@ -180,8 +178,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
return globalSync.child(directory)
|
return globalSync.child(directory)
|
||||||
}
|
}
|
||||||
const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
|
const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
|
||||||
const initialMessagePageSize = 80
|
const messagePageSize = 200
|
||||||
const historyMessagePageSize = 200
|
|
||||||
const inflight = new Map<string, Promise<void>>()
|
const inflight = new Map<string, Promise<void>>()
|
||||||
const inflightDiff = new Map<string, Promise<void>>()
|
const inflightDiff = new Map<string, Promise<void>>()
|
||||||
const inflightTodo = new Map<string, Promise<void>>()
|
const inflightTodo = new Map<string, Promise<void>>()
|
||||||
@@ -339,8 +336,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
batch(() => {
|
batch(() => {
|
||||||
input.setStore("message", input.sessionID, reconcile(message, { key: "id" }))
|
input.setStore("message", input.sessionID, reconcile(message, { key: "id" }))
|
||||||
for (const p of next.part) {
|
for (const p of next.part) {
|
||||||
const filtered = p.part.filter((x) => !SKIP_PARTS.has(x.type))
|
input.setStore("part", p.id, p.part)
|
||||||
if (filtered.length) input.setStore("part", p.id, filtered)
|
|
||||||
}
|
}
|
||||||
setMeta("limit", key, message.length)
|
setMeta("limit", key, message.length)
|
||||||
setMeta("cursor", key, next.cursor)
|
setMeta("cursor", key, next.cursor)
|
||||||
@@ -464,7 +460,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
const cached = store.message[sessionID] !== undefined && meta.limit[key] !== undefined
|
const cached = store.message[sessionID] !== undefined && meta.limit[key] !== undefined
|
||||||
if (cached && hasSession && !opts?.force) return
|
if (cached && hasSession && !opts?.force) return
|
||||||
|
|
||||||
const limit = meta.limit[key] ?? initialMessagePageSize
|
const limit = meta.limit[key] ?? messagePageSize
|
||||||
const sessionReq =
|
const sessionReq =
|
||||||
hasSession && !opts?.force
|
hasSession && !opts?.force
|
||||||
? Promise.resolve()
|
? Promise.resolve()
|
||||||
@@ -561,7 +557,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
const [, setStore] = globalSync.child(directory)
|
const [, setStore] = globalSync.child(directory)
|
||||||
touch(directory, setStore, sessionID)
|
touch(directory, setStore, sessionID)
|
||||||
const key = keyFor(directory, sessionID)
|
const key = keyFor(directory, sessionID)
|
||||||
const step = count ?? historyMessagePageSize
|
const step = count ?? messagePageSize
|
||||||
if (meta.loading[key]) return
|
if (meta.loading[key]) return
|
||||||
if (meta.complete[key]) return
|
if (meta.complete[key]) return
|
||||||
const before = meta.cursor[key]
|
const before = meta.cursor[key]
|
||||||
|
|||||||
@@ -1,18 +1,45 @@
|
|||||||
const template = "Terminal {{number}}"
|
import { dict as ar } from "@/i18n/ar"
|
||||||
|
import { dict as br } from "@/i18n/br"
|
||||||
|
import { dict as bs } from "@/i18n/bs"
|
||||||
|
import { dict as da } from "@/i18n/da"
|
||||||
|
import { dict as de } from "@/i18n/de"
|
||||||
|
import { dict as en } from "@/i18n/en"
|
||||||
|
import { dict as es } from "@/i18n/es"
|
||||||
|
import { dict as fr } from "@/i18n/fr"
|
||||||
|
import { dict as ja } from "@/i18n/ja"
|
||||||
|
import { dict as ko } from "@/i18n/ko"
|
||||||
|
import { dict as no } from "@/i18n/no"
|
||||||
|
import { dict as pl } from "@/i18n/pl"
|
||||||
|
import { dict as ru } from "@/i18n/ru"
|
||||||
|
import { dict as th } from "@/i18n/th"
|
||||||
|
import { dict as tr } from "@/i18n/tr"
|
||||||
|
import { dict as zh } from "@/i18n/zh"
|
||||||
|
import { dict as zht } from "@/i18n/zht"
|
||||||
|
|
||||||
const numbered = [
|
const numbered = Array.from(
|
||||||
template,
|
new Set([
|
||||||
"محطة طرفية {{number}}",
|
en["terminal.title.numbered"],
|
||||||
"Терминал {{number}}",
|
ar["terminal.title.numbered"],
|
||||||
"ターミナル {{number}}",
|
br["terminal.title.numbered"],
|
||||||
"터미널 {{number}}",
|
bs["terminal.title.numbered"],
|
||||||
"เทอร์มินัล {{number}}",
|
da["terminal.title.numbered"],
|
||||||
"终端 {{number}}",
|
de["terminal.title.numbered"],
|
||||||
"終端機 {{number}}",
|
es["terminal.title.numbered"],
|
||||||
]
|
fr["terminal.title.numbered"],
|
||||||
|
ja["terminal.title.numbered"],
|
||||||
|
ko["terminal.title.numbered"],
|
||||||
|
no["terminal.title.numbered"],
|
||||||
|
pl["terminal.title.numbered"],
|
||||||
|
ru["terminal.title.numbered"],
|
||||||
|
th["terminal.title.numbered"],
|
||||||
|
tr["terminal.title.numbered"],
|
||||||
|
zh["terminal.title.numbered"],
|
||||||
|
zht["terminal.title.numbered"],
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
|
||||||
export function defaultTitle(number: number) {
|
export function defaultTitle(number: number) {
|
||||||
return template.replace("{{number}}", String(number))
|
return en["terminal.title.numbered"].replace("{{number}}", String(number))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isDefaultTitle(title: string, number: number) {
|
export function isDefaultTitle(title: string, number: number) {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function useProviders() {
|
|||||||
const providers = () => {
|
const providers = () => {
|
||||||
if (dir()) {
|
if (dir()) {
|
||||||
const [projectStore] = globalSync.child(dir())
|
const [projectStore] = globalSync.child(dir())
|
||||||
if (projectStore.provider.all.length > 0) return projectStore.provider
|
return projectStore.provider
|
||||||
}
|
}
|
||||||
return globalSync.data.provider
|
return globalSync.data.provider
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -722,6 +722,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.skill.description": "تحميل مهارة بالاسم",
|
"settings.permissions.tool.skill.description": "تحميل مهارة بالاسم",
|
||||||
"settings.permissions.tool.lsp.title": "LSP",
|
"settings.permissions.tool.lsp.title": "LSP",
|
||||||
"settings.permissions.tool.lsp.description": "تشغيل استعلامات خادم اللغة",
|
"settings.permissions.tool.lsp.description": "تشغيل استعلامات خادم اللغة",
|
||||||
|
"settings.permissions.tool.todoread.title": "قراءة المهام",
|
||||||
|
"settings.permissions.tool.todoread.description": "قراءة قائمة المهام",
|
||||||
"settings.permissions.tool.todowrite.title": "كتابة المهام",
|
"settings.permissions.tool.todowrite.title": "كتابة المهام",
|
||||||
"settings.permissions.tool.todowrite.description": "تحديث قائمة المهام",
|
"settings.permissions.tool.todowrite.description": "تحديث قائمة المهام",
|
||||||
"settings.permissions.tool.webfetch.title": "جلب الويب",
|
"settings.permissions.tool.webfetch.title": "جلب الويب",
|
||||||
|
|||||||
@@ -732,6 +732,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.skill.description": "Carregar uma habilidade por nome",
|
"settings.permissions.tool.skill.description": "Carregar uma habilidade por nome",
|
||||||
"settings.permissions.tool.lsp.title": "LSP",
|
"settings.permissions.tool.lsp.title": "LSP",
|
||||||
"settings.permissions.tool.lsp.description": "Executar consultas de servidor de linguagem",
|
"settings.permissions.tool.lsp.description": "Executar consultas de servidor de linguagem",
|
||||||
|
"settings.permissions.tool.todoread.title": "Ler Tarefas",
|
||||||
|
"settings.permissions.tool.todoread.description": "Ler a lista de tarefas",
|
||||||
"settings.permissions.tool.todowrite.title": "Escrever Tarefas",
|
"settings.permissions.tool.todowrite.title": "Escrever Tarefas",
|
||||||
"settings.permissions.tool.todowrite.description": "Atualizar a lista de tarefas",
|
"settings.permissions.tool.todowrite.description": "Atualizar a lista de tarefas",
|
||||||
"settings.permissions.tool.webfetch.title": "Buscar Web",
|
"settings.permissions.tool.webfetch.title": "Buscar Web",
|
||||||
|
|||||||
@@ -806,6 +806,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.skill.description": "Učitaj vještinu po nazivu",
|
"settings.permissions.tool.skill.description": "Učitaj vještinu po nazivu",
|
||||||
"settings.permissions.tool.lsp.title": "LSP",
|
"settings.permissions.tool.lsp.title": "LSP",
|
||||||
"settings.permissions.tool.lsp.description": "Pokreni upite jezičnog servera",
|
"settings.permissions.tool.lsp.description": "Pokreni upite jezičnog servera",
|
||||||
|
"settings.permissions.tool.todoread.title": "Čitanje liste zadataka",
|
||||||
|
"settings.permissions.tool.todoread.description": "Čitanje liste zadataka",
|
||||||
"settings.permissions.tool.todowrite.title": "Ažuriranje liste zadataka",
|
"settings.permissions.tool.todowrite.title": "Ažuriranje liste zadataka",
|
||||||
"settings.permissions.tool.todowrite.description": "Ažuriraj listu zadataka",
|
"settings.permissions.tool.todowrite.description": "Ažuriraj listu zadataka",
|
||||||
"settings.permissions.tool.webfetch.title": "Web preuzimanje",
|
"settings.permissions.tool.webfetch.title": "Web preuzimanje",
|
||||||
|
|||||||
@@ -800,6 +800,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.skill.description": "Indlæs en færdighed efter navn",
|
"settings.permissions.tool.skill.description": "Indlæs en færdighed efter navn",
|
||||||
"settings.permissions.tool.lsp.title": "LSP",
|
"settings.permissions.tool.lsp.title": "LSP",
|
||||||
"settings.permissions.tool.lsp.description": "Kør sprogserverforespørgsler",
|
"settings.permissions.tool.lsp.description": "Kør sprogserverforespørgsler",
|
||||||
|
"settings.permissions.tool.todoread.title": "Læs To-do",
|
||||||
|
"settings.permissions.tool.todoread.description": "Læs to-do listen",
|
||||||
"settings.permissions.tool.todowrite.title": "Skriv To-do",
|
"settings.permissions.tool.todowrite.title": "Skriv To-do",
|
||||||
"settings.permissions.tool.todowrite.description": "Opdater to-do listen",
|
"settings.permissions.tool.todowrite.description": "Opdater to-do listen",
|
||||||
"settings.permissions.tool.webfetch.title": "Webhentning",
|
"settings.permissions.tool.webfetch.title": "Webhentning",
|
||||||
|
|||||||
@@ -743,6 +743,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.skill.description": "Eine Fähigkeit nach Namen laden",
|
"settings.permissions.tool.skill.description": "Eine Fähigkeit nach Namen laden",
|
||||||
"settings.permissions.tool.lsp.title": "LSP",
|
"settings.permissions.tool.lsp.title": "LSP",
|
||||||
"settings.permissions.tool.lsp.description": "Language-Server-Abfragen ausführen",
|
"settings.permissions.tool.lsp.description": "Language-Server-Abfragen ausführen",
|
||||||
|
"settings.permissions.tool.todoread.title": "Todo lesen",
|
||||||
|
"settings.permissions.tool.todoread.description": "Die Todo-Liste lesen",
|
||||||
"settings.permissions.tool.todowrite.title": "Todo schreiben",
|
"settings.permissions.tool.todowrite.title": "Todo schreiben",
|
||||||
"settings.permissions.tool.todowrite.description": "Die Todo-Liste aktualisieren",
|
"settings.permissions.tool.todowrite.description": "Die Todo-Liste aktualisieren",
|
||||||
"settings.permissions.tool.webfetch.title": "Web-Abruf",
|
"settings.permissions.tool.webfetch.title": "Web-Abruf",
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ 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",
|
||||||
@@ -276,7 +274,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 files",
|
"prompt.action.attachFile": "Add file",
|
||||||
"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",
|
||||||
@@ -535,8 +533,6 @@ export const dict = {
|
|||||||
"session.review.noVcs.createGit.action": "Create Git repository",
|
"session.review.noVcs.createGit.action": "Create Git repository",
|
||||||
"session.review.noSnapshot": "Snapshot tracking is disabled in config, so session changes are unavailable",
|
"session.review.noSnapshot": "Snapshot tracking is disabled in config, so session changes are unavailable",
|
||||||
"session.review.noChanges": "No changes",
|
"session.review.noChanges": "No changes",
|
||||||
"session.review.noUncommittedChanges": "No uncommitted changes yet",
|
|
||||||
"session.review.noBranchChanges": "No branch changes yet",
|
|
||||||
|
|
||||||
"session.files.selectToOpen": "Select a file to open",
|
"session.files.selectToOpen": "Select a file to open",
|
||||||
"session.files.all": "All files",
|
"session.files.all": "All files",
|
||||||
@@ -902,6 +898,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.skill.description": "Load a skill by name",
|
"settings.permissions.tool.skill.description": "Load a skill by name",
|
||||||
"settings.permissions.tool.lsp.title": "LSP",
|
"settings.permissions.tool.lsp.title": "LSP",
|
||||||
"settings.permissions.tool.lsp.description": "Run language server queries",
|
"settings.permissions.tool.lsp.description": "Run language server queries",
|
||||||
|
"settings.permissions.tool.todoread.title": "Todo Read",
|
||||||
|
"settings.permissions.tool.todoread.description": "Read the todo list",
|
||||||
"settings.permissions.tool.todowrite.title": "Todo Write",
|
"settings.permissions.tool.todowrite.title": "Todo Write",
|
||||||
"settings.permissions.tool.todowrite.description": "Update the todo list",
|
"settings.permissions.tool.todowrite.description": "Update the todo list",
|
||||||
"settings.permissions.tool.webfetch.title": "Web Fetch",
|
"settings.permissions.tool.webfetch.title": "Web Fetch",
|
||||||
|
|||||||
@@ -813,6 +813,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.skill.description": "Cargar una habilidad por nombre",
|
"settings.permissions.tool.skill.description": "Cargar una habilidad por nombre",
|
||||||
"settings.permissions.tool.lsp.title": "LSP",
|
"settings.permissions.tool.lsp.title": "LSP",
|
||||||
"settings.permissions.tool.lsp.description": "Ejecutar consultas de servidor de lenguaje",
|
"settings.permissions.tool.lsp.description": "Ejecutar consultas de servidor de lenguaje",
|
||||||
|
"settings.permissions.tool.todoread.title": "Leer Todo",
|
||||||
|
"settings.permissions.tool.todoread.description": "Leer la lista de tareas",
|
||||||
"settings.permissions.tool.todowrite.title": "Escribir Todo",
|
"settings.permissions.tool.todowrite.title": "Escribir Todo",
|
||||||
"settings.permissions.tool.todowrite.description": "Actualizar la lista de tareas",
|
"settings.permissions.tool.todowrite.description": "Actualizar la lista de tareas",
|
||||||
"settings.permissions.tool.webfetch.title": "Web Fetch",
|
"settings.permissions.tool.webfetch.title": "Web Fetch",
|
||||||
|
|||||||
@@ -741,6 +741,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.skill.description": "Charger une compétence par son nom",
|
"settings.permissions.tool.skill.description": "Charger une compétence par son nom",
|
||||||
"settings.permissions.tool.lsp.title": "LSP",
|
"settings.permissions.tool.lsp.title": "LSP",
|
||||||
"settings.permissions.tool.lsp.description": "Exécuter des requêtes de serveur de langage",
|
"settings.permissions.tool.lsp.description": "Exécuter des requêtes de serveur de langage",
|
||||||
|
"settings.permissions.tool.todoread.title": "Lire Todo",
|
||||||
|
"settings.permissions.tool.todoread.description": "Lire la liste de tâches",
|
||||||
"settings.permissions.tool.todowrite.title": "Écrire Todo",
|
"settings.permissions.tool.todowrite.title": "Écrire Todo",
|
||||||
"settings.permissions.tool.todowrite.description": "Mettre à jour la liste de tâches",
|
"settings.permissions.tool.todowrite.description": "Mettre à jour la liste de tâches",
|
||||||
"settings.permissions.tool.webfetch.title": "Récupération Web",
|
"settings.permissions.tool.webfetch.title": "Récupération Web",
|
||||||
|
|||||||
@@ -727,6 +727,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.skill.description": "名前によるスキルの読み込み",
|
"settings.permissions.tool.skill.description": "名前によるスキルの読み込み",
|
||||||
"settings.permissions.tool.lsp.title": "LSP",
|
"settings.permissions.tool.lsp.title": "LSP",
|
||||||
"settings.permissions.tool.lsp.description": "言語サーバークエリの実行",
|
"settings.permissions.tool.lsp.description": "言語サーバークエリの実行",
|
||||||
|
"settings.permissions.tool.todoread.title": "Todo読み込み",
|
||||||
|
"settings.permissions.tool.todoread.description": "Todoリストの読み込み",
|
||||||
"settings.permissions.tool.todowrite.title": "Todo書き込み",
|
"settings.permissions.tool.todowrite.title": "Todo書き込み",
|
||||||
"settings.permissions.tool.todowrite.description": "Todoリストの更新",
|
"settings.permissions.tool.todowrite.description": "Todoリストの更新",
|
||||||
"settings.permissions.tool.webfetch.title": "Web取得",
|
"settings.permissions.tool.webfetch.title": "Web取得",
|
||||||
|
|||||||
@@ -726,6 +726,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.skill.description": "이름으로 기술 로드",
|
"settings.permissions.tool.skill.description": "이름으로 기술 로드",
|
||||||
"settings.permissions.tool.lsp.title": "LSP",
|
"settings.permissions.tool.lsp.title": "LSP",
|
||||||
"settings.permissions.tool.lsp.description": "언어 서버 쿼리 실행",
|
"settings.permissions.tool.lsp.description": "언어 서버 쿼리 실행",
|
||||||
|
"settings.permissions.tool.todoread.title": "할 일 읽기",
|
||||||
|
"settings.permissions.tool.todoread.description": "할 일 목록 읽기",
|
||||||
"settings.permissions.tool.todowrite.title": "할 일 쓰기",
|
"settings.permissions.tool.todowrite.title": "할 일 쓰기",
|
||||||
"settings.permissions.tool.todowrite.description": "할 일 목록 업데이트",
|
"settings.permissions.tool.todowrite.description": "할 일 목록 업데이트",
|
||||||
"settings.permissions.tool.webfetch.title": "웹 가져오기",
|
"settings.permissions.tool.webfetch.title": "웹 가져오기",
|
||||||
|
|||||||
@@ -807,6 +807,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.skill.description": "Last en ferdighet etter navn",
|
"settings.permissions.tool.skill.description": "Last en ferdighet etter navn",
|
||||||
"settings.permissions.tool.lsp.title": "LSP",
|
"settings.permissions.tool.lsp.title": "LSP",
|
||||||
"settings.permissions.tool.lsp.description": "Kjør språkserverforespørsler",
|
"settings.permissions.tool.lsp.description": "Kjør språkserverforespørsler",
|
||||||
|
"settings.permissions.tool.todoread.title": "Les gjøremål",
|
||||||
|
"settings.permissions.tool.todoread.description": "Les gjøremålslisten",
|
||||||
"settings.permissions.tool.todowrite.title": "Skriv gjøremål",
|
"settings.permissions.tool.todowrite.title": "Skriv gjøremål",
|
||||||
"settings.permissions.tool.todowrite.description": "Oppdater gjøremålslisten",
|
"settings.permissions.tool.todowrite.description": "Oppdater gjøremålslisten",
|
||||||
"settings.permissions.tool.webfetch.title": "Webhenting",
|
"settings.permissions.tool.webfetch.title": "Webhenting",
|
||||||
|
|||||||
@@ -729,6 +729,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.skill.description": "Ładowanie umiejętności według nazwy",
|
"settings.permissions.tool.skill.description": "Ładowanie umiejętności według nazwy",
|
||||||
"settings.permissions.tool.lsp.title": "LSP",
|
"settings.permissions.tool.lsp.title": "LSP",
|
||||||
"settings.permissions.tool.lsp.description": "Uruchamianie zapytań serwera językowego",
|
"settings.permissions.tool.lsp.description": "Uruchamianie zapytań serwera językowego",
|
||||||
|
"settings.permissions.tool.todoread.title": "Odczyt Todo",
|
||||||
|
"settings.permissions.tool.todoread.description": "Odczyt listy zadań",
|
||||||
"settings.permissions.tool.todowrite.title": "Zapis Todo",
|
"settings.permissions.tool.todowrite.title": "Zapis Todo",
|
||||||
"settings.permissions.tool.todowrite.description": "Aktualizacja listy zadań",
|
"settings.permissions.tool.todowrite.description": "Aktualizacja listy zadań",
|
||||||
"settings.permissions.tool.webfetch.title": "Pobieranie z sieci",
|
"settings.permissions.tool.webfetch.title": "Pobieranie z sieci",
|
||||||
|
|||||||
@@ -808,6 +808,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.skill.description": "Загрузка навыка по имени",
|
"settings.permissions.tool.skill.description": "Загрузка навыка по имени",
|
||||||
"settings.permissions.tool.lsp.title": "LSP",
|
"settings.permissions.tool.lsp.title": "LSP",
|
||||||
"settings.permissions.tool.lsp.description": "Запросы к языковому серверу",
|
"settings.permissions.tool.lsp.description": "Запросы к языковому серверу",
|
||||||
|
"settings.permissions.tool.todoread.title": "Todo Read",
|
||||||
|
"settings.permissions.tool.todoread.description": "Чтение списка задач",
|
||||||
"settings.permissions.tool.todowrite.title": "Todo Write",
|
"settings.permissions.tool.todowrite.title": "Todo Write",
|
||||||
"settings.permissions.tool.todowrite.description": "Обновление списка задач",
|
"settings.permissions.tool.todowrite.description": "Обновление списка задач",
|
||||||
"settings.permissions.tool.webfetch.title": "Web Fetch",
|
"settings.permissions.tool.webfetch.title": "Web Fetch",
|
||||||
|
|||||||
@@ -796,6 +796,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.skill.description": "โหลดทักษะตามชื่อ",
|
"settings.permissions.tool.skill.description": "โหลดทักษะตามชื่อ",
|
||||||
"settings.permissions.tool.lsp.title": "LSP",
|
"settings.permissions.tool.lsp.title": "LSP",
|
||||||
"settings.permissions.tool.lsp.description": "เรียกใช้การสืบค้นเซิร์ฟเวอร์ภาษา",
|
"settings.permissions.tool.lsp.description": "เรียกใช้การสืบค้นเซิร์ฟเวอร์ภาษา",
|
||||||
|
"settings.permissions.tool.todoread.title": "อ่านรายการงาน",
|
||||||
|
"settings.permissions.tool.todoread.description": "อ่านรายการงาน",
|
||||||
"settings.permissions.tool.todowrite.title": "เขียนรายการงาน",
|
"settings.permissions.tool.todowrite.title": "เขียนรายการงาน",
|
||||||
"settings.permissions.tool.todowrite.description": "อัปเดตรายการงาน",
|
"settings.permissions.tool.todowrite.description": "อัปเดตรายการงาน",
|
||||||
"settings.permissions.tool.webfetch.title": "ดึงข้อมูลจากเว็บ",
|
"settings.permissions.tool.webfetch.title": "ดึงข้อมูลจากเว็บ",
|
||||||
|
|||||||
@@ -816,6 +816,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.skill.description": "Ada göre bir beceri yükle",
|
"settings.permissions.tool.skill.description": "Ada göre bir beceri yükle",
|
||||||
"settings.permissions.tool.lsp.title": "LSP",
|
"settings.permissions.tool.lsp.title": "LSP",
|
||||||
"settings.permissions.tool.lsp.description": "Dil sunucusu sorguları çalıştır",
|
"settings.permissions.tool.lsp.description": "Dil sunucusu sorguları çalıştır",
|
||||||
|
"settings.permissions.tool.todoread.title": "Görev Oku",
|
||||||
|
"settings.permissions.tool.todoread.description": "Görev listesini oku",
|
||||||
"settings.permissions.tool.todowrite.title": "Görev Yaz",
|
"settings.permissions.tool.todowrite.title": "Görev Yaz",
|
||||||
"settings.permissions.tool.todowrite.description": "Görev listesini güncelle",
|
"settings.permissions.tool.todowrite.description": "Görev listesini güncelle",
|
||||||
"settings.permissions.tool.webfetch.title": "Web Getir",
|
"settings.permissions.tool.webfetch.title": "Web Getir",
|
||||||
|
|||||||
@@ -795,6 +795,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.skill.description": "按名称加载技能",
|
"settings.permissions.tool.skill.description": "按名称加载技能",
|
||||||
"settings.permissions.tool.lsp.title": "LSP",
|
"settings.permissions.tool.lsp.title": "LSP",
|
||||||
"settings.permissions.tool.lsp.description": "运行语言服务器查询",
|
"settings.permissions.tool.lsp.description": "运行语言服务器查询",
|
||||||
|
"settings.permissions.tool.todoread.title": "读取待办",
|
||||||
|
"settings.permissions.tool.todoread.description": "读取待办列表",
|
||||||
"settings.permissions.tool.todowrite.title": "更新待办",
|
"settings.permissions.tool.todowrite.title": "更新待办",
|
||||||
"settings.permissions.tool.todowrite.description": "更新待办列表",
|
"settings.permissions.tool.todowrite.description": "更新待办列表",
|
||||||
"settings.permissions.tool.webfetch.title": "网页获取",
|
"settings.permissions.tool.webfetch.title": "网页获取",
|
||||||
|
|||||||
@@ -790,6 +790,8 @@ export const dict = {
|
|||||||
"settings.permissions.tool.skill.description": "按名稱載入技能",
|
"settings.permissions.tool.skill.description": "按名稱載入技能",
|
||||||
"settings.permissions.tool.lsp.title": "LSP",
|
"settings.permissions.tool.lsp.title": "LSP",
|
||||||
"settings.permissions.tool.lsp.description": "執行語言伺服器查詢",
|
"settings.permissions.tool.lsp.description": "執行語言伺服器查詢",
|
||||||
|
"settings.permissions.tool.todoread.title": "讀取待辦",
|
||||||
|
"settings.permissions.tool.todoread.description": "讀取待辦清單",
|
||||||
"settings.permissions.tool.todowrite.title": "更新待辦",
|
"settings.permissions.tool.todowrite.title": "更新待辦",
|
||||||
"settings.permissions.tool.todowrite.description": "更新待辦清單",
|
"settings.permissions.tool.todowrite.description": "更新待辦清單",
|
||||||
"settings.permissions.tool.webfetch.title": "Web Fetch",
|
"settings.permissions.tool.webfetch.title": "Web Fetch",
|
||||||
|
|||||||
@@ -1,7 +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 { loadLocaleDict, normalizeLocale, type Locale } from "./context/language"
|
|
||||||
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"
|
||||||
export { handleNotificationClick } from "./utils/notification-click"
|
export { handleNotificationClick } from "./utils/notification-click"
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { DataProvider } from "@opencode-ai/ui/context"
|
|||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { base64Encode } from "@opencode-ai/util/encode"
|
import { base64Encode } from "@opencode-ai/util/encode"
|
||||||
import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
||||||
import { createEffect, createMemo, type ParentProps, Show } from "solid-js"
|
import { createMemo, createResource, type ParentProps, Show } from "solid-js"
|
||||||
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { LocalProvider } from "@/context/local"
|
import { LocalProvider } from "@/context/local"
|
||||||
import { SDKProvider } from "@/context/sdk"
|
import { SDKProvider } from "@/context/sdk"
|
||||||
@@ -10,18 +11,10 @@ import { SyncProvider, useSync } from "@/context/sync"
|
|||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
|
|
||||||
function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
|
function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
|
||||||
const location = useLocation()
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const slug = createMemo(() => base64Encode(props.directory))
|
const slug = createMemo(() => base64Encode(props.directory))
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const next = sync.data.path.directory
|
|
||||||
if (!next || next === props.directory) return
|
|
||||||
const path = location.pathname.slice(slug().length + 1)
|
|
||||||
navigate(`/${base64Encode(next)}${path}${location.search}${location.hash}`, { replace: true })
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataProvider
|
<DataProvider
|
||||||
data={sync.data}
|
data={sync.data}
|
||||||
@@ -36,31 +29,50 @@ 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 location = useLocation()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
const globalSDK = useGlobalSDK()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
let invalid = ""
|
let invalid = ""
|
||||||
|
|
||||||
const resolved = createMemo(() => {
|
const [resolved] = createResource(
|
||||||
if (!params.dir) return ""
|
() => {
|
||||||
return decode64(params.dir) ?? ""
|
if (params.dir) return [location.pathname, params.dir] as const
|
||||||
})
|
},
|
||||||
|
async ([pathname, b64Dir]) => {
|
||||||
|
const directory = decode64(b64Dir)
|
||||||
|
|
||||||
createEffect(() => {
|
if (!directory) {
|
||||||
const dir = params.dir
|
if (invalid === params.dir) return
|
||||||
if (!dir) return
|
invalid = b64Dir
|
||||||
if (resolved()) {
|
showToast({
|
||||||
invalid = ""
|
variant: "error",
|
||||||
return
|
title: language.t("common.requestFailed"),
|
||||||
}
|
description: language.t("directory.error.invalidUrl"),
|
||||||
if (invalid === dir) return
|
})
|
||||||
invalid = dir
|
navigate("/", { replace: true })
|
||||||
showToast({
|
return
|
||||||
variant: "error",
|
}
|
||||||
title: language.t("common.requestFailed"),
|
|
||||||
description: language.t("directory.error.invalidUrl"),
|
return await globalSDK
|
||||||
})
|
.createClient({
|
||||||
navigate("/", { replace: true })
|
directory,
|
||||||
})
|
throwOnError: true,
|
||||||
|
})
|
||||||
|
.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={resolved()} keyed>
|
<Show when={resolved()} keyed>
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
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, onMount } from "solid-js"
|
import { Component, Show } 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
|
||||||
@@ -227,13 +226,6 @@ 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)
|
||||||
|
|||||||
@@ -113,14 +113,6 @@ export default function Home() {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={!sync.ready}>
|
|
||||||
<div class="mt-30 mx-auto flex flex-col items-center gap-3">
|
|
||||||
<div class="text-12-regular text-text-weak">{language.t("common.loading")}</div>
|
|
||||||
<Button class="px-3" onClick={chooseProject}>
|
|
||||||
{language.t("command.project.open")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Match>
|
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<div class="mt-30 mx-auto flex flex-col items-center gap-3">
|
<div class="mt-30 mx-auto flex flex-col items-center gap-3">
|
||||||
<Icon name="folder-add-left" size="large" />
|
<Icon name="folder-add-left" size="large" />
|
||||||
|
|||||||
@@ -49,16 +49,21 @@ import { useNotification } from "@/context/notification"
|
|||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
import { Binary } from "@opencode-ai/util/binary"
|
import { Binary } from "@opencode-ai/util/binary"
|
||||||
import { retry } from "@opencode-ai/util/retry"
|
import { retry } from "@opencode-ai/util/retry"
|
||||||
import { playSoundById } from "@/utils/sound"
|
import { playSound, soundSrc } from "@/utils/sound"
|
||||||
import { createAim } from "@/utils/aim"
|
import { createAim } from "@/utils/aim"
|
||||||
import { setNavigate } from "@/utils/notification-click"
|
import { setNavigate } from "@/utils/notification-click"
|
||||||
import { Worktree as WorktreeState } from "@/utils/worktree"
|
import { Worktree as WorktreeState } from "@/utils/worktree"
|
||||||
import { setSessionHandoff } from "@/pages/session/handoff"
|
import { setSessionHandoff } from "@/pages/session/handoff"
|
||||||
|
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context"
|
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
||||||
|
import { DialogSelectProvider } from "@/components/dialog-select-provider"
|
||||||
|
import { DialogSelectServer } from "@/components/dialog-select-server"
|
||||||
|
import { DialogSettings } from "@/components/dialog-settings"
|
||||||
import { useCommand, type CommandOption } from "@/context/command"
|
import { useCommand, type CommandOption } from "@/context/command"
|
||||||
import { ConstrainDragXAxis, getDraggableId } from "@/utils/solid-dnd"
|
import { ConstrainDragXAxis, getDraggableId } from "@/utils/solid-dnd"
|
||||||
|
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
|
||||||
|
import { DialogEditProject } from "@/components/dialog-edit-project"
|
||||||
import { DebugBar } from "@/components/debug-bar"
|
import { DebugBar } from "@/components/debug-bar"
|
||||||
import { Titlebar } from "@/components/titlebar"
|
import { Titlebar } from "@/components/titlebar"
|
||||||
import { useServer } from "@/context/server"
|
import { useServer } from "@/context/server"
|
||||||
@@ -105,8 +110,6 @@ export default function Layout(props: ParentProps) {
|
|||||||
const pageReady = createMemo(() => ready())
|
const pageReady = createMemo(() => ready())
|
||||||
|
|
||||||
let scrollContainerRef: HTMLDivElement | undefined
|
let scrollContainerRef: HTMLDivElement | undefined
|
||||||
let dialogRun = 0
|
|
||||||
let dialogDead = false
|
|
||||||
|
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const globalSDK = useGlobalSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
@@ -126,17 +129,7 @@ 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 availableThemeEntries = createMemo(() => Object.entries(theme.themes()))
|
||||||
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(() => theme.ids().map((id) => [id, theme.themes()[id]] as const))
|
|
||||||
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"> = {
|
||||||
system: "theme.scheme.system",
|
system: "theme.scheme.system",
|
||||||
@@ -144,7 +137,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(() => route().dir)
|
const currentDir = createMemo(() => decode64(params.dir) ?? "")
|
||||||
|
|
||||||
const [state, setState] = createStore({
|
const [state, setState] = createStore({
|
||||||
autoselect: !initialDirectory,
|
autoselect: !initialDirectory,
|
||||||
@@ -198,8 +191,6 @@ export default function Layout(props: ParentProps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
dialogDead = true
|
|
||||||
dialogRun += 1
|
|
||||||
if (navLeave.current !== undefined) clearTimeout(navLeave.current)
|
if (navLeave.current !== undefined) clearTimeout(navLeave.current)
|
||||||
clearTimeout(sortNowTimeout)
|
clearTimeout(sortNowTimeout)
|
||||||
if (sortNowInterval) clearInterval(sortNowInterval)
|
if (sortNowInterval) clearInterval(sortNowInterval)
|
||||||
@@ -210,22 +201,13 @@ 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)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -245,12 +227,6 @@ 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
|
||||||
@@ -319,7 +295,8 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
const clearSidebarHoverState = () => {
|
const clearSidebarHoverState = () => {
|
||||||
if (layout.sidebar.opened()) return
|
if (layout.sidebar.opened()) return
|
||||||
reset()
|
setState("hoverSession", undefined)
|
||||||
|
setHoverProject(undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
const navigateWithSidebarReset = (href: string) => {
|
const navigateWithSidebarReset = (href: string) => {
|
||||||
@@ -335,9 +312,10 @@ export default function Layout(props: ParentProps) {
|
|||||||
const nextIndex = currentIndex === -1 ? 0 : (currentIndex + direction + ids.length) % ids.length
|
const nextIndex = currentIndex === -1 ? 0 : (currentIndex + direction + ids.length) % ids.length
|
||||||
const nextThemeId = ids[nextIndex]
|
const nextThemeId = ids[nextIndex]
|
||||||
theme.setTheme(nextThemeId)
|
theme.setTheme(nextThemeId)
|
||||||
|
const nextTheme = theme.themes()[nextThemeId]
|
||||||
showToast({
|
showToast({
|
||||||
title: language.t("toast.theme.title"),
|
title: language.t("toast.theme.title"),
|
||||||
description: theme.name(nextThemeId),
|
description: nextTheme?.name ?? nextThemeId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,7 +470,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
if (e.details.type === "permission.asked") {
|
if (e.details.type === "permission.asked") {
|
||||||
if (settings.sounds.permissionsEnabled()) {
|
if (settings.sounds.permissionsEnabled()) {
|
||||||
void playSoundById(settings.sounds.permissions())
|
playSound(soundSrc(settings.sounds.permissions()))
|
||||||
}
|
}
|
||||||
if (settings.notifications.permissions()) {
|
if (settings.notifications.permissions()) {
|
||||||
void platform.notify(title, description, href)
|
void platform.notify(title, description, href)
|
||||||
@@ -506,8 +484,8 @@ export default function Layout(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentSession = params.id
|
const currentSession = params.id
|
||||||
if (workspaceKey(directory) === workspaceKey(currentDir()) && props.sessionID === currentSession) return
|
if (directory === currentDir() && props.sessionID === currentSession) return
|
||||||
if (workspaceKey(directory) === workspaceKey(currentDir()) && session?.parentID === currentSession) return
|
if (directory === currentDir() && session?.parentID === currentSession) return
|
||||||
|
|
||||||
dismissSessionAlert(sessionKey)
|
dismissSessionAlert(sessionKey)
|
||||||
|
|
||||||
@@ -642,7 +620,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 = workspaceKey(directory) === workspaceKey(activeDir)
|
const active = directory === activeDir
|
||||||
return expanded || active
|
return expanded || active
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -709,7 +687,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
seen: lru,
|
seen: lru,
|
||||||
keep: sessionID,
|
keep: sessionID,
|
||||||
limit: PREFETCH_MAX_SESSIONS_PER_DIR,
|
limit: PREFETCH_MAX_SESSIONS_PER_DIR,
|
||||||
preserve: params.id && workspaceKey(directory) === workspaceKey(currentDir()) ? [params.id] : undefined,
|
preserve: directory === params.dir && params.id ? [params.id] : undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -722,7 +700,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
route()
|
params.dir
|
||||||
globalSDK.url
|
globalSDK.url
|
||||||
|
|
||||||
prefetchToken.value += 1
|
prefetchToken.value += 1
|
||||||
@@ -948,28 +926,6 @@ 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
|
|
||||||
|
|
||||||
// warm up child store to prevent flicker
|
|
||||||
globalSync.child(target.worktree)
|
|
||||||
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
|
||||||
@@ -1036,20 +992,6 @@ 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"),
|
||||||
@@ -1152,10 +1094,10 @@ export default function Layout(props: ParentProps) {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const [id] of availableThemeEntries()) {
|
for (const [id, definition] of availableThemeEntries()) {
|
||||||
commands.push({
|
commands.push({
|
||||||
id: `theme.set.${id}`,
|
id: `theme.set.${id}`,
|
||||||
title: language.t("command.theme.set", { theme: theme.name(id) }),
|
title: language.t("command.theme.set", { theme: definition.name ?? id }),
|
||||||
category: language.t("command.category.theme"),
|
category: language.t("command.category.theme"),
|
||||||
onSelect: () => theme.commitPreview(),
|
onSelect: () => theme.commitPreview(),
|
||||||
onHighlight: () => {
|
onHighlight: () => {
|
||||||
@@ -1206,27 +1148,15 @@ export default function Layout(props: ParentProps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function connectProvider() {
|
function connectProvider() {
|
||||||
const run = ++dialogRun
|
dialog.show(() => <DialogSelectProvider />)
|
||||||
void import("@/components/dialog-select-provider").then((x) => {
|
|
||||||
if (dialogDead || dialogRun !== run) return
|
|
||||||
dialog.show(() => <x.DialogSelectProvider />)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openServer() {
|
function openServer() {
|
||||||
const run = ++dialogRun
|
dialog.show(() => <DialogSelectServer />)
|
||||||
void import("@/components/dialog-select-server").then((x) => {
|
|
||||||
if (dialogDead || dialogRun !== run) return
|
|
||||||
dialog.show(() => <x.DialogSelectServer />)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openSettings() {
|
function openSettings() {
|
||||||
const run = ++dialogRun
|
dialog.show(() => <DialogSettings />)
|
||||||
void import("@/components/dialog-settings").then((x) => {
|
|
||||||
if (dialogDead || dialogRun !== run) return
|
|
||||||
dialog.show(() => <x.DialogSettings />)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function projectRoot(directory: string) {
|
function projectRoot(directory: string) {
|
||||||
@@ -1453,13 +1383,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
layout.sidebar.toggleWorkspaces(project.worktree)
|
layout.sidebar.toggleWorkspaces(project.worktree)
|
||||||
}
|
}
|
||||||
|
|
||||||
const showEditProjectDialog = (project: LocalProject) => {
|
const showEditProjectDialog = (project: LocalProject) => dialog.show(() => <DialogEditProject project={project} />)
|
||||||
const run = ++dialogRun
|
|
||||||
void import("@/components/dialog-edit-project").then((x) => {
|
|
||||||
if (dialogDead || dialogRun !== run) return
|
|
||||||
dialog.show(() => <x.DialogEditProject project={project} />)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function chooseProject() {
|
async function chooseProject() {
|
||||||
function resolve(result: string | string[] | null) {
|
function resolve(result: string | string[] | null) {
|
||||||
@@ -1480,14 +1404,10 @@ export default function Layout(props: ParentProps) {
|
|||||||
})
|
})
|
||||||
resolve(result)
|
resolve(result)
|
||||||
} else {
|
} else {
|
||||||
const run = ++dialogRun
|
dialog.show(
|
||||||
void import("@/components/dialog-select-directory").then((x) => {
|
() => <DialogSelectDirectory multiple={true} onSelect={resolve} />,
|
||||||
if (dialogDead || dialogRun !== run) return
|
() => resolve(null),
|
||||||
dialog.show(
|
)
|
||||||
() => <x.DialogSelectDirectory multiple={true} onSelect={resolve} />,
|
|
||||||
() => resolve(null),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1772,10 +1692,13 @@ export default function Layout(props: ParentProps) {
|
|||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => {
|
() => {
|
||||||
return [pageReady(), route().slug, params.id, currentProject()?.worktree, currentDir()] as const
|
const dir = params.dir
|
||||||
|
const directory = dir ? decode64(dir) : undefined
|
||||||
|
const resolved = directory ? globalSync.child(directory, { bootstrap: false })[0].path.directory : ""
|
||||||
|
return [pageReady(), dir, params.id, currentProject()?.worktree, directory, resolved] as const
|
||||||
},
|
},
|
||||||
([ready, slug, id, root, dir]) => {
|
([ready, dir, id, root, directory, resolved]) => {
|
||||||
if (!ready || !slug || !dir) {
|
if (!ready || !dir || !directory) {
|
||||||
activeRoute.session = ""
|
activeRoute.session = ""
|
||||||
activeRoute.sessionProject = ""
|
activeRoute.sessionProject = ""
|
||||||
activeRoute.directory = ""
|
activeRoute.directory = ""
|
||||||
@@ -1789,28 +1712,29 @@ export default function Layout(props: ParentProps) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = `${slug}/${id}`
|
const next = resolved || directory
|
||||||
|
const session = `${dir}/${id}`
|
||||||
|
|
||||||
if (!root) {
|
if (!root) {
|
||||||
activeRoute.session = session
|
activeRoute.session = session
|
||||||
activeRoute.directory = dir
|
activeRoute.directory = next
|
||||||
activeRoute.sessionProject = ""
|
activeRoute.sessionProject = ""
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (server.projects.last() !== root) server.projects.touch(root)
|
if (server.projects.last() !== root) server.projects.touch(root)
|
||||||
|
|
||||||
const changed = session !== activeRoute.session || dir !== activeRoute.directory
|
const changed = session !== activeRoute.session || next !== activeRoute.directory
|
||||||
if (changed) {
|
if (changed) {
|
||||||
activeRoute.session = session
|
activeRoute.session = session
|
||||||
activeRoute.directory = dir
|
activeRoute.directory = next
|
||||||
activeRoute.sessionProject = syncSessionRoute(dir, id, root)
|
activeRoute.sessionProject = syncSessionRoute(next, id, root)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root === activeRoute.sessionProject) return
|
if (root === activeRoute.sessionProject) return
|
||||||
activeRoute.directory = dir
|
activeRoute.directory = next
|
||||||
activeRoute.sessionProject = rememberSessionRoute(dir, id, root)
|
activeRoute.sessionProject = rememberSessionRoute(next, id, root)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -1820,9 +1744,6 @@ export default function Layout(props: ParentProps) {
|
|||||||
document.documentElement.style.setProperty("--dialog-left-margin", `${sidebarWidth}px`)
|
document.documentElement.style.setProperty("--dialog-left-margin", `${sidebarWidth}px`)
|
||||||
})
|
})
|
||||||
|
|
||||||
const side = createMemo(() => Math.max(layout.sidebar.width(), 244))
|
|
||||||
const panel = createMemo(() => Math.max(side() - 64, 0))
|
|
||||||
|
|
||||||
const loadedSessionDirs = new Set<string>()
|
const loadedSessionDirs = new Set<string>()
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
@@ -2006,7 +1927,6 @@ 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,
|
||||||
@@ -2014,10 +1934,6 @@ 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,
|
||||||
@@ -2099,7 +2015,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
"max-w-full overflow-hidden": panelProps.mobile,
|
"max-w-full overflow-hidden": panelProps.mobile,
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
width: panelProps.mobile ? undefined : `${panel()}px`,
|
width: panelProps.mobile ? undefined : `${Math.max(Math.max(layout.sidebar.width(), 244) - 64, 0)}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show
|
<Show
|
||||||
@@ -2162,11 +2078,9 @@ export default function Layout(props: ParentProps) {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
data-action="project-menu"
|
data-action="project-menu"
|
||||||
data-project={slug()}
|
data-project={slug()}
|
||||||
class="shrink-0 size-6 rounded-md transition-opacity data-[expanded]:bg-surface-base-active"
|
class="shrink-0 size-6 rounded-md data-[expanded]:bg-surface-base-active"
|
||||||
classList={{
|
classList={{
|
||||||
"opacity-100": panelProps.mobile || merged(),
|
"opacity-0 group-hover/project:opacity-100 data-[expanded]:opacity-100": !panelProps.mobile,
|
||||||
"opacity-0 group-hover/project:opacity-100 group-focus-within/project:opacity-100 data-[expanded]:opacity-100":
|
|
||||||
!panelProps.mobile && !merged(),
|
|
||||||
}}
|
}}
|
||||||
aria-label={language.t("common.moreOptions")}
|
aria-label={language.t("common.moreOptions")}
|
||||||
/>
|
/>
|
||||||
@@ -2391,7 +2305,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
"absolute inset-y-0 left-0": true,
|
"absolute inset-y-0 left-0": true,
|
||||||
"z-10": true,
|
"z-10": true,
|
||||||
}}
|
}}
|
||||||
style={{ width: `${side()}px` }}
|
style={{ width: `${Math.max(layout.sidebar.width(), 244)}px` }}
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
setState("nav", el)
|
setState("nav", el)
|
||||||
}}
|
}}
|
||||||
@@ -2406,29 +2320,26 @@ export default function Layout(props: ParentProps) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="@container w-full h-full contain-strict">{sidebarContent()}</div>
|
<div class="@container w-full h-full contain-strict">{sidebarContent()}</div>
|
||||||
|
<Show when={layout.sidebar.opened()}>
|
||||||
|
<div onPointerDown={() => setState("sizing", true)}>
|
||||||
|
<ResizeHandle
|
||||||
|
direction="horizontal"
|
||||||
|
size={layout.sidebar.width()}
|
||||||
|
min={244}
|
||||||
|
max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64}
|
||||||
|
collapseThreshold={244}
|
||||||
|
onResize={(w) => {
|
||||||
|
setState("sizing", true)
|
||||||
|
if (sizet !== undefined) clearTimeout(sizet)
|
||||||
|
sizet = window.setTimeout(() => setState("sizing", false), 120)
|
||||||
|
layout.sidebar.resize(w)
|
||||||
|
}}
|
||||||
|
onCollapse={layout.sidebar.close}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<Show when={layout.sidebar.opened()}>
|
|
||||||
<div
|
|
||||||
class="hidden xl:block absolute inset-y-0 z-30 w-0 overflow-visible"
|
|
||||||
style={{ left: `${side()}px` }}
|
|
||||||
onPointerDown={() => setState("sizing", true)}
|
|
||||||
>
|
|
||||||
<ResizeHandle
|
|
||||||
direction="horizontal"
|
|
||||||
size={layout.sidebar.width()}
|
|
||||||
min={244}
|
|
||||||
max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64}
|
|
||||||
onResize={(w) => {
|
|
||||||
setState("sizing", true)
|
|
||||||
if (sizet !== undefined) clearTimeout(sizet)
|
|
||||||
sizet = window.setTimeout(() => setState("sizing", false), 120)
|
|
||||||
layout.sidebar.resize(w)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="hidden xl:block pointer-events-none absolute top-0 right-0 z-0 border-t border-border-weaker-base"
|
class="hidden xl:block pointer-events-none absolute top-0 right-0 z-0 border-t border-border-weaker-base"
|
||||||
style={{ left: "calc(4rem + 12px)" }}
|
style={{ left: "calc(4rem + 12px)" }}
|
||||||
@@ -2468,7 +2379,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
!state.sizing,
|
!state.sizing,
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
"--main-left": layout.sidebar.opened() ? `${side()}px` : "4rem",
|
"--main-left": layout.sidebar.opened() ? `${Math.max(layout.sidebar.width(), 244)}px` : "4rem",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<main
|
<main
|
||||||
@@ -2515,7 +2426,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
"duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
|
"duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
|
||||||
"duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
|
"duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
|
||||||
}}
|
}}
|
||||||
style={{ left: `calc(4rem + ${panel()}px)` }}
|
style={{ left: `calc(4rem + ${Math.max(Math.max(layout.sidebar.width(), 244) - 64, 0)}px)` }}
|
||||||
>
|
>
|
||||||
<div class="h-full w-px" style={{ "box-shadow": "var(--shadow-sidebar-overlay)" }} />
|
<div class="h-full w-px" style={{ "box-shadow": "var(--shadow-sidebar-overlay)" }} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,10 +40,10 @@ export const latestRootSession = (stores: SessionStore[], now: number) =>
|
|||||||
stores.flatMap(roots).sort(sortSessions(now))[0]
|
stores.flatMap(roots).sort(sortSessions(now))[0]
|
||||||
|
|
||||||
export function hasProjectPermissions<T>(
|
export function hasProjectPermissions<T>(
|
||||||
request: Record<string, T[] | undefined> | undefined,
|
request: Record<string, T[] | 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[] | undefined) => {
|
export const childMapByParent = (sessions: Session[] | undefined) => {
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ const SessionRow = (props: {
|
|||||||
}): JSX.Element => (
|
}): JSX.Element => (
|
||||||
<A
|
<A
|
||||||
href={`/${props.slug}/session/${props.session.id}`}
|
href={`/${props.slug}/session/${props.session.id}`}
|
||||||
class={`flex items-center gap-1 min-w-0 w-full text-left focus:outline-none ${props.dense ? "py-0.5" : "py-1"}`}
|
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] ${props.mobile ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
|
||||||
onPointerDown={props.warmPress}
|
onPointerDown={props.warmPress}
|
||||||
onPointerEnter={props.warmHover}
|
onPointerEnter={props.warmHover}
|
||||||
onPointerLeave={props.cancelHoverPrefetch}
|
onPointerLeave={props.cancelHoverPrefetch}
|
||||||
@@ -115,26 +115,30 @@ const SessionRow = (props: {
|
|||||||
props.clearHoverProjectSoon()
|
props.clearHoverProjectSoon()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div class="flex items-center gap-1 w-full">
|
||||||
class="shrink-0 size-6 flex items-center justify-center"
|
<div
|
||||||
style={{ color: props.tint() ?? "var(--icon-interactive-base)" }}
|
class="shrink-0 size-6 flex items-center justify-center"
|
||||||
>
|
style={{ color: props.tint() ?? "var(--icon-interactive-base)" }}
|
||||||
<Switch fallback={<Icon name="dash" size="small" class="text-icon-weak" />}>
|
>
|
||||||
<Match when={props.isWorking()}>
|
<Switch fallback={<Icon name="dash" size="small" class="text-icon-weak" />}>
|
||||||
<Spinner class="size-[15px]" />
|
<Match when={props.isWorking()}>
|
||||||
</Match>
|
<Spinner class="size-[15px]" />
|
||||||
<Match when={props.hasPermissions()}>
|
</Match>
|
||||||
<div class="size-1.5 rounded-full bg-surface-warning-strong" />
|
<Match when={props.hasPermissions()}>
|
||||||
</Match>
|
<div class="size-1.5 rounded-full bg-surface-warning-strong" />
|
||||||
<Match when={props.hasError()}>
|
</Match>
|
||||||
<div class="size-1.5 rounded-full bg-text-diff-delete-base" />
|
<Match when={props.hasError()}>
|
||||||
</Match>
|
<div class="size-1.5 rounded-full bg-text-diff-delete-base" />
|
||||||
<Match when={props.unseenCount() > 0}>
|
</Match>
|
||||||
<div class="size-1.5 rounded-full bg-text-interactive-base" />
|
<Match when={props.unseenCount() > 0}>
|
||||||
</Match>
|
<div class="size-1.5 rounded-full bg-text-interactive-base" />
|
||||||
</Switch>
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
<span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate">
|
||||||
|
{props.session.title}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-14-regular text-text-strong min-w-0 flex-1 truncate">{props.session.title}</span>
|
|
||||||
</A>
|
</A>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -153,49 +157,34 @@ 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 => (
|
||||||
let ref: HTMLDivElement | undefined
|
<HoverCard
|
||||||
|
openDelay={1000}
|
||||||
return (
|
closeDelay={props.sidebarHovering() ? 600 : 0}
|
||||||
<HoverCard
|
placement="right-start"
|
||||||
openDelay={1000}
|
gutter={16}
|
||||||
closeDelay={props.sidebarHovering() ? 600 : 0}
|
shift={-2}
|
||||||
placement="right-start"
|
trigger={props.trigger}
|
||||||
gutter={16}
|
open={props.hoverSession() === props.session.id}
|
||||||
shift={-2}
|
onOpenChange={(open) => props.setHoverSession(open ? props.session.id : undefined)}
|
||||||
trigger={
|
>
|
||||||
<div ref={ref} class="min-w-0 w-full">
|
<Show
|
||||||
{props.trigger}
|
when={props.hoverReady()}
|
||||||
</div>
|
fallback={<div class="text-12-regular text-text-weak">{props.language.t("session.messages.loading")}</div>}
|
||||||
}
|
|
||||||
open={props.hoverSession() === props.session.id}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open) {
|
|
||||||
props.setHoverSession(undefined)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!ref?.matches(":hover")) return
|
|
||||||
props.setHoverSession(props.session.id)
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Show
|
<div class="overflow-y-auto overflow-x-hidden max-h-72 h-full">
|
||||||
when={props.hoverReady()}
|
<MessageNav
|
||||||
fallback={<div class="text-12-regular text-text-weak">{props.language.t("session.messages.loading")}</div>}
|
messages={props.hoverMessages() ?? []}
|
||||||
>
|
current={undefined}
|
||||||
<div class="overflow-y-auto overflow-x-hidden max-h-72 h-full">
|
getLabel={props.messageLabel}
|
||||||
<MessageNav
|
onMessageSelect={props.onMessageSelect}
|
||||||
messages={props.hoverMessages() ?? []}
|
size="normal"
|
||||||
current={undefined}
|
class="w-60"
|
||||||
getLabel={props.messageLabel}
|
/>
|
||||||
onMessageSelect={props.onMessageSelect}
|
</div>
|
||||||
size="normal"
|
</Show>
|
||||||
class="w-60"
|
</HoverCard>
|
||||||
/>
|
)
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</HoverCard>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
@@ -309,71 +298,62 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-session-id={props.session.id}
|
data-session-id={props.session.id}
|
||||||
class="group/session relative w-full min-w-0 rounded-md cursor-default pl-2 pr-3 transition-colors
|
class="group/session relative w-full rounded-md cursor-default pl-2 pr-3 transition-colors
|
||||||
hover:bg-surface-raised-base-hover [&:has(:focus-visible)]:bg-surface-raised-base-hover has-[[data-expanded]]:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active"
|
hover:bg-surface-raised-base-hover [&:has(:focus-visible)]:bg-surface-raised-base-hover has-[[data-expanded]]:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active"
|
||||||
>
|
>
|
||||||
<div class="flex min-w-0 items-center gap-1">
|
<Show
|
||||||
<div class="min-w-0 flex-1">
|
when={hoverEnabled()}
|
||||||
<Show
|
fallback={
|
||||||
when={hoverEnabled()}
|
<Tooltip placement={props.mobile ? "bottom" : "right"} value={props.session.title} gutter={10}>
|
||||||
fallback={
|
{item}
|
||||||
<Tooltip
|
|
||||||
placement={props.mobile ? "bottom" : "right"}
|
|
||||||
value={props.session.title}
|
|
||||||
gutter={10}
|
|
||||||
class="min-w-0 w-full"
|
|
||||||
>
|
|
||||||
{item}
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SessionHoverPreview
|
|
||||||
mobile={props.mobile}
|
|
||||||
nav={props.nav}
|
|
||||||
hoverSession={props.hoverSession}
|
|
||||||
session={props.session}
|
|
||||||
sidebarHovering={props.sidebarHovering}
|
|
||||||
hoverReady={hoverReady}
|
|
||||||
hoverMessages={hoverMessages}
|
|
||||||
language={language}
|
|
||||||
isActive={isActive}
|
|
||||||
slug={props.slug}
|
|
||||||
setHoverSession={props.setHoverSession}
|
|
||||||
messageLabel={messageLabel}
|
|
||||||
onMessageSelect={(message) => {
|
|
||||||
if (!isActive())
|
|
||||||
layout.pendingMessage.set(`${base64Encode(props.session.directory)}/${props.session.id}`, message.id)
|
|
||||||
|
|
||||||
navigate(`${props.slug}/session/${props.session.id}#message-${message.id}`)
|
|
||||||
}}
|
|
||||||
trigger={item}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="shrink-0 overflow-hidden transition-[width,opacity]"
|
|
||||||
classList={{
|
|
||||||
"w-6 opacity-100 pointer-events-auto": !!props.mobile,
|
|
||||||
"w-0 opacity-0 pointer-events-none": !props.mobile,
|
|
||||||
"group-hover/session:w-6 group-hover/session:opacity-100 group-hover/session:pointer-events-auto": true,
|
|
||||||
"group-focus-within/session:w-6 group-focus-within/session:opacity-100 group-focus-within/session:pointer-events-auto": true,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tooltip value={language.t("common.archive")} placement="top">
|
|
||||||
<IconButton
|
|
||||||
icon="archive"
|
|
||||||
variant="ghost"
|
|
||||||
class="size-6 rounded-md"
|
|
||||||
aria-label={language.t("common.archive")}
|
|
||||||
onClick={(event) => {
|
|
||||||
event.preventDefault()
|
|
||||||
event.stopPropagation()
|
|
||||||
void props.archiveSession(props.session)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
}
|
||||||
|
>
|
||||||
|
<SessionHoverPreview
|
||||||
|
mobile={props.mobile}
|
||||||
|
nav={props.nav}
|
||||||
|
hoverSession={props.hoverSession}
|
||||||
|
session={props.session}
|
||||||
|
sidebarHovering={props.sidebarHovering}
|
||||||
|
hoverReady={hoverReady}
|
||||||
|
hoverMessages={hoverMessages}
|
||||||
|
language={language}
|
||||||
|
isActive={isActive}
|
||||||
|
slug={props.slug}
|
||||||
|
setHoverSession={props.setHoverSession}
|
||||||
|
messageLabel={messageLabel}
|
||||||
|
onMessageSelect={(message) => {
|
||||||
|
if (!isActive())
|
||||||
|
layout.pendingMessage.set(`${base64Encode(props.session.directory)}/${props.session.id}`, message.id)
|
||||||
|
|
||||||
|
navigate(`${props.slug}/session/${props.session.id}#message-${message.id}`)
|
||||||
|
}}
|
||||||
|
trigger={item}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class={`absolute ${props.dense ? "top-0.5 right-0.5" : "top-1 right-1"} flex items-center gap-0.5 transition-opacity`}
|
||||||
|
classList={{
|
||||||
|
"opacity-100 pointer-events-auto": !!props.mobile,
|
||||||
|
"opacity-0 pointer-events-none": !props.mobile,
|
||||||
|
"group-hover/session:opacity-100 group-hover/session:pointer-events-auto": true,
|
||||||
|
"group-focus-within/session:opacity-100 group-focus-within/session:pointer-events-auto": true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tooltip value={language.t("common.archive")} placement="top">
|
||||||
|
<IconButton
|
||||||
|
icon="archive"
|
||||||
|
variant="ghost"
|
||||||
|
class="size-6 rounded-md"
|
||||||
|
aria-label={language.t("common.archive")}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
void props.archiveSession(props.session)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -395,26 +375,30 @@ export const NewSessionItem = (props: {
|
|||||||
<A
|
<A
|
||||||
href={`/${props.slug}/session`}
|
href={`/${props.slug}/session`}
|
||||||
end
|
end
|
||||||
class={`flex items-center gap-1 min-w-0 w-full text-left focus:outline-none ${props.dense ? "py-0.5" : "py-1"}`}
|
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none ${props.dense ? "py-0.5" : "py-1"}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.setHoverSession(undefined)
|
props.setHoverSession(undefined)
|
||||||
if (layout.sidebar.opened()) return
|
if (layout.sidebar.opened()) return
|
||||||
props.clearHoverProjectSoon()
|
props.clearHoverProjectSoon()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="shrink-0 size-6 flex items-center justify-center">
|
<div class="flex items-center gap-1 w-full">
|
||||||
<Icon name="new-session" size="small" class="text-icon-weak" />
|
<div class="shrink-0 size-6 flex items-center justify-center">
|
||||||
|
<Icon name="new-session" size="small" class="text-icon-weak" />
|
||||||
|
</div>
|
||||||
|
<span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-14-regular text-text-strong min-w-0 flex-1 truncate">{label}</span>
|
|
||||||
</A>
|
</A>
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="group/session relative w-full min-w-0 rounded-md cursor-default transition-colors pl-2 pr-3 hover:bg-surface-raised-base-hover [&:has(:focus-visible)]:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active">
|
<div class="group/session relative w-full rounded-md cursor-default transition-colors pl-2 pr-3 hover:bg-surface-raised-base-hover [&:has(:focus-visible)]:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active">
|
||||||
<Show
|
<Show
|
||||||
when={!tooltip()}
|
when={!tooltip()}
|
||||||
fallback={
|
fallback={
|
||||||
<Tooltip placement={props.mobile ? "bottom" : "right"} value={label} gutter={10} class="min-w-0 w-full">
|
<Tooltip placement={props.mobile ? "bottom" : "right"} value={label} gutter={10}>
|
||||||
{item}
|
{item}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ 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>
|
||||||
@@ -23,7 +22,6 @@ 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
|
||||||
@@ -110,14 +108,8 @@ 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()
|
||||||
}}
|
}}
|
||||||
@@ -137,11 +129,12 @@ 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)}
|
||||||
@@ -198,6 +191,7 @@ 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 => (
|
||||||
@@ -264,7 +258,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.ctx.onHoverOpenChanged(props.project.worktree, false)
|
props.setOpen(false)
|
||||||
if (props.selected()) return
|
if (props.selected()) return
|
||||||
props.ctx.navigateToProject(props.project.worktree)
|
props.ctx.navigateToProject(props.project.worktree)
|
||||||
}}
|
}}
|
||||||
@@ -284,21 +278,37 @@ 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(() => props.ctx.currentProject()?.worktree === props.project.worktree)
|
const selected = createMemo(
|
||||||
|
() =>
|
||||||
|
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(() => state.menu || (preview() ? isHoverProject() : overlay() && isHoverProject()))
|
const active = createMemo(
|
||||||
|
() => state.menu || (preview() ? state.open : overlay() && props.ctx.hoverProject() === props.project.worktree),
|
||||||
|
)
|
||||||
|
|
||||||
const hoverOpen = () => isHoverProject() && preview() && !selected() && !state.menu
|
createEffect(() => {
|
||||||
|
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 })
|
||||||
@@ -339,7 +349,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) => props.ctx.onHoverOpenChanged(props.project.worktree, value)}
|
setOpen={(value) => setState("open", value)}
|
||||||
setSuppressHover={(value) => setState("suppressHover", value)}
|
setSuppressHover={(value) => setState("suppressHover", value)}
|
||||||
language={language}
|
language={language}
|
||||||
/>
|
/>
|
||||||
@@ -350,7 +360,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 && hoverOpen() && !state.menu}
|
open={!state.suppressHover && state.open && !state.menu}
|
||||||
openDelay={0}
|
openDelay={0}
|
||||||
closeDelay={0}
|
closeDelay={0}
|
||||||
placement="right-start"
|
placement="right-start"
|
||||||
@@ -359,7 +369,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
|
||||||
props.ctx.onHoverOpenChanged(props.project.worktree, value)
|
setState("open", value)
|
||||||
if (value) props.ctx.setHoverSession(undefined)
|
if (value) props.ctx.setHoverSession(undefined)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -374,6 +384,7 @@ 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, workspaceKey } from "./helpers"
|
import { childMapByParent, sortedRootSessions } 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(() => workspaceKey(props.ctx.currentDir()) === workspaceKey(props.directory))
|
const active = createMemo(() => props.ctx.currentDir() === 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)
|
||||||
|
|||||||
+171
-383
@@ -1,6 +1,5 @@
|
|||||||
import type { FileDiff, 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,
|
||||||
@@ -41,13 +40,7 @@ import { useSync } from "@/context/sync"
|
|||||||
import { useTerminal } from "@/context/terminal"
|
import { useTerminal } from "@/context/terminal"
|
||||||
import { type FollowupDraft, sendFollowupDraft } from "@/components/prompt-input/submit"
|
import { type FollowupDraft, sendFollowupDraft } from "@/components/prompt-input/submit"
|
||||||
import { createSessionComposerState, SessionComposerRegion } from "@/pages/session/composer"
|
import { createSessionComposerState, SessionComposerRegion } from "@/pages/session/composer"
|
||||||
import {
|
import { createOpenReviewFile, createSessionTabs, createSizing, focusTerminalById } from "@/pages/session/helpers"
|
||||||
createOpenReviewFile,
|
|
||||||
createSessionTabs,
|
|
||||||
createSizing,
|
|
||||||
focusTerminalById,
|
|
||||||
shouldFocusTerminalOnKeyDown,
|
|
||||||
} from "@/pages/session/helpers"
|
|
||||||
import { MessageTimeline } from "@/pages/session/message-timeline"
|
import { MessageTimeline } from "@/pages/session/message-timeline"
|
||||||
import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab"
|
import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab"
|
||||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
@@ -64,9 +57,6 @@ import { formatServerError } from "@/utils/server-errors"
|
|||||||
const emptyUserMessages: UserMessage[] = []
|
const emptyUserMessages: UserMessage[] = []
|
||||||
const emptyFollowups: (FollowupDraft & { id: string })[] = []
|
const emptyFollowups: (FollowupDraft & { id: string })[] = []
|
||||||
|
|
||||||
type ChangeMode = "git" | "branch" | "session" | "turn"
|
|
||||||
type VcsMode = "git" | "branch"
|
|
||||||
|
|
||||||
type SessionHistoryWindowInput = {
|
type SessionHistoryWindowInput = {
|
||||||
sessionID: () => string | undefined
|
sessionID: () => string | undefined
|
||||||
messagesReady: () => boolean
|
messagesReady: () => boolean
|
||||||
@@ -249,19 +239,14 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
|||||||
|
|
||||||
if (added <= 0) return
|
if (added <= 0) return
|
||||||
if (growth <= 0) return
|
if (growth <= 0) return
|
||||||
|
|
||||||
if (opts?.prefetch) {
|
|
||||||
const current = turnStart()
|
|
||||||
preserveScroll(() => setTurnStart(current + growth))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (turnStart() !== start) return
|
if (turnStart() !== start) return
|
||||||
|
|
||||||
|
const reveal = !opts?.prefetch
|
||||||
const currentRendered = renderedUserMessages().length
|
const currentRendered = renderedUserMessages().length
|
||||||
const base = Math.max(beforeRendered, currentRendered)
|
const base = Math.max(beforeRendered, currentRendered)
|
||||||
const target = Math.min(afterVisible, base + turnBatch)
|
const target = reveal ? Math.min(afterVisible, base + turnBatch) : base
|
||||||
preserveScroll(() => setTurnStart(Math.max(0, afterVisible - target)))
|
const nextStart = Math.max(0, afterVisible - target)
|
||||||
|
preserveScroll(() => setTurnStart(nextStart))
|
||||||
}
|
}
|
||||||
|
|
||||||
const onScrollerScroll = () => {
|
const onScrollerScroll = () => {
|
||||||
@@ -342,7 +327,10 @@ 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: {
|
||||||
@@ -427,16 +415,15 @@ export default function Page() {
|
|||||||
|
|
||||||
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||||
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
|
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
|
||||||
const sessionCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
|
const reviewCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
|
||||||
const hasSessionReview = createMemo(() => sessionCount() > 0)
|
const hasReview = createMemo(() => reviewCount() > 0)
|
||||||
const canReview = createMemo(() => !!params.id)
|
|
||||||
const reviewTab = createMemo(() => isDesktop())
|
const reviewTab = createMemo(() => isDesktop())
|
||||||
const tabState = createSessionTabs({
|
const tabState = createSessionTabs({
|
||||||
tabs,
|
tabs,
|
||||||
pathFromTab: file.pathFromTab,
|
pathFromTab: file.pathFromTab,
|
||||||
normalizeTab,
|
normalizeTab,
|
||||||
review: reviewTab,
|
review: reviewTab,
|
||||||
hasReview: canReview,
|
hasReview,
|
||||||
})
|
})
|
||||||
const contextOpen = tabState.contextOpen
|
const contextOpen = tabState.contextOpen
|
||||||
const openedTabs = tabState.openedTabs
|
const openedTabs = tabState.openedTabs
|
||||||
@@ -459,12 +446,6 @@ export default function Page() {
|
|||||||
if (!id) return false
|
if (!id) return false
|
||||||
return sync.session.history.loading(id)
|
return sync.session.history.loading(id)
|
||||||
})
|
})
|
||||||
const diffsReady = createMemo(() => {
|
|
||||||
const id = params.id
|
|
||||||
if (!id) return true
|
|
||||||
if (!hasSessionReview()) return true
|
|
||||||
return sync.data.session_diff[id] !== undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
const userMessages = createMemo(
|
const userMessages = createMemo(
|
||||||
() => messages().filter((m) => m.role === "user") as UserMessage[],
|
() => messages().filter((m) => m.role === "user") as UserMessage[],
|
||||||
@@ -518,24 +499,14 @@ export default function Page() {
|
|||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
messageId: undefined as string | undefined,
|
messageId: undefined as string | undefined,
|
||||||
mobileTab: "session" as "session" | "changes",
|
mobileTab: "session" as "session" | "changes",
|
||||||
changes: "git" as ChangeMode,
|
changes: "session" as "session" | "turn",
|
||||||
newSessionWorktree: "main",
|
newSessionWorktree: "main",
|
||||||
deferRender: false,
|
deferRender: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const [vcs, setVcs] = createStore({
|
|
||||||
diff: {
|
|
||||||
git: [] as FileDiff[],
|
|
||||||
branch: [] as FileDiff[],
|
|
||||||
},
|
|
||||||
ready: {
|
|
||||||
git: false,
|
|
||||||
branch: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
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<
|
||||||
@@ -560,68 +531,6 @@ export default function Page() {
|
|||||||
let refreshTimer: number | undefined
|
let refreshTimer: number | undefined
|
||||||
let diffFrame: number | undefined
|
let diffFrame: number | undefined
|
||||||
let diffTimer: number | undefined
|
let diffTimer: number | undefined
|
||||||
const vcsTask = new Map<VcsMode, Promise<void>>()
|
|
||||||
const vcsRun = new Map<VcsMode, number>()
|
|
||||||
|
|
||||||
const bumpVcs = (mode: VcsMode) => {
|
|
||||||
const next = (vcsRun.get(mode) ?? 0) + 1
|
|
||||||
vcsRun.set(mode, next)
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetVcs = (mode?: VcsMode) => {
|
|
||||||
const list = mode ? [mode] : (["git", "branch"] as const)
|
|
||||||
list.forEach((item) => {
|
|
||||||
bumpVcs(item)
|
|
||||||
vcsTask.delete(item)
|
|
||||||
setVcs("diff", item, [])
|
|
||||||
setVcs("ready", item, false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadVcs = (mode: VcsMode, force = false) => {
|
|
||||||
if (sync.project?.vcs !== "git") return Promise.resolve()
|
|
||||||
if (!force && vcs.ready[mode]) return Promise.resolve()
|
|
||||||
|
|
||||||
if (force) {
|
|
||||||
if (vcsTask.has(mode)) bumpVcs(mode)
|
|
||||||
vcsTask.delete(mode)
|
|
||||||
setVcs("ready", mode, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const current = vcsTask.get(mode)
|
|
||||||
if (current) return current
|
|
||||||
|
|
||||||
const run = bumpVcs(mode)
|
|
||||||
|
|
||||||
const task = sdk.client.vcs
|
|
||||||
.diff({ mode })
|
|
||||||
.then((result) => {
|
|
||||||
if (vcsRun.get(mode) !== run) return
|
|
||||||
setVcs("diff", mode, result.data ?? [])
|
|
||||||
setVcs("ready", mode, true)
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
if (vcsRun.get(mode) !== run) return
|
|
||||||
console.debug("[session-review] failed to load vcs diff", { mode, error })
|
|
||||||
setVcs("diff", mode, [])
|
|
||||||
setVcs("ready", mode, true)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
if (vcsTask.get(mode) === task) vcsTask.delete(mode)
|
|
||||||
})
|
|
||||||
|
|
||||||
vcsTask.set(mode, task)
|
|
||||||
return task
|
|
||||||
}
|
|
||||||
|
|
||||||
const refreshVcs = () => {
|
|
||||||
resetVcs()
|
|
||||||
const mode = untrack(vcsMode)
|
|
||||||
if (!mode) return
|
|
||||||
if (!untrack(wantsReview)) return
|
|
||||||
void loadVcs(mode, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
createComputed((prev) => {
|
createComputed((prev) => {
|
||||||
const open = desktopReviewOpen()
|
const open = desktopReviewOpen()
|
||||||
@@ -637,42 +546,7 @@ export default function Page() {
|
|||||||
}, desktopReviewOpen())
|
}, desktopReviewOpen())
|
||||||
|
|
||||||
const turnDiffs = createMemo(() => lastUserMessage()?.summary?.diffs ?? [])
|
const turnDiffs = createMemo(() => lastUserMessage()?.summary?.diffs ?? [])
|
||||||
const changesOptions = createMemo<ChangeMode[]>(() => {
|
const reviewDiffs = createMemo(() => (store.changes === "session" ? diffs() : turnDiffs()))
|
||||||
const list: ChangeMode[] = []
|
|
||||||
if (sync.project?.vcs === "git") list.push("git")
|
|
||||||
if (
|
|
||||||
sync.project?.vcs === "git" &&
|
|
||||||
sync.data.vcs?.branch &&
|
|
||||||
sync.data.vcs?.default_branch &&
|
|
||||||
sync.data.vcs.branch !== sync.data.vcs.default_branch
|
|
||||||
) {
|
|
||||||
list.push("branch")
|
|
||||||
}
|
|
||||||
list.push("session", "turn")
|
|
||||||
return list
|
|
||||||
})
|
|
||||||
const vcsMode = createMemo<VcsMode | undefined>(() => {
|
|
||||||
if (store.changes === "git" || store.changes === "branch") return store.changes
|
|
||||||
})
|
|
||||||
const reviewDiffs = createMemo(() => {
|
|
||||||
if (store.changes === "git") return vcs.diff.git
|
|
||||||
if (store.changes === "branch") return vcs.diff.branch
|
|
||||||
if (store.changes === "session") return diffs()
|
|
||||||
return turnDiffs()
|
|
||||||
})
|
|
||||||
const reviewCount = createMemo(() => {
|
|
||||||
if (store.changes === "git") return vcs.diff.git.length
|
|
||||||
if (store.changes === "branch") return vcs.diff.branch.length
|
|
||||||
if (store.changes === "session") return sessionCount()
|
|
||||||
return turnDiffs().length
|
|
||||||
})
|
|
||||||
const hasReview = createMemo(() => reviewCount() > 0)
|
|
||||||
const reviewReady = createMemo(() => {
|
|
||||||
if (store.changes === "git") return vcs.ready.git
|
|
||||||
if (store.changes === "branch") return vcs.ready.branch
|
|
||||||
if (store.changes === "session") return !hasSessionReview() || diffsReady()
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
const newSessionWorktree = createMemo(() => {
|
const newSessionWorktree = createMemo(() => {
|
||||||
if (store.newSessionWorktree === "create") return "create"
|
if (store.newSessionWorktree === "create") return "create"
|
||||||
@@ -738,7 +612,13 @@ export default function Page() {
|
|||||||
scrollToMessage(msgs[targetIndex], "auto")
|
scrollToMessage(msgs[targetIndex], "auto")
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionEmptyKey = createMemo(() => {
|
const diffsReady = createMemo(() => {
|
||||||
|
const id = params.id
|
||||||
|
if (!id) return true
|
||||||
|
if (!hasReview()) return true
|
||||||
|
return sync.data.session_diff[id] !== undefined
|
||||||
|
})
|
||||||
|
const reviewEmptyKey = createMemo(() => {
|
||||||
const project = sync.project
|
const project = sync.project
|
||||||
if (project && !project.vcs) return "session.review.noVcs"
|
if (project && !project.vcs) return "session.review.noVcs"
|
||||||
if (sync.data.config.snapshot === false) return "session.review.noSnapshot"
|
if (sync.data.config.snapshot === false) return "session.review.noSnapshot"
|
||||||
@@ -764,24 +644,25 @@ 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 (gitMutation.isPending) return
|
if (ui.git) return
|
||||||
gitMutation.mutate()
|
setUi("git", true)
|
||||||
|
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
|
||||||
@@ -860,46 +741,13 @@ export default function Page() {
|
|||||||
sessionKey,
|
sessionKey,
|
||||||
() => {
|
() => {
|
||||||
setStore("messageId", undefined)
|
setStore("messageId", undefined)
|
||||||
setStore("changes", "git")
|
setStore("changes", "session")
|
||||||
setUi("pendingMessage", undefined)
|
setUi("pendingMessage", undefined)
|
||||||
},
|
},
|
||||||
{ defer: true },
|
{ defer: true },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
() => sdk.directory,
|
|
||||||
() => {
|
|
||||||
resetVcs()
|
|
||||||
},
|
|
||||||
{ defer: true },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
() => [sync.data.vcs?.branch, sync.data.vcs?.default_branch] as const,
|
|
||||||
(next, prev) => {
|
|
||||||
if (prev === undefined || same(next, prev)) return
|
|
||||||
refreshVcs()
|
|
||||||
},
|
|
||||||
{ defer: true },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const stopVcs = sdk.event.listen((evt) => {
|
|
||||||
if (evt.details.type !== "file.watcher.updated") return
|
|
||||||
const props =
|
|
||||||
typeof evt.details.properties === "object" && evt.details.properties
|
|
||||||
? (evt.details.properties as Record<string, unknown>)
|
|
||||||
: undefined
|
|
||||||
const file = typeof props?.file === "string" ? props.file : undefined
|
|
||||||
if (!file || file.startsWith(".git/")) return
|
|
||||||
refreshVcs()
|
|
||||||
})
|
|
||||||
onCleanup(stopVcs)
|
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => params.dir,
|
() => params.dir,
|
||||||
@@ -1006,7 +854,7 @@ export default function Page() {
|
|||||||
// Prefer the open terminal over the composer when it can take focus
|
// Prefer the open terminal over the composer when it can take focus
|
||||||
if (view().terminal.opened()) {
|
if (view().terminal.opened()) {
|
||||||
const id = terminal.active()
|
const id = terminal.active()
|
||||||
if (id && shouldFocusTerminalOnKeyDown(event) && focusTerminalById(id)) return
|
if (id && focusTerminalById(id)) return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only treat explicit scroll keys as potential "user scroll" gestures.
|
// Only treat explicit scroll keys as potential "user scroll" gestures.
|
||||||
@@ -1022,40 +870,6 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes")
|
const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes")
|
||||||
const wantsReview = createMemo(() =>
|
|
||||||
isDesktop()
|
|
||||||
? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
|
|
||||||
: store.mobileTab === "changes",
|
|
||||||
)
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const list = changesOptions()
|
|
||||||
if (list.includes(store.changes)) return
|
|
||||||
const next = list[0]
|
|
||||||
if (!next) return
|
|
||||||
setStore("changes", next)
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const mode = vcsMode()
|
|
||||||
if (!mode) return
|
|
||||||
if (!wantsReview()) return
|
|
||||||
void loadVcs(mode)
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
() => sync.data.session_status[params.id ?? ""]?.type,
|
|
||||||
(next, prev) => {
|
|
||||||
const mode = vcsMode()
|
|
||||||
if (!mode) return
|
|
||||||
if (!wantsReview()) return
|
|
||||||
if (next !== "idle" || prev === undefined || prev === "idle") return
|
|
||||||
void loadVcs(mode, true)
|
|
||||||
},
|
|
||||||
{ defer: true },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const fileTreeTab = () => layout.fileTree.tab()
|
const fileTreeTab = () => layout.fileTree.tab()
|
||||||
const setFileTreeTab = (value: "changes" | "all") => layout.fileTree.setTab(value)
|
const setFileTreeTab = (value: "changes" | "all") => layout.fileTree.setTab(value)
|
||||||
@@ -1102,23 +916,21 @@ export default function Page() {
|
|||||||
loadFile: file.load,
|
loadFile: file.load,
|
||||||
})
|
})
|
||||||
|
|
||||||
const changesTitle = () => {
|
const changesOptions = ["session", "turn"] as const
|
||||||
if (!canReview()) {
|
const changesOptionsList = [...changesOptions]
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const label = (option: ChangeMode) => {
|
const changesTitle = () => {
|
||||||
if (option === "git") return language.t("ui.sessionReview.title.git")
|
if (!hasReview()) {
|
||||||
if (option === "branch") return language.t("ui.sessionReview.title.branch")
|
return null
|
||||||
if (option === "session") return language.t("ui.sessionReview.title")
|
|
||||||
return language.t("ui.sessionReview.title.lastTurn")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
options={changesOptions()}
|
options={changesOptionsList}
|
||||||
current={store.changes}
|
current={store.changes}
|
||||||
label={label}
|
label={(option) =>
|
||||||
|
option === "session" ? language.t("ui.sessionReview.title") : language.t("ui.sessionReview.title.lastTurn")
|
||||||
|
}
|
||||||
onSelect={(option) => option && setStore("changes", option)}
|
onSelect={(option) => option && setStore("changes", option)}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -1127,34 +939,20 @@ export default function Page() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const empty = (text: string) => (
|
const emptyTurn = () => (
|
||||||
<div class="h-full pb-64 -mt-4 flex flex-col items-center justify-center text-center gap-6">
|
<div class="h-full pb-64 -mt-4 flex flex-col items-center justify-center text-center gap-6">
|
||||||
<div class="text-14-regular text-text-weak max-w-56">{text}</div>
|
<div class="text-14-regular text-text-weak max-w-56">{language.t("session.review.noChanges")}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
const reviewEmptyText = createMemo(() => {
|
|
||||||
if (store.changes === "git") return language.t("session.review.noUncommittedChanges")
|
|
||||||
if (store.changes === "branch") return language.t("session.review.noBranchChanges")
|
|
||||||
if (store.changes === "turn") return language.t("session.review.noChanges")
|
|
||||||
return language.t(sessionEmptyKey())
|
|
||||||
})
|
|
||||||
|
|
||||||
const reviewEmpty = (input: { loadingClass: string; emptyClass: string }) => {
|
const reviewEmpty = (input: { loadingClass: string; emptyClass: string }) => {
|
||||||
if (store.changes === "git" || store.changes === "branch") {
|
if (store.changes === "turn") return emptyTurn()
|
||||||
if (!reviewReady()) return <div class={input.loadingClass}>{language.t("session.review.loadingChanges")}</div>
|
|
||||||
return empty(reviewEmptyText())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (store.changes === "turn") {
|
if (hasReview() && !diffsReady()) {
|
||||||
return empty(reviewEmptyText())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasSessionReview() && !diffsReady()) {
|
|
||||||
return <div class={input.loadingClass}>{language.t("session.review.loadingChanges")}</div>
|
return <div class={input.loadingClass}>{language.t("session.review.loadingChanges")}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sessionEmptyKey() === "session.review.noVcs") {
|
if (reviewEmptyKey() === "session.review.noVcs") {
|
||||||
return (
|
return (
|
||||||
<div class={input.emptyClass}>
|
<div class={input.emptyClass}>
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
@@ -1163,8 +961,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={gitMutation.isPending} onClick={initGit}>
|
<Button size="large" disabled={ui.git} onClick={initGit}>
|
||||||
{gitMutation.isPending
|
{ui.git
|
||||||
? 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>
|
||||||
@@ -1174,7 +972,7 @@ export default function Page() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={input.emptyClass}>
|
<div class={input.emptyClass}>
|
||||||
<div class="text-14-regular text-text-weak max-w-56">{reviewEmptyText()}</div>
|
<div class="text-14-regular text-text-weak max-w-56">{language.t(reviewEmptyKey())}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1278,7 +1076,7 @@ export default function Page() {
|
|||||||
const pending = tree.pendingDiff
|
const pending = tree.pendingDiff
|
||||||
if (!pending) return
|
if (!pending) return
|
||||||
if (!tree.reviewScroll) return
|
if (!tree.reviewScroll) return
|
||||||
if (!reviewReady()) return
|
if (!diffsReady()) return
|
||||||
|
|
||||||
const attempt = (count: number) => {
|
const attempt = (count: number) => {
|
||||||
if (tree.pendingDiff !== pending) return
|
if (tree.pendingDiff !== pending) return
|
||||||
@@ -1319,7 +1117,10 @@ export default function Page() {
|
|||||||
const id = params.id
|
const id = params.id
|
||||||
if (!id) return
|
if (!id) return
|
||||||
|
|
||||||
if (!wantsReview()) return
|
const wants = isDesktop()
|
||||||
|
? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
|
||||||
|
: store.mobileTab === "changes"
|
||||||
|
if (!wants) return
|
||||||
if (sync.data.session_diff[id] !== undefined) return
|
if (sync.data.session_diff[id] !== undefined) return
|
||||||
if (sync.status === "loading") return
|
if (sync.status === "loading") return
|
||||||
|
|
||||||
@@ -1328,7 +1129,13 @@ export default function Page() {
|
|||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => [sessionKey(), wantsReview()] as const,
|
() =>
|
||||||
|
[
|
||||||
|
sessionKey(),
|
||||||
|
isDesktop()
|
||||||
|
? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
|
||||||
|
: store.mobileTab === "changes",
|
||||||
|
] as const,
|
||||||
([key, wants]) => {
|
([key, wants]) => {
|
||||||
if (diffFrame !== undefined) cancelAnimationFrame(diffFrame)
|
if (diffFrame !== undefined) cancelAnimationFrame(diffFrame)
|
||||||
if (diffTimer !== undefined) window.clearTimeout(diffTimer)
|
if (diffTimer !== undefined) window.clearTimeout(diffTimer)
|
||||||
@@ -1370,6 +1177,8 @@ export default function Page() {
|
|||||||
on(
|
on(
|
||||||
() => sdk.directory,
|
() => sdk.directory,
|
||||||
() => {
|
() => {
|
||||||
|
void file.tree.list("")
|
||||||
|
|
||||||
const tab = activeFileTab()
|
const tab = activeFileTab()
|
||||||
if (!tab) return
|
if (!tab) return
|
||||||
const path = file.pathFromTab(tab)
|
const path = file.pathFromTab(tab)
|
||||||
@@ -1570,40 +1379,10 @@ 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
|
||||||
if (!followupBusy(id)) return
|
return followup.sending[id]
|
||||||
return followupMutation.variables?.id
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const queueEnabled = createMemo(() => {
|
const queueEnabled = createMemo(() => {
|
||||||
@@ -1643,15 +1422,37 @@ 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 (followupBusy(sessionID)) return Promise.resolve()
|
if (followup.sending[sessionID]) return Promise.resolve()
|
||||||
|
|
||||||
return followupMutation.mutateAsync({ sessionID, id, manual: opts?.manual })
|
if (opts?.manual) setFollowup("paused", sessionID, undefined)
|
||||||
|
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 (followupBusy(sessionID)) return
|
if (followup.sending[sessionID]) return
|
||||||
|
|
||||||
const item = queuedFollowups().find((entry) => entry.id === id)
|
const item = queuedFollowups().find((entry) => entry.id === id)
|
||||||
if (!item) return
|
if (!item) return
|
||||||
@@ -1674,74 +1475,6 @@ 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)
|
||||||
@@ -1763,13 +1496,77 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const revert = (input: { sessionID: string; messageID: string }) => {
|
const revert = (input: { sessionID: string; messageID: string }) => {
|
||||||
if (reverting()) return
|
if (ui.reverting || ui.restoring) return
|
||||||
return revertMutation.mutateAsync(input)
|
const prev = prompt.current().slice()
|
||||||
|
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) => {
|
||||||
if (!params.id || reverting()) return
|
const sessionID = params.id
|
||||||
return restoreMutation.mutateAsync(id)
|
if (!sessionID || ui.restoring || ui.reverting) return
|
||||||
|
|
||||||
|
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(() => {
|
||||||
@@ -1788,7 +1585,7 @@ export default function Page() {
|
|||||||
|
|
||||||
const item = queuedFollowups()[0]
|
const item = queuedFollowups()[0]
|
||||||
if (!item) return
|
if (!item) return
|
||||||
if (followupBusy(sessionID)) return
|
if (followup.sending[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
|
||||||
@@ -1824,9 +1621,6 @@ export default function Page() {
|
|||||||
sessionID: () => params.id,
|
sessionID: () => params.id,
|
||||||
messagesReady,
|
messagesReady,
|
||||||
visibleUserMessages,
|
visibleUserMessages,
|
||||||
historyMore,
|
|
||||||
historyLoading,
|
|
||||||
loadMore: (sessionID) => sync.session.history.loadMore(sessionID),
|
|
||||||
turnStart: historyWindow.turnStart,
|
turnStart: historyWindow.turnStart,
|
||||||
currentMessageId: () => store.messageId,
|
currentMessageId: () => store.messageId,
|
||||||
pendingMessage: () => ui.pendingMessage,
|
pendingMessage: () => ui.pendingMessage,
|
||||||
@@ -1898,7 +1692,7 @@ export default function Page() {
|
|||||||
<div class="flex-1 min-h-0 overflow-hidden">
|
<div class="flex-1 min-h-0 overflow-hidden">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={params.id}>
|
<Match when={params.id}>
|
||||||
<Show when={messagesReady()}>
|
<Show when={lastUserMessage()}>
|
||||||
<MessageTimeline
|
<MessageTimeline
|
||||||
mobileChanges={mobileChanges()}
|
mobileChanges={mobileChanges()}
|
||||||
mobileFallback={reviewContent({
|
mobileFallback={reviewContent({
|
||||||
@@ -1986,8 +1780,8 @@ export default function Page() {
|
|||||||
rolled().length > 0
|
rolled().length > 0
|
||||||
? {
|
? {
|
||||||
items: rolled(),
|
items: rolled(),
|
||||||
restoring: restoring(),
|
restoring: ui.restoring,
|
||||||
disabled: reverting(),
|
disabled: ui.reverting,
|
||||||
onRestore: restore,
|
onRestore: restore,
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
@@ -2014,12 +1808,6 @@ export default function Page() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SessionSidePanel
|
<SessionSidePanel
|
||||||
canReview={canReview}
|
|
||||||
diffs={reviewDiffs}
|
|
||||||
diffsReady={reviewReady}
|
|
||||||
empty={reviewEmptyText}
|
|
||||||
hasReview={hasReview}
|
|
||||||
reviewCount={reviewCount}
|
|
||||||
reviewPanel={reviewPanel}
|
reviewPanel={reviewPanel}
|
||||||
activeDiff={tree.activeDiff}
|
activeDiff={tree.activeDiff}
|
||||||
focusReviewDiff={focusReviewDiff}
|
focusReviewDiff={focusReviewDiff}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
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"
|
||||||
@@ -25,6 +24,7 @@ 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,40 +126,36 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
showToast({ title: language.t("common.requestFailed"), description: message })
|
showToast({ title: language.t("common.requestFailed"), description: message })
|
||||||
}
|
}
|
||||||
|
|
||||||
const replyMutation = useMutation(() => ({
|
|
||||||
mutationFn: (answers: QuestionAnswer[]) => sdk.client.question.reply({ requestID: props.request.id, answers }),
|
|
||||||
onMutate: () => {
|
|
||||||
props.onSubmit()
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
replied = true
|
|
||||||
cache.delete(props.request.id)
|
|
||||||
},
|
|
||||||
onError: fail,
|
|
||||||
}))
|
|
||||||
|
|
||||||
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[]) => {
|
const reply = async (answers: QuestionAnswer[]) => {
|
||||||
if (sending()) return
|
if (store.sending) return
|
||||||
await replyMutation.mutateAsync(answers)
|
|
||||||
|
props.onSubmit()
|
||||||
|
setStore("sending", true)
|
||||||
|
try {
|
||||||
|
await sdk.client.question.reply({ requestID: props.request.id, answers })
|
||||||
|
replied = true
|
||||||
|
cache.delete(props.request.id)
|
||||||
|
} catch (err) {
|
||||||
|
fail(err)
|
||||||
|
} finally {
|
||||||
|
setStore("sending", false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const reject = async () => {
|
const reject = async () => {
|
||||||
if (sending()) return
|
if (store.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] ?? []))
|
||||||
@@ -179,7 +175,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
}
|
}
|
||||||
|
|
||||||
const customToggle = () => {
|
const customToggle = () => {
|
||||||
if (sending()) return
|
if (store.sending) return
|
||||||
|
|
||||||
if (!multi()) {
|
if (!multi()) {
|
||||||
setStore("customOn", store.tab, true)
|
setStore("customOn", store.tab, true)
|
||||||
@@ -202,14 +198,14 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
}
|
}
|
||||||
|
|
||||||
const customOpen = () => {
|
const customOpen = () => {
|
||||||
if (sending()) return
|
if (store.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 (sending()) return
|
if (store.sending) return
|
||||||
|
|
||||||
if (optIndex === options().length) {
|
if (optIndex === options().length) {
|
||||||
customOpen()
|
customOpen()
|
||||||
@@ -231,7 +227,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
}
|
}
|
||||||
|
|
||||||
const next = () => {
|
const next = () => {
|
||||||
if (sending()) return
|
if (store.sending) return
|
||||||
if (store.editing) commitCustom()
|
if (store.editing) commitCustom()
|
||||||
|
|
||||||
if (store.tab >= total() - 1) {
|
if (store.tab >= total() - 1) {
|
||||||
@@ -244,14 +240,14 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
}
|
}
|
||||||
|
|
||||||
const back = () => {
|
const back = () => {
|
||||||
if (sending()) return
|
if (store.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 (sending()) return
|
if (store.sending) return
|
||||||
setStore("tab", tab)
|
setStore("tab", tab)
|
||||||
setStore("editing", false)
|
setStore("editing", false)
|
||||||
}
|
}
|
||||||
@@ -274,7 +270,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={sending()}
|
disabled={store.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}`}
|
||||||
/>
|
/>
|
||||||
@@ -285,16 +281,16 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
}
|
}
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
<Button variant="ghost" size="large" disabled={sending()} onClick={reject}>
|
<Button variant="ghost" size="large" disabled={store.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={sending()} onClick={back}>
|
<Button variant="secondary" size="large" disabled={store.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={sending()} onClick={next}>
|
<Button variant={last() ? "primary" : "secondary"} size="large" disabled={store.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>
|
||||||
@@ -315,7 +311,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={sending()}
|
disabled={store.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">
|
||||||
@@ -349,7 +345,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={sending()}
|
disabled={store.sending}
|
||||||
onClick={customOpen}
|
onClick={customOpen}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -381,7 +377,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 (sending()) {
|
if (store.sending) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -423,7 +419,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={sending()}
|
disabled={store.sending}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|||||||
@@ -217,6 +217,17 @@ 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(() => {
|
||||||
@@ -415,6 +426,7 @@ 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",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user