mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-16 09:28:27 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cac531148a |
@@ -34,48 +34,10 @@ jobs:
|
|||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const twoHours = 2 * 60 * 60 * 1000;
|
const twoHours = 2 * 60 * 60 * 1000;
|
||||||
const orgMemberAssociations = new Set(['OWNER', 'MEMBER']);
|
|
||||||
const agentLogin = 'opencode-agent[bot]';
|
|
||||||
const { data: file } = await github.rest.repos.getContent({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
path: '.github/TEAM_MEMBERS',
|
|
||||||
ref: 'dev',
|
|
||||||
});
|
|
||||||
const teamMembers = new Set(
|
|
||||||
Buffer.from(file.content, 'base64')
|
|
||||||
.toString()
|
|
||||||
.split('\n')
|
|
||||||
.map((line) => line.trim().toLowerCase())
|
|
||||||
.filter(Boolean)
|
|
||||||
);
|
|
||||||
|
|
||||||
function isExempt(item) {
|
|
||||||
const login = item.user?.login?.toLowerCase();
|
|
||||||
return (
|
|
||||||
login === agentLogin ||
|
|
||||||
orgMemberAssociations.has(item.author_association) ||
|
|
||||||
(login && teamMembers.has(login))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const isPR = !!item.pull_request;
|
const isPR = !!item.pull_request;
|
||||||
const kind = isPR ? 'PR' : 'issue';
|
const kind = isPR ? 'PR' : 'issue';
|
||||||
const login = item.user?.login;
|
|
||||||
|
|
||||||
if (isExempt(item)) {
|
|
||||||
core.info(`Skipping ${kind} #${item.number}; author ${login || 'unknown'} is exempt`);
|
|
||||||
try {
|
|
||||||
await github.rest.issues.removeLabel({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: item.number,
|
|
||||||
name: 'needs:compliance',
|
|
||||||
});
|
|
||||||
} catch (e) {}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data: comments } = await github.rest.issues.listComments({
|
const { data: comments } = await github.rest.issues.listComments({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
|
|||||||
@@ -17,31 +17,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Check exempt issue author
|
|
||||||
id: author
|
|
||||||
run: |
|
|
||||||
LOGIN="${{ github.event.issue.user.login }}"
|
|
||||||
ASSOCIATION="${{ github.event.issue.author_association }}"
|
|
||||||
|
|
||||||
if [ "$LOGIN" = "opencode-agent[bot]" ] ||
|
|
||||||
[ "$ASSOCIATION" = "OWNER" ] ||
|
|
||||||
[ "$ASSOCIATION" = "MEMBER" ] ||
|
|
||||||
grep -qxiF "$LOGIN" .github/TEAM_MEMBERS; then
|
|
||||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Skipping issue automation for exempt author: $LOGIN ($ASSOCIATION)"
|
|
||||||
else
|
|
||||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- uses: ./.github/actions/setup-bun
|
- uses: ./.github/actions/setup-bun
|
||||||
if: steps.author.outputs.skip != 'true'
|
|
||||||
|
|
||||||
- name: Install opencode
|
- name: Install opencode
|
||||||
if: steps.author.outputs.skip != 'true'
|
|
||||||
run: curl -fsSL https://opencode.ai/install | bash
|
run: curl -fsSL https://opencode.ai/install | bash
|
||||||
|
|
||||||
- name: Check duplicates and compliance
|
- name: Check duplicates and compliance
|
||||||
if: steps.author.outputs.skip != 'true'
|
|
||||||
env:
|
env:
|
||||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -57,7 +38,6 @@ jobs:
|
|||||||
opencode run -m opencode/claude-sonnet-4-6 "A new issue has been created:
|
opencode run -m opencode/claude-sonnet-4-6 "A new issue has been created:
|
||||||
|
|
||||||
Issue number: ${{ github.event.issue.number }}
|
Issue number: ${{ github.event.issue.number }}
|
||||||
Issue author association: ${{ github.event.issue.author_association }}
|
|
||||||
|
|
||||||
Lookup this issue with gh issue view ${{ github.event.issue.number }}.
|
Lookup this issue with gh issue view ${{ github.event.issue.number }}.
|
||||||
|
|
||||||
@@ -69,8 +49,6 @@ jobs:
|
|||||||
|
|
||||||
Check whether the issue follows our contributing guidelines and issue templates.
|
Check whether the issue follows our contributing guidelines and issue templates.
|
||||||
|
|
||||||
If the issue author association is OWNER or MEMBER, skip this compliance check. Do not add the needs:compliance label for organization-owned issues.
|
|
||||||
|
|
||||||
This project has three issue templates that every issue MUST use one of:
|
This project has three issue templates that every issue MUST use one of:
|
||||||
|
|
||||||
1. Bug Report - requires a Description field with real content
|
1. Bug Report - requires a Description field with real content
|
||||||
@@ -105,7 +83,7 @@ jobs:
|
|||||||
|
|
||||||
Based on your findings, post a SINGLE comment on issue #${{ github.event.issue.number }}. Build the comment as follows:
|
Based on your findings, post a SINGLE comment on issue #${{ github.event.issue.number }}. Build the comment as follows:
|
||||||
|
|
||||||
If the issue is NOT compliant and the author association is not OWNER or MEMBER, start the comment with:
|
If the issue is NOT compliant, start the comment with:
|
||||||
<!-- issue-compliance -->
|
<!-- issue-compliance -->
|
||||||
Then explain what needs to be fixed and that they have 2 hours to edit the issue before it is automatically closed. Also add the label needs:compliance to the issue using: gh issue edit ${{ github.event.issue.number }} --add-label needs:compliance
|
Then explain what needs to be fixed and that they have 2 hours to edit the issue before it is automatically closed. Also add the label needs:compliance to the issue using: gh issue edit ${{ github.event.issue.number }} --add-label needs:compliance
|
||||||
|
|
||||||
@@ -151,31 +129,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Check exempt issue author
|
|
||||||
id: author
|
|
||||||
run: |
|
|
||||||
LOGIN="${{ github.event.issue.user.login }}"
|
|
||||||
ASSOCIATION="${{ github.event.issue.author_association }}"
|
|
||||||
|
|
||||||
if [ "$LOGIN" = "opencode-agent[bot]" ] ||
|
|
||||||
[ "$ASSOCIATION" = "OWNER" ] ||
|
|
||||||
[ "$ASSOCIATION" = "MEMBER" ] ||
|
|
||||||
grep -qxiF "$LOGIN" .github/TEAM_MEMBERS; then
|
|
||||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Skipping issue automation for exempt author: $LOGIN ($ASSOCIATION)"
|
|
||||||
else
|
|
||||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- uses: ./.github/actions/setup-bun
|
- uses: ./.github/actions/setup-bun
|
||||||
if: steps.author.outputs.skip != 'true'
|
|
||||||
|
|
||||||
- name: Install opencode
|
- name: Install opencode
|
||||||
if: steps.author.outputs.skip != 'true'
|
|
||||||
run: curl -fsSL https://opencode.ai/install | bash
|
run: curl -fsSL https://opencode.ai/install | bash
|
||||||
|
|
||||||
- name: Recheck compliance
|
- name: Recheck compliance
|
||||||
if: steps.author.outputs.skip != 'true'
|
|
||||||
env:
|
env:
|
||||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -189,12 +148,9 @@ jobs:
|
|||||||
}
|
}
|
||||||
run: |
|
run: |
|
||||||
opencode run -m opencode/claude-sonnet-4-6 "Issue #${{ github.event.issue.number }} was previously flagged as non-compliant and has been edited.
|
opencode run -m opencode/claude-sonnet-4-6 "Issue #${{ github.event.issue.number }} was previously flagged as non-compliant and has been edited.
|
||||||
Issue author association: ${{ github.event.issue.author_association }}
|
|
||||||
|
|
||||||
Lookup this issue with gh issue view ${{ github.event.issue.number }}.
|
Lookup this issue with gh issue view ${{ github.event.issue.number }}.
|
||||||
|
|
||||||
If the issue author association is OWNER or MEMBER, remove the needs:compliance label if present, delete the previous compliance comment if present, and do not post a new comment.
|
|
||||||
|
|
||||||
Re-check whether the issue now follows our contributing guidelines and issue templates.
|
Re-check whether the issue now follows our contributing guidelines and issue templates.
|
||||||
|
|
||||||
This project has three issue templates that every issue MUST use one of:
|
This project has three issue templates that every issue MUST use one of:
|
||||||
|
|||||||
@@ -16,32 +16,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Check exempt issue author
|
|
||||||
id: author
|
|
||||||
run: |
|
|
||||||
LOGIN="${{ github.event.issue.user.login }}"
|
|
||||||
ASSOCIATION="${{ github.event.issue.author_association }}"
|
|
||||||
|
|
||||||
if [ "$LOGIN" = "opencode-agent[bot]" ] ||
|
|
||||||
[ "$ASSOCIATION" = "OWNER" ] ||
|
|
||||||
[ "$ASSOCIATION" = "MEMBER" ] ||
|
|
||||||
grep -qxiF "$LOGIN" .github/TEAM_MEMBERS; then
|
|
||||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Skipping issue automation for exempt author: $LOGIN ($ASSOCIATION)"
|
|
||||||
else
|
|
||||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
if: steps.author.outputs.skip != 'true'
|
|
||||||
uses: ./.github/actions/setup-bun
|
uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
- name: Install opencode
|
- name: Install opencode
|
||||||
if: steps.author.outputs.skip != 'true'
|
|
||||||
run: curl -fsSL https://opencode.ai/install | bash
|
run: curl -fsSL https://opencode.ai/install | bash
|
||||||
|
|
||||||
- name: Triage issue
|
- name: Triage issue
|
||||||
if: steps.author.outputs.skip != 'true'
|
|
||||||
env:
|
env:
|
||||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/abstract": "0.5.0",
|
"@dnd-kit/abstract": "0.5.0",
|
||||||
"@dnd-kit/dom": "0.5.0",
|
"@dnd-kit/dom": "0.5.0",
|
||||||
@@ -83,7 +83,6 @@
|
|||||||
"@types/luxon": "catalog:",
|
"@types/luxon": "catalog:",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
"tw-animate-css": "1.4.0",
|
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"vite": "catalog:",
|
"vite": "catalog:",
|
||||||
"vite-plugin-icons-spritesheet": "3.0.1",
|
"vite-plugin-icons-spritesheet": "3.0.1",
|
||||||
@@ -92,7 +91,7 @@
|
|||||||
},
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
"name": "@opencode-ai/cli",
|
"name": "@opencode-ai/cli",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"bin": {
|
"bin": {
|
||||||
"lildax": "./bin/lildax.cjs",
|
"lildax": "./bin/lildax.cjs",
|
||||||
},
|
},
|
||||||
@@ -140,7 +139,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
@@ -176,7 +175,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"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",
|
||||||
@@ -203,7 +202,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "3.0.82",
|
"@ai-sdk/anthropic": "3.0.82",
|
||||||
"@ai-sdk/openai": "3.0.48",
|
"@ai-sdk/openai": "3.0.48",
|
||||||
@@ -225,7 +224,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"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",
|
||||||
@@ -249,7 +248,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/support": {
|
"packages/console/support": {
|
||||||
"name": "@opencode-ai/console-support",
|
"name": "@opencode-ai/console-support",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@opencode-ai/console-core": "workspace:*",
|
"@opencode-ai/console-core": "workspace:*",
|
||||||
@@ -269,7 +268,7 @@
|
|||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@opencode-ai/core",
|
"name": "@opencode-ai/core",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -317,7 +316,6 @@
|
|||||||
"ai-gateway-provider": "3.1.2",
|
"ai-gateway-provider": "3.1.2",
|
||||||
"bun-pty": "0.4.8",
|
"bun-pty": "0.4.8",
|
||||||
"cross-spawn": "catalog:",
|
"cross-spawn": "catalog:",
|
||||||
"diff": "catalog:",
|
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
@@ -334,7 +332,7 @@
|
|||||||
"npm-package-arg": "13.0.2",
|
"npm-package-arg": "13.0.2",
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
"turndown": "7.2.0",
|
"turndown": "7.2.0",
|
||||||
"venice-ai-sdk-provider": "2.1.1",
|
"venice-ai-sdk-provider": "2.0.2",
|
||||||
"which": "6.0.1",
|
"which": "6.0.1",
|
||||||
"xdg-basedir": "5.1.0",
|
"xdg-basedir": "5.1.0",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
@@ -363,7 +361,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@zip.js/zip.js": "2.7.62",
|
"@zip.js/zip.js": "2.7.62",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -417,7 +415,7 @@
|
|||||||
},
|
},
|
||||||
"packages/effect-drizzle-sqlite": {
|
"packages/effect-drizzle-sqlite": {
|
||||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -431,7 +429,7 @@
|
|||||||
},
|
},
|
||||||
"packages/effect-sqlite-node": {
|
"packages/effect-sqlite-node": {
|
||||||
"name": "@opencode-ai/effect-sqlite-node",
|
"name": "@opencode-ai/effect-sqlite-node",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -443,7 +441,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hono/standard-validator": "catalog:",
|
"@hono/standard-validator": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
@@ -475,7 +473,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -491,7 +489,7 @@
|
|||||||
},
|
},
|
||||||
"packages/http-recorder": {
|
"packages/http-recorder": {
|
||||||
"name": "@opencode-ai/http-recorder",
|
"name": "@opencode-ai/http-recorder",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node": "4.0.0-beta.83",
|
"@effect/platform-node": "4.0.0-beta.83",
|
||||||
"@effect/platform-node-shared": "4.0.0-beta.83",
|
"@effect/platform-node-shared": "4.0.0-beta.83",
|
||||||
@@ -522,7 +520,7 @@
|
|||||||
},
|
},
|
||||||
"packages/llm": {
|
"packages/llm": {
|
||||||
"name": "@opencode-ai/llm",
|
"name": "@opencode-ai/llm",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/schema": "workspace:*",
|
"@opencode-ai/schema": "workspace:*",
|
||||||
"@smithy/eventstream-codec": "4.2.14",
|
"@smithy/eventstream-codec": "4.2.14",
|
||||||
@@ -541,7 +539,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -637,7 +635,7 @@
|
|||||||
"tree-sitter-powershell": "0.25.10",
|
"tree-sitter-powershell": "0.25.10",
|
||||||
"turndown": "7.2.0",
|
"turndown": "7.2.0",
|
||||||
"ulid": "catalog:",
|
"ulid": "catalog:",
|
||||||
"venice-ai-sdk-provider": "2.1.1",
|
"venice-ai-sdk-provider": "2.0.2",
|
||||||
"vscode-jsonrpc": "8.2.1",
|
"vscode-jsonrpc": "8.2.1",
|
||||||
"web-tree-sitter": "0.25.10",
|
"web-tree-sitter": "0.25.10",
|
||||||
"ws": "8.21.0",
|
"ws": "8.21.0",
|
||||||
@@ -671,7 +669,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "3.0.8",
|
"@ai-sdk/provider": "3.0.8",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -747,7 +745,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "catalog:",
|
"cross-spawn": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -762,7 +760,7 @@
|
|||||||
},
|
},
|
||||||
"packages/server": {
|
"packages/server": {
|
||||||
"name": "@opencode-ai/server",
|
"name": "@opencode-ai/server",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/protocol": "workspace:*",
|
"@opencode-ai/protocol": "workspace:*",
|
||||||
@@ -777,7 +775,7 @@
|
|||||||
},
|
},
|
||||||
"packages/session-ui": {
|
"packages/session-ui": {
|
||||||
"name": "@opencode-ai/session-ui",
|
"name": "@opencode-ai/session-ui",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
@@ -821,7 +819,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -834,7 +832,7 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/app": {
|
"packages/stats/app": {
|
||||||
"name": "@opencode-ai/stats-app",
|
"name": "@opencode-ai/stats-app",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
"@opencode-ai/stats-core": "workspace:*",
|
"@opencode-ai/stats-core": "workspace:*",
|
||||||
@@ -867,7 +865,7 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/core": {
|
"packages/stats/core": {
|
||||||
"name": "@opencode-ai/stats-core",
|
"name": "@opencode-ai/stats-core",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-athena": "3.933.0",
|
"@aws-sdk/client-athena": "3.933.0",
|
||||||
"@planetscale/database": "1.19.0",
|
"@planetscale/database": "1.19.0",
|
||||||
@@ -886,7 +884,7 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/server": {
|
"packages/stats/server": {
|
||||||
"name": "@opencode-ai/stats-server",
|
"name": "@opencode-ai/stats-server",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-firehose": "3.933.0",
|
"@aws-sdk/client-firehose": "3.933.0",
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
@@ -927,7 +925,7 @@
|
|||||||
},
|
},
|
||||||
"packages/tui": {
|
"packages/tui": {
|
||||||
"name": "@opencode-ai/tui",
|
"name": "@opencode-ai/tui",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
@@ -954,7 +952,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
@@ -992,7 +990,6 @@
|
|||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
"tailwindcss": "catalog:",
|
"tailwindcss": "catalog:",
|
||||||
"tw-animate-css": "1.4.0",
|
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"vite": "catalog:",
|
"vite": "catalog:",
|
||||||
"vite-plugin-icons-spritesheet": "3.0.1",
|
"vite-plugin-icons-spritesheet": "3.0.1",
|
||||||
@@ -1005,7 +1002,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
@@ -1051,7 +1048,6 @@
|
|||||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||||
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
|
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.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",
|
||||||
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
|
|
||||||
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
|
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
|
||||||
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
||||||
"@tanstack/solid-virtual@3.13.28": "patches/@tanstack%2Fsolid-virtual@3.13.28.patch",
|
"@tanstack/solid-virtual@3.13.28": "patches/@tanstack%2Fsolid-virtual@3.13.28.patch",
|
||||||
@@ -5319,8 +5315,6 @@
|
|||||||
|
|
||||||
"turndown": ["turndown@7.2.0", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A=="],
|
"turndown": ["turndown@7.2.0", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A=="],
|
||||||
|
|
||||||
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
|
|
||||||
|
|
||||||
"tw-to-css": ["tw-to-css@0.0.12", "", { "dependencies": { "postcss": "8.4.31", "postcss-css-variables": "0.18.0", "tailwindcss": "3.3.2" } }, "sha512-rQAsQvOtV1lBkyCw+iypMygNHrShYAItES5r8fMsrhhaj5qrV2LkZyXc8ccEH+u5bFjHjQ9iuxe90I7Kykf6pw=="],
|
"tw-to-css": ["tw-to-css@0.0.12", "", { "dependencies": { "postcss": "8.4.31", "postcss-css-variables": "0.18.0", "tailwindcss": "3.3.2" } }, "sha512-rQAsQvOtV1lBkyCw+iypMygNHrShYAItES5r8fMsrhhaj5qrV2LkZyXc8ccEH+u5bFjHjQ9iuxe90I7Kykf6pw=="],
|
||||||
|
|
||||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||||
@@ -5429,7 +5423,7 @@
|
|||||||
|
|
||||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||||
|
|
||||||
"venice-ai-sdk-provider": ["venice-ai-sdk-provider@2.1.1", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.51", "@ai-sdk/provider": "^3.0.10", "@ai-sdk/provider-utils": "^4.0.30" }, "peerDependencies": { "ai": "^6.0.90" } }, "sha512-w3OHkuzzKZ3r2TOxER6myBYzZJNoDqol+DUHu3NnfBN/GETnUVxecZJab0CHQQ8GZc0jjzpFymepjcLDPS4SQg=="],
|
"venice-ai-sdk-provider": ["venice-ai-sdk-provider@2.0.2", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.47", "@ai-sdk/provider": "^3.0.10", "@ai-sdk/provider-utils": "^4.0.27" }, "peerDependencies": { "ai": "^6.0.90" } }, "sha512-aoa05nI3BTK5aGbjBflq+Gfln2AHAkwNbWuGGvCzUIsOfp5Y3iPD4O4PUGDAEiWVJWbjpPn0KfDa0H/HebwsaA=="],
|
||||||
|
|
||||||
"verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="],
|
"verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="],
|
||||||
|
|
||||||
@@ -6449,11 +6443,11 @@
|
|||||||
|
|
||||||
"unzipper/fs-extra": ["fs-extra@11.3.5", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="],
|
"unzipper/fs-extra": ["fs-extra@11.3.5", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="],
|
||||||
|
|
||||||
"venice-ai-sdk-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.32" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-SoPSkrL5cbNQnAljRsJ7pOzJ2FmWgnhC0lfFOda873ycCdFJL1A+h3Ib7mX2spcv3XnNaO13y/45/0RyqNWlIQ=="],
|
"venice-ai-sdk-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.47", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Enm5UlL0zUCrW3792opk5h7hRWxZOZzDe6eQYVFqX9LUOGGCe1h8MZWAGim765nwzgnjlpeYOsuzZmLtRsTPlg=="],
|
||||||
|
|
||||||
"venice-ai-sdk-provider/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
"venice-ai-sdk-provider/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
||||||
|
|
||||||
"venice-ai-sdk-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.32", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Kwj499fTcN9bP/AfGoPU7JWIXeP6VZqKI6omsH062c9E2G4gdjeJczkz4z/tYSkzYjLE2AI3DtZbMfs6D7vn2Q=="],
|
"venice-ai-sdk-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
|
||||||
|
|
||||||
"verror/core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="],
|
"verror/core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="],
|
||||||
|
|
||||||
@@ -7073,10 +7067,6 @@
|
|||||||
|
|
||||||
"unzipper/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
|
"unzipper/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
|
||||||
|
|
||||||
"venice-ai-sdk-provider/@ai-sdk/openai-compatible/@ai-sdk/provider": ["@ai-sdk/provider@3.0.12", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-sj9DWTJ2Ze0WR9qsiOPqoqzNx3OxL6iMxHImbhvoe9qOspekbzxNDMiJ4TIGfYHYh9w4OmBjz3prvqhzTi96+Q=="],
|
|
||||||
|
|
||||||
"venice-ai-sdk-provider/@ai-sdk/provider-utils/@ai-sdk/provider": ["@ai-sdk/provider@3.0.12", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-sj9DWTJ2Ze0WR9qsiOPqoqzNx3OxL6iMxHImbhvoe9qOspekbzxNDMiJ4TIGfYHYh9w4OmBjz3prvqhzTi96+Q=="],
|
|
||||||
|
|
||||||
"venice-ai-sdk-provider/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
"venice-ai-sdk-provider/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
"vitest/@vitest/expect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
"vitest/@vitest/expect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-Ulflihjr8JDRVyNxSchoqaey6z/12256zs1FOiw+4vo=",
|
"x86_64-linux": "sha256-OiWvZ57vuyHwiIKNtW1n1KX+MLmOXVG3x4fLKvUoGQw=",
|
||||||
"aarch64-linux": "sha256-wP6p30a9f7s3tua5qbIq/10OHdbJb8xUni5KxE3F+/k=",
|
"aarch64-linux": "sha256-RnPLxVEg/UsL5IeIFWmXMSLUOG6rVrajYxhyDYj1vTA=",
|
||||||
"aarch64-darwin": "sha256-7+4skFiJ+tLSjNG6WR53bT/IektA5B7gEcWLYpkK4z8=",
|
"aarch64-darwin": "sha256-KPIgcBA0pTFBPrCTSZgIbvEorbtWcMgXvyX9bFAypVs=",
|
||||||
"x86_64-darwin": "sha256-NL945zD4J6HomH5SF0XIO8ZFCbIh2MTsJFtTyuxm2Q0="
|
"x86_64-darwin": "sha256-6jVU7/uVId0VD24MVQ8s8Ill5b6PsKdlBgHg+oceKRg="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -153,7 +153,6 @@
|
|||||||
"@tanstack/solid-virtual@3.13.28": "patches/@tanstack%2Fsolid-virtual@3.13.28.patch",
|
"@tanstack/solid-virtual@3.13.28": "patches/@tanstack%2Fsolid-virtual@3.13.28.patch",
|
||||||
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
||||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||||
"@tanstack/virtual-core@3.17.0": "patches/@tanstack%2Fvirtual-core@3.17.0.patch",
|
"@tanstack/virtual-core@3.17.0": "patches/@tanstack%2Fvirtual-core@3.17.0.patch"
|
||||||
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -38,7 +38,6 @@
|
|||||||
"@types/luxon": "catalog:",
|
"@types/luxon": "catalog:",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
"tw-animate-css": "1.4.0",
|
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"vite": "catalog:",
|
"vite": "catalog:",
|
||||||
"vite-plugin-icons-spritesheet": "3.0.1",
|
"vite-plugin-icons-spritesheet": "3.0.1",
|
||||||
|
|||||||
+13
-48
@@ -28,7 +28,7 @@ import {
|
|||||||
Show,
|
Show,
|
||||||
} from "solid-js"
|
} from "solid-js"
|
||||||
import { Dynamic } from "solid-js/web"
|
import { Dynamic } from "solid-js/web"
|
||||||
import { CommandProvider, useCommand, type CommandOption } from "@/context/command"
|
import { CommandProvider } from "@/context/command"
|
||||||
import { CommentsProvider } from "@/context/comments"
|
import { CommentsProvider } from "@/context/comments"
|
||||||
import { FileProvider } from "@/context/file"
|
import { FileProvider } from "@/context/file"
|
||||||
import { ServerSDKProvider, useServerSDK } from "@/context/server-sdk"
|
import { ServerSDKProvider, useServerSDK } from "@/context/server-sdk"
|
||||||
@@ -38,9 +38,8 @@ import { HighlightsProvider } from "@/context/highlights"
|
|||||||
import { LanguageProvider, type Locale, useLanguage } from "@/context/language"
|
import { LanguageProvider, type Locale, 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, useNotification } from "@/context/notification"
|
import { NotificationProvider } from "@/context/notification"
|
||||||
import { PermissionProvider } from "@/context/permission"
|
import { PermissionProvider } from "@/context/permission"
|
||||||
import { usePlatform } from "@/context/platform"
|
|
||||||
import { PromptProvider } from "@/context/prompt"
|
import { PromptProvider } from "@/context/prompt"
|
||||||
import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
|
import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
|
||||||
import { SettingsProvider, useSettings } from "@/context/settings"
|
import { SettingsProvider, useSettings } from "@/context/settings"
|
||||||
@@ -301,36 +300,12 @@ function SharedProviders(props: ParentProps) {
|
|||||||
<>
|
<>
|
||||||
<BodyDesignClass />
|
<BodyDesignClass />
|
||||||
<CommandProvider>
|
<CommandProvider>
|
||||||
<DesktopCommands />
|
|
||||||
<HighlightsProvider>{props.children}</HighlightsProvider>
|
<HighlightsProvider>{props.children}</HighlightsProvider>
|
||||||
</CommandProvider>
|
</CommandProvider>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DesktopCommands() {
|
|
||||||
const command = useCommand()
|
|
||||||
const language = useLanguage()
|
|
||||||
const platform = usePlatform()
|
|
||||||
|
|
||||||
command.register("desktop", () => {
|
|
||||||
const commands: CommandOption[] = []
|
|
||||||
if (platform.platform === "desktop" && platform.exportDebugLogs) {
|
|
||||||
commands.push({
|
|
||||||
id: "logs.export",
|
|
||||||
title: "Export logs",
|
|
||||||
category: language.t("command.category.settings"),
|
|
||||||
onSelect: () => {
|
|
||||||
void platform.exportDebugLogs?.()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return commands
|
|
||||||
})
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Server-scoped providers shared by the legacy shell and the top-level new shell.
|
// Server-scoped providers shared by the legacy shell and the top-level new shell.
|
||||||
type ServerScopedShellProps = ParentProps<{
|
type ServerScopedShellProps = ParentProps<{
|
||||||
directory?: () => string | undefined
|
directory?: () => string | undefined
|
||||||
@@ -341,7 +316,9 @@ function ServerScopedProviders(props: ServerScopedShellProps) {
|
|||||||
return (
|
return (
|
||||||
<PermissionProvider directory={props.directory}>
|
<PermissionProvider directory={props.directory}>
|
||||||
<LayoutProvider>
|
<LayoutProvider>
|
||||||
<ModelsProvider directory={props.directory}>{props.children}</ModelsProvider>
|
<NotificationProvider directory={props.directory} sessionID={props.sessionID}>
|
||||||
|
<ModelsProvider directory={props.directory}>{props.children}</ModelsProvider>
|
||||||
|
</NotificationProvider>
|
||||||
</LayoutProvider>
|
</LayoutProvider>
|
||||||
</PermissionProvider>
|
</PermissionProvider>
|
||||||
)
|
)
|
||||||
@@ -368,23 +345,13 @@ function NewAppLayout(props: ParentProps) {
|
|||||||
function TargetServerScopedProviders(props: ServerScopedShellProps) {
|
function TargetServerScopedProviders(props: ServerScopedShellProps) {
|
||||||
return (
|
return (
|
||||||
<PermissionProvider directory={props.directory}>
|
<PermissionProvider directory={props.directory}>
|
||||||
<MarkSessionNotificationsViewed sessionID={props.sessionID} />
|
<NotificationProvider directory={props.directory} sessionID={props.sessionID}>
|
||||||
<ModelsProvider directory={props.directory}>{props.children}</ModelsProvider>
|
<ModelsProvider directory={props.directory}>{props.children}</ModelsProvider>
|
||||||
|
</NotificationProvider>
|
||||||
</PermissionProvider>
|
</PermissionProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function MarkSessionNotificationsViewed(props: { sessionID?: () => string | undefined }) {
|
|
||||||
const notification = useNotification()
|
|
||||||
createEffect(() => {
|
|
||||||
const sessionID = props.sessionID?.()
|
|
||||||
if (!notification.ready() || !sessionID) return
|
|
||||||
if (notification.session.unseenCount(sessionID) === 0) return
|
|
||||||
notification.session.markViewed(sessionID)
|
|
||||||
})
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function SessionProviders(props: ParentProps) {
|
function SessionProviders(props: ParentProps) {
|
||||||
return (
|
return (
|
||||||
<TerminalProvider>
|
<TerminalProvider>
|
||||||
@@ -593,13 +560,11 @@ export function AppInterface(props: {
|
|||||||
component={props.router ?? Router}
|
component={props.router ?? Router}
|
||||||
root={(routerProps) => (
|
root={(routerProps) => (
|
||||||
<TabsProvider>
|
<TabsProvider>
|
||||||
<NotificationProvider>
|
<ServerShell>
|
||||||
<ServerShell>
|
<Show when={useSettings().general.newLayoutDesigns()} fallback={routerProps.children}>
|
||||||
<Show when={useSettings().general.newLayoutDesigns()} fallback={routerProps.children}>
|
<NewAppLayout>{routerProps.children}</NewAppLayout>
|
||||||
<NewAppLayout>{routerProps.children}</NewAppLayout>
|
</Show>
|
||||||
</Show>
|
</ServerShell>
|
||||||
</ServerShell>
|
|
||||||
</NotificationProvider>
|
|
||||||
</TabsProvider>
|
</TabsProvider>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { batch, createEffect, onCleanup, onMount } from "solid-js"
|
|||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
|
||||||
type Mem = Performance & {
|
type Mem = Performance & {
|
||||||
@@ -52,62 +51,31 @@ const bad = (n: number | undefined, limit: number, low = false) => {
|
|||||||
|
|
||||||
const session = (path: string) => path.includes("/session")
|
const session = (path: string) => path.includes("/session")
|
||||||
|
|
||||||
function Cell(props: {
|
function Cell(props: { bad?: boolean; dim?: boolean; label: string; tip: string; value: string; wide?: boolean }) {
|
||||||
bad?: boolean
|
|
||||||
dim?: boolean
|
|
||||||
inline?: boolean
|
|
||||||
label: string
|
|
||||||
tip: string
|
|
||||||
value: string
|
|
||||||
wide?: boolean
|
|
||||||
}) {
|
|
||||||
const content = () => (
|
|
||||||
<div
|
|
||||||
classList={{
|
|
||||||
"flex min-w-0 items-center": true,
|
|
||||||
"min-h-[20px] w-fit flex-row justify-start gap-1.5 px-1.5 py-0.5 text-left": !!props.inline,
|
|
||||||
"justify-center text-center": !props.inline,
|
|
||||||
"min-h-[42px] w-full flex-col rounded-[8px] px-0.5 py-1": !props.inline,
|
|
||||||
"col-span-2": !!props.wide && !props.inline,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
classList={{
|
|
||||||
"text-[10px] leading-none font-black uppercase tracking-[0.04em] opacity-70": true,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.label}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
classList={{
|
|
||||||
"uppercase leading-none font-bold tabular-nums": true,
|
|
||||||
"text-[11px]": !!props.inline,
|
|
||||||
"text-[13px] sm:text-[14px]": !props.inline,
|
|
||||||
"text-text-on-critical-base": !!props.bad,
|
|
||||||
"opacity-70": !!props.dim,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.value}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
if (props.inline) {
|
|
||||||
return (
|
|
||||||
<TooltipV2 value={props.tip} placement="top">
|
|
||||||
{content()}
|
|
||||||
</TooltipV2>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip value={props.tip} placement="top">
|
<Tooltip value={props.tip} placement="top">
|
||||||
{content()}
|
<div
|
||||||
|
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,
|
||||||
|
"col-span-2": !!props.wide,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="text-[10px] leading-none font-black uppercase tracking-[0.04em] opacity-70">{props.label}</div>
|
||||||
|
<div
|
||||||
|
classList={{
|
||||||
|
"text-[13px] leading-none font-bold tabular-nums sm:text-[14px]": true,
|
||||||
|
"text-text-on-critical-base": !!props.bad,
|
||||||
|
"opacity-70": !!props.dim,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.value}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DebugBar(props: { inline?: boolean } = {}) {
|
export function DebugBar() {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const routing = useIsRouting()
|
const routing = useIsRouting()
|
||||||
@@ -133,7 +101,7 @@ export function DebugBar(props: { inline?: boolean } = {}) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const na = () => language.t("debugBar.na").toUpperCase()
|
const na = () => language.t("debugBar.na")
|
||||||
const heap = () => (state.heap.limit ? (state.heap.used ?? 0) / state.heap.limit : undefined)
|
const heap = () => (state.heap.limit ? (state.heap.used ?? 0) / state.heap.limit : undefined)
|
||||||
const heapv = () => {
|
const heapv = () => {
|
||||||
const value = heap()
|
const value = heap()
|
||||||
@@ -395,30 +363,15 @@ export function DebugBar(props: { inline?: boolean } = {}) {
|
|||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
aria-label={language.t("debugBar.ariaLabel")}
|
aria-label={language.t("debugBar.ariaLabel")}
|
||||||
classList={{
|
class="pointer-events-auto fixed bottom-3 right-3 z-50 hidden 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)] md:block sm:bottom-4 sm:right-4 sm:w-[324px]"
|
||||||
"pointer-events-auto hidden overflow-hidden text-text-strong md:block": true,
|
|
||||||
"mt-[-6px] w-full shrink-0 px-3 py-1": !!props.inline,
|
|
||||||
"fixed bottom-3 right-3 z-50 w-[308px] max-w-[calc(100vw-1.5rem)] rounded-xl border border-border-base bg-surface-raised-stronger-non-alpha p-0.5 shadow-[var(--shadow-lg-border-base)] sm:bottom-4 sm:right-4 sm:w-[324px]":
|
|
||||||
!props.inline,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div
|
<div class="grid grid-cols-5 gap-px font-mono">
|
||||||
classList={{
|
|
||||||
"font-mono": true,
|
|
||||||
"gap-[9px]": !!props.inline,
|
|
||||||
"gap-px": !props.inline,
|
|
||||||
"flex w-full flex-nowrap items-center justify-start": !!props.inline,
|
|
||||||
"grid-cols-5": !props.inline,
|
|
||||||
grid: !props.inline,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Cell
|
<Cell
|
||||||
label={language.t("debugBar.nav.label")}
|
label={language.t("debugBar.nav.label")}
|
||||||
tip={language.t("debugBar.nav.tip")}
|
tip={language.t("debugBar.nav.tip")}
|
||||||
value={navv()}
|
value={navv()}
|
||||||
bad={bad(state.nav.dur, 400)}
|
bad={bad(state.nav.dur, 400)}
|
||||||
dim={state.nav.dur === undefined && !state.nav.pending}
|
dim={state.nav.dur === undefined && !state.nav.pending}
|
||||||
inline={props.inline}
|
|
||||||
/>
|
/>
|
||||||
<Cell
|
<Cell
|
||||||
label={language.t("debugBar.fps.label")}
|
label={language.t("debugBar.fps.label")}
|
||||||
@@ -426,7 +379,6 @@ export function DebugBar(props: { inline?: boolean } = {}) {
|
|||||||
value={state.fps === undefined ? na() : `${Math.round(state.fps)}`}
|
value={state.fps === undefined ? na() : `${Math.round(state.fps)}`}
|
||||||
bad={bad(state.fps, 50, true)}
|
bad={bad(state.fps, 50, true)}
|
||||||
dim={state.fps === undefined}
|
dim={state.fps === undefined}
|
||||||
inline={props.inline}
|
|
||||||
/>
|
/>
|
||||||
<Cell
|
<Cell
|
||||||
label={language.t("debugBar.frame.label")}
|
label={language.t("debugBar.frame.label")}
|
||||||
@@ -434,7 +386,6 @@ export function DebugBar(props: { inline?: boolean } = {}) {
|
|||||||
value={time(state.gap) ?? na()}
|
value={time(state.gap) ?? na()}
|
||||||
bad={bad(state.gap, 50)}
|
bad={bad(state.gap, 50)}
|
||||||
dim={state.gap === undefined}
|
dim={state.gap === undefined}
|
||||||
inline={props.inline}
|
|
||||||
/>
|
/>
|
||||||
<Cell
|
<Cell
|
||||||
label={language.t("debugBar.jank.label")}
|
label={language.t("debugBar.jank.label")}
|
||||||
@@ -442,7 +393,6 @@ export function DebugBar(props: { inline?: boolean } = {}) {
|
|||||||
value={state.jank === undefined ? na() : `${state.jank}`}
|
value={state.jank === undefined ? na() : `${state.jank}`}
|
||||||
bad={bad(state.jank, 8)}
|
bad={bad(state.jank, 8)}
|
||||||
dim={state.jank === undefined}
|
dim={state.jank === undefined}
|
||||||
inline={props.inline}
|
|
||||||
/>
|
/>
|
||||||
<Cell
|
<Cell
|
||||||
label={language.t("debugBar.long.label")}
|
label={language.t("debugBar.long.label")}
|
||||||
@@ -450,7 +400,6 @@ export function DebugBar(props: { inline?: boolean } = {}) {
|
|||||||
value={longv()}
|
value={longv()}
|
||||||
bad={bad(state.long.block, 200)}
|
bad={bad(state.long.block, 200)}
|
||||||
dim={state.long.count === undefined}
|
dim={state.long.count === undefined}
|
||||||
inline={props.inline}
|
|
||||||
/>
|
/>
|
||||||
<Cell
|
<Cell
|
||||||
label={language.t("debugBar.delay.label")}
|
label={language.t("debugBar.delay.label")}
|
||||||
@@ -458,7 +407,6 @@ export function DebugBar(props: { inline?: boolean } = {}) {
|
|||||||
value={time(state.delay) ?? na()}
|
value={time(state.delay) ?? na()}
|
||||||
bad={bad(state.delay, 100)}
|
bad={bad(state.delay, 100)}
|
||||||
dim={state.delay === undefined}
|
dim={state.delay === undefined}
|
||||||
inline={props.inline}
|
|
||||||
/>
|
/>
|
||||||
<Cell
|
<Cell
|
||||||
label={language.t("debugBar.inp.label")}
|
label={language.t("debugBar.inp.label")}
|
||||||
@@ -466,7 +414,6 @@ export function DebugBar(props: { inline?: boolean } = {}) {
|
|||||||
value={time(state.inp) ?? na()}
|
value={time(state.inp) ?? na()}
|
||||||
bad={bad(state.inp, 200)}
|
bad={bad(state.inp, 200)}
|
||||||
dim={state.inp === undefined}
|
dim={state.inp === undefined}
|
||||||
inline={props.inline}
|
|
||||||
/>
|
/>
|
||||||
<Cell
|
<Cell
|
||||||
label={language.t("debugBar.cls.label")}
|
label={language.t("debugBar.cls.label")}
|
||||||
@@ -474,7 +421,6 @@ export function DebugBar(props: { inline?: boolean } = {}) {
|
|||||||
value={state.cls === undefined ? na() : state.cls.toFixed(2)}
|
value={state.cls === undefined ? na() : state.cls.toFixed(2)}
|
||||||
bad={bad(state.cls, 0.1)}
|
bad={bad(state.cls, 0.1)}
|
||||||
dim={state.cls === undefined}
|
dim={state.cls === undefined}
|
||||||
inline={props.inline}
|
|
||||||
/>
|
/>
|
||||||
<Cell
|
<Cell
|
||||||
label={language.t("debugBar.mem.label")}
|
label={language.t("debugBar.mem.label")}
|
||||||
@@ -489,7 +435,6 @@ export function DebugBar(props: { inline?: boolean } = {}) {
|
|||||||
value={heapv()}
|
value={heapv()}
|
||||||
bad={bad(heap(), 0.8)}
|
bad={bad(heap(), 0.8)}
|
||||||
dim={state.heap.used === undefined}
|
dim={state.heap.used === undefined}
|
||||||
inline={props.inline}
|
|
||||||
wide
|
wide
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
|
|||||||
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,
|
||||||
promptInputs: undefined as undefined | Record<string, string>,
|
|
||||||
state: "pending" as undefined | "pending" | "complete" | "error" | "prompt",
|
state: "pending" as undefined | "pending" | "complete" | "error" | "prompt",
|
||||||
error: undefined as string | undefined,
|
error: undefined as string | undefined,
|
||||||
})
|
})
|
||||||
@@ -74,7 +73,6 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
|
|||||||
| { type: "method.select"; index: number }
|
| { type: "method.select"; index: number }
|
||||||
| { type: "method.reset" }
|
| { type: "method.reset" }
|
||||||
| { type: "auth.prompt" }
|
| { type: "auth.prompt" }
|
||||||
| { type: "auth.inputs"; inputs: Record<string, string> }
|
|
||||||
| { type: "auth.pending" }
|
| { type: "auth.pending" }
|
||||||
| { type: "auth.complete"; authorization: ProviderAuthAuthorization }
|
| { type: "auth.complete"; authorization: ProviderAuthAuthorization }
|
||||||
| { type: "auth.error"; error: string }
|
| { type: "auth.error"; error: string }
|
||||||
@@ -85,7 +83,6 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
|
|||||||
if (action.type === "method.select") {
|
if (action.type === "method.select") {
|
||||||
draft.methodIndex = action.index
|
draft.methodIndex = action.index
|
||||||
draft.authorization = undefined
|
draft.authorization = undefined
|
||||||
draft.promptInputs = undefined
|
|
||||||
draft.state = undefined
|
draft.state = undefined
|
||||||
draft.error = undefined
|
draft.error = undefined
|
||||||
return
|
return
|
||||||
@@ -93,7 +90,6 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
|
|||||||
if (action.type === "method.reset") {
|
if (action.type === "method.reset") {
|
||||||
draft.methodIndex = undefined
|
draft.methodIndex = undefined
|
||||||
draft.authorization = undefined
|
draft.authorization = undefined
|
||||||
draft.promptInputs = undefined
|
|
||||||
draft.state = undefined
|
draft.state = undefined
|
||||||
draft.error = undefined
|
draft.error = undefined
|
||||||
return
|
return
|
||||||
@@ -103,12 +99,6 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
|
|||||||
draft.error = undefined
|
draft.error = undefined
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (action.type === "auth.inputs") {
|
|
||||||
draft.promptInputs = action.inputs
|
|
||||||
draft.state = undefined
|
|
||||||
draft.error = undefined
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (action.type === "auth.pending") {
|
if (action.type === "auth.pending") {
|
||||||
draft.state = "pending"
|
draft.state = "pending"
|
||||||
draft.error = undefined
|
draft.error = undefined
|
||||||
@@ -161,15 +151,6 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
|
|||||||
const method = methods()[index]
|
const method = methods()[index]
|
||||||
dispatch({ type: "method.select", index })
|
dispatch({ type: "method.select", index })
|
||||||
|
|
||||||
if (method.type === "api" && method.prompts?.length) {
|
|
||||||
if (!inputs) {
|
|
||||||
dispatch({ type: "auth.prompt" })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
dispatch({ type: "auth.inputs", inputs })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (method.type === "oauth") {
|
if (method.type === "oauth") {
|
||||||
if (method.prompts?.length && !inputs) {
|
if (method.prompts?.length && !inputs) {
|
||||||
dispatch({ type: "auth.prompt" })
|
dispatch({ type: "auth.prompt" })
|
||||||
@@ -209,7 +190,7 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function AuthPromptsView() {
|
function OAuthPromptsView() {
|
||||||
const [formStore, setFormStore] = createStore({
|
const [formStore, setFormStore] = createStore({
|
||||||
value: {} as Record<string, string>,
|
value: {} as Record<string, string>,
|
||||||
index: 0,
|
index: 0,
|
||||||
@@ -217,7 +198,8 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
|
|||||||
|
|
||||||
const prompts = createMemo<NonNullable<ProviderAuthMethod["prompts"]>>(() => {
|
const prompts = createMemo<NonNullable<ProviderAuthMethod["prompts"]>>(() => {
|
||||||
const value = method()
|
const value = method()
|
||||||
return value?.prompts ?? []
|
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
|
||||||
@@ -248,10 +230,6 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
|
|||||||
setFormStore("index", next)
|
setFormStore("index", next)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (method()?.type === "api") {
|
|
||||||
dispatch({ type: "auth.inputs", inputs: value })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await selectMethod(store.methodIndex, value)
|
await selectMethod(store.methodIndex, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,7 +414,6 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
|
|||||||
auth: {
|
auth: {
|
||||||
type: "api",
|
type: "api",
|
||||||
key: apiKey,
|
key: apiKey,
|
||||||
...(store.promptInputs ? { metadata: store.promptInputs } : {}),
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await complete()
|
await complete()
|
||||||
@@ -645,7 +622,7 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
|
|||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={store.state === "prompt"}>
|
<Match when={store.state === "prompt"}>
|
||||||
<AuthPromptsView />
|
<OAuthPromptsView />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={store.state === "error"}>
|
<Match when={store.state === "error"}>
|
||||||
<div class="text-14-regular text-text-base">
|
<div class="text-14-regular text-text-base">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import "@pierre/trees/web-components"
|
import "@pierre/trees/web-components"
|
||||||
import { FileTree } from "@pierre/trees"
|
import { FileTree } from "@pierre/trees"
|
||||||
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/v2/dialog-v2"
|
import { Dialog, DialogFooter } from "@opencode-ai/ui/v2/dialog-v2"
|
||||||
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||||
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
@@ -27,7 +27,6 @@ import {
|
|||||||
pickerRoot,
|
pickerRoot,
|
||||||
} from "./directory-picker-domain"
|
} from "./directory-picker-domain"
|
||||||
import "./dialog-select-directory-v2.css"
|
import "./dialog-select-directory-v2.css"
|
||||||
import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2"
|
|
||||||
|
|
||||||
interface DialogSelectDirectoryV2Props {
|
interface DialogSelectDirectoryV2Props {
|
||||||
title?: string
|
title?: string
|
||||||
@@ -267,12 +266,8 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) {
|
|||||||
onCleanup(() => tree?.cleanUp())
|
onCleanup(() => tree?.cleanUp())
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog size="large" class="directory-picker-v2">
|
<Dialog title={props.title ?? language.t("command.project.open")} size="large" class="directory-picker-v2">
|
||||||
<DialogHeader>
|
<div class="directory-picker-v2-body">
|
||||||
<DialogTitle>{props.title ?? language.t("command.project.open")}</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<DividerV2 />
|
|
||||||
<DialogBody class="directory-picker-v2-body pt-4!">
|
|
||||||
<div class="directory-picker-v2-path" ref={pathArea}>
|
<div class="directory-picker-v2-path" ref={pathArea}>
|
||||||
<TextInputV2
|
<TextInputV2
|
||||||
value={input()}
|
value={input()}
|
||||||
@@ -354,7 +349,7 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<div class="directory-picker-v2-selection">{policy.result(root(), selected(), rootValid())}</div>
|
<div class="directory-picker-v2-selection">{policy.result(root(), selected(), rootValid())}</div>
|
||||||
</DialogBody>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<ButtonV2 variant="neutral" onClick={() => dialog.close()}>
|
<ButtonV2 variant="neutral" onClick={() => dialog.close()}>
|
||||||
{language.t("common.cancel")}
|
{language.t("common.cancel")}
|
||||||
|
|||||||
@@ -35,8 +35,6 @@ import { DockShellForm, DockTray } from "@opencode-ai/ui/dock-surface"
|
|||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
|
|
||||||
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Select } from "@opencode-ai/ui/select"
|
import { Select } from "@opencode-ai/ui/select"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
@@ -68,7 +66,7 @@ import { promptPlaceholder } from "./prompt-input/placeholder"
|
|||||||
import { createPromptInputTransientState } from "./prompt-input/transient-state"
|
import { createPromptInputTransientState } from "./prompt-input/transient-state"
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
||||||
import type { ReferenceInfo } from "@opencode-ai/sdk/v2/client"
|
import { SessionProgressIndicatorV2 } from "@opencode-ai/session-ui/v2/session-progress-indicator-v2"
|
||||||
|
|
||||||
export type PromptInputState = ReturnType<typeof usePrompt>
|
export type PromptInputState = ReturnType<typeof usePrompt>
|
||||||
|
|
||||||
@@ -215,7 +213,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
let fileInputRef: HTMLInputElement | undefined
|
let fileInputRef: HTMLInputElement | undefined
|
||||||
let scrollRef!: HTMLDivElement
|
let scrollRef!: HTMLDivElement
|
||||||
let slashPopoverRef!: HTMLDivElement
|
let slashPopoverRef!: HTMLDivElement
|
||||||
let restoreEndOnFocus = true
|
|
||||||
|
|
||||||
const mirror = { input: false }
|
const mirror = { input: false }
|
||||||
const inset = 56
|
const inset = 56
|
||||||
@@ -343,6 +340,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
() => prompt.capture(),
|
() => prompt.capture(),
|
||||||
Math.floor(Math.random() * EXAMPLES.length),
|
Math.floor(Math.random() * EXAMPLES.length),
|
||||||
)
|
)
|
||||||
|
const [submissionState, setSubmissionState] = createStore({ interrupting: false })
|
||||||
const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 })
|
const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 })
|
||||||
const motion = (value: number) => ({
|
const motion = (value: number) => ({
|
||||||
opacity: value,
|
opacity: value,
|
||||||
@@ -595,16 +593,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFocus = () => {
|
|
||||||
if (!restoreEndOnFocus) return
|
|
||||||
restoreEndOnFocus = false
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
if (document.activeElement !== editorRef) return
|
|
||||||
setCursorPosition(editorRef, prompt.cursor() ?? promptLength(prompt.current()))
|
|
||||||
queueScroll()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderEditorWithCursor = (parts: Prompt) => {
|
const renderEditorWithCursor = (parts: Prompt) => {
|
||||||
const cursor = currentCursor()
|
const cursor = currentCursor()
|
||||||
renderEditor(parts)
|
renderEditor(parts)
|
||||||
@@ -641,89 +629,24 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const referenceDescription = (reference: ReferenceInfo) =>
|
|
||||||
reference.source.type === "git" ? reference.source.repository : reference.source.path
|
|
||||||
|
|
||||||
const referenceList = createMemo(() =>
|
|
||||||
sync()
|
|
||||||
.data.reference.filter((reference) => !reference.hidden)
|
|
||||||
.map(
|
|
||||||
(reference): AtOption => ({
|
|
||||||
type: "reference",
|
|
||||||
name: reference.name,
|
|
||||||
path: reference.path,
|
|
||||||
display: reference.name,
|
|
||||||
description: reference.description ?? referenceDescription(reference),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const agentList = createMemo(() =>
|
const agentList = createMemo(() =>
|
||||||
props.controls.agents.available
|
props.controls.agents.available
|
||||||
.filter((agent) => !agent.hidden && agent.mode !== "primary")
|
.filter((agent) => !agent.hidden && agent.mode !== "primary")
|
||||||
.map((agent): AtOption => ({ type: "agent", name: agent.name, display: agent.name })),
|
.map((agent): AtOption => ({ type: "agent", name: agent.name, display: agent.name })),
|
||||||
)
|
)
|
||||||
|
|
||||||
const mcpResourceList = createMemo(() =>
|
|
||||||
Object.values(sync().data.mcp_resource).map(
|
|
||||||
(resource): AtOption => ({
|
|
||||||
type: "resource",
|
|
||||||
name: resource.name,
|
|
||||||
uri: resource.uri,
|
|
||||||
client: resource.client,
|
|
||||||
display: resource.name,
|
|
||||||
description: resource.description,
|
|
||||||
mime: resource.mimeType,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleAtSelect = (option: AtOption | undefined) => {
|
const handleAtSelect = (option: AtOption | undefined) => {
|
||||||
if (!option) return
|
if (!option) return
|
||||||
if (option.type === "agent") {
|
if (option.type === "agent") {
|
||||||
addPart({ type: "agent", name: option.name, content: "@" + option.name, start: 0, end: 0 })
|
addPart({ type: "agent", name: option.name, content: "@" + option.name, start: 0, end: 0 })
|
||||||
return
|
} else {
|
||||||
|
addPart({ type: "file", path: option.path, content: "@" + option.path, start: 0, end: 0 })
|
||||||
}
|
}
|
||||||
if (option.type === "reference") {
|
|
||||||
addPart({
|
|
||||||
type: "file",
|
|
||||||
path: option.path,
|
|
||||||
content: "@" + option.name,
|
|
||||||
start: 0,
|
|
||||||
end: 0,
|
|
||||||
mime: "application/x-directory",
|
|
||||||
filename: option.name,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (option.type === "resource") {
|
|
||||||
addPart({
|
|
||||||
type: "file",
|
|
||||||
path: option.uri,
|
|
||||||
content: "@" + option.name,
|
|
||||||
start: 0,
|
|
||||||
end: 0,
|
|
||||||
mime: option.mime ?? "text/plain",
|
|
||||||
filename: option.name,
|
|
||||||
url: option.uri,
|
|
||||||
source: {
|
|
||||||
type: "resource",
|
|
||||||
text: { value: "@" + option.name, start: 0, end: 0 },
|
|
||||||
clientName: option.client,
|
|
||||||
uri: option.uri,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
addPart({ type: "file", path: option.path, content: "@" + option.path, start: 0, end: 0 })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const atKey = (x: AtOption | undefined) => {
|
const atKey = (x: AtOption | undefined) => {
|
||||||
if (!x) return ""
|
if (!x) return ""
|
||||||
if (x.type === "agent") return `agent:${x.name}`
|
return x.type === "agent" ? `agent:${x.name}` : `file:${x.path}`
|
||||||
if (x.type === "reference") return `reference:${x.name}`
|
|
||||||
if (x.type === "resource") return `resource:${x.client}:${x.uri}`
|
|
||||||
return `file:${x.path}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -734,36 +657,30 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
onKeyDown: atOnKeyDown,
|
onKeyDown: atOnKeyDown,
|
||||||
} = useFilteredList<AtOption>({
|
} = useFilteredList<AtOption>({
|
||||||
items: async (query) => {
|
items: async (query) => {
|
||||||
const references = referenceList()
|
|
||||||
const agents = agentList()
|
const agents = agentList()
|
||||||
const mcpResources = mcpResourceList()
|
|
||||||
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 [...references, ...agents, ...mcpResources, ...pinned]
|
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))
|
||||||
.map((path) => ({ type: "file", path, display: path }))
|
.map((path) => ({ type: "file", path, display: path }))
|
||||||
return [...references, ...agents, ...mcpResources, ...pinned, ...fileOptions]
|
return [...agents, ...pinned, ...fileOptions]
|
||||||
},
|
},
|
||||||
key: atKey,
|
key: atKey,
|
||||||
filterKeys: ["display"],
|
filterKeys: ["display"],
|
||||||
skipFilter: (item) => item.type === "file" && !item.recent,
|
skipFilter: (item) => item.type === "file" && !item.recent,
|
||||||
groupBy: (item) => {
|
groupBy: (item) => {
|
||||||
if (item.type === "reference") return "reference"
|
|
||||||
if (item.type === "agent") return "agent"
|
if (item.type === "agent") return "agent"
|
||||||
if (item.type === "resource") return "resource"
|
|
||||||
if (item.recent) return "recent"
|
if (item.recent) return "recent"
|
||||||
return "file"
|
return "file"
|
||||||
},
|
},
|
||||||
sortGroupsBy: (a, b) => {
|
sortGroupsBy: (a, b) => {
|
||||||
const rank = (category: string) => {
|
const rank = (category: string) => {
|
||||||
if (category === "reference") return 0
|
if (category === "agent") return 0
|
||||||
if (category === "agent") return 1
|
if (category === "recent") return 1
|
||||||
if (category === "resource") return 2
|
return 2
|
||||||
if (category === "recent") return 3
|
|
||||||
return 4
|
|
||||||
}
|
}
|
||||||
return rank(a.category) - rank(b.category)
|
return rank(a.category) - rank(b.category)
|
||||||
},
|
},
|
||||||
@@ -829,17 +746,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const pill = document.createElement("span")
|
const pill = document.createElement("span")
|
||||||
pill.textContent = part.content
|
pill.textContent = part.content
|
||||||
pill.setAttribute("data-type", part.type)
|
pill.setAttribute("data-type", part.type)
|
||||||
if (part.type === "file") {
|
if (part.type === "file") pill.setAttribute("data-path", part.path)
|
||||||
pill.setAttribute("data-path", part.path)
|
|
||||||
if (part.mime) pill.setAttribute("data-mime", part.mime)
|
|
||||||
if (part.filename) pill.setAttribute("data-filename", part.filename)
|
|
||||||
if (part.url) pill.setAttribute("data-url", part.url)
|
|
||||||
if (part.source?.type === "resource") {
|
|
||||||
pill.setAttribute("data-source-type", part.source.type)
|
|
||||||
pill.setAttribute("data-source-client-name", part.source.clientName)
|
|
||||||
pill.setAttribute("data-source-uri", part.source.uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (part.type === "agent") pill.setAttribute("data-name", part.name)
|
if (part.type === "agent") pill.setAttribute("data-name", part.name)
|
||||||
pill.setAttribute("contenteditable", "false")
|
pill.setAttribute("contenteditable", "false")
|
||||||
pill.style.userSelect = "text"
|
pill.style.userSelect = "text"
|
||||||
@@ -884,7 +791,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollSlashActiveIntoView = () => {
|
// Auto-scroll active command into view when navigating with keyboard
|
||||||
|
createEffect(() => {
|
||||||
const activeId = slashActive()
|
const activeId = slashActive()
|
||||||
if (!activeId || !slashPopoverRef) return
|
if (!activeId || !slashPopoverRef) return
|
||||||
|
|
||||||
@@ -892,7 +800,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const element = slashPopoverRef.querySelector(`[data-slash-id="${activeId}"]`)
|
const element = slashPopoverRef.querySelector(`[data-slash-id="${activeId}"]`)
|
||||||
element?.scrollIntoView({ block: "nearest", behavior: "smooth" })
|
element?.scrollIntoView({ block: "nearest", behavior: "smooth" })
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
const selectPopoverActive = () => {
|
const selectPopoverActive = () => {
|
||||||
if (store.popover === "at") {
|
if (store.popover === "at") {
|
||||||
const items = atFlat()
|
const items = atFlat()
|
||||||
@@ -954,29 +862,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
|
|
||||||
const pushFile = (file: HTMLElement) => {
|
const pushFile = (file: HTMLElement) => {
|
||||||
const content = file.textContent ?? ""
|
const content = file.textContent ?? ""
|
||||||
const source =
|
|
||||||
file.dataset.sourceType === "resource" && file.dataset.sourceClientName && file.dataset.sourceUri
|
|
||||||
? {
|
|
||||||
type: "resource" as const,
|
|
||||||
text: {
|
|
||||||
value: content,
|
|
||||||
start: position,
|
|
||||||
end: position + content.length,
|
|
||||||
},
|
|
||||||
clientName: file.dataset.sourceClientName,
|
|
||||||
uri: file.dataset.sourceUri,
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
parts.push({
|
parts.push({
|
||||||
type: "file",
|
type: "file",
|
||||||
path: file.dataset.path!,
|
path: file.dataset.path!,
|
||||||
content,
|
content,
|
||||||
start: position,
|
start: position,
|
||||||
end: position + content.length,
|
end: position + content.length,
|
||||||
...(file.dataset.mime ? { mime: file.dataset.mime } : {}),
|
|
||||||
...(file.dataset.filename ? { filename: file.dataset.filename } : {}),
|
|
||||||
...(file.dataset.url ? { url: file.dataset.url } : {}),
|
|
||||||
...(source ? { source } : {}),
|
|
||||||
})
|
})
|
||||||
position += content.length
|
position += content.length
|
||||||
}
|
}
|
||||||
@@ -1291,6 +1182,22 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
onSubmit: props.onSubmit,
|
onSubmit: props.onSubmit,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const interrupt = () => {
|
||||||
|
if (submissionState.interrupting) return
|
||||||
|
if (platform.platform !== "desktop" || !props.controls.newLayoutDesigns) return abort()
|
||||||
|
|
||||||
|
setSubmissionState("interrupting", true)
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() => abort())
|
||||||
|
.finally(() => setSubmissionState("interrupting", false))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleV2Submit = (event: Event) => {
|
||||||
|
if (!stopping() || platform.platform !== "desktop") return handleSubmit(event)
|
||||||
|
event.preventDefault()
|
||||||
|
return interrupt()
|
||||||
|
}
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "u") {
|
if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "u") {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -1343,7 +1250,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (working()) {
|
if (working()) {
|
||||||
void abort()
|
void interrupt()
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
return
|
return
|
||||||
@@ -1396,9 +1303,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
}
|
}
|
||||||
if (store.popover === "slash") {
|
if (store.popover === "slash") {
|
||||||
slashOnKeyDown(event)
|
slashOnKeyDown(event)
|
||||||
if (event.key === "ArrowUp" || event.key === "ArrowDown" || ctrlNav) {
|
|
||||||
scrollSlashActiveIntoView()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
return
|
return
|
||||||
@@ -1412,7 +1316,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (working()) {
|
if (working()) {
|
||||||
void abort()
|
void interrupt()
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -1457,9 +1361,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const agentsLoading = () => props.controls.agents.loading
|
const agentsLoading = () => props.controls.agents.loading
|
||||||
const agentsShouldFadeIn = createMemo<boolean>((prev) => prev ?? agentsLoading())
|
const agentsShouldFadeIn = createMemo((prev) => prev ?? agentsLoading())
|
||||||
const providersLoading = () => props.controls.model.loading
|
const providersLoading = () => props.controls.model.loading
|
||||||
const providersShouldFadeIn = createMemo<boolean>((prev) => prev ?? providersLoading())
|
const providersShouldFadeIn = createMemo((prev) => prev ?? providersLoading())
|
||||||
|
|
||||||
const [promptReady] = createResource(
|
const [promptReady] = createResource(
|
||||||
() => prompt.ready.promise,
|
() => prompt.ready.promise,
|
||||||
@@ -1473,10 +1377,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
|
|
||||||
const modelControlState = createMemo<ComposerModelControlState>(() => ({
|
const modelControlState = createMemo<ComposerModelControlState>(() => ({
|
||||||
loading: providersLoading(),
|
loading: providersLoading(),
|
||||||
shouldAnimate: providersShouldFadeIn(),
|
|
||||||
paid: props.controls.model.paid,
|
paid: props.controls.model.paid,
|
||||||
title: language.t("command.model.choose"),
|
title: language.t("command.model.choose"),
|
||||||
keybind: command.keybindParts("model.choose"),
|
keybind: command.keybind("model.choose"),
|
||||||
model: props.controls.model.selection,
|
model: props.controls.model.selection,
|
||||||
providerID: props.controls.model.selection.current()?.provider?.id,
|
providerID: props.controls.model.selection.current()?.provider?.id,
|
||||||
modelName: props.controls.model.selection.current()?.name ?? language.t("dialog.model.select.title"),
|
modelName: props.controls.model.selection.current()?.name ?? language.t("dialog.model.select.title"),
|
||||||
@@ -1490,15 +1393,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
const newSession = () => props.variant === "new-session"
|
const newSession = () => props.variant === "new-session"
|
||||||
const bindEditorRef = (el: HTMLDivElement) => {
|
|
||||||
editorRef = el
|
|
||||||
restoreEndOnFocus = true
|
|
||||||
props.ref?.(el)
|
|
||||||
}
|
|
||||||
const showAgentControl = createMemo(() => props.controls.agents.visible && props.controls.agents.options.length > 0)
|
const showAgentControl = createMemo(() => props.controls.agents.visible && props.controls.agents.options.length > 0)
|
||||||
const agentControlState = createMemo<ComposerAgentControlState>(() => ({
|
const agentControlState = createMemo<ComposerAgentControlState>(() => ({
|
||||||
title: language.t("command.agent.cycle"),
|
title: language.t("command.agent.cycle"),
|
||||||
keybind: command.keybindParts("agent.cycle"),
|
keybind: command.keybind("agent.cycle"),
|
||||||
options: props.controls.agents.options,
|
options: props.controls.agents.options,
|
||||||
current: props.controls.agents.current,
|
current: props.controls.agents.current,
|
||||||
style: control(),
|
style: control(),
|
||||||
@@ -1523,8 +1421,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
setSlashActive={setSlashActive}
|
setSlashActive={setSlashActive}
|
||||||
onSlashSelect={handleSlashSelect}
|
onSlashSelect={handleSlashSelect}
|
||||||
commandKeybind={command.keybind}
|
commandKeybind={command.keybind}
|
||||||
commandKeybindParts={command.keybindParts}
|
|
||||||
newLayoutDesigns={props.controls.newLayoutDesigns}
|
|
||||||
t={(key) => language.t(key as Parameters<typeof language.t>[0])}
|
t={(key) => language.t(key as Parameters<typeof language.t>[0])}
|
||||||
/>
|
/>
|
||||||
<Switch>
|
<Switch>
|
||||||
@@ -1532,7 +1428,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<DockShellForm
|
<DockShellForm
|
||||||
data-component={newSession() ? "session-new-composer" : "session-composer"}
|
data-component={newSession() ? "session-new-composer" : "session-composer"}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleV2Submit}
|
||||||
classList={{
|
classList={{
|
||||||
"group/prompt-input min-h-[96px] w-full rounded-xl bg-v2-background-bg-base shadow-[var(--v2-elevation-raised)]": true,
|
"group/prompt-input min-h-[96px] w-full rounded-xl bg-v2-background-bg-base shadow-[var(--v2-elevation-raised)]": true,
|
||||||
"border-icon-info-active border-dashed": store.draggingType !== null,
|
"border-icon-info-active border-dashed": store.draggingType !== null,
|
||||||
@@ -1578,7 +1474,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
<div class="relative max-h-[180px] overflow-y-auto no-scrollbar" ref={(el) => (scrollRef = el)}>
|
<div class="relative max-h-[180px] overflow-y-auto no-scrollbar" ref={(el) => (scrollRef = el)}>
|
||||||
<div
|
<div
|
||||||
data-component="prompt-input"
|
data-component="prompt-input"
|
||||||
ref={bindEditorRef}
|
ref={(el) => {
|
||||||
|
editorRef = el
|
||||||
|
props.ref?.(el)
|
||||||
|
}}
|
||||||
role="textbox"
|
role="textbox"
|
||||||
aria-multiline="true"
|
aria-multiline="true"
|
||||||
aria-label={designPlaceholder()}
|
aria-label={designPlaceholder()}
|
||||||
@@ -1593,7 +1492,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
onPaste={handlePaste}
|
onPaste={handlePaste}
|
||||||
onCompositionStart={handleCompositionStart}
|
onCompositionStart={handleCompositionStart}
|
||||||
onCompositionEnd={handleCompositionEnd}
|
onCompositionEnd={handleCompositionEnd}
|
||||||
onFocus={handleFocus}
|
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
classList={{
|
classList={{
|
||||||
@@ -1614,16 +1512,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex h-11 items-center px-2">
|
<div class="flex h-11 items-center px-2">
|
||||||
<div class="flex min-w-0 flex-1 items-center gap-1">
|
<div class="flex min-w-0 flex-1 items-center gap-0">
|
||||||
{fileAttachmentInput()}
|
{fileAttachmentInput()}
|
||||||
<TooltipV2
|
<TooltipKeybind
|
||||||
placement="top"
|
placement="top"
|
||||||
value={
|
title={language.t("prompt.action.attachFile")}
|
||||||
<>
|
keybind={command.keybind("file.attach")}
|
||||||
{language.t("prompt.action.attachFile")}
|
|
||||||
<KeybindV2 keys={command.keybindParts("file.attach")} variant="neutral" />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
data-action="prompt-attach"
|
data-action="prompt-attach"
|
||||||
@@ -1637,30 +1531,25 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
tabIndex={store.mode === "normal" ? undefined : -1}
|
tabIndex={store.mode === "normal" ? undefined : -1}
|
||||||
aria-label={language.t("prompt.action.attachFile")}
|
aria-label={language.t("prompt.action.attachFile")}
|
||||||
/>
|
/>
|
||||||
</TooltipV2>
|
</TooltipKeybind>
|
||||||
<Show when={showAgentControl()}>
|
<Show when={showAgentControl()}>
|
||||||
<ComposerAgentControl state={agentControlState()} />
|
<ComposerAgentControl state={agentControlState()} />
|
||||||
</Show>
|
</Show>
|
||||||
{props.toolbar}
|
{props.toolbar}
|
||||||
<ComposerModelControl state={modelControlState()} />
|
<ComposerModelControl state={modelControlState()} />
|
||||||
<Show when={!providersLoading() && store.mode !== "shell" && showVariantControl()}>
|
<Show when={store.mode !== "shell" && showVariantControl()}>
|
||||||
<div
|
<div
|
||||||
data-component="prompt-variant-control"
|
data-component="prompt-variant-control"
|
||||||
classList={{
|
classList={{
|
||||||
"animate-in fade-in": providersShouldFadeIn(),
|
|
||||||
"hidden group-hover/prompt-input:block group-focus-within/prompt-input:block":
|
"hidden group-hover/prompt-input:block group-focus-within/prompt-input:block":
|
||||||
!props.controls.model.selection.variant.current() && !store.variantOpen,
|
!props.controls.model.selection.variant.current() && !store.variantOpen,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TooltipV2
|
<TooltipKeybind
|
||||||
placement="top"
|
placement="top"
|
||||||
gutter={4}
|
gutter={4}
|
||||||
value={
|
title={language.t("command.model.variant.cycle")}
|
||||||
<>
|
keybind={command.keybind("model.variant.cycle")}
|
||||||
{language.t("command.model.variant.cycle")}
|
|
||||||
<KeybindV2 keys={command.keybindParts("model.variant.cycle")} variant="neutral" />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
size="normal"
|
size="normal"
|
||||||
@@ -1678,26 +1567,33 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
triggerProps={{ "data-action": "prompt-model-variant" }}
|
triggerProps={{ "data-action": "prompt-model-variant" }}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
/>
|
/>
|
||||||
</TooltipV2>
|
</TooltipKeybind>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<TooltipV2 placement="top" inactive={!working() && blank()} value={tip()}>
|
<Tooltip placement="top" inactive={!working() && blank()} value={tip()}>
|
||||||
<IconButton
|
<span class="relative flex size-7">
|
||||||
data-action="prompt-submit"
|
<IconButton
|
||||||
type="submit"
|
data-action="prompt-submit"
|
||||||
disabled={!working() && blank()}
|
type="submit"
|
||||||
tabIndex={store.mode === "normal" ? undefined : -1}
|
disabled={submissionState.interrupting || (!working() && blank())}
|
||||||
icon={stopping() ? "stop" : store.mode === "shell" ? "arrow-undo-down" : "arrow-up"}
|
tabIndex={store.mode === "normal" ? undefined : -1}
|
||||||
variant="primary"
|
icon={stopping() ? "stop" : store.mode === "shell" ? "arrow-undo-down" : "arrow-up"}
|
||||||
class="size-7 rounded-md p-[6px] text-v2-icon-icon-muted shadow-[var(--v2-elevation-button-contrast)] disabled:opacity-50"
|
variant="primary"
|
||||||
style={{
|
class="size-7 rounded-md p-[6px] text-v2-icon-icon-muted shadow-[var(--v2-elevation-button-contrast)] disabled:opacity-50"
|
||||||
"background-image":
|
classList={{ "[&_[data-slot=icon-svg]]:invisible": submissionState.interrupting }}
|
||||||
"linear-gradient(180deg,var(--v2-alpha-light-20) 0%,var(--v2-alpha-light-0) 100%),linear-gradient(90deg,var(--v2-background-bg-contrast) 0%,var(--v2-background-bg-contrast) 100%)",
|
style={{
|
||||||
}}
|
"background-image":
|
||||||
aria-label={stopping() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
|
"linear-gradient(180deg,var(--v2-alpha-light-20) 0%,var(--v2-alpha-light-0) 100%),linear-gradient(90deg,var(--v2-background-bg-contrast) 0%,var(--v2-background-bg-contrast) 100%)",
|
||||||
/>
|
}}
|
||||||
</TooltipV2>
|
aria-label={stopping() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
|
||||||
|
aria-busy={submissionState.interrupting}
|
||||||
|
/>
|
||||||
|
<Show when={submissionState.interrupting}>
|
||||||
|
<SessionProgressIndicatorV2 class="pointer-events-none absolute inset-[6px] size-4 opacity-50" />
|
||||||
|
</Show>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</DockShellForm>
|
</DockShellForm>
|
||||||
</div>
|
</div>
|
||||||
@@ -1757,7 +1653,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-component="prompt-input"
|
data-component="prompt-input"
|
||||||
ref={bindEditorRef}
|
ref={(el) => {
|
||||||
|
editorRef = el
|
||||||
|
props.ref?.(el)
|
||||||
|
}}
|
||||||
role="textbox"
|
role="textbox"
|
||||||
aria-multiline="true"
|
aria-multiline="true"
|
||||||
aria-label={placeholder()}
|
aria-label={placeholder()}
|
||||||
@@ -1772,7 +1671,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
onPaste={handlePaste}
|
onPaste={handlePaste}
|
||||||
onCompositionStart={handleCompositionStart}
|
onCompositionStart={handleCompositionStart}
|
||||||
onCompositionEnd={handleCompositionEnd}
|
onCompositionEnd={handleCompositionEnd}
|
||||||
onFocus={handleFocus}
|
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
classList={{
|
classList={{
|
||||||
@@ -1892,7 +1790,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
<Show when={!agentsLoading()}>
|
<Show when={!agentsLoading()}>
|
||||||
<div
|
<div
|
||||||
data-component="prompt-agent-control"
|
data-component="prompt-agent-control"
|
||||||
classList={{ "animate-in fade-in duration-300": agentsShouldFadeIn() }}
|
style={agentsShouldFadeIn() ? { animation: "fade-in 0.3s" } : undefined}
|
||||||
>
|
>
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
placement="top"
|
placement="top"
|
||||||
@@ -1921,7 +1819,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
<Show when={store.mode !== "shell"}>
|
<Show when={store.mode !== "shell"}>
|
||||||
<div
|
<div
|
||||||
data-component="prompt-model-control"
|
data-component="prompt-model-control"
|
||||||
classList={{ "animate-in fade-in duration-300": providersShouldFadeIn() }}
|
style={providersShouldFadeIn() ? { animation: "fade-in 0.3s" } : undefined}
|
||||||
>
|
>
|
||||||
<Show
|
<Show
|
||||||
when={props.controls.model.paid}
|
when={props.controls.model.paid}
|
||||||
@@ -2000,7 +1898,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
<Show when={showVariantControl()}>
|
<Show when={showVariantControl()}>
|
||||||
<div
|
<div
|
||||||
data-component="prompt-variant-control"
|
data-component="prompt-variant-control"
|
||||||
classList={{ "animate-in fade-in duration-300": providersShouldFadeIn() }}
|
style={providersShouldFadeIn() ? { animation: "fade-in 0.3s" } : undefined}
|
||||||
>
|
>
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
placement="top"
|
placement="top"
|
||||||
@@ -2041,7 +1939,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
|
|
||||||
type ComposerAgentControlState = {
|
type ComposerAgentControlState = {
|
||||||
title: string
|
title: string
|
||||||
keybind: string[]
|
keybind: string
|
||||||
options: string[]
|
options: string[]
|
||||||
current: string
|
current: string
|
||||||
style: JSX.CSSProperties | undefined
|
style: JSX.CSSProperties | undefined
|
||||||
@@ -2050,10 +1948,9 @@ type ComposerAgentControlState = {
|
|||||||
|
|
||||||
type ComposerModelControlState = {
|
type ComposerModelControlState = {
|
||||||
loading: boolean
|
loading: boolean
|
||||||
shouldAnimate: boolean
|
|
||||||
paid: boolean
|
paid: boolean
|
||||||
title: string
|
title: string
|
||||||
keybind: string[]
|
keybind: string
|
||||||
model: ReturnType<typeof useLocal>["model"]
|
model: ReturnType<typeof useLocal>["model"]
|
||||||
providerID?: string
|
providerID?: string
|
||||||
modelName: string
|
modelName: string
|
||||||
@@ -2068,16 +1965,7 @@ function ComposerAgentControl(props: { state: ComposerAgentControlState }) {
|
|||||||
<div class="pointer-events-none absolute left-2 top-1/2 z-10 flex size-4 -translate-y-1/2 items-center justify-center text-v2-icon-icon-muted">
|
<div class="pointer-events-none absolute left-2 top-1/2 z-10 flex size-4 -translate-y-1/2 items-center justify-center text-v2-icon-icon-muted">
|
||||||
<Icon name="sliders" size="small" />
|
<Icon name="sliders" size="small" />
|
||||||
</div>
|
</div>
|
||||||
<TooltipV2
|
<TooltipKeybind placement="top" gutter={4} title={props.state.title} keybind={props.state.keybind}>
|
||||||
placement="top"
|
|
||||||
gutter={4}
|
|
||||||
value={
|
|
||||||
<>
|
|
||||||
{props.state.title}
|
|
||||||
<KeybindV2 keys={props.state.keybind} variant="neutral" />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Select
|
<Select
|
||||||
size="normal"
|
size="normal"
|
||||||
options={props.state.options}
|
options={props.state.options}
|
||||||
@@ -2089,7 +1977,7 @@ function ComposerAgentControl(props: { state: ComposerAgentControlState }) {
|
|||||||
triggerProps={{ "data-action": "prompt-agent" }}
|
triggerProps={{ "data-action": "prompt-agent" }}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
/>
|
/>
|
||||||
</TooltipV2>
|
</TooltipKeybind>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -2100,23 +1988,13 @@ function ComposerModelControl(props: { state: ComposerModelControlState }) {
|
|||||||
<Show
|
<Show
|
||||||
when={props.state.paid}
|
when={props.state.paid}
|
||||||
fallback={
|
fallback={
|
||||||
<TooltipV2
|
<TooltipKeybind placement="top" gutter={4} title={props.state.title} keybind={props.state.keybind}>
|
||||||
placement="top"
|
|
||||||
gutter={4}
|
|
||||||
value={
|
|
||||||
<>
|
|
||||||
{props.state.title}
|
|
||||||
<KeybindV2 keys={props.state.keybind} variant="neutral" />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
data-action="prompt-model"
|
data-action="prompt-model"
|
||||||
as="div"
|
as="div"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="normal"
|
size="normal"
|
||||||
class="min-w-0 max-w-[220px] justify-start text-[13px] font-[440] leading-5 text-v2-text-text-faint group"
|
class="min-w-0 max-w-[220px] justify-start text-[13px] font-[440] leading-5 text-v2-text-text-faint group"
|
||||||
classList={{ "animate-in fade-in": props.state.shouldAnimate }}
|
|
||||||
style={props.state.style}
|
style={props.state.style}
|
||||||
onClick={props.state.onUnpaidClick}
|
onClick={props.state.onUnpaidClick}
|
||||||
>
|
>
|
||||||
@@ -2134,19 +2012,10 @@ function ComposerModelControl(props: { state: ComposerModelControlState }) {
|
|||||||
<Icon name="chevron-down" size="small" class="text-v2-icon-icon-muted" />
|
<Icon name="chevron-down" size="small" class="text-v2-icon-icon-muted" />
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipV2>
|
</TooltipKeybind>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TooltipV2
|
<TooltipKeybind placement="top" gutter={4} title={props.state.title} keybind={props.state.keybind}>
|
||||||
placement="top"
|
|
||||||
gutter={4}
|
|
||||||
value={
|
|
||||||
<>
|
|
||||||
{props.state.title}
|
|
||||||
<KeybindV2 keys={props.state.keybind} variant="neutral" />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ModelSelectorPopover
|
<ModelSelectorPopover
|
||||||
model={props.state.model}
|
model={props.state.model}
|
||||||
triggerAs={Button}
|
triggerAs={Button}
|
||||||
@@ -2156,7 +2025,6 @@ function ComposerModelControl(props: { state: ComposerModelControlState }) {
|
|||||||
style: props.state.style,
|
style: props.state.style,
|
||||||
class:
|
class:
|
||||||
"min-w-0 max-w-[220px] justify-start text-[13px] font-[440] leading-5 text-v2-text-text-faint group",
|
"min-w-0 max-w-[220px] justify-start text-[13px] font-[440] leading-5 text-v2-text-text-faint group",
|
||||||
classList: { "animate-in fade-in": props.state.shouldAnimate },
|
|
||||||
"data-action": "prompt-model",
|
"data-action": "prompt-model",
|
||||||
}}
|
}}
|
||||||
onClose={props.state.onClose}
|
onClose={props.state.onClose}
|
||||||
@@ -2175,7 +2043,7 @@ function ComposerModelControl(props: { state: ComposerModelControlState }) {
|
|||||||
<Icon name="chevron-down" size="small" class="text-v2-icon-icon-muted" />
|
<Icon name="chevron-down" size="small" class="text-v2-icon-icon-muted" />
|
||||||
</span>
|
</span>
|
||||||
</ModelSelectorPopover>
|
</ModelSelectorPopover>
|
||||||
</TooltipV2>
|
</TooltipKeybind>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -100,41 +100,6 @@ describe("buildRequestParts", () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("preserves reference aliases as directory file parts", () => {
|
|
||||||
const result = buildRequestParts({
|
|
||||||
prompt: [
|
|
||||||
{
|
|
||||||
type: "file",
|
|
||||||
path: "/repo/../docs",
|
|
||||||
content: "@docs",
|
|
||||||
start: 0,
|
|
||||||
end: 5,
|
|
||||||
mime: "application/x-directory",
|
|
||||||
filename: "docs",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
context: [],
|
|
||||||
images: [],
|
|
||||||
text: "@docs",
|
|
||||||
messageID: "msg_reference",
|
|
||||||
sessionID: "ses_reference",
|
|
||||||
sessionDirectory: "/repo/app",
|
|
||||||
})
|
|
||||||
|
|
||||||
const filePart = result.requestParts.find((part) => part.type === "file")
|
|
||||||
expect(filePart).toBeDefined()
|
|
||||||
if (filePart?.type === "file") {
|
|
||||||
expect(filePart.mime).toBe("application/x-directory")
|
|
||||||
expect(filePart.filename).toBe("docs")
|
|
||||||
expect(filePart.url).toBe("file:///repo/../docs")
|
|
||||||
expect(filePart.source?.type).toBe("file")
|
|
||||||
if (filePart.source?.type === "file") {
|
|
||||||
expect(filePart.source.path).toBe("/repo/../docs")
|
|
||||||
expect(filePart.source.text.value).toBe("@docs")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
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 }]
|
||||||
|
|
||||||
|
|||||||
@@ -99,31 +99,21 @@ export function buildRequestParts(input: BuildRequestPartsInput) {
|
|||||||
|
|
||||||
const files = input.prompt.filter(isFileAttachment).map((attachment) => {
|
const files = input.prompt.filter(isFileAttachment).map((attachment) => {
|
||||||
const path = absolute(input.sessionDirectory, attachment.path)
|
const path = absolute(input.sessionDirectory, attachment.path)
|
||||||
const source = attachment.source
|
|
||||||
? {
|
|
||||||
...attachment.source,
|
|
||||||
text: {
|
|
||||||
value: attachment.content,
|
|
||||||
start: attachment.start,
|
|
||||||
end: attachment.end,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
type: "file" as const,
|
|
||||||
text: {
|
|
||||||
value: attachment.content,
|
|
||||||
start: attachment.start,
|
|
||||||
end: attachment.end,
|
|
||||||
},
|
|
||||||
path,
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
id: Identifier.ascending("part"),
|
id: Identifier.ascending("part"),
|
||||||
type: "file",
|
type: "file",
|
||||||
mime: attachment.mime ?? "text/plain",
|
mime: "text/plain",
|
||||||
url: attachment.url ?? `file://${encodeFilePath(path)}${fileQuery(attachment.selection)}`,
|
url: `file://${encodeFilePath(path)}${fileQuery(attachment.selection)}`,
|
||||||
filename: attachment.filename ?? getFilename(attachment.path),
|
filename: getFilename(attachment.path),
|
||||||
source,
|
source: {
|
||||||
|
type: "file",
|
||||||
|
text: {
|
||||||
|
value: attachment.content,
|
||||||
|
start: attachment.start,
|
||||||
|
end: attachment.end,
|
||||||
|
},
|
||||||
|
path,
|
||||||
|
},
|
||||||
} satisfies PromptRequestPart
|
} satisfies PromptRequestPart
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const PromptContextItems: Component<ContextItemsProps> = (props) => {
|
|||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
placement="top"
|
placement="top"
|
||||||
openDelay={800}
|
openDelay={2000}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
|
|||||||
@@ -1,22 +1,10 @@
|
|||||||
import { Component, For, Match, Show, Switch } from "solid-js"
|
import { Component, For, Match, Show, Switch } from "solid-js"
|
||||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Tag } from "@opencode-ai/ui/v2/badge-v2"
|
|
||||||
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
|
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
||||||
|
|
||||||
export type AtOption =
|
export type AtOption =
|
||||||
| { type: "agent"; name: string; display: string }
|
| { type: "agent"; name: string; display: string }
|
||||||
| {
|
|
||||||
type: "resource"
|
|
||||||
name: string
|
|
||||||
uri: string
|
|
||||||
client: string
|
|
||||||
display: string
|
|
||||||
description?: string
|
|
||||||
mime?: string
|
|
||||||
}
|
|
||||||
| { type: "reference"; name: string; path: string; display: string; description: string }
|
|
||||||
| { type: "file"; path: string; display: string; recent?: boolean }
|
| { type: "file"; path: string; display: string; recent?: boolean }
|
||||||
|
|
||||||
export interface SlashCommand {
|
export interface SlashCommand {
|
||||||
@@ -42,8 +30,6 @@ type PromptPopoverProps = {
|
|||||||
setSlashActive: (id: string) => void
|
setSlashActive: (id: string) => void
|
||||||
onSlashSelect: (item: SlashCommand) => void
|
onSlashSelect: (item: SlashCommand) => void
|
||||||
commandKeybind: (id: string) => string | undefined
|
commandKeybind: (id: string) => string | undefined
|
||||||
commandKeybindParts: (id: string) => string[]
|
|
||||||
newLayoutDesigns: boolean
|
|
||||||
t: (key: string) => string
|
t: (key: string) => string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,29 +41,15 @@ export const PromptPopover: Component<PromptPopoverProps> = (props) => {
|
|||||||
if (props.popover === "slash") props.setSlashPopoverRef(el)
|
if (props.popover === "slash") props.setSlashPopoverRef(el)
|
||||||
}}
|
}}
|
||||||
class="absolute inset-x-0 -top-2 -translate-y-full origin-bottom-left max-h-80 min-h-10
|
class="absolute inset-x-0 -top-2 -translate-y-full origin-bottom-left max-h-80 min-h-10
|
||||||
overflow-auto no-scrollbar flex flex-col p-2"
|
overflow-auto no-scrollbar flex flex-col p-2 rounded-[12px]
|
||||||
classList={{
|
bg-surface-raised-stronger-non-alpha shadow-[var(--shadow-lg-border-base)]"
|
||||||
"z-[70] rounded-[10px] bg-v2-background-bg-base shadow-[var(--v2-elevation-raised)]": props.newLayoutDesigns,
|
|
||||||
"rounded-[12px] bg-surface-raised-stronger-non-alpha shadow-[var(--shadow-lg-border-base)]":
|
|
||||||
!props.newLayoutDesigns,
|
|
||||||
}}
|
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.popover === "at"}>
|
<Match when={props.popover === "at"}>
|
||||||
<Show
|
<Show
|
||||||
when={props.atFlat.length > 0}
|
when={props.atFlat.length > 0}
|
||||||
fallback={
|
fallback={<div class="text-text-weak px-2 py-1">{props.t("prompt.popover.emptyResults")}</div>}
|
||||||
<div
|
|
||||||
class="px-2 py-1"
|
|
||||||
classList={{
|
|
||||||
"text-v2-text-text-muted": props.newLayoutDesigns,
|
|
||||||
"text-text-weak": !props.newLayoutDesigns,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.t("prompt.popover.emptyResults")}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<For each={props.atFlat.slice(0, 10)}>
|
<For each={props.atFlat.slice(0, 10)}>
|
||||||
{(item) => {
|
{(item) => {
|
||||||
@@ -86,117 +58,13 @@ export const PromptPopover: Component<PromptPopoverProps> = (props) => {
|
|||||||
if (item.type === "agent") {
|
if (item.type === "agent") {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
class="w-full flex items-center gap-x-2 px-2 py-0.5"
|
class="w-full flex items-center gap-x-2 rounded-md px-2 py-0.5"
|
||||||
classList={{
|
classList={{ "bg-surface-raised-base-hover": props.atActive === key }}
|
||||||
"rounded-[4px]": props.newLayoutDesigns,
|
|
||||||
"rounded-md": !props.newLayoutDesigns,
|
|
||||||
"bg-v2-overlay-simple-overlay-hover": props.newLayoutDesigns && props.atActive === key,
|
|
||||||
"bg-surface-raised-base-hover": !props.newLayoutDesigns && props.atActive === key,
|
|
||||||
}}
|
|
||||||
onClick={() => props.onAtSelect(item)}
|
onClick={() => props.onAtSelect(item)}
|
||||||
onPointerMove={() => props.setAtActive(key)}
|
onMouseEnter={() => props.setAtActive(key)}
|
||||||
>
|
>
|
||||||
<Icon name="brain" size="small" class="text-icon-info-active shrink-0" />
|
<Icon name="brain" size="small" class="text-icon-info-active shrink-0" />
|
||||||
<span
|
<span class="text-14-regular text-text-strong whitespace-nowrap">@{item.name}</span>
|
||||||
class="whitespace-nowrap"
|
|
||||||
classList={{
|
|
||||||
"text-[13px] leading-[calc(var(--font-size-base)*1.8)] tracking-[-0.04px] [font-weight:440]":
|
|
||||||
props.newLayoutDesigns,
|
|
||||||
"text-v2-text-text-base": props.newLayoutDesigns,
|
|
||||||
"text-14-regular": !props.newLayoutDesigns,
|
|
||||||
"text-text-strong": !props.newLayoutDesigns,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
@{item.name}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.type === "resource") {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
class="w-full flex items-center gap-x-2 px-2 py-0.5"
|
|
||||||
classList={{
|
|
||||||
"rounded-[4px]": props.newLayoutDesigns,
|
|
||||||
"rounded-md": !props.newLayoutDesigns,
|
|
||||||
"bg-v2-overlay-simple-overlay-hover": props.newLayoutDesigns && props.atActive === key,
|
|
||||||
"bg-surface-raised-base-hover": !props.newLayoutDesigns && props.atActive === key,
|
|
||||||
}}
|
|
||||||
onClick={() => props.onAtSelect(item)}
|
|
||||||
onPointerMove={() => props.setAtActive(key)}
|
|
||||||
>
|
|
||||||
<FileIcon node={{ path: item.uri, type: "file" }} class="shrink-0 size-4" />
|
|
||||||
<div
|
|
||||||
class="flex items-center min-w-0"
|
|
||||||
classList={{
|
|
||||||
"text-[13px] leading-[calc(var(--font-size-base)*1.8)] tracking-[-0.04px] [font-weight:440]":
|
|
||||||
props.newLayoutDesigns,
|
|
||||||
"text-14-regular": !props.newLayoutDesigns,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="text-text-strong whitespace-nowrap"
|
|
||||||
classList={{ "text-v2-text-text-base": props.newLayoutDesigns }}
|
|
||||||
>
|
|
||||||
@{item.name}
|
|
||||||
</span>
|
|
||||||
<Show when={item.description}>
|
|
||||||
{(description) => (
|
|
||||||
<span
|
|
||||||
class="whitespace-nowrap truncate min-w-0 ml-2"
|
|
||||||
classList={{
|
|
||||||
"text-v2-text-text-muted": props.newLayoutDesigns,
|
|
||||||
"text-text-weak": !props.newLayoutDesigns,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{description()}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.type === "reference") {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
class="w-full flex items-center gap-x-2 px-2 py-0.5"
|
|
||||||
classList={{
|
|
||||||
"rounded-[4px]": props.newLayoutDesigns,
|
|
||||||
"rounded-md": !props.newLayoutDesigns,
|
|
||||||
"bg-v2-overlay-simple-overlay-hover": props.newLayoutDesigns && props.atActive === key,
|
|
||||||
"bg-surface-raised-base-hover": !props.newLayoutDesigns && props.atActive === key,
|
|
||||||
}}
|
|
||||||
onClick={() => props.onAtSelect(item)}
|
|
||||||
onPointerMove={() => props.setAtActive(key)}
|
|
||||||
>
|
|
||||||
<FileIcon node={{ path: item.path, type: "directory" }} class="shrink-0 size-4" />
|
|
||||||
<div
|
|
||||||
class="flex items-center min-w-0"
|
|
||||||
classList={{
|
|
||||||
"text-[13px] leading-[calc(var(--font-size-base)*1.8)] tracking-[-0.04px] [font-weight:440]":
|
|
||||||
props.newLayoutDesigns,
|
|
||||||
"text-14-regular": !props.newLayoutDesigns,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="text-text-strong whitespace-nowrap"
|
|
||||||
classList={{ "text-v2-text-text-base": props.newLayoutDesigns }}
|
|
||||||
>
|
|
||||||
@{item.name}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="whitespace-nowrap truncate min-w-0 ml-2"
|
|
||||||
classList={{
|
|
||||||
"text-v2-text-text-muted": props.newLayoutDesigns,
|
|
||||||
"text-text-weak": !props.newLayoutDesigns,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.description}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -207,44 +75,16 @@ export const PromptPopover: Component<PromptPopoverProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
class="w-full flex items-center gap-x-2 px-2 py-0.5"
|
class="w-full flex items-center gap-x-2 rounded-md px-2 py-0.5"
|
||||||
classList={{
|
classList={{ "bg-surface-raised-base-hover": props.atActive === key }}
|
||||||
"rounded-[4px]": props.newLayoutDesigns,
|
|
||||||
"rounded-md": !props.newLayoutDesigns,
|
|
||||||
"bg-v2-overlay-simple-overlay-hover": props.newLayoutDesigns && props.atActive === key,
|
|
||||||
"bg-surface-raised-base-hover": !props.newLayoutDesigns && props.atActive === key,
|
|
||||||
}}
|
|
||||||
onClick={() => props.onAtSelect(item)}
|
onClick={() => props.onAtSelect(item)}
|
||||||
onPointerMove={() => props.setAtActive(key)}
|
onMouseEnter={() => props.setAtActive(key)}
|
||||||
>
|
>
|
||||||
<FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-4" />
|
<FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-4" />
|
||||||
<div
|
<div class="flex items-center text-14-regular min-w-0">
|
||||||
class="flex items-center min-w-0"
|
<span class="text-text-weak whitespace-nowrap truncate min-w-0">{directory}</span>
|
||||||
classList={{
|
|
||||||
"text-[13px] leading-[calc(var(--font-size-base)*1.8)] tracking-[-0.04px] [font-weight:440]":
|
|
||||||
props.newLayoutDesigns,
|
|
||||||
"text-14-regular": !props.newLayoutDesigns,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="whitespace-nowrap truncate min-w-0"
|
|
||||||
classList={{
|
|
||||||
"text-v2-text-text-muted": props.newLayoutDesigns,
|
|
||||||
"text-text-weak": !props.newLayoutDesigns,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{directory}
|
|
||||||
</span>
|
|
||||||
<Show when={!isDirectory}>
|
<Show when={!isDirectory}>
|
||||||
<span
|
<span class="text-text-strong whitespace-nowrap">{filename}</span>
|
||||||
class="whitespace-nowrap"
|
|
||||||
classList={{
|
|
||||||
"text-v2-text-text-base": props.newLayoutDesigns,
|
|
||||||
"text-text-strong": !props.newLayoutDesigns,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{filename}
|
|
||||||
</span>
|
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -256,98 +96,41 @@ export const PromptPopover: Component<PromptPopoverProps> = (props) => {
|
|||||||
<Match when={props.popover === "slash"}>
|
<Match when={props.popover === "slash"}>
|
||||||
<Show
|
<Show
|
||||||
when={props.slashFlat.length > 0}
|
when={props.slashFlat.length > 0}
|
||||||
fallback={
|
fallback={<div class="text-text-weak px-2 py-1">{props.t("prompt.popover.emptyCommands")}</div>}
|
||||||
<div
|
|
||||||
class="px-2 py-1"
|
|
||||||
classList={{
|
|
||||||
"text-v2-text-text-muted": props.newLayoutDesigns,
|
|
||||||
"text-text-weak": !props.newLayoutDesigns,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.t("prompt.popover.emptyCommands")}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<For each={props.slashFlat}>
|
<For each={props.slashFlat}>
|
||||||
{(cmd) => {
|
{(cmd) => (
|
||||||
const keybind = () => props.commandKeybind(cmd.id)
|
<button
|
||||||
const keybindParts = () => props.commandKeybindParts(cmd.id)
|
data-slash-id={cmd.id}
|
||||||
return (
|
classList={{
|
||||||
<button
|
"w-full flex items-center justify-between gap-4 rounded-md px-2 py-1": true,
|
||||||
data-slash-id={cmd.id}
|
"bg-surface-raised-base-hover": props.slashActive === cmd.id,
|
||||||
classList={{
|
}}
|
||||||
"w-full flex items-center justify-between gap-4 px-2 py-1": true,
|
onClick={() => props.onSlashSelect(cmd)}
|
||||||
"rounded-[4px] scroll-my-2": props.newLayoutDesigns,
|
onMouseEnter={() => props.setSlashActive(cmd.id)}
|
||||||
"rounded-md": !props.newLayoutDesigns,
|
>
|
||||||
"bg-v2-overlay-simple-overlay-hover": props.newLayoutDesigns && props.slashActive === cmd.id,
|
<div class="flex items-center gap-2 min-w-0">
|
||||||
"bg-surface-raised-base-hover": !props.newLayoutDesigns && props.slashActive === cmd.id,
|
<span class="text-14-regular text-text-strong whitespace-nowrap">/{cmd.trigger}</span>
|
||||||
}}
|
<Show when={cmd.description}>
|
||||||
onClick={() => props.onSlashSelect(cmd)}
|
<span class="text-14-regular text-text-weak truncate">{cmd.description}</span>
|
||||||
onPointerMove={() => props.setSlashActive(cmd.id)}
|
</Show>
|
||||||
>
|
</div>
|
||||||
<div class="flex items-center gap-2 min-w-0">
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
<span
|
<Show when={cmd.type === "custom" && cmd.source !== "command"}>
|
||||||
class="whitespace-nowrap"
|
<span class="text-11-regular text-text-subtle px-1.5 py-0.5 bg-surface-base rounded">
|
||||||
classList={{
|
{cmd.source === "skill"
|
||||||
"text-[13px] leading-[calc(var(--font-size-base)*1.8)] tracking-[-0.04px] [font-weight:440]":
|
? props.t("prompt.slash.badge.skill")
|
||||||
props.newLayoutDesigns,
|
: cmd.source === "mcp"
|
||||||
"text-v2-text-text-base": props.newLayoutDesigns,
|
? props.t("prompt.slash.badge.mcp")
|
||||||
"text-14-regular": !props.newLayoutDesigns,
|
: props.t("prompt.slash.badge.custom")}
|
||||||
"text-text-strong": !props.newLayoutDesigns,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
/{cmd.trigger}
|
|
||||||
</span>
|
</span>
|
||||||
<Show when={cmd.description}>
|
</Show>
|
||||||
<span
|
<Show when={props.commandKeybind(cmd.id)}>
|
||||||
class="truncate"
|
<span class="text-12-regular text-text-subtle">{props.commandKeybind(cmd.id)}</span>
|
||||||
classList={{
|
</Show>
|
||||||
"text-[13px] leading-[calc(var(--font-size-base)*1.8)] tracking-[-0.04px] [font-weight:440]":
|
</div>
|
||||||
props.newLayoutDesigns,
|
</button>
|
||||||
"text-v2-text-text-muted": props.newLayoutDesigns,
|
)}
|
||||||
"text-14-regular": !props.newLayoutDesigns,
|
|
||||||
"text-text-weak": !props.newLayoutDesigns,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{cmd.description}
|
|
||||||
</span>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2 shrink-0">
|
|
||||||
<Show when={cmd.type === "custom" && cmd.source !== "command"}>
|
|
||||||
<Show
|
|
||||||
when={props.newLayoutDesigns}
|
|
||||||
fallback={
|
|
||||||
<span class="text-11-regular px-1.5 py-0.5 rounded bg-surface-base text-text-subtle">
|
|
||||||
{cmd.source === "skill"
|
|
||||||
? props.t("prompt.slash.badge.skill")
|
|
||||||
: cmd.source === "mcp"
|
|
||||||
? props.t("prompt.slash.badge.mcp")
|
|
||||||
: props.t("prompt.slash.badge.custom")}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Tag>
|
|
||||||
{cmd.source === "skill"
|
|
||||||
? props.t("prompt.slash.badge.skill")
|
|
||||||
: cmd.source === "mcp"
|
|
||||||
? props.t("prompt.slash.badge.mcp")
|
|
||||||
: props.t("prompt.slash.badge.custom")}
|
|
||||||
</Tag>
|
|
||||||
</Show>
|
|
||||||
</Show>
|
|
||||||
<Show when={props.newLayoutDesigns ? keybindParts().length > 0 : keybind()}>
|
|
||||||
<Show
|
|
||||||
when={props.newLayoutDesigns}
|
|
||||||
fallback={<span class="text-12-regular text-text-subtle">{keybind()}</span>}
|
|
||||||
>
|
|
||||||
<KeybindV2 keys={keybindParts()} variant="neutral" />
|
|
||||||
</Show>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</For>
|
</For>
|
||||||
</Show>
|
</Show>
|
||||||
</Match>
|
</Match>
|
||||||
|
|||||||
@@ -69,14 +69,12 @@ export function createPromptProjectController(input: {
|
|||||||
if (servers().length <= 1) {
|
if (servers().length <= 1) {
|
||||||
return [...projects().map(projectKey), actionKey(servers()[0]?.key)]
|
return [...projects().map(projectKey), actionKey(servers()[0]?.key)]
|
||||||
}
|
}
|
||||||
return [
|
return servers().flatMap((server) => [
|
||||||
...servers().flatMap((server) =>
|
...projects()
|
||||||
projects()
|
.filter((project) => project.server?.key === server!.key)
|
||||||
.filter((project) => project.server?.key === server!.key)
|
.map(projectKey),
|
||||||
.map(projectKey),
|
actionKey(server!.key),
|
||||||
),
|
])
|
||||||
actionKey(),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
const initialActive = () => {
|
const initialActive = () => {
|
||||||
const selectedKey = selected() ? projectKey(selected()!) : undefined
|
const selectedKey = selected() ? projectKey(selected()!) : undefined
|
||||||
@@ -132,10 +130,7 @@ export function createPromptProjectController(input: {
|
|||||||
const first = input
|
const first = input
|
||||||
.controls()
|
.controls()
|
||||||
.available.find((project) => !search || displayName(project).toLowerCase().includes(search))
|
.available.find((project) => !search || displayName(project).toLowerCase().includes(search))
|
||||||
setStore({
|
setStore({ search: value, active: first ? projectKey(first) : actionKey(servers()[0]?.key) })
|
||||||
search: value,
|
|
||||||
active: first ? projectKey(first) : actionKey(servers().length > 1 ? undefined : servers()[0]?.key),
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
clearSearch() {
|
clearSearch() {
|
||||||
setStore({ search: "", active: initialActive() })
|
setStore({ search: "", active: initialActive() })
|
||||||
@@ -161,9 +156,6 @@ export function createPromptProjectController(input: {
|
|||||||
? decodeURIComponent(store.active.slice(actionPrefix.length)) || undefined
|
? decodeURIComponent(store.active.slice(actionPrefix.length)) || undefined
|
||||||
: undefined
|
: undefined
|
||||||
},
|
},
|
||||||
activeAction() {
|
|
||||||
return store.active.startsWith(actionPrefix)
|
|
||||||
},
|
|
||||||
setSearchRef(el: HTMLInputElement) {
|
setSearchRef(el: HTMLInputElement) {
|
||||||
searchRef = el
|
searchRef = el
|
||||||
},
|
},
|
||||||
@@ -175,10 +167,7 @@ export function createPromptProjectController(input: {
|
|||||||
|
|
||||||
export type PromptProjectController = ReturnType<typeof createPromptProjectController>
|
export type PromptProjectController = ReturnType<typeof createPromptProjectController>
|
||||||
|
|
||||||
export function PromptProjectSelector(props: {
|
export function PromptProjectSelector(props: { controller: PromptProjectController }) {
|
||||||
controller: PromptProjectController
|
|
||||||
placement?: "bottom" | "bottom-start"
|
|
||||||
}) {
|
|
||||||
let contentRef: HTMLDivElement | undefined
|
let contentRef: HTMLDivElement | undefined
|
||||||
let restoreTrigger = true
|
let restoreTrigger = true
|
||||||
|
|
||||||
@@ -212,12 +201,6 @@ export function PromptProjectSelector(props: {
|
|||||||
selectProject(project)
|
selectProject(project)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (props.controller.activeAction() && props.controller.servers().length > 1) {
|
|
||||||
const item = activeItem()
|
|
||||||
item?.focus()
|
|
||||||
item?.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true }))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
selectAction(props.controller.activeServer())
|
selectAction(props.controller.activeServer())
|
||||||
}
|
}
|
||||||
const moveActive = (delta: number) => {
|
const moveActive = (delta: number) => {
|
||||||
@@ -246,8 +229,9 @@ export function PromptProjectSelector(props: {
|
|||||||
return (
|
return (
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
open={props.controller.open()}
|
open={props.controller.open()}
|
||||||
placement={props.placement ?? "bottom"}
|
placement="bottom-start"
|
||||||
gutter={4}
|
gutter={4}
|
||||||
|
shift={-6}
|
||||||
modal={false}
|
modal={false}
|
||||||
onOpenChange={(open) => props.controller.setOpen(open)}
|
onOpenChange={(open) => props.controller.setOpen(open)}
|
||||||
>
|
>
|
||||||
@@ -334,13 +318,7 @@ export function PromptProjectSelector(props: {
|
|||||||
</DropdownMenu.RadioGroup>
|
</DropdownMenu.RadioGroup>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<For
|
<For each={props.controller.servers()}>
|
||||||
each={props.controller
|
|
||||||
.servers()
|
|
||||||
.filter((server) =>
|
|
||||||
props.controller.projects().some((project) => project.server?.key === server!.key),
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{(server) => (
|
{(server) => (
|
||||||
<div>
|
<div>
|
||||||
<div class="flex h-7 select-none items-center pl-1.5 pr-3 text-[11px] font-[530] leading-none tracking-[0.05px] text-v2-text-text-faint">
|
<div class="flex h-7 select-none items-center pl-1.5 pr-3 text-[11px] font-[530] leading-none tracking-[0.05px] text-v2-text-text-faint">
|
||||||
@@ -353,49 +331,22 @@ export function PromptProjectSelector(props: {
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</DropdownMenu.RadioGroup>
|
</DropdownMenu.RadioGroup>
|
||||||
|
<ProjectAction server={server!.key} controller={props.controller} onSelect={selectAction} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-px bg-v2-border-border-muted" />
|
<Show when={props.controller.servers().length <= 1}>
|
||||||
<div class="flex flex-col p-0.5">
|
<div class="h-px bg-v2-border-border-muted" />
|
||||||
<Show
|
<div class="flex flex-col p-0.5">
|
||||||
when={props.controller.servers().length > 1}
|
<ProjectAction
|
||||||
fallback={
|
server={props.controller.servers()[0]?.key}
|
||||||
<ProjectAction
|
controller={props.controller}
|
||||||
server={props.controller.servers()[0]?.key}
|
onSelect={selectAction}
|
||||||
controller={props.controller}
|
/>
|
||||||
onSelect={selectAction}
|
</div>
|
||||||
/>
|
</Show>
|
||||||
}
|
|
||||||
>
|
|
||||||
<DropdownMenu.Sub>
|
|
||||||
<DropdownMenu.SubTrigger
|
|
||||||
id={props.controller.actionKey()}
|
|
||||||
data-option-key={props.controller.actionKey()}
|
|
||||||
class={projectActionClass}
|
|
||||||
classList={{
|
|
||||||
"!bg-v2-overlay-simple-overlay-hover": props.controller.active() === props.controller.actionKey(),
|
|
||||||
}}
|
|
||||||
onMouseEnter={() => props.controller.setActive(props.controller.actionKey())}
|
|
||||||
>
|
|
||||||
<Icon name="plus" size="small" />
|
|
||||||
<span data-slot="dropdown-menu-item-label" class="min-w-0 flex-1 truncate leading-5">
|
|
||||||
{props.controller.labels.add()}
|
|
||||||
</span>
|
|
||||||
<Icon name="chevron-right" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
|
||||||
</DropdownMenu.SubTrigger>
|
|
||||||
<DropdownMenu.Portal>
|
|
||||||
<DropdownMenu.SubContent class="min-w-[180px] overflow-hidden rounded-md border-0 bg-v2-background-bg-layer-01 p-0.5 shadow-[var(--v2-elevation-floating)] focus:outline-none">
|
|
||||||
<For each={props.controller.servers()}>
|
|
||||||
{(server) => <ServerAction server={server!} onSelect={selectAction} />}
|
|
||||||
</For>
|
|
||||||
</DropdownMenu.SubContent>
|
|
||||||
</DropdownMenu.Portal>
|
|
||||||
</DropdownMenu.Sub>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Portal>
|
</DropdownMenu.Portal>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
@@ -425,12 +376,11 @@ function ProjectTrigger(props: ComponentProps<"button"> & { controller: PromptPr
|
|||||||
{...rest}
|
{...rest}
|
||||||
data-action="prompt-project"
|
data-action="prompt-project"
|
||||||
type="button"
|
type="button"
|
||||||
class="flex h-7 min-w-0 max-w-[203px] items-center gap-1.5 rounded-sm px-1.5 transition-colors focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
|
class="flex h-7 min-w-0 max-w-[203px] items-center gap-1.5 rounded-sm px-2 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-faint transition-colors focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
|
||||||
classList={{
|
classList={{
|
||||||
...local.classList,
|
...local.classList,
|
||||||
"hover:bg-v2-overlay-simple-overlay-hover": !local.controller.open(),
|
"hover:bg-v2-overlay-simple-overlay-hover": !local.controller.open(),
|
||||||
"bg-v2-overlay-simple-overlay-pressed": local.controller.open(),
|
"bg-v2-overlay-simple-overlay-pressed": local.controller.open(),
|
||||||
"text-v2-text-text-muted": local.controller.open(),
|
|
||||||
}}
|
}}
|
||||||
onClick={local.onClick ?? (() => local.controller.setOpen(true))}
|
onClick={local.onClick ?? (() => local.controller.setOpen(true))}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
@@ -504,9 +454,6 @@ function ProjectItem(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectActionClass =
|
|
||||||
"h-7 gap-2 rounded-sm px-3 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base [font-family:var(--v2-font-family-sans)] data-[highlighted]:!bg-v2-overlay-simple-overlay-hover"
|
|
||||||
|
|
||||||
function ProjectAction(props: {
|
function ProjectAction(props: {
|
||||||
server?: string
|
server?: string
|
||||||
controller: PromptProjectController
|
controller: PromptProjectController
|
||||||
@@ -541,11 +488,3 @@ function ProjectAction(props: {
|
|||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ServerAction(props: { server: { key: string; name: string }; onSelect: (server: string) => void }) {
|
|
||||||
return (
|
|
||||||
<DropdownMenu.Item class={projectActionClass} onSelect={() => props.onSelect(props.server.key)}>
|
|
||||||
<DropdownMenu.ItemLabel class="min-w-0 flex-1 truncate leading-5">{props.server.name}</DropdownMenu.ItemLabel>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
import { For, Show } from "solid-js"
|
|
||||||
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
|
||||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
|
||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
|
||||||
import { useLanguage } from "@/context/language"
|
|
||||||
|
|
||||||
export function PromptWorkspaceSelector(props: {
|
|
||||||
value: string
|
|
||||||
projectRoot: string
|
|
||||||
workspaces: string[]
|
|
||||||
branch?: string
|
|
||||||
onChange: (value: string) => void
|
|
||||||
onDone: () => void
|
|
||||||
}) {
|
|
||||||
const language = useLanguage()
|
|
||||||
let pending: string | undefined
|
|
||||||
const selected = () => (props.value === props.projectRoot ? "main" : props.value)
|
|
||||||
const icon = () => {
|
|
||||||
if (selected() === "main") return "monitor"
|
|
||||||
if (selected() === "create") return "workspace-new"
|
|
||||||
return "workspace"
|
|
||||||
}
|
|
||||||
const select = (value: string) => {
|
|
||||||
pending = value
|
|
||||||
}
|
|
||||||
const onOpenChange = (open: boolean) => {
|
|
||||||
if (open) return
|
|
||||||
const value = pending
|
|
||||||
pending = undefined
|
|
||||||
if (value) props.onChange(value)
|
|
||||||
props.onDone()
|
|
||||||
}
|
|
||||||
const label = () => {
|
|
||||||
if (selected() === "main") return language.t("session.new.workspace.triggerLocal")
|
|
||||||
if (props.value === "create") return language.t("workspace.new")
|
|
||||||
return getFilename(props.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<span class="hidden select-none opacity-50 sm:inline mx-1">/</span>
|
|
||||||
<MenuV2 placement="bottom" gutter={4} onOpenChange={onOpenChange}>
|
|
||||||
<MenuV2.Trigger class="flex h-7 min-w-0 max-w-[203px] items-center gap-1.5 rounded-sm px-1.5 hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none data-[expanded]:bg-v2-overlay-simple-overlay-pressed data-[expanded]:text-v2-text-text-muted">
|
|
||||||
<IconV2 name={icon()} class="shrink-0 text-v2-icon-icon-muted" />
|
|
||||||
<span class="min-w-0 truncate">{label()}</span>
|
|
||||||
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
|
||||||
</MenuV2.Trigger>
|
|
||||||
<MenuV2.Portal>
|
|
||||||
<MenuV2.Content class="w-[180px]">
|
|
||||||
<MenuV2.Group>
|
|
||||||
<MenuV2.GroupLabel>{language.t("session.new.workspace.runIn")}</MenuV2.GroupLabel>
|
|
||||||
<MenuV2.Item onSelect={() => select("main")}>
|
|
||||||
<IconV2 name="monitor" />
|
|
||||||
<span class="min-w-0 flex-1 truncate">{language.t("session.new.workspace.local")}</span>
|
|
||||||
<Show when={selected() === "main"}>
|
|
||||||
<Icon name="check" size="small" class="shrink-0" />
|
|
||||||
</Show>
|
|
||||||
</MenuV2.Item>
|
|
||||||
<MenuV2.Item onSelect={() => select("create")}>
|
|
||||||
<IconV2 name="workspace-new" />
|
|
||||||
<span class="min-w-0 flex-1 truncate">{language.t("workspace.new")}</span>
|
|
||||||
<Show when={selected() === "create"}>
|
|
||||||
<Icon name="check" size="small" class="shrink-0" />
|
|
||||||
</Show>
|
|
||||||
</MenuV2.Item>
|
|
||||||
</MenuV2.Group>
|
|
||||||
<Show when={props.workspaces.length > 0}>
|
|
||||||
<MenuV2.Separator />
|
|
||||||
<MenuV2.Sub gutter={0} overlap overflowPadding={8}>
|
|
||||||
<MenuV2.SubTrigger>
|
|
||||||
<IconV2 name="workspace" />
|
|
||||||
{language.t("session.new.workspace.existing")}
|
|
||||||
</MenuV2.SubTrigger>
|
|
||||||
<MenuV2.Portal>
|
|
||||||
<MenuV2.SubContent class="max-w-[200px]">
|
|
||||||
<For each={props.workspaces}>
|
|
||||||
{(workspace) => (
|
|
||||||
<MenuV2.Item onSelect={() => select(workspace)}>
|
|
||||||
<IconV2 name="workspace-isolated" />
|
|
||||||
<span class="min-w-0 flex-1 truncate">{getFilename(workspace)}</span>
|
|
||||||
<Show when={selected() === workspace}>
|
|
||||||
<Icon name="check" size="small" class="shrink-0" />
|
|
||||||
</Show>
|
|
||||||
</MenuV2.Item>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</MenuV2.SubContent>
|
|
||||||
</MenuV2.Portal>
|
|
||||||
</MenuV2.Sub>
|
|
||||||
</Show>
|
|
||||||
</MenuV2.Content>
|
|
||||||
</MenuV2.Portal>
|
|
||||||
</MenuV2>
|
|
||||||
<Show when={props.branch}>
|
|
||||||
{(branch) => (
|
|
||||||
<>
|
|
||||||
<span class="hidden select-none opacity-50 sm:inline mx-1">/</span>
|
|
||||||
<div class="flex h-7 min-w-0 max-w-[220px] items-center gap-1.5 px-2 text-[13px] font-[440] leading-5 tracking-[-0.04px]">
|
|
||||||
<Icon name="branch" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
|
||||||
<span class="min-w-0 truncate">{branch()}</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
import { Match, Show, Switch, createMemo } from "solid-js"
|
import { Match, Show, Switch, createMemo } from "solid-js"
|
||||||
import { Tooltip, type TooltipProps } from "@opencode-ai/ui/tooltip"
|
import { Tooltip, type TooltipProps } from "@opencode-ai/ui/tooltip"
|
||||||
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
|
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
|
||||||
import { ProgressCircleV2 } from "@opencode-ai/ui/v2/progress-circle-v2"
|
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
|
||||||
|
|
||||||
import { useFile } from "@/context/file"
|
import { useFile } from "@/context/file"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
@@ -11,13 +9,12 @@ import { useSync } from "@/context/sync"
|
|||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useProviders } from "@/hooks/use-providers"
|
import { useProviders } from "@/hooks/use-providers"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { getSessionContext, getSessionTokenTotal } from "@/components/session/session-context-metrics"
|
import { getSessionContextMetrics } from "@/components/session/session-context-metrics"
|
||||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
import { createSessionTabs } from "@/pages/session/helpers"
|
import { createSessionTabs } from "@/pages/session/helpers"
|
||||||
|
|
||||||
interface SessionContextUsageProps {
|
interface SessionContextUsageProps {
|
||||||
variant?: "button" | "indicator"
|
variant?: "button" | "indicator"
|
||||||
buttonAppearance?: "default" | "v2"
|
|
||||||
placement?: TooltipProps["placement"]
|
placement?: TooltipProps["placement"]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +23,7 @@ function openSessionContext(args: {
|
|||||||
layout: ReturnType<typeof useLayout>
|
layout: ReturnType<typeof useLayout>
|
||||||
tabs: ReturnType<ReturnType<typeof useLayout>["tabs"]>
|
tabs: ReturnType<ReturnType<typeof useLayout>["tabs"]>
|
||||||
}) {
|
}) {
|
||||||
args.view.reviewPanel.open(args.view.reviewPanel.opened() ? "other" : "context-button")
|
if (!args.view.reviewPanel.opened()) args.view.reviewPanel.open()
|
||||||
if (args.layout.fileTree.opened() && args.layout.fileTree.tab() !== "all") args.layout.fileTree.setTab("all")
|
if (args.layout.fileTree.opened() && args.layout.fileTree.tab() !== "all") args.layout.fileTree.setTab("all")
|
||||||
void args.tabs.open("context")
|
void args.tabs.open("context")
|
||||||
args.tabs.setActive("context")
|
args.tabs.setActive("context")
|
||||||
@@ -42,14 +39,12 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
|||||||
const { params, tabs, view } = useSessionLayout()
|
const { params, tabs, view } = useSessionLayout()
|
||||||
|
|
||||||
const variant = createMemo(() => props.variant ?? "button")
|
const variant = createMemo(() => props.variant ?? "button")
|
||||||
const buttonAppearance = createMemo(() => props.buttonAppearance ?? "default")
|
|
||||||
const tabState = createSessionTabs({
|
const tabState = createSessionTabs({
|
||||||
tabs,
|
tabs,
|
||||||
pathFromTab: file.pathFromTab,
|
pathFromTab: file.pathFromTab,
|
||||||
normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab),
|
normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab),
|
||||||
})
|
})
|
||||||
const messages = createMemo(() => (params.id ? (sync().data.message[params.id] ?? []) : []))
|
const messages = createMemo(() => (params.id ? (sync().data.message[params.id] ?? []) : []))
|
||||||
const info = createMemo(() => (params.id ? sync().session.get(params.id) : undefined))
|
|
||||||
|
|
||||||
const usd = createMemo(
|
const usd = createMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -59,30 +54,21 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const context = createMemo(() => getSessionContext(messages(), [...providers.all().values()]))
|
const metrics = createMemo(() => getSessionContextMetrics(messages(), [...providers.all().values()]))
|
||||||
const tokens = createMemo(() => info()?.tokens)
|
const context = createMemo(() => metrics().context)
|
||||||
const cost = createMemo(() => {
|
const cost = createMemo(() => {
|
||||||
return usd().format(info()?.cost ?? 0)
|
return usd().format(metrics().totalCost)
|
||||||
})
|
})
|
||||||
const contextVisible = createMemo(() => view().reviewPanel.opened() && tabState.activeTab() === "context")
|
|
||||||
const hasOtherTabs = createMemo(() =>
|
|
||||||
tabs()
|
|
||||||
.all()
|
|
||||||
.some((tab) => tab !== "context" && tab !== "review"),
|
|
||||||
)
|
|
||||||
|
|
||||||
const openContext = () => {
|
const openContext = () => {
|
||||||
if (!params.id) return
|
if (!params.id) return
|
||||||
|
|
||||||
const sessionView = view()
|
if (tabState.activeTab() === "context") {
|
||||||
if (contextVisible()) {
|
|
||||||
tabs().close("context")
|
tabs().close("context")
|
||||||
if (sessionView.reviewPanel.source() === "context-button" && !hasOtherTabs()) sessionView.reviewPanel.close()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
openSessionContext({
|
openSessionContext({
|
||||||
view: sessionView,
|
view: view(),
|
||||||
layout,
|
layout,
|
||||||
tabs: tabs(),
|
tabs: tabs(),
|
||||||
})
|
})
|
||||||
@@ -90,46 +76,24 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
|||||||
|
|
||||||
const circle = () => (
|
const circle = () => (
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<ProgressCircle
|
<ProgressCircle size={16} strokeWidth={2} percentage={context()?.usage ?? 0} />
|
||||||
size={16}
|
|
||||||
strokeWidth={2}
|
|
||||||
percentage={context()?.usage ?? 0}
|
|
||||||
style={
|
|
||||||
variant() === "indicator"
|
|
||||||
? {
|
|
||||||
"--progress-circle-background": "var(--v2-background-bg-layer-04, var(--border-weak-base))",
|
|
||||||
"--progress-circle-background-overlay": "var(--v2-overlay-simple-overlay-pressed, transparent)",
|
|
||||||
"--progress-circle-progress": "var(--v2-icon-icon-base, var(--icon-base))",
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
const circleV2 = () => (
|
|
||||||
<div class="flex items-center justify-center">
|
|
||||||
<ProgressCircleV2 percentage={context()?.usage ?? 0} />
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
const tooltipValue = () => (
|
const tooltipValue = () => (
|
||||||
<div>
|
<div>
|
||||||
<Show when={tokens()}>
|
|
||||||
{(value) => (
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="text-text-invert-strong">
|
|
||||||
{getSessionTokenTotal(value())?.toLocaleString(language.intl())}
|
|
||||||
</span>
|
|
||||||
<span class="text-text-invert-base">{language.t("context.usage.tokens")}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
<Show when={context()}>
|
<Show when={context()}>
|
||||||
{(ctx) => (
|
{(ctx) => (
|
||||||
<div class="flex items-center gap-2">
|
<>
|
||||||
<span class="text-text-invert-strong">{ctx().usage ?? 0}%</span>
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-text-invert-base">{language.t("context.usage.usage")}</span>
|
<span class="text-text-invert-strong">{ctx().total.toLocaleString(language.intl())}</span>
|
||||||
</div>
|
<span class="text-text-invert-base">{language.t("context.usage.tokens")}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-text-invert-strong">{ctx().usage ?? 0}%</span>
|
||||||
|
<span class="text-text-invert-base">{language.t("context.usage.usage")}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -141,22 +105,10 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={params.id}>
|
<Show when={params.id}>
|
||||||
<Switch>
|
<Tooltip value={tooltipValue()} placement={props.placement ?? "top"}>
|
||||||
<Match when={variant() === "indicator"}>{circle()}</Match>
|
<Switch>
|
||||||
<Match when={buttonAppearance() === "v2"}>
|
<Match when={variant() === "indicator"}>{circle()}</Match>
|
||||||
<Tooltip value={tooltipValue()} placement={props.placement ?? "top"}>
|
<Match when={true}>
|
||||||
<IconButtonV2
|
|
||||||
type="button"
|
|
||||||
variant="ghost-muted"
|
|
||||||
size="large"
|
|
||||||
icon={circleV2()}
|
|
||||||
onClick={openContext}
|
|
||||||
aria-label={language.t("context.usage.view")}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</Match>
|
|
||||||
<Match when={true}>
|
|
||||||
<Tooltip value={tooltipValue()} placement={props.placement ?? "top"}>
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -166,9 +118,9 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
|||||||
>
|
>
|
||||||
{circle()}
|
{circle()}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Match>
|
||||||
</Match>
|
</Switch>
|
||||||
</Switch>
|
</Tooltip>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import type { Message } from "@opencode-ai/sdk/v2/client"
|
import type { Message } from "@opencode-ai/sdk/v2/client"
|
||||||
import { getSessionContext, getSessionTokenTotal } from "./session-context-metrics"
|
import { getSessionContextMetrics } from "./session-context-metrics"
|
||||||
|
|
||||||
const assistant = (
|
const assistant = (
|
||||||
id: string,
|
id: string,
|
||||||
@@ -37,8 +37,8 @@ const user = (id: string) => {
|
|||||||
} as unknown as Message
|
} as unknown as Message
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("getSessionContext", () => {
|
describe("getSessionContextMetrics", () => {
|
||||||
test("computes usage from latest assistant with tokens", () => {
|
test("computes totals and usage from latest assistant with tokens", () => {
|
||||||
const messages = [
|
const messages = [
|
||||||
user("u1"),
|
user("u1"),
|
||||||
assistant("a1", { input: 0, output: 0, reasoning: 0, read: 0, write: 0 }, 0.5),
|
assistant("a1", { input: 0, output: 0, reasoning: 0, read: 0, write: 0 }, 0.5),
|
||||||
@@ -57,52 +57,45 @@ describe("getSessionContext", () => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const ctx = getSessionContext(messages, providers)
|
const metrics = getSessionContextMetrics(messages, providers)
|
||||||
|
|
||||||
expect(ctx?.message.id).toBe("a2")
|
expect(metrics.totalCost).toBe(1.75)
|
||||||
expect(ctx?.usage).toBe(50)
|
expect(metrics.context?.message.id).toBe("a2")
|
||||||
expect(ctx?.providerLabel).toBe("OpenAI")
|
expect(metrics.context?.total).toBe(500)
|
||||||
expect(ctx?.modelLabel).toBe("GPT-4.1")
|
expect(metrics.context?.usage).toBe(50)
|
||||||
|
expect(metrics.context?.providerLabel).toBe("OpenAI")
|
||||||
|
expect(metrics.context?.modelLabel).toBe("GPT-4.1")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("preserves fallback labels and null usage when model metadata is missing", () => {
|
test("preserves fallback labels and null usage when model metadata is missing", () => {
|
||||||
const messages = [assistant("a1", { input: 40, output: 10, reasoning: 0, read: 0, write: 0 }, 0.1, "p-1", "m-1")]
|
const messages = [assistant("a1", { input: 40, output: 10, reasoning: 0, read: 0, write: 0 }, 0.1, "p-1", "m-1")]
|
||||||
const providers = [{ id: "p-1", models: {} }]
|
const providers = [{ id: "p-1", models: {} }]
|
||||||
|
|
||||||
const ctx = getSessionContext(messages, providers)
|
const metrics = getSessionContextMetrics(messages, providers)
|
||||||
|
|
||||||
expect(ctx?.providerLabel).toBe("p-1")
|
expect(metrics.context?.providerLabel).toBe("p-1")
|
||||||
expect(ctx?.modelLabel).toBe("m-1")
|
expect(metrics.context?.modelLabel).toBe("m-1")
|
||||||
expect(ctx?.limit).toBeUndefined()
|
expect(metrics.context?.limit).toBeUndefined()
|
||||||
expect(ctx?.usage).toBeNull()
|
expect(metrics.context?.usage).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("recomputes when message array is mutated in place", () => {
|
test("recomputes when message array is mutated in place", () => {
|
||||||
const messages = [assistant("a1", { input: 10, output: 10, reasoning: 10, read: 10, write: 10 }, 0.25)]
|
const messages = [assistant("a1", { input: 10, output: 10, reasoning: 10, read: 10, write: 10 }, 0.25)]
|
||||||
const providers = [{ id: "openai", models: {} }]
|
const providers = [{ id: "openai", models: {} }]
|
||||||
|
|
||||||
const one = getSessionContext(messages, providers)
|
const one = getSessionContextMetrics(messages, providers)
|
||||||
messages.push(assistant("a2", { input: 100, output: 20, reasoning: 0, read: 0, write: 0 }, 0.75))
|
messages.push(assistant("a2", { input: 100, output: 20, reasoning: 0, read: 0, write: 0 }, 0.75))
|
||||||
const two = getSessionContext(messages, providers)
|
const two = getSessionContextMetrics(messages, providers)
|
||||||
|
|
||||||
expect(one?.message.id).toBe("a1")
|
expect(one.context?.message.id).toBe("a1")
|
||||||
expect(two?.message.id).toBe("a2")
|
expect(two.context?.message.id).toBe("a2")
|
||||||
|
expect(two.totalCost).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("returns undefined when inputs are undefined", () => {
|
test("returns empty metrics when inputs are undefined", () => {
|
||||||
const ctx = getSessionContext(undefined, undefined)
|
const metrics = getSessionContextMetrics(undefined, undefined)
|
||||||
|
|
||||||
expect(ctx).toBeUndefined()
|
expect(metrics.totalCost).toBe(0)
|
||||||
})
|
expect(metrics.context).toBeUndefined()
|
||||||
|
|
||||||
test("computes stored session token totals", () => {
|
|
||||||
expect(
|
|
||||||
getSessionTokenTotal({
|
|
||||||
input: 10,
|
|
||||||
output: 20,
|
|
||||||
reasoning: 30,
|
|
||||||
cache: { read: 40, write: 50 },
|
|
||||||
}),
|
|
||||||
).toBe(150)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { AssistantMessage, Message, Session } from "@opencode-ai/sdk/v2/client"
|
import type { AssistantMessage, Message } from "@opencode-ai/sdk/v2/client"
|
||||||
|
|
||||||
type Provider = {
|
type Provider = {
|
||||||
id: string
|
id: string
|
||||||
@@ -21,9 +21,19 @@ type Context = {
|
|||||||
modelLabel: string
|
modelLabel: string
|
||||||
limit: number | undefined
|
limit: number | undefined
|
||||||
input: number
|
input: number
|
||||||
|
output: number
|
||||||
|
reasoning: number
|
||||||
|
cacheRead: number
|
||||||
|
cacheWrite: number
|
||||||
|
total: number
|
||||||
usage: number | null
|
usage: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Metrics = {
|
||||||
|
totalCost: number
|
||||||
|
context: Context | undefined
|
||||||
|
}
|
||||||
|
|
||||||
const tokenTotal = (msg: AssistantMessage) => {
|
const tokenTotal = (msg: AssistantMessage) => {
|
||||||
return msg.tokens.input + msg.tokens.output + msg.tokens.reasoning + msg.tokens.cache.read + msg.tokens.cache.write
|
return msg.tokens.input + msg.tokens.output + msg.tokens.reasoning + msg.tokens.cache.read + msg.tokens.cache.write
|
||||||
}
|
}
|
||||||
@@ -37,9 +47,10 @@ const lastAssistantWithTokens = (messages: Message[]) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const build = (messages: Message[] = [], providers: Provider[] = []): Context | undefined => {
|
const build = (messages: Message[] = [], providers: Provider[] = []): Metrics => {
|
||||||
|
const totalCost = messages.reduce((sum, msg) => sum + (msg.role === "assistant" ? msg.cost : 0), 0)
|
||||||
const message = lastAssistantWithTokens(messages)
|
const message = lastAssistantWithTokens(messages)
|
||||||
if (!message) return undefined
|
if (!message) return { totalCost, context: undefined }
|
||||||
|
|
||||||
const provider = providers.find((item) => item.id === message.providerID)
|
const provider = providers.find((item) => item.id === message.providerID)
|
||||||
const model = provider?.models[message.modelID]
|
const model = provider?.models[message.modelID]
|
||||||
@@ -47,22 +58,25 @@ const build = (messages: Message[] = [], providers: Provider[] = []): Context |
|
|||||||
const total = tokenTotal(message)
|
const total = tokenTotal(message)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message,
|
totalCost,
|
||||||
provider,
|
context: {
|
||||||
model,
|
message,
|
||||||
providerLabel: provider?.name ?? message.providerID,
|
provider,
|
||||||
modelLabel: model?.name ?? message.modelID,
|
model,
|
||||||
limit,
|
providerLabel: provider?.name ?? message.providerID,
|
||||||
input: message.tokens.input,
|
modelLabel: model?.name ?? message.modelID,
|
||||||
usage: limit ? Math.round((total / limit) * 100) : null,
|
limit,
|
||||||
|
input: message.tokens.input,
|
||||||
|
output: message.tokens.output,
|
||||||
|
reasoning: message.tokens.reasoning,
|
||||||
|
cacheRead: message.tokens.cache.read,
|
||||||
|
cacheWrite: message.tokens.cache.write,
|
||||||
|
total,
|
||||||
|
usage: limit ? Math.round((total / limit) * 100) : null,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSessionContext(messages: Message[] = [], providers: Provider[] = []) {
|
export function getSessionContextMetrics(messages: Message[] = [], providers: Provider[] = []) {
|
||||||
return build(messages, providers)
|
return build(messages, providers)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSessionTokenTotal(tokens: Session["tokens"] | undefined) {
|
|
||||||
if (!tokens) return undefined
|
|
||||||
return tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { useLanguage } from "@/context/language"
|
|||||||
import { useProviders } from "@/hooks/use-providers"
|
import { useProviders } from "@/hooks/use-providers"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
import { getSessionContext, getSessionTokenTotal } from "./session-context-metrics"
|
import { getSessionContextMetrics } from "./session-context-metrics"
|
||||||
import { estimateSessionContextBreakdown, type SessionContextBreakdownKey } from "./session-context-breakdown"
|
import { estimateSessionContextBreakdown, type SessionContextBreakdownKey } from "./session-context-breakdown"
|
||||||
import { createSessionContextFormatter } from "./session-context-format"
|
import { createSessionContextFormatter } from "./session-context-format"
|
||||||
|
|
||||||
@@ -134,12 +134,12 @@ export function SessionContextTab() {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const ctx = createMemo(() => getSessionContext(messages(), [...providers.all().values()]))
|
const metrics = createMemo(() => getSessionContextMetrics(messages(), [...providers.all().values()]))
|
||||||
const tokens = createMemo(() => info()?.tokens)
|
const ctx = createMemo(() => metrics().context)
|
||||||
const formatter = createMemo(() => createSessionContextFormatter(language.intl()))
|
const formatter = createMemo(() => createSessionContextFormatter(language.intl()))
|
||||||
|
|
||||||
const cost = createMemo(() => {
|
const cost = createMemo(() => {
|
||||||
return usd().format(info()?.cost ?? 0)
|
return usd().format(metrics().totalCost)
|
||||||
})
|
})
|
||||||
|
|
||||||
const counts = createMemo(() => {
|
const counts = createMemo(() => {
|
||||||
@@ -204,14 +204,14 @@ export function SessionContextTab() {
|
|||||||
{ label: "context.stats.provider", value: providerLabel },
|
{ label: "context.stats.provider", value: providerLabel },
|
||||||
{ label: "context.stats.model", value: modelLabel },
|
{ label: "context.stats.model", value: modelLabel },
|
||||||
{ label: "context.stats.limit", value: () => formatter().number(ctx()?.limit) },
|
{ label: "context.stats.limit", value: () => formatter().number(ctx()?.limit) },
|
||||||
{ label: "context.stats.totalTokens", value: () => formatter().number(getSessionTokenTotal(tokens())) },
|
{ label: "context.stats.totalTokens", value: () => formatter().number(ctx()?.total) },
|
||||||
{ label: "context.stats.usage", value: () => formatter().percent(ctx()?.usage) },
|
{ label: "context.stats.usage", value: () => formatter().percent(ctx()?.usage) },
|
||||||
{ label: "context.stats.inputTokens", value: () => formatter().number(tokens()?.input) },
|
{ label: "context.stats.inputTokens", value: () => formatter().number(ctx()?.input) },
|
||||||
{ label: "context.stats.outputTokens", value: () => formatter().number(tokens()?.output) },
|
{ label: "context.stats.outputTokens", value: () => formatter().number(ctx()?.output) },
|
||||||
{ label: "context.stats.reasoningTokens", value: () => formatter().number(tokens()?.reasoning) },
|
{ label: "context.stats.reasoningTokens", value: () => formatter().number(ctx()?.reasoning) },
|
||||||
{
|
{
|
||||||
label: "context.stats.cacheTokens",
|
label: "context.stats.cacheTokens",
|
||||||
value: () => `${formatter().number(tokens()?.cache.read)} / ${formatter().number(tokens()?.cache.write)}`,
|
value: () => `${formatter().number(ctx()?.cacheRead)} / ${formatter().number(ctx()?.cacheWrite)}`,
|
||||||
},
|
},
|
||||||
{ label: "context.stats.userMessages", value: () => counts().user.toLocaleString(language.intl()) },
|
{ label: "context.stats.userMessages", value: () => counts().user.toLocaleString(language.intl()) },
|
||||||
{ label: "context.stats.assistantMessages", value: () => counts().assistant.toLocaleString(language.intl()) },
|
{ label: "context.stats.assistantMessages", value: () => counts().assistant.toLocaleString(language.intl()) },
|
||||||
|
|||||||
@@ -541,7 +541,6 @@ function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) {
|
|||||||
</Show>
|
</Show>
|
||||||
<Show when={props.state.reviewVisible}>
|
<Show when={props.state.reviewVisible}>
|
||||||
<TooltipV2
|
<TooltipV2
|
||||||
class="shrink-0"
|
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
value={
|
value={
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||||
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/v2/dialog-v2"
|
import { Dialog, DialogFooter } from "@opencode-ai/ui/v2/dialog-v2"
|
||||||
import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2"
|
|
||||||
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { type Component, Show, createEffect, createSignal, onCleanup, onMount } from "solid-js"
|
import { type Component, Show, createEffect, createSignal, onCleanup, onMount } from "solid-js"
|
||||||
@@ -53,12 +52,8 @@ export const DialogServerV2: Component<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog fit class="settings-v2-server-dialog">
|
<Dialog title={title()} fit class="settings-v2-server-dialog">
|
||||||
<DialogHeader hideClose={true}>
|
<div class="flex w-full min-w-0 flex-1 flex-col px-4">
|
||||||
<DialogTitle>{title()}</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<DividerV2 />
|
|
||||||
<DialogBody class="flex w-full min-w-0 flex-1 flex-col px-4 pt-4 pb-2">
|
|
||||||
<div class="flex w-full min-w-0 flex-col gap-6">
|
<div class="flex w-full min-w-0 flex-col gap-6">
|
||||||
<div class="flex w-full min-w-0 flex-col gap-2">
|
<div class="flex w-full min-w-0 flex-col gap-2">
|
||||||
<label class="settings-v2-server-dialog-label">{language.t("dialog.server.add.url")}</label>
|
<label class="settings-v2-server-dialog-label">{language.t("dialog.server.add.url")}</label>
|
||||||
@@ -120,7 +115,7 @@ export const DialogServerV2: Component<{
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogBody>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<ButtonV2 variant="neutral" disabled={controller.formBusy()} onClick={() => dialog.close()}>
|
<ButtonV2 variant="neutral" disabled={controller.formBusy()} onClick={() => dialog.close()}>
|
||||||
{language.t("common.cancel")}
|
{language.t("common.cancel")}
|
||||||
|
|||||||
@@ -633,7 +633,7 @@
|
|||||||
|
|
||||||
[data-component="dialog-v2"].settings-v2-server-dialog [data-slot="dialog-header"] {
|
[data-component="dialog-v2"].settings-v2-server-dialog [data-slot="dialog-header"] {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 24px 24px 16px;
|
padding: 24px 24px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="dialog-v2"].settings-v2-server-dialog [data-slot="dialog-body"] {
|
[data-component="dialog-v2"].settings-v2-server-dialog [data-slot="dialog-body"] {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-titlebar-tab-slot]:not(:first-child):not([data-active="true"])::before {
|
[data-titlebar-tab-slot]:not(:first-child)::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8px;
|
top: 8px;
|
||||||
@@ -32,10 +32,6 @@
|
|||||||
background: var(--v2-background-bg-layer-02);
|
background: var(--v2-background-bg-layer-02);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-titlebar-tab-slot][data-active="true"] + [data-titlebar-tab-slot]::before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-titlebar-tab] [data-slot="tab-close"]::before {
|
[data-titlebar-tab] [data-slot="tab-close"]::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -13,9 +13,6 @@ import type { Session } from "@opencode-ai/sdk/v2"
|
|||||||
import { canOpenTabRename, forwardTabRef } from "./titlebar-tab-gesture"
|
import { canOpenTabRename, forwardTabRef } from "./titlebar-tab-gesture"
|
||||||
import "./titlebar-tab-nav.css"
|
import "./titlebar-tab-nav.css"
|
||||||
|
|
||||||
// MouseEvent.button uses 1 for the middle/wheel button.
|
|
||||||
const MIDDLE_MOUSE_BUTTON = 1
|
|
||||||
|
|
||||||
export function TabNavItem(props: {
|
export function TabNavItem(props: {
|
||||||
ref?: Ref<HTMLDivElement>
|
ref?: Ref<HTMLDivElement>
|
||||||
href: string
|
href: string
|
||||||
@@ -187,12 +184,7 @@ export function TabNavItem(props: {
|
|||||||
data-dragging={props.dragging}
|
data-dragging={props.dragging}
|
||||||
data-pressed={props.pressed}
|
data-pressed={props.pressed}
|
||||||
onMouseDown={(event) => {
|
onMouseDown={(event) => {
|
||||||
if (event.button !== MIDDLE_MOUSE_BUTTON) return
|
if (event.button !== 1) return
|
||||||
event.preventDefault()
|
|
||||||
event.stopPropagation()
|
|
||||||
}}
|
|
||||||
onAuxClick={(event) => {
|
|
||||||
if (event.button !== MIDDLE_MOUSE_BUTTON) return
|
|
||||||
closeTab(event)
|
closeTab(event)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -310,12 +302,7 @@ export function DraftTabItem(props: {
|
|||||||
class="group relative flex h-7 w-full min-w-0 flex-row items-center gap-1.5 overflow-hidden rounded-[6px] bg-[var(--tab-bg)] px-1.5 [container-type:inline-size] whitespace-nowrap [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] has-[>a:focus-visible]:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[dragging='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[pressed='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[editing='true']:[--tab-bg:var(--v2-background-bg-layer-02)]"
|
class="group relative flex h-7 w-full min-w-0 flex-row items-center gap-1.5 overflow-hidden rounded-[6px] bg-[var(--tab-bg)] px-1.5 [container-type:inline-size] whitespace-nowrap [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] has-[>a:focus-visible]:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[dragging='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[pressed='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[editing='true']:[--tab-bg:var(--v2-background-bg-layer-02)]"
|
||||||
classList={{ invisible: props.hidden }}
|
classList={{ invisible: props.hidden }}
|
||||||
onMouseDown={(event) => {
|
onMouseDown={(event) => {
|
||||||
if (event.button !== MIDDLE_MOUSE_BUTTON) return
|
if (event.button !== 1) return
|
||||||
event.preventDefault()
|
|
||||||
event.stopPropagation()
|
|
||||||
}}
|
|
||||||
onAuxClick={(event) => {
|
|
||||||
if (event.button !== MIDDLE_MOUSE_BUTTON) return
|
|
||||||
closeTab(event)
|
closeTab(event)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ function SessionTabSlot(props: {
|
|||||||
ref={sortable.ref}
|
ref={sortable.ref}
|
||||||
data-titlebar-tab-slot
|
data-titlebar-tab-slot
|
||||||
data-tab-key={props.id}
|
data-tab-key={props.id}
|
||||||
data-active={props.active()}
|
|
||||||
class="relative flex w-56 min-w-7 max-w-56 flex-shrink"
|
class="relative flex w-56 min-w-7 max-w-56 flex-shrink"
|
||||||
classList={{ hidden: !session() }}
|
classList={{ hidden: !session() }}
|
||||||
>
|
>
|
||||||
@@ -141,7 +140,6 @@ function DraftTabSlot(props: {
|
|||||||
ref={sortable.ref}
|
ref={sortable.ref}
|
||||||
data-titlebar-tab-slot
|
data-titlebar-tab-slot
|
||||||
data-tab-key={props.id}
|
data-tab-key={props.id}
|
||||||
data-active={props.active()}
|
|
||||||
class="relative flex w-56 min-w-7 max-w-56 flex-shrink"
|
class="relative flex w-56 min-w-7 max-w-56 flex-shrink"
|
||||||
>
|
>
|
||||||
<DraftTabItem
|
<DraftTabItem
|
||||||
|
|||||||
@@ -99,8 +99,6 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
|
|
||||||
const path = () => `${location.pathname}${location.search}${location.hash}`
|
const path = () => `${location.pathname}${location.search}${location.hash}`
|
||||||
const creating = createMemo(() => {
|
const creating = createMemo(() => {
|
||||||
const route = layout.route()
|
|
||||||
if (route.type === "draft" || route.type === "dir-new-sesssion") return true
|
|
||||||
if (!params.dir) return false
|
if (!params.dir) return false
|
||||||
if (params.id) return false
|
if (params.id) return false
|
||||||
const parts = location.pathname.replace(/\/+$/, "").split("/")
|
const parts = location.pathname.replace(/\/+$/, "").split("/")
|
||||||
@@ -229,8 +227,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
"min-height": minHeight(),
|
"min-height": minHeight(),
|
||||||
// Keep native macOS traffic lights clear even when the desktop window is narrow.
|
"padding-left": mac() && !mobile() ? `${84 / zoom()}px` : 0,
|
||||||
"padding-left": mac() ? `${84 / zoom()}px` : 0,
|
|
||||||
width: electronWindows() ? `env(titlebar-area-width, calc(100vw - ${windowsControlsWidth()}))` : undefined,
|
width: electronWindows() ? `env(titlebar-area-width, calc(100vw - ${windowsControlsWidth()}))` : undefined,
|
||||||
"max-width": electronWindows()
|
"max-width": electronWindows()
|
||||||
? `env(titlebar-area-width, calc(100vw - ${windowsControlsWidth()}))`
|
? `env(titlebar-area-width, calc(100vw - ${windowsControlsWidth()}))`
|
||||||
@@ -469,7 +466,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
}}
|
}}
|
||||||
onReorder={(keys) => tabsStoreActions.reorder(keys)}
|
onReorder={(keys) => tabsStoreActions.reorder(keys)}
|
||||||
/>
|
/>
|
||||||
<Show when={!creating()}>
|
<Show when={!(creating() && params.dir)}>
|
||||||
<TooltipV2
|
<TooltipV2
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
value={
|
value={
|
||||||
@@ -572,7 +569,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
placement="bottom"
|
placement="bottom"
|
||||||
title={language.t("command.session.new")}
|
title={language.t("command.session.new")}
|
||||||
keybind={command.keybind("session.new")}
|
keybind={command.keybind("session.new")}
|
||||||
openDelay={800}
|
openDelay={2000}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -602,7 +599,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
>
|
>
|
||||||
<Show when={hasProjects() && nav()}>
|
<Show when={hasProjects() && nav()}>
|
||||||
<div class="flex items-center gap-0 transition-transform">
|
<div class="flex items-center gap-0 transition-transform">
|
||||||
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={800}>
|
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
icon="chevron-left"
|
icon="chevron-left"
|
||||||
@@ -612,7 +609,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
aria-label={language.t("common.goBack")}
|
aria-label={language.t("common.goBack")}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={800}>
|
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
icon="chevron-right"
|
icon="chevron-right"
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ describe("bootstrapDirectory", () => {
|
|||||||
status: "loading",
|
status: "loading",
|
||||||
agent: [],
|
agent: [],
|
||||||
command: [],
|
command: [],
|
||||||
reference: [],
|
|
||||||
project: "",
|
project: "",
|
||||||
projectMeta: undefined,
|
projectMeta: undefined,
|
||||||
icon: undefined,
|
icon: undefined,
|
||||||
@@ -36,7 +35,6 @@ describe("bootstrapDirectory", () => {
|
|||||||
question: {},
|
question: {},
|
||||||
mcp_ready: true,
|
mcp_ready: true,
|
||||||
mcp: {},
|
mcp: {},
|
||||||
mcp_resource: {},
|
|
||||||
lsp_ready: true,
|
lsp_ready: true,
|
||||||
lsp: [],
|
lsp: [],
|
||||||
vcs: undefined,
|
vcs: undefined,
|
||||||
@@ -69,7 +67,6 @@ describe("bootstrapDirectory", () => {
|
|||||||
},
|
},
|
||||||
permission: { list: async () => ({ data: [] }) },
|
permission: { list: async () => ({ data: [] }) },
|
||||||
question: { list: async () => ({ data: [] }) },
|
question: { list: async () => ({ data: [] }) },
|
||||||
v2: { reference: { list: async () => ({ data: { data: [] } }) } },
|
|
||||||
mcp: {
|
mcp: {
|
||||||
status: async () => {
|
status: async () => {
|
||||||
mcpReads.push("status")
|
mcpReads.push("status")
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import type {
|
|||||||
Project,
|
Project,
|
||||||
ProviderAuthResponse,
|
ProviderAuthResponse,
|
||||||
QuestionRequest,
|
QuestionRequest,
|
||||||
ReferenceInfo,
|
|
||||||
Session,
|
Session,
|
||||||
} from "@opencode-ai/sdk/v2/client"
|
} from "@opencode-ai/sdk/v2/client"
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
@@ -19,7 +18,7 @@ import type { ServerSession } from "../server-session"
|
|||||||
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
|
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
|
||||||
import { formatServerError } from "@/utils/server-errors"
|
import { formatServerError } from "@/utils/server-errors"
|
||||||
import { QueryClient, queryOptions } from "@tanstack/solid-query"
|
import { QueryClient, queryOptions } from "@tanstack/solid-query"
|
||||||
import { loadMcpQuery, loadMcpResourcesQuery } from "../server-sync"
|
import { loadMcpQuery } from "../server-sync"
|
||||||
import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
|
import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
|
||||||
import { ScopedKey, type ServerScope } from "@/utils/server-scope"
|
import { ScopedKey, type ServerScope } from "@/utils/server-scope"
|
||||||
|
|
||||||
@@ -196,13 +195,6 @@ export const loadPathQuery = (scope: ServerScope, directory: string | null, sdk:
|
|||||||
queryFn: () => retry(() => sdk.path.get().then((x) => x.data!)),
|
queryFn: () => retry(() => sdk.path.get().then((x) => x.data!)),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const loadReferencesQuery = (scope: ServerScope, directory: string, sdk: OpencodeClient) =>
|
|
||||||
queryOptions<ReferenceInfo[]>({
|
|
||||||
queryKey: [scope, directory, "references"] as const,
|
|
||||||
queryFn: () => retry(() => sdk.v2.reference.list().then((x) => x.data?.data ?? [])).catch(() => []),
|
|
||||||
placeholderData: [],
|
|
||||||
})
|
|
||||||
|
|
||||||
export async function bootstrapDirectory(input: {
|
export async function bootstrapDirectory(input: {
|
||||||
directory: string
|
directory: string
|
||||||
scope: ServerScope
|
scope: ServerScope
|
||||||
@@ -285,7 +277,6 @@ export async function bootstrapDirectory(input: {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
input.mcp && (() => retry(() => input.sdk.command.list().then((x) => input.setStore("command", x.data ?? [])))),
|
input.mcp && (() => retry(() => input.sdk.command.list().then((x) => input.setStore("command", x.data ?? [])))),
|
||||||
() => input.queryClient.fetchQuery(loadReferencesQuery(input.scope, input.directory, input.sdk)),
|
|
||||||
() =>
|
() =>
|
||||||
retry(() =>
|
retry(() =>
|
||||||
input.sdk.permission.list().then((x) => {
|
input.sdk.permission.list().then((x) => {
|
||||||
@@ -348,7 +339,6 @@ export async function bootstrapDirectory(input: {
|
|||||||
),
|
),
|
||||||
() => Promise.resolve(input.loadSessions(input.directory)),
|
() => Promise.resolve(input.loadSessions(input.directory)),
|
||||||
input.mcp && (() => input.queryClient.fetchQuery(loadMcpQuery(input.scope, input.directory, input.sdk))),
|
input.mcp && (() => input.queryClient.fetchQuery(loadMcpQuery(input.scope, input.directory, input.sdk))),
|
||||||
input.mcp && (() => input.queryClient.fetchQuery(loadMcpResourcesQuery(input.scope, input.directory, input.sdk))),
|
|
||||||
() =>
|
() =>
|
||||||
input.queryClient.fetchQuery(loadProvidersQuery(input.scope, input.directory, input.sdk)).catch((err) => {
|
input.queryClient.fetchQuery(loadProvidersQuery(input.scope, input.directory, input.sdk)).catch((err) => {
|
||||||
const project = getFilename(input.directory)
|
const project = getFilename(input.directory)
|
||||||
|
|||||||
@@ -34,9 +34,7 @@ const queryOptionsApi = {
|
|||||||
}),
|
}),
|
||||||
agents: (directory: string) => ({ queryKey: [directory, "agents"], queryFn: async () => [] }),
|
agents: (directory: string) => ({ queryKey: [directory, "agents"], queryFn: async () => [] }),
|
||||||
mcp: (directory: string) => ({ queryKey: [directory, "mcp"], queryFn: async () => ({}) }),
|
mcp: (directory: string) => ({ queryKey: [directory, "mcp"], queryFn: async () => ({}) }),
|
||||||
mcpResources: (directory: string) => ({ queryKey: [directory, "mcpResources"], queryFn: async () => ({}) }),
|
|
||||||
lsp: (directory: string) => ({ queryKey: [directory, "lsp"], queryFn: async () => [] }),
|
lsp: (directory: string) => ({ queryKey: [directory, "lsp"], queryFn: async () => [] }),
|
||||||
references: (directory: string) => ({ queryKey: [directory, "references"], queryFn: async () => [] }),
|
|
||||||
sessions: (directory: string) => ({ queryKey: [directory, "loadSessions"] as const }),
|
sessions: (directory: string) => ({ queryKey: [directory, "loadSessions"] as const }),
|
||||||
} as unknown as QueryOptionsApi
|
} as unknown as QueryOptionsApi
|
||||||
|
|
||||||
@@ -199,18 +197,14 @@ describe("createChildStoreManager", () => {
|
|||||||
try {
|
try {
|
||||||
if (!manager) throw new Error("manager required")
|
if (!manager) throw new Error("manager required")
|
||||||
const [store, setStore] = manager.child("/project", { bootstrap: false })
|
const [store, setStore] = manager.child("/project", { bootstrap: false })
|
||||||
expect(querySingles.length - offset).toBe(6)
|
expect(querySingles.length - offset).toBe(4)
|
||||||
const query = querySingles[offset + 1]
|
const query = querySingles[offset + 1]
|
||||||
const resourceQuery = querySingles[offset + 2]
|
|
||||||
if (!query) throw new Error("query required")
|
if (!query) throw new Error("query required")
|
||||||
if (!resourceQuery) throw new Error("resource query required")
|
|
||||||
expect(query().enabled).toBe(false)
|
expect(query().enabled).toBe(false)
|
||||||
expect(resourceQuery().enabled).toBe(false)
|
|
||||||
|
|
||||||
setStore("status", "complete")
|
setStore("status", "complete")
|
||||||
manager.child("/project", { bootstrap: false, mcp: true })
|
manager.child("/project", { bootstrap: false, mcp: true })
|
||||||
expect(query().enabled).toBe(true)
|
expect(query().enabled).toBe(true)
|
||||||
expect(resourceQuery().enabled).toBe(true)
|
|
||||||
expect(store.mcp).toEqual({ demo: { status: "disabled" } })
|
expect(store.mcp).toEqual({ demo: { status: "disabled" } })
|
||||||
expect(mcpLoads).toEqual(["/project"])
|
expect(mcpLoads).toEqual(["/project"])
|
||||||
|
|
||||||
|
|||||||
@@ -185,10 +185,8 @@ export function createChildStoreManager(input: {
|
|||||||
|
|
||||||
const pathQuery = useQuery(() => input.queryOptions.path(key))
|
const pathQuery = useQuery(() => input.queryOptions.path(key))
|
||||||
const mcpQuery = useQuery(() => ({ ...input.queryOptions.mcp(key), enabled: mcpEnabled() }))
|
const mcpQuery = useQuery(() => ({ ...input.queryOptions.mcp(key), enabled: mcpEnabled() }))
|
||||||
const mcpResourceQuery = useQuery(() => ({ ...input.queryOptions.mcpResources(key), enabled: mcpEnabled() }))
|
|
||||||
const lspQuery = useQuery(() => input.queryOptions.lsp(key))
|
const lspQuery = useQuery(() => input.queryOptions.lsp(key))
|
||||||
const providerQuery = useQuery(() => input.queryOptions.providers(key))
|
const providerQuery = useQuery(() => input.queryOptions.providers(key))
|
||||||
const referenceQuery = useQuery(() => input.queryOptions.references(key))
|
|
||||||
|
|
||||||
const child = createStore<State>({
|
const child = createStore<State>({
|
||||||
project: "",
|
project: "",
|
||||||
@@ -212,9 +210,6 @@ export function createChildStoreManager(input: {
|
|||||||
status: "loading" as const,
|
status: "loading" as const,
|
||||||
agent: [],
|
agent: [],
|
||||||
command: [],
|
command: [],
|
||||||
get reference() {
|
|
||||||
return referenceQuery.isLoading ? [] : (referenceQuery.data ?? [])
|
|
||||||
},
|
|
||||||
session: [],
|
session: [],
|
||||||
sessionTotal: 0,
|
sessionTotal: 0,
|
||||||
session_status: {},
|
session_status: {},
|
||||||
@@ -232,9 +227,6 @@ export function createChildStoreManager(input: {
|
|||||||
get mcp() {
|
get mcp() {
|
||||||
return mcpQuery.isLoading ? {} : (mcpQuery.data ?? {})
|
return mcpQuery.isLoading ? {} : (mcpQuery.data ?? {})
|
||||||
},
|
},
|
||||||
get mcp_resource() {
|
|
||||||
return mcpResourceQuery.isLoading ? {} : (mcpResourceQuery.data ?? {})
|
|
||||||
},
|
|
||||||
get lsp_ready() {
|
get lsp_ready() {
|
||||||
return !lspQuery.isLoading
|
return !lspQuery.isLoading
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -112,7 +112,6 @@ export function applyDirectoryEvent(input: {
|
|||||||
push: (directory: string) => void
|
push: (directory: string) => void
|
||||||
directory: string
|
directory: string
|
||||||
loadLsp: () => void
|
loadLsp: () => void
|
||||||
loadReferences?: () => void
|
|
||||||
vcsCache?: VcsCache
|
vcsCache?: VcsCache
|
||||||
setSessionTodo?: (sessionID: string, todos: Todo[] | undefined) => void
|
setSessionTodo?: (sessionID: string, todos: Todo[] | undefined) => void
|
||||||
retainedLimit?: number
|
retainedLimit?: number
|
||||||
@@ -405,9 +404,5 @@ export function applyDirectoryEvent(input: {
|
|||||||
input.loadLsp()
|
input.loadLsp()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "reference.updated": {
|
|
||||||
input.loadReferences?.()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,12 @@ import type {
|
|||||||
Command,
|
Command,
|
||||||
Config,
|
Config,
|
||||||
LspStatus,
|
LspStatus,
|
||||||
McpResource,
|
|
||||||
McpStatus,
|
McpStatus,
|
||||||
Message,
|
Message,
|
||||||
Part,
|
Part,
|
||||||
Path,
|
Path,
|
||||||
PermissionRequest,
|
PermissionRequest,
|
||||||
QuestionRequest,
|
QuestionRequest,
|
||||||
ReferenceInfo,
|
|
||||||
Session,
|
Session,
|
||||||
SessionStatus,
|
SessionStatus,
|
||||||
SnapshotFileDiff,
|
SnapshotFileDiff,
|
||||||
@@ -36,7 +34,6 @@ export type State = {
|
|||||||
status: "loading" | "partial" | "complete"
|
status: "loading" | "partial" | "complete"
|
||||||
agent: Agent[]
|
agent: Agent[]
|
||||||
command: Command[]
|
command: Command[]
|
||||||
reference: ReferenceInfo[]
|
|
||||||
project: string
|
project: string
|
||||||
projectMeta: ProjectMeta | undefined
|
projectMeta: ProjectMeta | undefined
|
||||||
icon: string | undefined
|
icon: string | undefined
|
||||||
@@ -66,9 +63,6 @@ export type State = {
|
|||||||
mcp: {
|
mcp: {
|
||||||
[name: string]: McpStatus
|
[name: string]: McpStatus
|
||||||
}
|
}
|
||||||
mcp_resource: {
|
|
||||||
[key: string]: McpResource
|
|
||||||
}
|
|
||||||
lsp_ready: boolean
|
lsp_ready: boolean
|
||||||
lsp: LspStatus[]
|
lsp: LspStatus[]
|
||||||
vcs: VcsInfo | undefined
|
vcs: VcsInfo | undefined
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ const DEFAULT_SIDEBAR_WIDTH = 344
|
|||||||
const DEFAULT_FILE_TREE_WIDTH = 200
|
const DEFAULT_FILE_TREE_WIDTH = 200
|
||||||
const DEFAULT_SESSION_WIDTH = 600
|
const DEFAULT_SESSION_WIDTH = 600
|
||||||
const DEFAULT_TERMINAL_HEIGHT = 280
|
const DEFAULT_TERMINAL_HEIGHT = 280
|
||||||
const DEFAULT_REVIEW_PANEL_OPENED = false
|
|
||||||
export type AvatarColorKey = (typeof AVATAR_COLOR_KEYS)[number]
|
export type AvatarColorKey = (typeof AVATAR_COLOR_KEYS)[number]
|
||||||
|
|
||||||
export function getAvatarColors(key?: string) {
|
export function getAvatarColors(key?: string) {
|
||||||
@@ -78,7 +77,6 @@ export type LocalProject = Partial<Project> & { worktree: string; expanded: bool
|
|||||||
export type HomeProjectSelection = { server: ServerConnection.Key; directory?: string }
|
export type HomeProjectSelection = { server: ServerConnection.Key; directory?: string }
|
||||||
|
|
||||||
export type ReviewDiffStyle = "unified" | "split"
|
export type ReviewDiffStyle = "unified" | "split"
|
||||||
export type ReviewPanelSource = "context-button" | "other"
|
|
||||||
|
|
||||||
export type LayoutRoute =
|
export type LayoutRoute =
|
||||||
| { type: "home" }
|
| { type: "home" }
|
||||||
@@ -212,8 +210,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
if (!isRecord(review)) return review
|
if (!isRecord(review)) return review
|
||||||
if (typeof review.panelOpened === "boolean") return review
|
if (typeof review.panelOpened === "boolean") return review
|
||||||
|
|
||||||
const opened =
|
const opened = isRecord(fileTree) && typeof fileTree.opened === "boolean" ? fileTree.opened : true
|
||||||
isRecord(fileTree) && typeof fileTree.opened === "boolean" ? fileTree.opened : DEFAULT_REVIEW_PANEL_OPENED
|
|
||||||
return {
|
return {
|
||||||
...review,
|
...review,
|
||||||
panelOpened: opened,
|
panelOpened: opened,
|
||||||
@@ -282,7 +279,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
},
|
},
|
||||||
review: {
|
review: {
|
||||||
diffStyle: "split" as ReviewDiffStyle,
|
diffStyle: "split" as ReviewDiffStyle,
|
||||||
panelOpened: DEFAULT_REVIEW_PANEL_OPENED,
|
panelOpened: true,
|
||||||
},
|
},
|
||||||
fileTree: {
|
fileTree: {
|
||||||
opened: false,
|
opened: false,
|
||||||
@@ -305,9 +302,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const [ephemeral, setEphemeral] = createStore({
|
|
||||||
reviewPanelSource: "other" as ReviewPanelSource,
|
|
||||||
})
|
|
||||||
|
|
||||||
const MAX_SESSION_KEYS = 50
|
const MAX_SESSION_KEYS = 50
|
||||||
const PENDING_MESSAGE_TTL_MS = 2 * 60 * 1000
|
const PENDING_MESSAGE_TTL_MS = 2 * 60 * 1000
|
||||||
@@ -668,7 +662,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
diffStyle: createMemo(() => store.review?.diffStyle ?? "split"),
|
diffStyle: createMemo(() => store.review?.diffStyle ?? "split"),
|
||||||
setDiffStyle(diffStyle: ReviewDiffStyle) {
|
setDiffStyle(diffStyle: ReviewDiffStyle) {
|
||||||
if (!store.review) {
|
if (!store.review) {
|
||||||
setStore("review", { diffStyle, panelOpened: DEFAULT_REVIEW_PANEL_OPENED })
|
setStore("review", { diffStyle, panelOpened: true })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setStore("review", "diffStyle", diffStyle)
|
setStore("review", "diffStyle", diffStyle)
|
||||||
@@ -783,8 +777,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
const key = createSessionKeyReader(sessionKey, ensureKey)
|
const key = createSessionKeyReader(sessionKey, ensureKey)
|
||||||
const s = createMemo(() => store.sessionView[key()] ?? { scroll: {} })
|
const s = createMemo(() => store.sessionView[key()] ?? { scroll: {} })
|
||||||
const terminalOpened = createMemo(() => store.terminal?.opened ?? false)
|
const terminalOpened = createMemo(() => store.terminal?.opened ?? false)
|
||||||
const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? DEFAULT_REVIEW_PANEL_OPENED)
|
const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? true)
|
||||||
const reviewPanelSource = createMemo(() => (reviewPanelOpened() ? ephemeral.reviewPanelSource : "other"))
|
|
||||||
|
|
||||||
function setTerminalOpened(next: boolean) {
|
function setTerminalOpened(next: boolean) {
|
||||||
const current = store.terminal
|
const current = store.terminal
|
||||||
@@ -798,26 +791,16 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
setStore("terminal", "opened", next)
|
setStore("terminal", "opened", next)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setReviewPanelOpened(next: boolean, source: ReviewPanelSource) {
|
function setReviewPanelOpened(next: boolean) {
|
||||||
const nextSource = next ? source : "other"
|
|
||||||
const current = store.review
|
const current = store.review
|
||||||
if (!current) {
|
if (!current) {
|
||||||
batch(() => {
|
setStore("review", { diffStyle: "split" as ReviewDiffStyle, panelOpened: next })
|
||||||
setStore("review", { diffStyle: "split" as ReviewDiffStyle, panelOpened: next })
|
|
||||||
setEphemeral("reviewPanelSource", nextSource)
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = current.panelOpened ?? DEFAULT_REVIEW_PANEL_OPENED
|
const value = current.panelOpened ?? true
|
||||||
if (value === next) {
|
if (value === next) return
|
||||||
if (ephemeral.reviewPanelSource !== nextSource) setEphemeral("reviewPanelSource", nextSource)
|
setStore("review", "panelOpened", next)
|
||||||
return
|
|
||||||
}
|
|
||||||
batch(() => {
|
|
||||||
setStore("review", "panelOpened", next)
|
|
||||||
setEphemeral("reviewPanelSource", nextSource)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -853,15 +836,14 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
},
|
},
|
||||||
reviewPanel: {
|
reviewPanel: {
|
||||||
opened: reviewPanelOpened,
|
opened: reviewPanelOpened,
|
||||||
source: reviewPanelSource,
|
open() {
|
||||||
open(source: ReviewPanelSource = "other") {
|
setReviewPanelOpened(true)
|
||||||
setReviewPanelOpened(true, source)
|
|
||||||
},
|
},
|
||||||
close() {
|
close() {
|
||||||
setReviewPanelOpened(false, "other")
|
setReviewPanelOpened(false)
|
||||||
},
|
},
|
||||||
toggle() {
|
toggle() {
|
||||||
setReviewPanelOpened(!reviewPanelOpened(), "other")
|
setReviewPanelOpened(!reviewPanelOpened())
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
review: {
|
review: {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { batch, createEffect, createMemo, startTransition } from "solid-js"
|
import { batch, createEffect, createMemo } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useModels } from "@/context/models"
|
import { useModels } from "@/context/models"
|
||||||
import { useProviders } from "@/hooks/use-providers"
|
import { useProviders } from "@/hooks/use-providers"
|
||||||
@@ -80,7 +80,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
const [store, setStore] = createStore<{
|
const [store, setStore] = createStore<{
|
||||||
current?: string
|
current?: string
|
||||||
draft?: State
|
draft?: State
|
||||||
promoting?: State
|
|
||||||
last?: {
|
last?: {
|
||||||
type: "agent" | "model" | "variant"
|
type: "agent" | "model" | "variant"
|
||||||
agent?: string
|
agent?: string
|
||||||
@@ -124,7 +123,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
|
|
||||||
const scope = createMemo<State | undefined>(() => {
|
const scope = createMemo<State | undefined>(() => {
|
||||||
const session = id()
|
const session = id()
|
||||||
if (!session) return store.draft ?? store.promoting
|
if (!session) return store.draft
|
||||||
return saved.session[session] ?? handoff.get(handoffKey(serverSDK().scope, sdk().directory, session))
|
return saved.session[session] ?? handoff.get(handoffKey(serverSDK().scope, sdk().directory, session))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -137,13 +136,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
if (!next) return
|
if (!next) return
|
||||||
if (saved.session[session] !== undefined) {
|
if (saved.session[session] !== undefined) {
|
||||||
handoff.delete(key)
|
handoff.delete(key)
|
||||||
setStore("promoting", undefined)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setSaved("session", session, clone(next))
|
setSaved("session", session, clone(next))
|
||||||
handoff.delete(key)
|
handoff.delete(key)
|
||||||
setStore("promoting", undefined)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const configuredModel = () => {
|
const configuredModel = () => {
|
||||||
@@ -297,21 +294,19 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
model.set({ providerID: entry.provider.id, modelID: entry.id })
|
model.set({ providerID: entry.provider.id, modelID: entry.id })
|
||||||
},
|
},
|
||||||
set(item: ModelKey | undefined, options?: { recent?: boolean }) {
|
set(item: ModelKey | undefined, options?: { recent?: boolean }) {
|
||||||
startTransition(() =>
|
batch(() => {
|
||||||
batch(() => {
|
setStore("last", {
|
||||||
setStore("last", {
|
type: "model",
|
||||||
type: "model",
|
agent: agent.current()?.name,
|
||||||
agent: agent.current()?.name,
|
model: item ?? null,
|
||||||
model: item ?? null,
|
variant: selected(),
|
||||||
variant: selected(),
|
})
|
||||||
})
|
write({ model: item })
|
||||||
write({ model: item })
|
if (!item) return
|
||||||
if (!item) return
|
models.setVisibility(item, true)
|
||||||
models.setVisibility(item, true)
|
if (!options?.recent) return
|
||||||
if (!options?.recent) return
|
models.recent.push(item)
|
||||||
models.recent.push(item)
|
})
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
visible(item: ModelKey) {
|
visible(item: ModelKey) {
|
||||||
return models.visible(item)
|
return models.visible(item)
|
||||||
@@ -340,21 +335,19 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
return Object.keys(item.variants)
|
return Object.keys(item.variants)
|
||||||
},
|
},
|
||||||
set(value: string | undefined) {
|
set(value: string | undefined) {
|
||||||
startTransition(() =>
|
batch(() => {
|
||||||
batch(() => {
|
const model = current()
|
||||||
const model = current()
|
setStore("last", {
|
||||||
setStore("last", {
|
type: "variant",
|
||||||
type: "variant",
|
agent: agent.current()?.name,
|
||||||
agent: agent.current()?.name,
|
model: model ? { providerID: model.provider.id, modelID: model.id } : null,
|
||||||
model: model ? { providerID: model.provider.id, modelID: model.id } : null,
|
variant: value ?? null,
|
||||||
variant: value ?? null,
|
})
|
||||||
})
|
write({ variant: value ?? null })
|
||||||
write({ variant: value ?? null })
|
if (model) {
|
||||||
if (model) {
|
models.variant.set({ providerID: model.provider.id, modelID: model.id }, value ?? undefined)
|
||||||
models.variant.set({ providerID: model.provider.id, modelID: model.id }, value ?? undefined)
|
}
|
||||||
}
|
})
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
cycle() {
|
cycle() {
|
||||||
const items = this.list()
|
const items = this.list()
|
||||||
@@ -376,19 +369,19 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
agent,
|
agent,
|
||||||
session: {
|
session: {
|
||||||
reset() {
|
reset() {
|
||||||
setStore({ draft: undefined, promoting: undefined })
|
setStore("draft", undefined)
|
||||||
},
|
},
|
||||||
promote(dir: string, session: string) {
|
promote(dir: string, session: string) {
|
||||||
const next = clone(snapshot())
|
const next = clone(snapshot())
|
||||||
if (!next) return
|
if (!next) return
|
||||||
const key = handoffKey(serverSDK().scope, dir, session)
|
|
||||||
handoff.set(key, next)
|
|
||||||
|
|
||||||
if (dir === sdk().directory) {
|
if (dir === sdk().directory) {
|
||||||
setSaved("session", session, next)
|
setSaved("session", session, next)
|
||||||
|
setStore("draft", undefined)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setStore("promoting", next)
|
handoff.set(handoffKey(serverSDK().scope, dir, session), next)
|
||||||
setStore("draft", undefined)
|
setStore("draft", undefined)
|
||||||
},
|
},
|
||||||
restore(msg: { sessionID: string; agent: string; model: ModelKey }) {
|
restore(msg: { sessionID: string; agent: string; model: ModelKey }) {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { createStore, reconcile } from "solid-js/store"
|
import { createStore, reconcile } from "solid-js/store"
|
||||||
import { type Accessor, batch, createEffect, createMemo, createRoot, getOwner, onCleanup } from "solid-js"
|
import { type Accessor, batch, createEffect, createMemo, onCleanup } from "solid-js"
|
||||||
import { useParams, useSearchParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import type { ServerSDK } from "./server-sdk"
|
import { useServerSDK } from "./server-sdk"
|
||||||
import type { ServerSync } from "./server-sync"
|
import { useServerSync } from "./server-sync"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
@@ -12,11 +12,6 @@ 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 { playSoundById } from "@/utils/sound"
|
||||||
import { useGlobal } from "./global"
|
|
||||||
import { ServerConnection, useServer } from "./server"
|
|
||||||
import { type DraftTab, useTabs } from "./tabs"
|
|
||||||
import { requireServerKey } from "@/utils/session-route"
|
|
||||||
import type { ServerScope } from "@/utils/server-scope"
|
|
||||||
|
|
||||||
type NotificationBase = {
|
type NotificationBase = {
|
||||||
directory?: string
|
directory?: string
|
||||||
@@ -112,360 +107,267 @@ function buildNotificationIndex(list: Notification[]) {
|
|||||||
export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({
|
export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({
|
||||||
name: "Notification",
|
name: "Notification",
|
||||||
gate: false,
|
gate: false,
|
||||||
init: () => {
|
init: (props: { directory?: Accessor<string | undefined>; sessionID?: Accessor<string | undefined> }) => {
|
||||||
const params = useParams<{ serverKey?: string; dir?: string; id?: string }>()
|
const params = useParams()
|
||||||
const [search] = useSearchParams<{ draftId?: string }>()
|
const serverSDK = useServerSDK()
|
||||||
const global = useGlobal()
|
const serverSync = useServerSync()
|
||||||
const server = useServer()
|
|
||||||
const tabs = useTabs()
|
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const owner = getOwner()
|
|
||||||
const states = new Map<ServerScope, { dispose: () => void; state: NotificationState }>()
|
|
||||||
|
|
||||||
const activeServer = createMemo(() => {
|
const empty: Notification[] = []
|
||||||
if (params.serverKey) return requireServerKey(params.serverKey)
|
|
||||||
if (search.draftId) {
|
const currentDirectory = createMemo(() => {
|
||||||
const draft = tabs.store.find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === search.draftId)
|
return props.directory?.() ?? decode64(params.dir)
|
||||||
if (draft) return draft.server
|
|
||||||
}
|
|
||||||
return server.key
|
|
||||||
})
|
})
|
||||||
const activeDirectory = createMemo(() => decode64(params.dir))
|
|
||||||
const activeSession = createMemo(() => params.id)
|
|
||||||
|
|
||||||
const ensure = (key: ServerConnection.Key) => {
|
const currentSession = createMemo(() => props.sessionID?.() ?? params.id)
|
||||||
const conn = global.servers.list().find((item) => ServerConnection.key(item) === key)
|
|
||||||
if (!conn) throw new Error(`Notification server not found: ${key}`)
|
const [store, setStore, _, ready] = persisted(
|
||||||
const ctx = global.ensureServerCtx(conn)
|
Persist.serverGlobal(serverSDK().scope, "notification", ["notification.v1"]),
|
||||||
const existing = states.get(ctx.sdk.scope)
|
createStore({
|
||||||
if (existing) return existing.state
|
list: [] as Notification[],
|
||||||
const root = createRoot(
|
}),
|
||||||
(dispose) => ({
|
)
|
||||||
dispose,
|
const [index, setIndex] = createStore<NotificationIndex>(buildNotificationIndex(store.list))
|
||||||
state: createServerNotificationState({
|
|
||||||
sdk: ctx.sdk,
|
const meta = { pruned: false, disposed: false }
|
||||||
sync: ctx.sync,
|
|
||||||
active: () => server.scope(activeServer()) === ctx.sdk.scope,
|
const updateUnseen = (scope: "session" | "project", key: string, unseen: Notification[]) => {
|
||||||
directory: activeDirectory,
|
setIndex(scope, "unseen", key, unseen)
|
||||||
sessionID: activeSession,
|
setIndex(scope, "unseenCount", key, unseen.length)
|
||||||
platform,
|
setIndex(
|
||||||
settings,
|
scope,
|
||||||
language,
|
"unseenHasError",
|
||||||
}),
|
key,
|
||||||
}),
|
unseen.some((notification) => notification.type === "error"),
|
||||||
owner ?? undefined,
|
|
||||||
)
|
)
|
||||||
states.set(ctx.sdk.scope, root)
|
}
|
||||||
return root.state
|
|
||||||
|
const appendToIndex = (notification: Notification) => {
|
||||||
|
if (notification.session) {
|
||||||
|
setIndex("session", "all", notification.session, (all = []) => [...all, notification])
|
||||||
|
if (!notification.viewed) {
|
||||||
|
setIndex("session", "unseen", notification.session, (unseen = []) => [...unseen, notification])
|
||||||
|
setIndex("session", "unseenCount", notification.session, (count = 0) => count + 1)
|
||||||
|
if (notification.type === "error") setIndex("session", "unseenHasError", notification.session, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notification.directory) {
|
||||||
|
setIndex("project", "all", notification.directory, (all = []) => [...all, notification])
|
||||||
|
if (!notification.viewed) {
|
||||||
|
setIndex("project", "unseen", notification.directory, (unseen = []) => [...unseen, notification])
|
||||||
|
setIndex("project", "unseenCount", notification.directory, (count = 0) => count + 1)
|
||||||
|
if (notification.type === "error") setIndex("project", "unseenHasError", notification.directory, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeFromIndex = (notification: Notification) => {
|
||||||
|
if (notification.session) {
|
||||||
|
setIndex("session", "all", notification.session, (all = []) => all.filter((n) => n !== notification))
|
||||||
|
if (!notification.viewed) {
|
||||||
|
const unseen = (index.session.unseen[notification.session] ?? empty).filter((n) => n !== notification)
|
||||||
|
updateUnseen("session", notification.session, unseen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notification.directory) {
|
||||||
|
setIndex("project", "all", notification.directory, (all = []) => all.filter((n) => n !== notification))
|
||||||
|
if (!notification.viewed) {
|
||||||
|
const unseen = (index.project.unseen[notification.directory] ?? empty).filter((n) => n !== notification)
|
||||||
|
updateUnseen("project", notification.directory, unseen)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
global.servers.list().forEach((conn) => ensure(ServerConnection.key(conn)))
|
if (!ready()) return
|
||||||
})
|
if (meta.pruned) return
|
||||||
|
meta.pruned = true
|
||||||
createEffect(() => {
|
const list = pruneNotifications(store.list)
|
||||||
const scopes = new Set(global.servers.list().map((conn) => server.scope(ServerConnection.key(conn))))
|
batch(() => {
|
||||||
states.forEach((value, scope) => {
|
setStore("list", list)
|
||||||
if (scopes.has(scope)) return
|
setIndex(reconcile(buildNotificationIndex(list), { merge: false }))
|
||||||
value.dispose()
|
|
||||||
states.delete(scope)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
onCleanup(() => states.forEach((value) => value.dispose()))
|
const append = (notification: Notification) => {
|
||||||
|
const list = pruneNotifications([...store.list, notification])
|
||||||
|
const keep = new Set(list)
|
||||||
|
const removed = store.list.filter((n) => !keep.has(n))
|
||||||
|
|
||||||
const selected = () => ensure(activeServer())
|
batch(() => {
|
||||||
|
if (keep.has(notification)) appendToIndex(notification)
|
||||||
|
removed.forEach((n) => removeFromIndex(n))
|
||||||
|
setStore("list", list)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const lookup = async (directory: string, sessionID?: string) => {
|
||||||
|
if (!sessionID) return undefined
|
||||||
|
const sync = serverSync().ensureDirSyncContext(directory)
|
||||||
|
const session = sync.session.get(sessionID)
|
||||||
|
if (session) return session
|
||||||
|
return sync.session
|
||||||
|
.sync(sessionID)
|
||||||
|
.then(() => sync.session.get(sessionID))
|
||||||
|
.catch(() => undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewedInCurrentSession = (directory: string, sessionID?: string) => {
|
||||||
|
const activeDirectory = currentDirectory()
|
||||||
|
const activeSession = currentSession()
|
||||||
|
if (!activeDirectory) return false
|
||||||
|
if (!activeSession) return false
|
||||||
|
if (!sessionID) return false
|
||||||
|
if (directory !== activeDirectory) return false
|
||||||
|
return sessionID === activeSession
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSessionIdle = (directory: string, event: { properties: { sessionID?: string } }, time: number) => {
|
||||||
|
const sessionID = event.properties.sessionID
|
||||||
|
void lookup(directory, sessionID).then((session) => {
|
||||||
|
if (meta.disposed) return
|
||||||
|
if (!session) return
|
||||||
|
if (session.parentID) return
|
||||||
|
|
||||||
|
if (settings.sounds.agentEnabled()) {
|
||||||
|
void playSoundById(settings.sounds.agent())
|
||||||
|
}
|
||||||
|
|
||||||
|
append({
|
||||||
|
directory,
|
||||||
|
time,
|
||||||
|
viewed: viewedInCurrentSession(directory, sessionID),
|
||||||
|
type: "turn-complete",
|
||||||
|
session: sessionID,
|
||||||
|
})
|
||||||
|
|
||||||
|
const href = `/${base64Encode(directory)}/session/${sessionID}`
|
||||||
|
if (settings.notifications.agent()) {
|
||||||
|
void platform.notify(language.t("notification.session.responseReady.title"), session.title ?? sessionID, href)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSessionError = (
|
||||||
|
directory: string,
|
||||||
|
event: { properties: { sessionID?: string; error?: EventSessionError["properties"]["error"] } },
|
||||||
|
time: number,
|
||||||
|
) => {
|
||||||
|
const sessionID = event.properties.sessionID
|
||||||
|
void lookup(directory, sessionID).then((session) => {
|
||||||
|
if (meta.disposed) return
|
||||||
|
if (session?.parentID) return
|
||||||
|
|
||||||
|
if (settings.sounds.errorsEnabled()) {
|
||||||
|
void playSoundById(settings.sounds.errors())
|
||||||
|
}
|
||||||
|
|
||||||
|
const error = "error" in event.properties ? event.properties.error : undefined
|
||||||
|
append({
|
||||||
|
directory,
|
||||||
|
time,
|
||||||
|
viewed: viewedInCurrentSession(directory, sessionID),
|
||||||
|
type: "error",
|
||||||
|
session: sessionID ?? "global",
|
||||||
|
error,
|
||||||
|
})
|
||||||
|
const description =
|
||||||
|
session?.title ??
|
||||||
|
(typeof error === "string" ? error : language.t("notification.session.error.fallbackDescription"))
|
||||||
|
const href = sessionID ? `/${base64Encode(directory)}/session/${sessionID}` : `/${base64Encode(directory)}`
|
||||||
|
if (settings.notifications.errors()) {
|
||||||
|
void platform.notify(language.t("notification.session.error.title"), description, href)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsub = serverSDK().event.listen((e) => {
|
||||||
|
const event = e.details
|
||||||
|
if (event.type !== "session.idle" && event.type !== "session.error") return
|
||||||
|
|
||||||
|
const directory = e.name
|
||||||
|
const time = Date.now()
|
||||||
|
if (event.type === "session.idle") {
|
||||||
|
handleSessionIdle(directory, event, time)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handleSessionError(directory, event, time)
|
||||||
|
})
|
||||||
|
onCleanup(() => {
|
||||||
|
meta.disposed = true
|
||||||
|
unsub()
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ready: () => selected().ready(),
|
ready,
|
||||||
ensureServerState: ensure,
|
|
||||||
session: {
|
session: {
|
||||||
all: (session: string) => selected().session.all(session),
|
all(session: string) {
|
||||||
unseen: (session: string) => selected().session.unseen(session),
|
return index.session.all[session] ?? empty
|
||||||
unseenCount: (session: string) => selected().session.unseenCount(session),
|
},
|
||||||
unseenHasError: (session: string) => selected().session.unseenHasError(session),
|
unseen(session: string) {
|
||||||
markViewed: (session: string) => selected().session.markViewed(session),
|
return index.session.unseen[session] ?? empty
|
||||||
|
},
|
||||||
|
unseenCount(session: string) {
|
||||||
|
return index.session.unseenCount[session] ?? 0
|
||||||
|
},
|
||||||
|
unseenHasError(session: string) {
|
||||||
|
return index.session.unseenHasError[session] ?? false
|
||||||
|
},
|
||||||
|
markViewed(session: string) {
|
||||||
|
const unseen = index.session.unseen[session] ?? empty
|
||||||
|
if (!unseen.length) return
|
||||||
|
|
||||||
|
const projects = [
|
||||||
|
...new Set(unseen.flatMap((notification) => (notification.directory ? [notification.directory] : []))),
|
||||||
|
]
|
||||||
|
batch(() => {
|
||||||
|
setStore("list", (n) => n.session === session && !n.viewed, "viewed", true)
|
||||||
|
updateUnseen("session", session, [])
|
||||||
|
projects.forEach((directory) => {
|
||||||
|
const next = (index.project.unseen[directory] ?? empty).filter(
|
||||||
|
(notification) => notification.session !== session,
|
||||||
|
)
|
||||||
|
updateUnseen("project", directory, next)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
project: {
|
project: {
|
||||||
all: (directory: string) => selected().project.all(directory),
|
all(directory: string) {
|
||||||
unseen: (directory: string) => selected().project.unseen(directory),
|
return index.project.all[directory] ?? empty
|
||||||
unseenCount: (directory: string) => selected().project.unseenCount(directory),
|
},
|
||||||
unseenHasError: (directory: string) => selected().project.unseenHasError(directory),
|
unseen(directory: string) {
|
||||||
markViewed: (directory: string) => selected().project.markViewed(directory),
|
return index.project.unseen[directory] ?? empty
|
||||||
|
},
|
||||||
|
unseenCount(directory: string) {
|
||||||
|
return index.project.unseenCount[directory] ?? 0
|
||||||
|
},
|
||||||
|
unseenHasError(directory: string) {
|
||||||
|
return index.project.unseenHasError[directory] ?? false
|
||||||
|
},
|
||||||
|
markViewed(directory: string) {
|
||||||
|
const unseen = index.project.unseen[directory] ?? empty
|
||||||
|
if (!unseen.length) return
|
||||||
|
|
||||||
|
const sessions = [
|
||||||
|
...new Set(unseen.flatMap((notification) => (notification.session ? [notification.session] : []))),
|
||||||
|
]
|
||||||
|
batch(() => {
|
||||||
|
setStore("list", (n) => n.directory === directory && !n.viewed, "viewed", true)
|
||||||
|
updateUnseen("project", directory, [])
|
||||||
|
sessions.forEach((session) => {
|
||||||
|
const next = (index.session.unseen[session] ?? empty).filter(
|
||||||
|
(notification) => notification.directory !== directory,
|
||||||
|
)
|
||||||
|
updateUnseen("session", session, next)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
type NotificationState = ReturnType<typeof createServerNotificationState>
|
|
||||||
|
|
||||||
function createServerNotificationState(input: {
|
|
||||||
sdk: ServerSDK
|
|
||||||
sync: ServerSync
|
|
||||||
active: Accessor<boolean>
|
|
||||||
directory: Accessor<string | undefined>
|
|
||||||
sessionID: Accessor<string | undefined>
|
|
||||||
platform: ReturnType<typeof usePlatform>
|
|
||||||
settings: ReturnType<typeof useSettings>
|
|
||||||
language: ReturnType<typeof useLanguage>
|
|
||||||
}) {
|
|
||||||
const serverSDK = () => input.sdk
|
|
||||||
const serverSync = () => input.sync
|
|
||||||
const platform = input.platform
|
|
||||||
const settings = input.settings
|
|
||||||
const language = input.language
|
|
||||||
|
|
||||||
const empty: Notification[] = []
|
|
||||||
|
|
||||||
const currentDirectory = input.directory
|
|
||||||
const currentSession = input.sessionID
|
|
||||||
|
|
||||||
const [store, setStore, _, ready] = persisted(
|
|
||||||
Persist.serverGlobal(serverSDK().scope, "notification", ["notification.v1"]),
|
|
||||||
createStore({
|
|
||||||
list: [] as Notification[],
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
const [index, setIndex] = createStore<NotificationIndex>(buildNotificationIndex(store.list))
|
|
||||||
|
|
||||||
const meta = { pruned: false, disposed: false }
|
|
||||||
|
|
||||||
const updateUnseen = (scope: "session" | "project", key: string, unseen: Notification[]) => {
|
|
||||||
setIndex(scope, "unseen", key, unseen)
|
|
||||||
setIndex(scope, "unseenCount", key, unseen.length)
|
|
||||||
setIndex(
|
|
||||||
scope,
|
|
||||||
"unseenHasError",
|
|
||||||
key,
|
|
||||||
unseen.some((notification) => notification.type === "error"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const appendToIndex = (notification: Notification) => {
|
|
||||||
if (notification.session) {
|
|
||||||
setIndex("session", "all", notification.session, (all = []) => [...all, notification])
|
|
||||||
if (!notification.viewed) {
|
|
||||||
setIndex("session", "unseen", notification.session, (unseen = []) => [...unseen, notification])
|
|
||||||
setIndex("session", "unseenCount", notification.session, (count = 0) => count + 1)
|
|
||||||
if (notification.type === "error") setIndex("session", "unseenHasError", notification.session, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notification.directory) {
|
|
||||||
setIndex("project", "all", notification.directory, (all = []) => [...all, notification])
|
|
||||||
if (!notification.viewed) {
|
|
||||||
setIndex("project", "unseen", notification.directory, (unseen = []) => [...unseen, notification])
|
|
||||||
setIndex("project", "unseenCount", notification.directory, (count = 0) => count + 1)
|
|
||||||
if (notification.type === "error") setIndex("project", "unseenHasError", notification.directory, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeFromIndex = (notification: Notification) => {
|
|
||||||
if (notification.session) {
|
|
||||||
setIndex("session", "all", notification.session, (all = []) => all.filter((n) => n !== notification))
|
|
||||||
if (!notification.viewed) {
|
|
||||||
const unseen = (index.session.unseen[notification.session] ?? empty).filter((n) => n !== notification)
|
|
||||||
updateUnseen("session", notification.session, unseen)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notification.directory) {
|
|
||||||
setIndex("project", "all", notification.directory, (all = []) => all.filter((n) => n !== notification))
|
|
||||||
if (!notification.viewed) {
|
|
||||||
const unseen = (index.project.unseen[notification.directory] ?? empty).filter((n) => n !== notification)
|
|
||||||
updateUnseen("project", notification.directory, unseen)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (!ready()) return
|
|
||||||
if (meta.pruned) return
|
|
||||||
meta.pruned = true
|
|
||||||
const list = pruneNotifications(store.list)
|
|
||||||
batch(() => {
|
|
||||||
setStore("list", list)
|
|
||||||
setIndex(reconcile(buildNotificationIndex(list), { merge: false }))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const append = (notification: Notification) => {
|
|
||||||
const list = pruneNotifications([...store.list, notification])
|
|
||||||
const keep = new Set(list)
|
|
||||||
const removed = store.list.filter((n) => !keep.has(n))
|
|
||||||
|
|
||||||
batch(() => {
|
|
||||||
if (keep.has(notification)) appendToIndex(notification)
|
|
||||||
removed.forEach((n) => removeFromIndex(n))
|
|
||||||
setStore("list", list)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const lookup = async (directory: string, sessionID?: string) => {
|
|
||||||
if (!sessionID) return undefined
|
|
||||||
const sync = serverSync().ensureDirSyncContext(directory)
|
|
||||||
const session = sync.session.get(sessionID)
|
|
||||||
if (session) return session
|
|
||||||
return sync.session
|
|
||||||
.sync(sessionID)
|
|
||||||
.then(() => sync.session.get(sessionID))
|
|
||||||
.catch(() => undefined)
|
|
||||||
}
|
|
||||||
|
|
||||||
const viewedInCurrentSession = (directory: string, sessionID?: string) => {
|
|
||||||
if (!input.active()) return false
|
|
||||||
const activeDirectory = currentDirectory()
|
|
||||||
const activeSession = currentSession()
|
|
||||||
if (!activeSession) return false
|
|
||||||
if (!sessionID) return false
|
|
||||||
if (activeDirectory && directory !== activeDirectory) return false
|
|
||||||
return sessionID === activeSession
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSessionIdle = (directory: string, event: { properties: { sessionID?: string } }, time: number) => {
|
|
||||||
const sessionID = event.properties.sessionID
|
|
||||||
void lookup(directory, sessionID).then((session) => {
|
|
||||||
if (meta.disposed) return
|
|
||||||
if (!session) return
|
|
||||||
if (session.parentID) return
|
|
||||||
|
|
||||||
if (settings.sounds.agentEnabled()) {
|
|
||||||
void playSoundById(settings.sounds.agent())
|
|
||||||
}
|
|
||||||
|
|
||||||
append({
|
|
||||||
directory,
|
|
||||||
time,
|
|
||||||
viewed: viewedInCurrentSession(directory, sessionID),
|
|
||||||
type: "turn-complete",
|
|
||||||
session: sessionID,
|
|
||||||
})
|
|
||||||
|
|
||||||
const href = `/${base64Encode(directory)}/session/${sessionID}`
|
|
||||||
if (settings.notifications.agent()) {
|
|
||||||
void platform.notify(language.t("notification.session.responseReady.title"), session.title ?? sessionID, href)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSessionError = (
|
|
||||||
directory: string,
|
|
||||||
event: { properties: { sessionID?: string; error?: EventSessionError["properties"]["error"] } },
|
|
||||||
time: number,
|
|
||||||
) => {
|
|
||||||
const sessionID = event.properties.sessionID
|
|
||||||
void lookup(directory, sessionID).then((session) => {
|
|
||||||
if (meta.disposed) return
|
|
||||||
if (session?.parentID) return
|
|
||||||
|
|
||||||
if (settings.sounds.errorsEnabled()) {
|
|
||||||
void playSoundById(settings.sounds.errors())
|
|
||||||
}
|
|
||||||
|
|
||||||
const error = "error" in event.properties ? event.properties.error : undefined
|
|
||||||
append({
|
|
||||||
directory,
|
|
||||||
time,
|
|
||||||
viewed: viewedInCurrentSession(directory, sessionID),
|
|
||||||
type: "error",
|
|
||||||
session: sessionID ?? "global",
|
|
||||||
error,
|
|
||||||
})
|
|
||||||
const description =
|
|
||||||
session?.title ??
|
|
||||||
(typeof error === "string" ? error : language.t("notification.session.error.fallbackDescription"))
|
|
||||||
const href = sessionID ? `/${base64Encode(directory)}/session/${sessionID}` : `/${base64Encode(directory)}`
|
|
||||||
if (settings.notifications.errors()) {
|
|
||||||
void platform.notify(language.t("notification.session.error.title"), description, href)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const unsub = serverSDK().event.listen((e) => {
|
|
||||||
const event = e.details
|
|
||||||
if (event.type !== "session.idle" && event.type !== "session.error") return
|
|
||||||
|
|
||||||
const directory = e.name
|
|
||||||
const time = Date.now()
|
|
||||||
if (event.type === "session.idle") {
|
|
||||||
handleSessionIdle(directory, event, time)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
handleSessionError(directory, event, time)
|
|
||||||
})
|
|
||||||
onCleanup(() => {
|
|
||||||
meta.disposed = true
|
|
||||||
unsub()
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
ready,
|
|
||||||
session: {
|
|
||||||
all(session: string) {
|
|
||||||
return index.session.all[session] ?? empty
|
|
||||||
},
|
|
||||||
unseen(session: string) {
|
|
||||||
return index.session.unseen[session] ?? empty
|
|
||||||
},
|
|
||||||
unseenCount(session: string) {
|
|
||||||
return index.session.unseenCount[session] ?? 0
|
|
||||||
},
|
|
||||||
unseenHasError(session: string) {
|
|
||||||
return index.session.unseenHasError[session] ?? false
|
|
||||||
},
|
|
||||||
markViewed(session: string) {
|
|
||||||
const unseen = index.session.unseen[session] ?? empty
|
|
||||||
if (!unseen.length) return
|
|
||||||
|
|
||||||
const projects = [
|
|
||||||
...new Set(unseen.flatMap((notification) => (notification.directory ? [notification.directory] : []))),
|
|
||||||
]
|
|
||||||
batch(() => {
|
|
||||||
setStore("list", (n) => n.session === session && !n.viewed, "viewed", true)
|
|
||||||
updateUnseen("session", session, [])
|
|
||||||
projects.forEach((directory) => {
|
|
||||||
const next = (index.project.unseen[directory] ?? empty).filter(
|
|
||||||
(notification) => notification.session !== session,
|
|
||||||
)
|
|
||||||
updateUnseen("project", directory, next)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
project: {
|
|
||||||
all(directory: string) {
|
|
||||||
return index.project.all[directory] ?? empty
|
|
||||||
},
|
|
||||||
unseen(directory: string) {
|
|
||||||
return index.project.unseen[directory] ?? empty
|
|
||||||
},
|
|
||||||
unseenCount(directory: string) {
|
|
||||||
return index.project.unseenCount[directory] ?? 0
|
|
||||||
},
|
|
||||||
unseenHasError(directory: string) {
|
|
||||||
return index.project.unseenHasError[directory] ?? false
|
|
||||||
},
|
|
||||||
markViewed(directory: string) {
|
|
||||||
const unseen = index.project.unseen[directory] ?? empty
|
|
||||||
if (!unseen.length) return
|
|
||||||
|
|
||||||
const sessions = [
|
|
||||||
...new Set(unseen.flatMap((notification) => (notification.session ? [notification.session] : []))),
|
|
||||||
]
|
|
||||||
batch(() => {
|
|
||||||
setStore("list", (n) => n.directory === directory && !n.viewed, "viewed", true)
|
|
||||||
updateUnseen("project", directory, [])
|
|
||||||
sessions.forEach((session) => {
|
|
||||||
const next = (index.session.unseen[session] ?? empty).filter(
|
|
||||||
(notification) => notification.directory !== directory,
|
|
||||||
)
|
|
||||||
updateUnseen("session", session, next)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { useTabs, type Tab } from "./tabs"
|
|||||||
import { ServerConnection } from "./server"
|
import { ServerConnection } from "./server"
|
||||||
import { requireServerKey } from "@/utils/session-route"
|
import { requireServerKey } from "@/utils/session-route"
|
||||||
import { useSettings } from "./settings"
|
import { useSettings } from "./settings"
|
||||||
import type { FilePartSource } from "@opencode-ai/sdk/v2/client"
|
|
||||||
|
|
||||||
interface PartBase {
|
interface PartBase {
|
||||||
content: string
|
content: string
|
||||||
@@ -28,10 +27,6 @@ export interface FileAttachmentPart extends PartBase {
|
|||||||
type: "file"
|
type: "file"
|
||||||
path: string
|
path: string
|
||||||
selection?: FileSelection
|
selection?: FileSelection
|
||||||
mime?: string
|
|
||||||
filename?: string
|
|
||||||
url?: string
|
|
||||||
source?: FilePartSource
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentPart extends PartBase {
|
export interface AgentPart extends PartBase {
|
||||||
@@ -78,13 +73,7 @@ function isPartEqual(partA: ContentPart, partB: ContentPart) {
|
|||||||
case "text":
|
case "text":
|
||||||
return partB.type === "text" && partA.content === partB.content
|
return partB.type === "text" && partA.content === partB.content
|
||||||
case "file":
|
case "file":
|
||||||
return (
|
return partB.type === "file" && partA.path === partB.path && isSelectionEqual(partA.selection, partB.selection)
|
||||||
partB.type === "file" &&
|
|
||||||
partA.path === partB.path &&
|
|
||||||
partA.mime === partB.mime &&
|
|
||||||
partA.filename === partB.filename &&
|
|
||||||
isSelectionEqual(partA.selection, partB.selection)
|
|
||||||
)
|
|
||||||
case "agent":
|
case "agent":
|
||||||
return partB.type === "agent" && partA.name === partB.name
|
return partB.type === "agent" && partA.name === partB.name
|
||||||
case "image":
|
case "image":
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { coalesceServerEvents, enqueueServerEvent, resumeStreamAfterPageShow } from "./server-sdk"
|
import { coalesceServerEvents, resumeStreamAfterPageShow } from "./server-sdk"
|
||||||
import type { Event } from "@opencode-ai/sdk/v2/client"
|
import type { Event } from "@opencode-ai/sdk/v2/client"
|
||||||
|
|
||||||
describe("resumeStreamAfterPageShow", () => {
|
describe("resumeStreamAfterPageShow", () => {
|
||||||
@@ -15,23 +15,19 @@ describe("resumeStreamAfterPageShow", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("coalesceServerEvents", () => {
|
describe("coalesceServerEvents", () => {
|
||||||
const delta = (value: string, field = "text", partID = "part") => ({
|
const delta = (value: string, field = "text") => ({
|
||||||
directory: "/repo",
|
directory: "/repo",
|
||||||
payload: {
|
payload: {
|
||||||
type: "message.part.delta",
|
type: "message.part.delta",
|
||||||
properties: { messageID: "msg", partID, field, delta: value },
|
properties: { messageID: "msg", partID: "part", field, delta: value },
|
||||||
} as Event,
|
} as Event,
|
||||||
})
|
})
|
||||||
|
|
||||||
test("merges adjacent deltas for the same field", () => {
|
test("merges adjacent deltas for the same field", () => {
|
||||||
const first = delta("hello ")
|
const result = coalesceServerEvents([delta("hello "), delta("world")])
|
||||||
const second = delta("world")
|
|
||||||
first.payload.id = "first"
|
|
||||||
second.payload.id = "second"
|
|
||||||
const result = coalesceServerEvents([first, second])
|
|
||||||
|
|
||||||
expect(result).toHaveLength(1)
|
expect(result).toHaveLength(1)
|
||||||
expect(result[0]?.payload).toMatchObject({ id: "second", properties: { delta: "hello world" } })
|
expect(result[0]?.payload).toMatchObject({ properties: { delta: "hello world" } })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("preserves event boundaries and distinct fields", () => {
|
test("preserves event boundaries and distinct fields", () => {
|
||||||
@@ -49,112 +45,9 @@ describe("coalesceServerEvents", () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("preserves event ID order across interleaved deltas", () => {
|
test("drops stale deltas", () => {
|
||||||
const first = delta("a")
|
const result = coalesceServerEvents([delta("stale")], new Set(["/repo:msg:part"]))
|
||||||
const other = delta("b", "text", "other")
|
|
||||||
const last = delta("c")
|
|
||||||
first.payload.id = "1"
|
|
||||||
other.payload.id = "2"
|
|
||||||
last.payload.id = "3"
|
|
||||||
|
|
||||||
const result = coalesceServerEvents([first, other, last])
|
expect(result).toEqual([])
|
||||||
|
|
||||||
expect(result.map((event) => event.payload.id)).toEqual(["1", "2", "3"])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("enqueueServerEvent", () => {
|
|
||||||
const partUpdated = (text: string) =>
|
|
||||||
({
|
|
||||||
type: "message.part.updated",
|
|
||||||
properties: {
|
|
||||||
sessionID: "session",
|
|
||||||
part: { id: "part", sessionID: "session", messageID: "message", type: "text", text },
|
|
||||||
},
|
|
||||||
}) as Event
|
|
||||||
|
|
||||||
test("preserves part updates across message remove and re-add barriers", () => {
|
|
||||||
const events: Array<{ directory: string; payload: Event }> = []
|
|
||||||
const enqueue = (payload: Event) => enqueueServerEvent(events, { directory: "/repo", payload })
|
|
||||||
|
|
||||||
enqueue(partUpdated("old"))
|
|
||||||
enqueue({ type: "message.removed", properties: { sessionID: "session", messageID: "message" } } as Event)
|
|
||||||
enqueue({
|
|
||||||
type: "message.updated",
|
|
||||||
properties: {
|
|
||||||
sessionID: "session",
|
|
||||||
info: {
|
|
||||||
id: "message",
|
|
||||||
sessionID: "session",
|
|
||||||
role: "user",
|
|
||||||
time: { created: 1 },
|
|
||||||
agent: "build",
|
|
||||||
model: { providerID: "provider", modelID: "model" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as Event)
|
|
||||||
enqueue(partUpdated("new"))
|
|
||||||
|
|
||||||
expect(events.map((event) => event.payload.type)).toEqual([
|
|
||||||
"message.part.updated",
|
|
||||||
"message.removed",
|
|
||||||
"message.updated",
|
|
||||||
"message.part.updated",
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
test("preserves deltas after a replacement snapshot", () => {
|
|
||||||
const events: Array<{ directory: string; payload: Event }> = []
|
|
||||||
const enqueue = (payload: Event) => enqueueServerEvent(events, { directory: "/repo", payload })
|
|
||||||
|
|
||||||
enqueue(partUpdated("a"))
|
|
||||||
enqueue(partUpdated("ab"))
|
|
||||||
enqueue({
|
|
||||||
type: "message.part.delta",
|
|
||||||
properties: { sessionID: "session", messageID: "message", partID: "part", field: "text", delta: "c" },
|
|
||||||
} as Event)
|
|
||||||
|
|
||||||
const result = coalesceServerEvents(events)
|
|
||||||
expect(result.map((event) => event.payload.type)).toEqual(["message.part.updated", "message.part.delta"])
|
|
||||||
expect(result[0]?.payload).toMatchObject({ properties: { part: { text: "ab" } } })
|
|
||||||
expect(result[1]?.payload).toMatchObject({ properties: { delta: "c" } })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("preserves updates after session deletion", () => {
|
|
||||||
const events: Array<{ directory: string; payload: Event }> = []
|
|
||||||
const enqueue = (payload: Event) => enqueueServerEvent(events, { directory: "/repo", payload })
|
|
||||||
|
|
||||||
enqueue(partUpdated("old"))
|
|
||||||
enqueue({
|
|
||||||
type: "session.deleted",
|
|
||||||
properties: { sessionID: "session", info: { id: "session" } },
|
|
||||||
} as Event)
|
|
||||||
enqueue(partUpdated("new"))
|
|
||||||
|
|
||||||
expect(events.map((event) => event.payload.type)).toEqual([
|
|
||||||
"message.part.updated",
|
|
||||||
"session.deleted",
|
|
||||||
"message.part.updated",
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
test("does not coalesce edge-triggered session statuses", () => {
|
|
||||||
const events: Array<{ directory: string; payload: Event }> = []
|
|
||||||
const enqueue = (status: "retry" | "busy") =>
|
|
||||||
enqueueServerEvent(events, {
|
|
||||||
directory: "/repo",
|
|
||||||
payload: {
|
|
||||||
type: "session.status",
|
|
||||||
properties: {
|
|
||||||
sessionID: "session",
|
|
||||||
status: status === "retry" ? { type: "retry", attempt: 1, message: "retry", next: 1 } : { type: "busy" },
|
|
||||||
},
|
|
||||||
} as Event,
|
|
||||||
})
|
|
||||||
|
|
||||||
enqueue("retry")
|
|
||||||
enqueue("busy")
|
|
||||||
|
|
||||||
expect(events).toHaveLength(2)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,56 +17,34 @@ const isAbortError = (error: unknown) =>
|
|||||||
const isStreamClosed = (error: unknown, signal?: AbortSignal) => isAbortError(error) || signal?.aborted === true
|
const isStreamClosed = (error: unknown, signal?: AbortSignal) => isAbortError(error) || signal?.aborted === true
|
||||||
type QueuedServerEvent = { directory: string; payload: Event }
|
type QueuedServerEvent = { directory: string; payload: Event }
|
||||||
|
|
||||||
const coalescedKey = (event: QueuedServerEvent) => {
|
const deltaKey = (directory: string, messageID: string, partID: string) => `${directory}:${messageID}:${partID}`
|
||||||
if (event.payload.type === "lsp.updated") return `lsp.updated:${event.directory}`
|
|
||||||
if (event.payload.type === "message.part.updated") {
|
|
||||||
const part = event.payload.properties.part
|
|
||||||
return `message.part.updated:${event.directory}:${part.messageID}:${part.id}`
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export function enqueueServerEvent(queue: QueuedServerEvent[], event: QueuedServerEvent) {
|
export function coalesceServerEvents(events: QueuedServerEvent[], stale?: Set<string>) {
|
||||||
const key = coalescedKey(event)
|
|
||||||
const previous = queue[queue.length - 1]
|
|
||||||
if (key && previous && coalescedKey(previous) === key) {
|
|
||||||
queue[queue.length - 1] = event
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
queue.push(event)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
export function coalesceServerEvents(events: QueuedServerEvent[]) {
|
|
||||||
const output: QueuedServerEvent[] = []
|
const output: QueuedServerEvent[] = []
|
||||||
|
const deltas = new Map<string, number>()
|
||||||
events.forEach((event) => {
|
events.forEach((event) => {
|
||||||
|
if (stale && event.payload.type === "message.part.delta") {
|
||||||
|
const props = event.payload.properties
|
||||||
|
if (stale.has(deltaKey(event.directory, props.messageID, props.partID))) return
|
||||||
|
}
|
||||||
if (event.payload.type !== "message.part.delta") {
|
if (event.payload.type !== "message.part.delta") {
|
||||||
|
deltas.clear()
|
||||||
output.push(event)
|
output.push(event)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const props = event.payload.properties
|
const props = event.payload.properties
|
||||||
const previous = output[output.length - 1]
|
const id = `${deltaKey(event.directory, props.messageID, props.partID)}:${props.field}`
|
||||||
if (
|
const index = deltas.get(id)
|
||||||
!previous ||
|
const existing = index === undefined ? undefined : output[index]
|
||||||
previous.payload.type !== "message.part.delta" ||
|
if (!existing || existing.payload.type !== "message.part.delta") {
|
||||||
previous.directory !== event.directory ||
|
deltas.set(id, output.length)
|
||||||
previous.payload.properties.messageID !== props.messageID ||
|
|
||||||
previous.payload.properties.partID !== props.partID ||
|
|
||||||
previous.payload.properties.field !== props.field
|
|
||||||
) {
|
|
||||||
output.push({
|
output.push({
|
||||||
directory: event.directory,
|
directory: event.directory,
|
||||||
payload: { ...event.payload, properties: { ...props } },
|
payload: { ...event.payload, properties: { ...props } },
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
output[output.length - 1] = {
|
existing.payload.properties.delta += props.delta
|
||||||
directory: event.directory,
|
|
||||||
payload: {
|
|
||||||
...event.payload,
|
|
||||||
properties: { ...props, delta: previous.payload.properties.delta + props.delta },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
@@ -107,9 +85,20 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS
|
|||||||
|
|
||||||
let queue: Queued[] = []
|
let queue: Queued[] = []
|
||||||
let buffer: Queued[] = []
|
let buffer: Queued[] = []
|
||||||
|
const coalesced = new Map<string, number>()
|
||||||
|
const staleDeltas = new Set<string>()
|
||||||
let timer: ReturnType<typeof setTimeout> | undefined
|
let timer: ReturnType<typeof setTimeout> | undefined
|
||||||
let last = 0
|
let last = 0
|
||||||
|
|
||||||
|
const key = (directory: string, payload: Event) => {
|
||||||
|
if (payload.type === "session.status") return `session.status:${directory}:${payload.properties.sessionID}`
|
||||||
|
if (payload.type === "lsp.updated") return `lsp.updated:${directory}`
|
||||||
|
if (payload.type === "message.part.updated") {
|
||||||
|
const part = payload.properties.part
|
||||||
|
return `message.part.updated:${directory}:${part.messageID}:${part.id}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const flush = () => {
|
const flush = () => {
|
||||||
if (timer) clearTimeout(timer)
|
if (timer) clearTimeout(timer)
|
||||||
timer = undefined
|
timer = undefined
|
||||||
@@ -117,12 +106,15 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS
|
|||||||
if (queue.length === 0) return
|
if (queue.length === 0) return
|
||||||
|
|
||||||
const events = queue
|
const events = queue
|
||||||
|
const skip = staleDeltas.size > 0 ? new Set(staleDeltas) : undefined
|
||||||
queue = buffer
|
queue = buffer
|
||||||
buffer = events
|
buffer = events
|
||||||
queue.length = 0
|
queue.length = 0
|
||||||
|
coalesced.clear()
|
||||||
|
staleDeltas.clear()
|
||||||
|
|
||||||
last = Date.now()
|
last = Date.now()
|
||||||
const output = coalesceServerEvents(events)
|
const output = coalesceServerEvents(events, skip)
|
||||||
batch(() => {
|
batch(() => {
|
||||||
output.forEach((event) => emitter.emit(event.directory, event.payload))
|
output.forEach((event) => emitter.emit(event.directory, event.payload))
|
||||||
})
|
})
|
||||||
@@ -192,12 +184,29 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS
|
|||||||
for await (const event of events.stream) {
|
for await (const event of events.stream) {
|
||||||
resetHeartbeat()
|
resetHeartbeat()
|
||||||
streamErrorLogged = false
|
streamErrorLogged = false
|
||||||
if (event.payload.type !== "sync") {
|
const directory = event.directory ?? "global"
|
||||||
const directory = event.directory ?? "global"
|
if (event.payload.type === "sync") {
|
||||||
const payload = event.payload as Event
|
continue
|
||||||
if (enqueueServerEvent(queue, { directory, payload })) schedule()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const payload = event.payload as Event
|
||||||
|
|
||||||
|
const k = key(directory, payload)
|
||||||
|
if (k) {
|
||||||
|
const i = coalesced.get(k)
|
||||||
|
if (i !== undefined) {
|
||||||
|
queue[i] = { directory, payload }
|
||||||
|
if (payload.type === "message.part.updated") {
|
||||||
|
const part = payload.properties.part
|
||||||
|
staleDeltas.add(deltaKey(directory, part.messageID, part.id))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
coalesced.set(k, queue.length)
|
||||||
|
}
|
||||||
|
queue.push({ directory, payload })
|
||||||
|
schedule()
|
||||||
|
|
||||||
if (Date.now() - yielded < STREAM_YIELD_MS) continue
|
if (Date.now() - yielded < STREAM_YIELD_MS) continue
|
||||||
yielded = Date.now()
|
yielded = Date.now()
|
||||||
await wait(0)
|
await wait(0)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -18,67 +18,44 @@ import { rootSession } from "@/utils/session-route"
|
|||||||
import { dropSessionCaches, pickSessionCacheEvictions, SESSION_CACHE_LIMIT } from "./global-sync/session-cache"
|
import { dropSessionCaches, pickSessionCacheEvictions, SESSION_CACHE_LIMIT } from "./global-sync/session-cache"
|
||||||
|
|
||||||
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
||||||
const cmpMessage = (a: Message, b: Message) => a.time.created - b.time.created || cmp(a.id, b.id)
|
|
||||||
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
||||||
const initialMessagePageSize = 2
|
const initialMessagePageSize = 2
|
||||||
const historyMessagePageSize = 200
|
const historyMessagePageSize = 200
|
||||||
const sessionInfoLimit = 2_048
|
const sessionInfoLimit = 2_048
|
||||||
const emptyIDs: ReadonlySet<string> = new Set()
|
|
||||||
|
|
||||||
type OptimisticItem = {
|
type OptimisticItem = {
|
||||||
message: Message
|
message: Message
|
||||||
parts: Part[]
|
parts: Part[]
|
||||||
confirmedParts?: Part[]
|
|
||||||
confirmedMessage?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type MessagePage = {
|
const hasParts = (parts: Part[] | undefined, want: Part[]) => {
|
||||||
session: Message[]
|
if (!parts) return want.length === 0
|
||||||
part: { id: string; part: Part[] }[]
|
return want.every((part) => Binary.search(parts, part.id, (item) => item.id).found)
|
||||||
cursor?: string
|
|
||||||
complete: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Most markers describe the current HTTP attempt; deltaParts persists non-durable stream state across retries.
|
function mergeOptimisticPage(
|
||||||
type MessageLoadState = {
|
page: { session: Message[]; part: { id: string; part: Part[] }[]; cursor?: string; complete: boolean },
|
||||||
touchedMessages: Set<string>
|
items: OptimisticItem[],
|
||||||
removedMessages: Set<string>
|
) {
|
||||||
retainedMessages: Set<string>
|
if (items.length === 0) return { ...page, confirmed: [] as string[] }
|
||||||
touchedParts: Map<string, Set<string>>
|
|
||||||
deltaParts: Map<string, Set<string>>
|
|
||||||
carriedDeltaParts: Map<string, Set<string>>
|
|
||||||
removedParts: Map<string, Set<string>>
|
|
||||||
optimisticParts: Map<string, Set<string>>
|
|
||||||
orphanParents: Set<string>
|
|
||||||
clearedMessageParts: Set<string>
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeOptimisticPage(page: MessagePage, items: OptimisticItem[]) {
|
|
||||||
if (items.length === 0) return { ...page, observed: [] as { messageID: string; parts: Part[] }[] }
|
|
||||||
const session = [...page.session]
|
const session = [...page.session]
|
||||||
const part = new Map(page.part.map((item) => [item.id, item.part]))
|
const part = new Map(page.part.map((item) => [item.id, item.part]))
|
||||||
const observed: { messageID: string; parts: Part[] }[] = []
|
const confirmed: string[] = []
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const result = Binary.search(session, item.message.id, (message) => message.id)
|
const result = Binary.search(session, item.message.id, (message) => message.id)
|
||||||
if (!result.found) session.splice(result.index, 0, item.message)
|
if (!result.found) session.splice(result.index, 0, item.message)
|
||||||
const current = part.get(item.message.id)
|
const current = part.get(item.message.id)
|
||||||
const confirmed = result.found
|
if (result.found && hasParts(current, item.parts)) {
|
||||||
? item.parts.filter((part) => Binary.search(current ?? [], part.id, (value) => value.id).found)
|
confirmed.push(item.message.id)
|
||||||
: []
|
continue
|
||||||
if (result.found) observed.push({ messageID: item.message.id, parts: confirmed })
|
}
|
||||||
part.set(
|
part.set(item.message.id, merge(current ?? [], item.parts))
|
||||||
item.message.id,
|
|
||||||
merge(
|
|
||||||
result.found ? (current ?? []) : merge(item.confirmedParts ?? [], current ?? []),
|
|
||||||
item.parts.filter((part) => !confirmed.includes(part)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...page,
|
...page,
|
||||||
session,
|
session,
|
||||||
part: [...part.entries()].sort((a, b) => cmp(a[0], b[0])).map(([id, parts]) => ({ id, part: parts })),
|
part: [...part.entries()].sort((a, b) => cmp(a[0], b[0])).map(([id, parts]) => ({ id, part: parts })),
|
||||||
observed,
|
confirmed,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,38 +75,7 @@ function merge<T extends { id: string }>(a: readonly T[], b: readonly T[]) {
|
|||||||
return [...items.values()].sort((x, y) => cmp(x.id, y.id))
|
return [...items.values()].sort((x, y) => cmp(x.id, y.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
function reconcileFetched<T extends { id: string }>(
|
export function createServerSession(client: OpencodeClient) {
|
||||||
fetched: T[],
|
|
||||||
current: readonly T[],
|
|
||||||
options: {
|
|
||||||
touched?: ReadonlySet<string>
|
|
||||||
retained?: ReadonlySet<string>
|
|
||||||
preserveUnfetched?: boolean | ((item: T) => boolean)
|
|
||||||
} = {},
|
|
||||||
) {
|
|
||||||
const result = new Map(fetched.map((item) => [item.id, item]))
|
|
||||||
const live = new Map(current.map((item) => [item.id, item]))
|
|
||||||
if (options.preserveUnfetched) {
|
|
||||||
for (const item of current) {
|
|
||||||
if (!result.has(item.id) && (options.preserveUnfetched === true || options.preserveUnfetched(item)))
|
|
||||||
result.set(item.id, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const id of options.retained ?? emptyIDs) {
|
|
||||||
if (result.has(id)) continue
|
|
||||||
const item = live.get(id)
|
|
||||||
if (item) result.set(id, item)
|
|
||||||
}
|
|
||||||
// Events observed while the request is pending are the freshest client state for those identities.
|
|
||||||
for (const id of options.touched ?? emptyIDs) {
|
|
||||||
const item = live.get(id)
|
|
||||||
if (item) result.set(id, item)
|
|
||||||
if (!item) result.delete(id)
|
|
||||||
}
|
|
||||||
return [...result.values()].sort((a, b) => cmp(a.id, b.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createServerSession(client: OpencodeClient, options?: { retry?: typeof retry }) {
|
|
||||||
const [data, setData] = createStore({
|
const [data, setData] = createStore({
|
||||||
info: {} as Record<string, Session | undefined>,
|
info: {} as Record<string, Session | undefined>,
|
||||||
session_status: {} as Record<string, SessionStatus>,
|
session_status: {} as Record<string, SessionStatus>,
|
||||||
@@ -149,32 +95,10 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
|
|||||||
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>>()
|
||||||
const optimistic = new Map<string, Map<string, OptimisticItem>>()
|
const optimistic = new Map<string, Map<string, OptimisticItem>>()
|
||||||
const messageLoads = new Map<string, MessageLoadState>()
|
|
||||||
const pendingParts = new Map<string, Map<string, Set<string>>>()
|
|
||||||
const orphanParts = new Map<string, Set<string>>()
|
|
||||||
const removedMessages = new Map<string, Set<string>>()
|
|
||||||
const deltaBases = new Map<string, { base: string; sessionID: string }>()
|
|
||||||
const deleteMessageParts = (
|
|
||||||
cache: { part: Record<string, Part[] | undefined>; part_text_accum_delta: Record<string, string | undefined> },
|
|
||||||
messageID: string,
|
|
||||||
) => {
|
|
||||||
for (const part of cache.part[messageID] ?? []) {
|
|
||||||
delete cache.part_text_accum_delta[part.id]
|
|
||||||
deltaBases.delete(part.id)
|
|
||||||
}
|
|
||||||
delete cache.part[messageID]
|
|
||||||
}
|
|
||||||
const seen = new Set<string>()
|
const seen = new Set<string>()
|
||||||
const infoSeen = new Set<string>()
|
const infoSeen = new Set<string>()
|
||||||
const pinned = new Map<string, number>()
|
const pinned = new Map<string, number>()
|
||||||
const generations = new Map<string, object>()
|
const generations = new Map<string, number>()
|
||||||
const generation = (sessionID: string) => {
|
|
||||||
const current = generations.get(sessionID)
|
|
||||||
if (current) return current
|
|
||||||
const created = {}
|
|
||||||
generations.set(sessionID, created)
|
|
||||||
return created
|
|
||||||
}
|
|
||||||
const [meta, setMeta] = createStore({
|
const [meta, setMeta] = createStore({
|
||||||
limit: {} as Record<string, number | undefined>,
|
limit: {} as Record<string, number | undefined>,
|
||||||
cursor: {} as Record<string, string | undefined>,
|
cursor: {} as Record<string, string | undefined>,
|
||||||
@@ -191,11 +115,6 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
|
|||||||
const preserve = new Set([
|
const preserve = new Set([
|
||||||
...pinned.keys(),
|
...pinned.keys(),
|
||||||
...requests.keys(),
|
...requests.keys(),
|
||||||
...inflight.keys(),
|
|
||||||
...inflightDiff.keys(),
|
|
||||||
...inflightTodo.keys(),
|
|
||||||
...messageLoads.keys(),
|
|
||||||
...optimistic.keys(),
|
|
||||||
...Object.entries(data.permission)
|
...Object.entries(data.permission)
|
||||||
.filter(([, items]) => items.length > 0)
|
.filter(([, items]) => items.length > 0)
|
||||||
.map(([sessionID]) => sessionID),
|
.map(([sessionID]) => sessionID),
|
||||||
@@ -219,7 +138,6 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
|
|||||||
if (!preserve.has(sessionID)) stale.push(sessionID)
|
if (!preserve.has(sessionID)) stale.push(sessionID)
|
||||||
}
|
}
|
||||||
stale.forEach((sessionID) => infoSeen.delete(sessionID))
|
stale.forEach((sessionID) => infoSeen.delete(sessionID))
|
||||||
stale.forEach((sessionID) => generations.delete(sessionID))
|
|
||||||
setData(
|
setData(
|
||||||
"info",
|
"info",
|
||||||
produce((draft) => stale.forEach((sessionID) => delete draft[sessionID])),
|
produce((draft) => stale.forEach((sessionID) => delete draft[sessionID])),
|
||||||
@@ -233,27 +151,21 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
|
|||||||
if (cached && !options?.force) return Promise.resolve(cached)
|
if (cached && !options?.force) return Promise.resolve(cached)
|
||||||
const pending = requests.get(sessionID)
|
const pending = requests.get(sessionID)
|
||||||
if (pending) return pending
|
if (pending) return pending
|
||||||
const active = generation(sessionID)
|
const generation = generations.get(sessionID) ?? 0
|
||||||
const request = client.session.get({ sessionID }).then((result) => {
|
const request = client.session.get({ sessionID }).then((result) => {
|
||||||
if (!result.data) throw new Error(`Session not found: ${sessionID}`)
|
if (!result.data) throw new Error(`Session not found: ${sessionID}`)
|
||||||
if (generations.get(sessionID) !== active) return result.data
|
if ((generations.get(sessionID) ?? 0) !== generation) return result.data
|
||||||
return remember(result.data)
|
return remember(result.data)
|
||||||
})
|
})
|
||||||
requests.set(sessionID, request)
|
requests.set(sessionID, request)
|
||||||
const cleanup = () => {
|
void request.then(
|
||||||
if (requests.get(sessionID) === request) requests.delete(sessionID)
|
() => {
|
||||||
if (
|
if (requests.get(sessionID) === request) requests.delete(sessionID)
|
||||||
generations.get(sessionID) === active &&
|
},
|
||||||
!data.info[sessionID] &&
|
() => {
|
||||||
!requests.has(sessionID) &&
|
if (requests.get(sessionID) === request) requests.delete(sessionID)
|
||||||
!messageLoads.has(sessionID) &&
|
},
|
||||||
!inflight.has(sessionID) &&
|
)
|
||||||
!inflightDiff.has(sessionID) &&
|
|
||||||
!inflightTodo.has(sessionID)
|
|
||||||
)
|
|
||||||
generations.delete(sessionID)
|
|
||||||
}
|
|
||||||
void request.then(cleanup, cleanup)
|
|
||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,121 +195,15 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
|
|||||||
if (items.size === 0) optimistic.delete(sessionID)
|
if (items.size === 0) optimistic.delete(sessionID)
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearOptimisticPart = (sessionID: string, messageID: string, partID: string) => {
|
|
||||||
const items = optimistic.get(sessionID)
|
|
||||||
const item = items?.get(messageID)
|
|
||||||
if (!items || !item) return
|
|
||||||
const parts = item.parts.filter((part) => part.id !== partID)
|
|
||||||
const confirmedParts = item.confirmedParts?.filter((part) => part.id !== partID)
|
|
||||||
if (parts.length === 0) {
|
|
||||||
clearOptimistic(sessionID, messageID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
items.set(messageID, { ...item, parts, confirmedParts, confirmedMessage: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmOptimisticPart = (sessionID: string, messageID: string, part: Part) => {
|
|
||||||
const items = optimistic.get(sessionID)
|
|
||||||
const item = items?.get(messageID)
|
|
||||||
if (!items || !item) return
|
|
||||||
const parts = item.parts.filter((value) => value.id !== part.id)
|
|
||||||
if (parts.length === 0) {
|
|
||||||
clearOptimistic(sessionID, messageID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
items.set(messageID, {
|
|
||||||
...item,
|
|
||||||
parts,
|
|
||||||
confirmedParts: merge(item.confirmedParts ?? [], [part]),
|
|
||||||
confirmedMessage: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmOptimistic = (sessionID: string, messageID: string, confirmedParts: Part[]) => {
|
|
||||||
const items = optimistic.get(sessionID)
|
|
||||||
const item = items?.get(messageID)
|
|
||||||
if (!items || !item) return
|
|
||||||
const confirmed = new Set(confirmedParts.map((part) => part.id))
|
|
||||||
const parts = item.parts.filter((part) => !confirmed.has(part.id))
|
|
||||||
if (parts.length === 0) {
|
|
||||||
clearOptimistic(sessionID, messageID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
items.set(messageID, {
|
|
||||||
...item,
|
|
||||||
parts,
|
|
||||||
confirmedParts: merge(item.confirmedParts ?? [], confirmedParts),
|
|
||||||
confirmedMessage: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const trackPartChange = (sessionID: string, messageID: string, partID: string) => {
|
|
||||||
const load = messageLoads.get(sessionID)
|
|
||||||
if (!load) return
|
|
||||||
// A part event keeps an existing parent when the fetched page omits it without overriding fetched metadata.
|
|
||||||
const messages = data.message[sessionID]
|
|
||||||
if (messages && Binary.search(messages, messageID, (message) => message.id).found)
|
|
||||||
load.retainedMessages.add(messageID)
|
|
||||||
const parts = load.touchedParts.get(messageID)
|
|
||||||
if (parts) {
|
|
||||||
parts.add(partID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
load.touchedParts.set(messageID, new Set([partID]))
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetMessageLoad = (sessionID: string, load: MessageLoadState) => {
|
|
||||||
load.touchedMessages.clear()
|
|
||||||
load.retainedMessages.clear()
|
|
||||||
load.touchedParts.clear()
|
|
||||||
load.carriedDeltaParts.clear()
|
|
||||||
load.clearedMessageParts.clear()
|
|
||||||
for (const messageID of load.removedMessages) {
|
|
||||||
load.touchedMessages.add(messageID)
|
|
||||||
load.clearedMessageParts.add(messageID)
|
|
||||||
}
|
|
||||||
for (const [messageID, parts] of load.deltaParts) {
|
|
||||||
load.touchedParts.set(messageID, new Set(parts))
|
|
||||||
load.carriedDeltaParts.set(messageID, new Set(parts))
|
|
||||||
const messages = data.message[sessionID]
|
|
||||||
if (messages && Binary.search(messages, messageID, (message) => message.id).found)
|
|
||||||
load.retainedMessages.add(messageID)
|
|
||||||
}
|
|
||||||
for (const [messageID, parts] of load.removedParts) {
|
|
||||||
const touched = load.touchedParts.get(messageID) ?? new Set<string>()
|
|
||||||
parts.forEach((partID) => touched.add(partID))
|
|
||||||
load.touchedParts.set(messageID, touched)
|
|
||||||
const messages = data.message[sessionID]
|
|
||||||
if (messages && Binary.search(messages, messageID, (message) => message.id).found)
|
|
||||||
load.retainedMessages.add(messageID)
|
|
||||||
}
|
|
||||||
for (const [messageID, parts] of load.optimisticParts) {
|
|
||||||
load.removedMessages.delete(messageID)
|
|
||||||
load.clearedMessageParts.add(messageID)
|
|
||||||
load.touchedMessages.add(messageID)
|
|
||||||
const touched = load.touchedParts.get(messageID) ?? new Set<string>()
|
|
||||||
parts.forEach((partID) => touched.add(partID))
|
|
||||||
load.touchedParts.set(messageID, touched)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const evict = (sessionIDs: string[]) => {
|
const evict = (sessionIDs: string[]) => {
|
||||||
if (sessionIDs.length === 0) return
|
if (sessionIDs.length === 0) return
|
||||||
const evicted = new Set(sessionIDs)
|
|
||||||
for (const [partID, item] of deltaBases) {
|
|
||||||
if (evicted.has(item.sessionID)) deltaBases.delete(partID)
|
|
||||||
}
|
|
||||||
sessionIDs.forEach((sessionID) => {
|
sessionIDs.forEach((sessionID) => {
|
||||||
generations.delete(sessionID)
|
generations.set(sessionID, (generations.get(sessionID) ?? 0) + 1)
|
||||||
clearOptimistic(sessionID)
|
clearOptimistic(sessionID)
|
||||||
requests.delete(sessionID)
|
requests.delete(sessionID)
|
||||||
inflight.delete(sessionID)
|
inflight.delete(sessionID)
|
||||||
inflightDiff.delete(sessionID)
|
inflightDiff.delete(sessionID)
|
||||||
inflightTodo.delete(sessionID)
|
inflightTodo.delete(sessionID)
|
||||||
messageLoads.delete(sessionID)
|
|
||||||
pendingParts.delete(sessionID)
|
|
||||||
orphanParts.delete(sessionID)
|
|
||||||
removedMessages.delete(sessionID)
|
|
||||||
})
|
})
|
||||||
setData(
|
setData(
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
@@ -424,7 +230,6 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
|
|||||||
...inflight.keys(),
|
...inflight.keys(),
|
||||||
...inflightDiff.keys(),
|
...inflightDiff.keys(),
|
||||||
...inflightTodo.keys(),
|
...inflightTodo.keys(),
|
||||||
...messageLoads.keys(),
|
|
||||||
...optimistic.keys(),
|
...optimistic.keys(),
|
||||||
...Object.entries(data.permission)
|
...Object.entries(data.permission)
|
||||||
.filter(([, items]) => items.length > 0)
|
.filter(([, items]) => items.length > 0)
|
||||||
@@ -442,11 +247,8 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
|
|||||||
pickSessionCacheEvictions({ seen, keep: sessionID, limit: SESSION_CACHE_LIMIT, preserve: protectedSessions() }),
|
pickSessionCacheEvictions({ seen, keep: sessionID, limit: SESSION_CACHE_LIMIT, preserve: protectedSessions() }),
|
||||||
)
|
)
|
||||||
|
|
||||||
const fetchMessages = async (sessionID: string, limit: number, before?: string, onAttempt?: () => void) => {
|
const fetchMessages = async (sessionID: string, limit: number, before?: string) => {
|
||||||
const response = await (options?.retry ?? retry)(() => {
|
const response = await retry(() => client.session.messages({ sessionID, limit, before }))
|
||||||
onAttempt?.()
|
|
||||||
return client.session.messages({ sessionID, limit, before })
|
|
||||||
})
|
|
||||||
const items = (response.data ?? []).filter((item) => !!item?.info?.id)
|
const items = (response.data ?? []).filter((item) => !!item?.info?.id)
|
||||||
return {
|
return {
|
||||||
session: items.map((item) => cleanMessage(item.info)).sort((a, b) => cmp(a.id, b.id)),
|
session: items.map((item) => cleanMessage(item.info)).sort((a, b) => cmp(a.id, b.id)),
|
||||||
@@ -459,153 +261,30 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const replaceMessages = (sessionID: string, messages: Message[]) => {
|
|
||||||
const messageIDs = new Set(messages.map((message) => message.id))
|
|
||||||
const dropped = (data.message[sessionID] ?? []).filter((message) => !messageIDs.has(message.id))
|
|
||||||
setData("message", sessionID, reconcile(messages, { key: "id" }))
|
|
||||||
setData(
|
|
||||||
produce((draft) => {
|
|
||||||
for (const message of dropped) deleteMessageParts(draft, message.id)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
return messageIDs
|
|
||||||
}
|
|
||||||
|
|
||||||
const replaceParts = (
|
|
||||||
sessionID: string,
|
|
||||||
items: MessagePage["part"],
|
|
||||||
messageIDs: Set<string>,
|
|
||||||
load?: MessageLoadState,
|
|
||||||
) => {
|
|
||||||
for (const item of items) {
|
|
||||||
if (!messageIDs.has(item.id)) continue
|
|
||||||
const fetched = load?.clearedMessageParts.has(item.id)
|
|
||||||
? []
|
|
||||||
: item.part.filter((part) => !SKIP_PARTS.has(part.type))
|
|
||||||
const fetchedIDs = new Set(fetched.map((part) => part.id))
|
|
||||||
const pending = pendingParts.get(sessionID)?.get(item.id)
|
|
||||||
const touched = new Set([...(load?.touchedParts.get(item.id) ?? []), ...(pending ?? [])])
|
|
||||||
for (const part of fetched) {
|
|
||||||
const accumulated = data.part_text_accum_delta[part.id]
|
|
||||||
const base = deltaBases.get(part.id)?.base
|
|
||||||
const preserveDelta =
|
|
||||||
base !== undefined &&
|
|
||||||
accumulated !== undefined &&
|
|
||||||
"text" in part &&
|
|
||||||
typeof part.text === "string" &&
|
|
||||||
part.text.startsWith(base) &&
|
|
||||||
accumulated.startsWith(part.text) &&
|
|
||||||
accumulated !== part.text
|
|
||||||
if (preserveDelta) touched.add(part.id)
|
|
||||||
if (load?.carriedDeltaParts.get(item.id)?.has(part.id) && !preserveDelta) touched.delete(part.id)
|
|
||||||
}
|
|
||||||
for (const partID of load?.carriedDeltaParts.get(item.id) ?? []) {
|
|
||||||
if (!fetchedIDs.has(partID)) touched.delete(partID)
|
|
||||||
}
|
|
||||||
const parts = reconcileFetched(fetched, data.part[item.id] ?? [], { touched })
|
|
||||||
if (!parts.length) {
|
|
||||||
orphanParts.get(sessionID)?.delete(item.id)
|
|
||||||
setData(produce((draft) => deleteMessageParts(draft, item.id)))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const partIDs = new Set(parts.map((part) => part.id))
|
|
||||||
setData(
|
|
||||||
"part_text_accum_delta",
|
|
||||||
produce((draft) => {
|
|
||||||
for (const part of data.part[item.id] ?? []) {
|
|
||||||
if (!partIDs.has(part.id) || !touched.has(part.id)) {
|
|
||||||
delete draft[part.id]
|
|
||||||
deltaBases.delete(part.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
setData("part", item.id, reconcile(parts, { key: "id" }))
|
|
||||||
orphanParts.get(sessionID)?.delete(item.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const applyMessagePage = (
|
|
||||||
sessionID: string,
|
|
||||||
page: MessagePage,
|
|
||||||
load: MessageLoadState | undefined,
|
|
||||||
preserveUnfetched: boolean | ((message: Message) => boolean),
|
|
||||||
cleanupOrphans: boolean,
|
|
||||||
) => {
|
|
||||||
const merged = mergeOptimisticPage(page, [...(optimistic.get(sessionID)?.values() ?? [])])
|
|
||||||
merged.observed.forEach((item) => {
|
|
||||||
if (!load?.clearedMessageParts.has(item.messageID)) confirmOptimistic(sessionID, item.messageID, item.parts)
|
|
||||||
})
|
|
||||||
const touchedMessages = new Set([...(load?.touchedMessages ?? []), ...(removedMessages.get(sessionID) ?? [])])
|
|
||||||
const messages = reconcileFetched(merged.session, data.message[sessionID] ?? [], {
|
|
||||||
touched: touchedMessages,
|
|
||||||
retained: load?.retainedMessages,
|
|
||||||
preserveUnfetched,
|
|
||||||
})
|
|
||||||
batch(() => {
|
|
||||||
const messageIDs = replaceMessages(sessionID, messages)
|
|
||||||
replaceParts(sessionID, merged.part, messageIDs, load)
|
|
||||||
const orphans = orphanParts.get(sessionID)
|
|
||||||
if (cleanupOrphans && page.complete && orphans) {
|
|
||||||
for (const messageID of orphans) {
|
|
||||||
if (!messageIDs.has(messageID)) setData(produce((draft) => deleteMessageParts(draft, messageID)))
|
|
||||||
}
|
|
||||||
orphanParts.delete(sessionID)
|
|
||||||
}
|
|
||||||
setMeta("limit", sessionID, messages.length)
|
|
||||||
setMeta("cursor", sessionID, merged.cursor)
|
|
||||||
setMeta("complete", sessionID, merged.complete)
|
|
||||||
setMeta("at", sessionID, Date.now())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadMessages = async (sessionID: string, limit: number, before?: string, mode?: "replace" | "prepend") => {
|
const loadMessages = async (sessionID: string, limit: number, before?: string, mode?: "replace" | "prepend") => {
|
||||||
if (meta.loading[sessionID]) return
|
if (meta.loading[sessionID]) return
|
||||||
const active = generation(sessionID)
|
const generation = generations.get(sessionID) ?? 0
|
||||||
const load: MessageLoadState = {
|
|
||||||
touchedMessages: new Set(),
|
|
||||||
removedMessages: new Set(),
|
|
||||||
retainedMessages: new Set(),
|
|
||||||
touchedParts: new Map(),
|
|
||||||
deltaParts: new Map(),
|
|
||||||
carriedDeltaParts: new Map(),
|
|
||||||
removedParts: new Map(),
|
|
||||||
optimisticParts: new Map(),
|
|
||||||
orphanParents: new Set(),
|
|
||||||
clearedMessageParts: new Set(),
|
|
||||||
}
|
|
||||||
messageLoads.set(sessionID, load)
|
|
||||||
setMeta("loading", sessionID, true)
|
setMeta("loading", sessionID, true)
|
||||||
let applied = false
|
await fetchMessages(sessionID, limit, before)
|
||||||
await fetchMessages(sessionID, limit, before, () => resetMessageLoad(sessionID, load))
|
|
||||||
.then((page) => {
|
.then((page) => {
|
||||||
if (generations.get(sessionID) !== active) return
|
if ((generations.get(sessionID) ?? 0) !== generation) return
|
||||||
const first = page.session.reduce<Message | undefined>(
|
const next = mergeOptimisticPage(page, [...(optimistic.get(sessionID)?.values() ?? [])])
|
||||||
(oldest, message) => (!oldest || cmpMessage(message, oldest) < 0 ? message : oldest),
|
next.confirmed.forEach((messageID) => clearOptimistic(sessionID, messageID))
|
||||||
undefined,
|
const messages = mode === "prepend" ? merge(data.message[sessionID] ?? [], next.session) : next.session
|
||||||
)
|
batch(() => {
|
||||||
const preserveUnfetched =
|
setData("message", sessionID, reconcile(messages, { key: "id" }))
|
||||||
mode === "prepend" || (!page.complete && (!first || ((message: Message) => cmpMessage(message, first) < 0)))
|
for (const item of next.part) {
|
||||||
applyMessagePage(
|
const parts = item.part.filter((part) => !SKIP_PARTS.has(part.type))
|
||||||
sessionID,
|
if (parts.length) setData("part", item.id, reconcile(parts, { key: "id" }))
|
||||||
page,
|
}
|
||||||
messageLoads.get(sessionID) === load ? load : undefined,
|
setMeta("limit", sessionID, messages.length)
|
||||||
preserveUnfetched,
|
setMeta("cursor", sessionID, next.cursor)
|
||||||
mode !== "prepend",
|
setMeta("complete", sessionID, next.complete)
|
||||||
)
|
setMeta("at", sessionID, Date.now())
|
||||||
applied = true
|
})
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
if (!applied && generations.get(sessionID) === active && messageLoads.get(sessionID) === load) {
|
if ((generations.get(sessionID) ?? 0) === generation) setMeta("loading", sessionID, false)
|
||||||
for (const messageID of load.orphanParents) {
|
|
||||||
if (!orphanParts.get(sessionID)?.has(messageID)) continue
|
|
||||||
setData(produce((draft) => deleteMessageParts(draft, messageID)))
|
|
||||||
orphanParts.get(sessionID)?.delete(messageID)
|
|
||||||
}
|
|
||||||
if (orphanParts.get(sessionID)?.size === 0) orphanParts.delete(sessionID)
|
|
||||||
}
|
|
||||||
if (messageLoads.get(sessionID) === load) messageLoads.delete(sessionID)
|
|
||||||
if (generations.get(sessionID) === active) setMeta("loading", sessionID, false)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -660,13 +339,7 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
|
|||||||
const eventID = eventSessionID(event)
|
const eventID = eventSessionID(event)
|
||||||
if (eventID) {
|
if (eventID) {
|
||||||
touch(eventID)
|
touch(eventID)
|
||||||
if (
|
if (!data.info[eventID]) void resolve(eventID).catch(() => {})
|
||||||
!data.info[eventID] &&
|
|
||||||
event.type !== "session.created" &&
|
|
||||||
event.type !== "session.updated" &&
|
|
||||||
event.type !== "session.deleted"
|
|
||||||
)
|
|
||||||
void resolve(eventID).catch(() => {})
|
|
||||||
}
|
}
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case "session.created":
|
case "session.created":
|
||||||
@@ -705,21 +378,6 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
|
|||||||
}
|
}
|
||||||
case "message.updated": {
|
case "message.updated": {
|
||||||
const info = cleanMessage((event.properties as { info: Message }).info)
|
const info = cleanMessage((event.properties as { info: Message }).info)
|
||||||
const load = messageLoads.get(info.sessionID)
|
|
||||||
load?.touchedMessages.add(info.id)
|
|
||||||
load?.removedMessages.delete(info.id)
|
|
||||||
const items = optimistic.get(info.sessionID)
|
|
||||||
const item = items?.get(info.id)
|
|
||||||
if (items && item) {
|
|
||||||
if (item.parts.length === 0) clearOptimistic(info.sessionID, info.id)
|
|
||||||
if (item.parts.length > 0) items.set(info.id, { ...item, confirmedMessage: true })
|
|
||||||
}
|
|
||||||
const orphans = orphanParts.get(info.sessionID)
|
|
||||||
orphans?.delete(info.id)
|
|
||||||
if (orphans?.size === 0) orphanParts.delete(info.sessionID)
|
|
||||||
const removedMessagesForSession = removedMessages.get(info.sessionID)
|
|
||||||
removedMessagesForSession?.delete(info.id)
|
|
||||||
if (removedMessagesForSession?.size === 0) removedMessages.delete(info.sessionID)
|
|
||||||
const messages = data.message[info.sessionID]
|
const messages = data.message[info.sessionID]
|
||||||
if (!messages) {
|
if (!messages) {
|
||||||
setData("message", info.sessionID, [info])
|
setData("message", info.sessionID, [info])
|
||||||
@@ -737,20 +395,6 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
|
|||||||
}
|
}
|
||||||
case "message.removed": {
|
case "message.removed": {
|
||||||
const props = event.properties as { sessionID: string; messageID: string }
|
const props = event.properties as { sessionID: string; messageID: string }
|
||||||
const load = messageLoads.get(props.sessionID)
|
|
||||||
load?.touchedMessages.add(props.messageID)
|
|
||||||
load?.removedMessages.add(props.messageID)
|
|
||||||
load?.clearedMessageParts.add(props.messageID)
|
|
||||||
load?.deltaParts.delete(props.messageID)
|
|
||||||
load?.carriedDeltaParts.delete(props.messageID)
|
|
||||||
load?.removedParts.delete(props.messageID)
|
|
||||||
load?.optimisticParts.delete(props.messageID)
|
|
||||||
pendingParts.get(props.sessionID)?.delete(props.messageID)
|
|
||||||
if (pendingParts.get(props.sessionID)?.size === 0) pendingParts.delete(props.sessionID)
|
|
||||||
const removedMessagesForSession = removedMessages.get(props.sessionID) ?? new Set<string>()
|
|
||||||
removedMessagesForSession.add(props.messageID)
|
|
||||||
removedMessages.set(props.sessionID, removedMessagesForSession)
|
|
||||||
clearOptimistic(props.sessionID, props.messageID)
|
|
||||||
setData(
|
setData(
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
const messages = draft.message[props.sessionID]
|
const messages = draft.message[props.sessionID]
|
||||||
@@ -758,7 +402,8 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
|
|||||||
const result = Binary.search(messages, props.messageID, (message) => message.id)
|
const result = Binary.search(messages, props.messageID, (message) => message.id)
|
||||||
if (result.found) messages.splice(result.index, 1)
|
if (result.found) messages.splice(result.index, 1)
|
||||||
}
|
}
|
||||||
deleteMessageParts(draft, props.messageID)
|
for (const part of draft.part[props.messageID] ?? []) delete draft.part_text_accum_delta[part.id]
|
||||||
|
delete draft.part[props.messageID]
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -766,42 +411,6 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
|
|||||||
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)) return
|
if (SKIP_PARTS.has(part.type)) return
|
||||||
const messages = data.message[part.sessionID]
|
|
||||||
const load = messageLoads.get(part.sessionID)
|
|
||||||
const missing = !messages || !Binary.search(messages, part.messageID, (message) => message.id).found
|
|
||||||
// Outside a page load, accepting a part without its ordered parent event would create an unbounded orphan.
|
|
||||||
if (
|
|
||||||
missing &&
|
|
||||||
(!load ||
|
|
||||||
load.clearedMessageParts.has(part.messageID) ||
|
|
||||||
removedMessages.get(part.sessionID)?.has(part.messageID))
|
|
||||||
)
|
|
||||||
return
|
|
||||||
if (missing) {
|
|
||||||
const orphans = orphanParts.get(part.sessionID) ?? new Set<string>()
|
|
||||||
orphans.add(part.messageID)
|
|
||||||
orphanParts.set(part.sessionID, orphans)
|
|
||||||
load?.orphanParents.add(part.messageID)
|
|
||||||
}
|
|
||||||
const deltas = load?.deltaParts.get(part.messageID)
|
|
||||||
deltas?.delete(part.id)
|
|
||||||
if (deltas?.size === 0) load?.deltaParts.delete(part.messageID)
|
|
||||||
const carried = load?.carriedDeltaParts.get(part.messageID)
|
|
||||||
carried?.delete(part.id)
|
|
||||||
if (carried?.size === 0) load?.carriedDeltaParts.delete(part.messageID)
|
|
||||||
const removed = load?.removedParts.get(part.messageID)
|
|
||||||
removed?.delete(part.id)
|
|
||||||
if (removed?.size === 0) load?.removedParts.delete(part.messageID)
|
|
||||||
const pending = pendingParts.get(part.sessionID)?.get(part.messageID)
|
|
||||||
pending?.delete(part.id)
|
|
||||||
if (pending?.size === 0) pendingParts.get(part.sessionID)?.delete(part.messageID)
|
|
||||||
if (pendingParts.get(part.sessionID)?.size === 0) pendingParts.delete(part.sessionID)
|
|
||||||
const optimistic = load?.optimisticParts.get(part.messageID)
|
|
||||||
optimistic?.delete(part.id)
|
|
||||||
if (optimistic?.size === 0) load?.optimisticParts.delete(part.messageID)
|
|
||||||
deltaBases.delete(part.id)
|
|
||||||
trackPartChange(part.sessionID, part.messageID, part.id)
|
|
||||||
confirmOptimisticPart(part.sessionID, part.messageID, part)
|
|
||||||
setData(
|
setData(
|
||||||
"part_text_accum_delta",
|
"part_text_accum_delta",
|
||||||
produce((draft) => void delete draft[part.id]),
|
produce((draft) => void delete draft[part.id]),
|
||||||
@@ -822,34 +431,10 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
case "message.part.removed": {
|
case "message.part.removed": {
|
||||||
const props = event.properties as { sessionID: string; messageID: string; partID: string }
|
const props = event.properties as { messageID: string; partID: string }
|
||||||
// Part removal is event-only on the server, so its tombstone lasts until a later update or eviction.
|
|
||||||
const pending = pendingParts.get(props.sessionID) ?? new Map<string, Set<string>>()
|
|
||||||
const parts = pending.get(props.messageID) ?? new Set<string>()
|
|
||||||
parts.add(props.partID)
|
|
||||||
pending.set(props.messageID, parts)
|
|
||||||
pendingParts.set(props.sessionID, pending)
|
|
||||||
const deltas = messageLoads.get(props.sessionID)?.deltaParts.get(props.messageID)
|
|
||||||
deltas?.delete(props.partID)
|
|
||||||
if (deltas?.size === 0) messageLoads.get(props.sessionID)?.deltaParts.delete(props.messageID)
|
|
||||||
const load = messageLoads.get(props.sessionID)
|
|
||||||
const carried = load?.carriedDeltaParts.get(props.messageID)
|
|
||||||
carried?.delete(props.partID)
|
|
||||||
if (carried?.size === 0) load?.carriedDeltaParts.delete(props.messageID)
|
|
||||||
if (load) {
|
|
||||||
const parts = load.removedParts.get(props.messageID) ?? new Set<string>()
|
|
||||||
parts.add(props.partID)
|
|
||||||
load.removedParts.set(props.messageID, parts)
|
|
||||||
const optimistic = load.optimisticParts.get(props.messageID)
|
|
||||||
optimistic?.delete(props.partID)
|
|
||||||
if (optimistic?.size === 0) load.optimisticParts.delete(props.messageID)
|
|
||||||
}
|
|
||||||
trackPartChange(props.sessionID, props.messageID, props.partID)
|
|
||||||
clearOptimisticPart(props.sessionID, props.messageID, props.partID)
|
|
||||||
setData(
|
setData(
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
delete draft.part_text_accum_delta[props.partID]
|
delete draft.part_text_accum_delta[props.partID]
|
||||||
deltaBases.delete(props.partID)
|
|
||||||
const parts = draft.part[props.messageID]
|
const parts = draft.part[props.messageID]
|
||||||
if (!parts) return
|
if (!parts) return
|
||||||
const result = Binary.search(parts, props.partID, (part) => part.id)
|
const result = Binary.search(parts, props.partID, (part) => part.id)
|
||||||
@@ -860,31 +445,13 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
case "message.part.delta": {
|
case "message.part.delta": {
|
||||||
const props = event.properties as {
|
const props = event.properties as { messageID: string; partID: string; field: string; delta: string }
|
||||||
sessionID: string
|
|
||||||
messageID: string
|
|
||||||
partID: string
|
|
||||||
field: string
|
|
||||||
delta: string
|
|
||||||
}
|
|
||||||
const parts = data.part[props.messageID]
|
const parts = data.part[props.messageID]
|
||||||
if (!parts) return
|
if (!parts) return
|
||||||
const result = Binary.search(parts, props.partID, (part) => part.id)
|
const result = Binary.search(parts, props.partID, (part) => part.id)
|
||||||
if (!result.found) return
|
if (!result.found) return
|
||||||
trackPartChange(props.sessionID, props.messageID, props.partID)
|
|
||||||
const load = messageLoads.get(props.sessionID)
|
|
||||||
if (load) {
|
|
||||||
const parts = load.deltaParts.get(props.messageID) ?? new Set<string>()
|
|
||||||
parts.add(props.partID)
|
|
||||||
load.deltaParts.set(props.messageID, parts)
|
|
||||||
const carried = load.carriedDeltaParts.get(props.messageID)
|
|
||||||
carried?.delete(props.partID)
|
|
||||||
if (carried?.size === 0) load.carriedDeltaParts.delete(props.messageID)
|
|
||||||
}
|
|
||||||
const field = props.field as keyof (typeof parts)[number]
|
const field = props.field as keyof (typeof parts)[number]
|
||||||
const current = parts[result.index]?.[field]
|
const current = parts[result.index]?.[field]
|
||||||
if (!deltaBases.has(props.partID) && typeof current === "string")
|
|
||||||
deltaBases.set(props.partID, { base: current, sessionID: props.sessionID })
|
|
||||||
setData(
|
setData(
|
||||||
"part_text_accum_delta",
|
"part_text_accum_delta",
|
||||||
props.partID,
|
props.partID,
|
||||||
@@ -992,70 +559,32 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
|
|||||||
},
|
},
|
||||||
optimistic: {
|
optimistic: {
|
||||||
add(input: { sessionID: string; message: Message; parts: Part[] }) {
|
add(input: { sessionID: string; message: Message; parts: Part[] }) {
|
||||||
const parts = input.parts
|
|
||||||
.filter((part) => !!part?.id && !SKIP_PARTS.has(part.type))
|
|
||||||
.sort((a, b) => cmp(a.id, b.id))
|
|
||||||
const load = messageLoads.get(input.sessionID)
|
|
||||||
if (load?.clearedMessageParts.has(input.message.id)) {
|
|
||||||
const touched = load.touchedParts.get(input.message.id) ?? new Set<string>()
|
|
||||||
parts.forEach((part) => touched.add(part.id))
|
|
||||||
load.touchedParts.set(input.message.id, touched)
|
|
||||||
}
|
|
||||||
if (load) {
|
|
||||||
load.removedMessages.delete(input.message.id)
|
|
||||||
load.optimisticParts.set(input.message.id, new Set(parts.map((part) => part.id)))
|
|
||||||
}
|
|
||||||
const items = optimistic.get(input.sessionID)
|
const items = optimistic.get(input.sessionID)
|
||||||
const removedMessagesForSession = removedMessages.get(input.sessionID)
|
if (items) items.set(input.message.id, input)
|
||||||
removedMessagesForSession?.delete(input.message.id)
|
if (!items) optimistic.set(input.sessionID, new Map([[input.message.id, input]]))
|
||||||
if (removedMessagesForSession?.size === 0) removedMessages.delete(input.sessionID)
|
|
||||||
if (items) items.set(input.message.id, { ...input, parts, confirmedParts: [] })
|
|
||||||
if (!items)
|
|
||||||
optimistic.set(input.sessionID, new Map([[input.message.id, { ...input, parts, confirmedParts: [] }]]))
|
|
||||||
setData("message", input.sessionID, (messages = []) => merge(messages, [input.message]))
|
setData("message", input.sessionID, (messages = []) => merge(messages, [input.message]))
|
||||||
setData(
|
setData(
|
||||||
"part_text_accum_delta",
|
"part",
|
||||||
produce((draft) => {
|
input.message.id,
|
||||||
for (const part of [...(data.part[input.message.id] ?? []), ...parts]) {
|
input.parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id)),
|
||||||
delete draft[part.id]
|
|
||||||
deltaBases.delete(part.id)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
setData("part", input.message.id, parts)
|
|
||||||
},
|
},
|
||||||
remove(input: { sessionID: string; messageID: string }) {
|
remove(input: { sessionID: string; messageID: string }) {
|
||||||
const item = optimistic.get(input.sessionID)?.get(input.messageID)
|
|
||||||
if (!item) return
|
|
||||||
messageLoads.get(input.sessionID)?.optimisticParts.delete(input.messageID)
|
|
||||||
clearOptimistic(input.sessionID, input.messageID)
|
clearOptimistic(input.sessionID, input.messageID)
|
||||||
if (item.confirmedMessage) {
|
|
||||||
const partIDs = new Set(item.parts.map((part) => part.id))
|
|
||||||
setData(
|
|
||||||
produce((draft) => {
|
|
||||||
for (const part of item.parts) {
|
|
||||||
delete draft.part_text_accum_delta[part.id]
|
|
||||||
deltaBases.delete(part.id)
|
|
||||||
}
|
|
||||||
const parts = draft.part[input.messageID]
|
|
||||||
if (!parts) return
|
|
||||||
draft.part[input.messageID] = parts.filter((part) => !partIDs.has(part.id))
|
|
||||||
if (draft.part[input.messageID]?.length === 0) delete draft.part[input.messageID]
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setData("message", input.sessionID, (messages) => messages?.filter((message) => message.id !== input.messageID))
|
setData("message", input.sessionID, (messages) => messages?.filter((message) => message.id !== input.messageID))
|
||||||
setData(produce((draft) => deleteMessageParts(draft, input.messageID)))
|
setData(
|
||||||
|
"part",
|
||||||
|
produce((draft) => void delete draft[input.messageID]),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
diff(sessionID: string, options?: { force?: boolean }) {
|
diff(sessionID: string, options?: { force?: boolean }) {
|
||||||
touch(sessionID)
|
touch(sessionID)
|
||||||
if (data.session_diff[sessionID] !== undefined && !options?.force) return Promise.resolve()
|
if (data.session_diff[sessionID] !== undefined && !options?.force) return Promise.resolve()
|
||||||
return runInflight(inflightDiff, sessionID, () => {
|
return runInflight(inflightDiff, sessionID, () => {
|
||||||
const active = generation(sessionID)
|
const generation = generations.get(sessionID) ?? 0
|
||||||
return retry(() => client.session.diff({ sessionID })).then((result) => {
|
return retry(() => client.session.diff({ sessionID })).then((result) => {
|
||||||
if (generations.get(sessionID) !== active) return
|
if ((generations.get(sessionID) ?? 0) !== generation) return
|
||||||
setData("session_diff", sessionID, reconcile(cleanDiffs(result.data), { key: "file" }))
|
setData("session_diff", sessionID, reconcile(cleanDiffs(result.data), { key: "file" }))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -1064,9 +593,9 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
|
|||||||
touch(sessionID)
|
touch(sessionID)
|
||||||
if (data.todo[sessionID] !== undefined && !options?.force) return Promise.resolve()
|
if (data.todo[sessionID] !== undefined && !options?.force) return Promise.resolve()
|
||||||
return runInflight(inflightTodo, sessionID, () => {
|
return runInflight(inflightTodo, sessionID, () => {
|
||||||
const active = generation(sessionID)
|
const generation = generations.get(sessionID) ?? 0
|
||||||
return retry(() => client.session.todo({ sessionID })).then((result) => {
|
return retry(() => client.session.todo({ sessionID })).then((result) => {
|
||||||
if (generations.get(sessionID) !== active) return
|
if ((generations.get(sessionID) ?? 0) !== generation) return
|
||||||
setData("todo", sessionID, reconcile(result.data ?? [], { key: "id" }))
|
setData("todo", sessionID, reconcile(result.data ?? [], { key: "id" }))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,11 +1,4 @@
|
|||||||
import type {
|
import type { Config, OpencodeClient, Path, Project, ProviderAuthResponse } from "@opencode-ai/sdk/v2/client"
|
||||||
Config,
|
|
||||||
McpResource,
|
|
||||||
OpencodeClient,
|
|
||||||
Path,
|
|
||||||
Project,
|
|
||||||
ProviderAuthResponse,
|
|
||||||
} from "@opencode-ai/sdk/v2/client"
|
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
import { type Accessor, batch, createMemo, getOwner, onCleanup, onMount, untrack } from "solid-js"
|
import { type Accessor, batch, createMemo, getOwner, onCleanup, onMount, untrack } from "solid-js"
|
||||||
@@ -22,7 +15,6 @@ import {
|
|||||||
loadPathQuery,
|
loadPathQuery,
|
||||||
loadProjectsQuery,
|
loadProjectsQuery,
|
||||||
loadProvidersQuery,
|
loadProvidersQuery,
|
||||||
loadReferencesQuery,
|
|
||||||
} from "./global-sync/bootstrap"
|
} from "./global-sync/bootstrap"
|
||||||
import { createChildStoreManager } from "./global-sync/child-store"
|
import { createChildStoreManager } from "./global-sync/child-store"
|
||||||
import { applyDirectoryEvent, applyGlobalEvent } from "./global-sync/event-reducer"
|
import { applyDirectoryEvent, applyGlobalEvent } from "./global-sync/event-reducer"
|
||||||
@@ -64,13 +56,6 @@ export const loadMcpQuery = (scope: ServerScope, directory: string, sdk: Opencod
|
|||||||
queryFn: () => sdk.mcp.status().then((r) => r.data ?? {}),
|
queryFn: () => sdk.mcp.status().then((r) => r.data ?? {}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const loadMcpResourcesQuery = (scope: ServerScope, directory: string, sdk: OpencodeClient) =>
|
|
||||||
queryOptions<Record<string, McpResource>>({
|
|
||||||
queryKey: [scope, directory, "mcpResources"] as const,
|
|
||||||
queryFn: () => sdk.experimental.resource.list().then((r) => r.data ?? {}),
|
|
||||||
placeholderData: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
export const loadLspQuery = (scope: ServerScope, directory: string, sdk: OpencodeClient) =>
|
export const loadLspQuery = (scope: ServerScope, directory: string, sdk: OpencodeClient) =>
|
||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: [scope, directory, "lsp"] as const,
|
queryKey: [scope, directory, "lsp"] as const,
|
||||||
@@ -90,9 +75,7 @@ function makeQueryOptionsApi(
|
|||||||
path: (directory: PathKey | null) =>
|
path: (directory: PathKey | null) =>
|
||||||
loadPathQuery(scope, directory, directory === null ? serverSDK() : sdkFor(directory)),
|
loadPathQuery(scope, directory, directory === null ? serverSDK() : sdkFor(directory)),
|
||||||
agents: (directory: PathKey) => loadAgentsQuery(scope, directory, sdkFor(directory)),
|
agents: (directory: PathKey) => loadAgentsQuery(scope, directory, sdkFor(directory)),
|
||||||
references: (directory: PathKey) => loadReferencesQuery(scope, directory, sdkFor(directory)),
|
|
||||||
mcp: (directory: PathKey) => loadMcpQuery(scope, directory, sdkFor(directory)),
|
mcp: (directory: PathKey) => loadMcpQuery(scope, directory, sdkFor(directory)),
|
||||||
mcpResources: (directory: PathKey) => loadMcpResourcesQuery(scope, directory, sdkFor(directory)),
|
|
||||||
lsp: (directory: PathKey) => loadLspQuery(scope, directory, sdkFor(directory)),
|
lsp: (directory: PathKey) => loadLspQuery(scope, directory, sdkFor(directory)),
|
||||||
sessions: (directory: PathKey) => ({ queryKey: [scope, directory, "loadSessions"] as const }),
|
sessions: (directory: PathKey) => ({ queryKey: [scope, directory, "loadSessions"] as const }),
|
||||||
}
|
}
|
||||||
@@ -413,9 +396,6 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
|||||||
loadLsp: () => {
|
loadLsp: () => {
|
||||||
void queryClient.fetchQuery(queryOptionsApi.lsp(key))
|
void queryClient.fetchQuery(queryOptionsApi.lsp(key))
|
||||||
},
|
},
|
||||||
loadReferences: () => {
|
|
||||||
void queryClient.fetchQuery(queryOptionsApi.references(key))
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -504,7 +484,6 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
|||||||
},
|
},
|
||||||
refresh: async () => {
|
refresh: async () => {
|
||||||
await queryClient.refetchQueries(queryOptionsApi.mcp(key))
|
await queryClient.refetchQueries(queryOptionsApi.mcp(key))
|
||||||
await queryClient.refetchQueries(queryOptionsApi.mcpResources(key))
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -138,14 +138,12 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
|||||||
const next = { type: "session" as const, ...tab }
|
const next = { type: "session" as const, ...tab }
|
||||||
const existing = store.find((item) => tabKey(item) === tabKey(next))
|
const existing = store.find((item) => tabKey(item) === tabKey(next))
|
||||||
if (existing) return existing
|
if (existing) return existing
|
||||||
void startTransition(() => {
|
setStore(
|
||||||
setStore(
|
produce((tabs) => {
|
||||||
produce((tabs) => {
|
if (tabs.some((item) => tabKey(item) === tabKey(next))) return
|
||||||
if (tabs.some((item) => tabKey(item) === tabKey(next))) return
|
tabs.push(next)
|
||||||
tabs.push(next)
|
}),
|
||||||
}),
|
)
|
||||||
)
|
|
||||||
})
|
|
||||||
return next
|
return next
|
||||||
},
|
},
|
||||||
reorder(keys: string[]) {
|
reorder(keys: string[]) {
|
||||||
@@ -165,29 +163,27 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
|||||||
},
|
},
|
||||||
newDraft(draft: Omit<DraftTab, "type" | "draftID">, prompt?: string) {
|
newDraft(draft: Omit<DraftTab, "type" | "draftID">, prompt?: string) {
|
||||||
const draftID = uuid()
|
const draftID = uuid()
|
||||||
void startTransition(() => {
|
setStore(
|
||||||
setStore(
|
produce((tabs) => {
|
||||||
produce((tabs) => {
|
tabs.push({ type: "draft", draftID, ...draft })
|
||||||
tabs.push({ type: "draft", draftID, ...draft })
|
}),
|
||||||
}),
|
)
|
||||||
)
|
navigate(prompt ? `${draftHref(draftID)}&prompt=${encodeURIComponent(prompt)}` : draftHref(draftID))
|
||||||
navigate(prompt ? `${draftHref(draftID)}&prompt=${encodeURIComponent(prompt)}` : draftHref(draftID))
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
updateDraft(draftID: string, draft: Partial<Omit<DraftTab, "type" | "draftID">>) {
|
updateDraft(draftID: string, draft: Partial<Omit<DraftTab, "type" | "draftID">>) {
|
||||||
void startTransition(() => {
|
setStore(
|
||||||
setStore(
|
(tab) => tab.type === "draft" && tab.draftID === draftID,
|
||||||
(tab) => tab.type === "draft" && tab.draftID === draftID,
|
produce((tab) => Object.assign(tab, draft)),
|
||||||
produce((tab) => Object.assign(tab, draft)),
|
)
|
||||||
)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
promoteDraft(draftID: string, session: Omit<SessionTab, "type">) {
|
promoteDraft(draftID: string, session: Omit<SessionTab, "type">) {
|
||||||
// Keep the replacement and navigation atomic so /new-session never renders
|
// We're viewing this draft when /new-session?draftId=… points at it. Promoting
|
||||||
// after its backing draft tab has been removed from the store.
|
// replaces the draft tab with a session tab, so the draft route would stop resolving
|
||||||
|
// and fall back home. Navigate to the new session first so we leave /new-session
|
||||||
|
// before the draft is removed from the store.
|
||||||
const active = location.pathname === "/new-session" && location.query.draftId === draftID
|
const active = location.pathname === "/new-session" && location.query.draftId === draftID
|
||||||
const next = { type: "session" as const, ...session }
|
const next = { type: "session" as const, ...session }
|
||||||
void startTransition(() => {
|
startTransition(() => {
|
||||||
setStore(
|
setStore(
|
||||||
produce((tabs) => {
|
produce((tabs) => {
|
||||||
const index = tabs.findIndex((tab) => tab.type === "draft" && tab.draftID === draftID)
|
const index = tabs.findIndex((tab) => tab.type === "draft" && tab.draftID === draftID)
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
|
||||||
import { DESKTOP_MENU } from "./desktop-menu"
|
|
||||||
|
|
||||||
describe("desktop menu", () => {
|
|
||||||
test("exports logs through the desktop command registry", () => {
|
|
||||||
const items = DESKTOP_MENU.flatMap((menu) => menu.items ?? []).filter(
|
|
||||||
(item) => item.type === "item" && item.label === "Export Logs...",
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(items).toHaveLength(2)
|
|
||||||
expect(items.every((item) => item.type === "item" && item.command === "logs.export" && !item.action)).toBe(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -656,10 +656,6 @@ export const dict = {
|
|||||||
"session.new.worktree.main": "Main branch",
|
"session.new.worktree.main": "Main branch",
|
||||||
"session.new.worktree.mainWithBranch": "Main branch ({{branch}})",
|
"session.new.worktree.mainWithBranch": "Main branch ({{branch}})",
|
||||||
"session.new.worktree.create": "Create new worktree",
|
"session.new.worktree.create": "Create new worktree",
|
||||||
"session.new.workspace.runIn": "Run session in",
|
|
||||||
"session.new.workspace.triggerLocal": "Local",
|
|
||||||
"session.new.workspace.local": "Local repository",
|
|
||||||
"session.new.workspace.existing": "Workspace…",
|
|
||||||
"session.new.lastModified": "Last modified",
|
"session.new.lastModified": "Last modified",
|
||||||
|
|
||||||
"session.header.search.placeholder": "Search {{project}}",
|
"session.header.search.placeholder": "Search {{project}}",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
@import "@opencode-ai/ui/styles/tailwind";
|
@import "@opencode-ai/ui/styles/tailwind";
|
||||||
@import "@opencode-ai/session-ui/styles";
|
@import "@opencode-ai/session-ui/styles";
|
||||||
@import "@opencode-ai/ui/v2/styles/tailwind.css";
|
@import "@opencode-ai/ui/v2/styles/tailwind.css";
|
||||||
@import "tw-animate-css";
|
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "JetBrainsMono Nerd Font Mono";
|
font-family: "JetBrainsMono Nerd Font Mono";
|
||||||
@@ -108,45 +107,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-session-group-header::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: -12px;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 12px;
|
|
||||||
background: var(--v2-background-bg-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-session-group-header::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 16px;
|
|
||||||
pointer-events: none;
|
|
||||||
background: linear-gradient(
|
|
||||||
180deg,
|
|
||||||
var(--v2-background-bg-base) 0%,
|
|
||||||
color-mix(in srgb, var(--v2-background-bg-base) 92.0456%, transparent) 7.93%,
|
|
||||||
color-mix(in srgb, var(--v2-background-bg-base) 84.9947%, transparent) 14.14%,
|
|
||||||
color-mix(in srgb, var(--v2-background-bg-base) 78.6813%, transparent) 19%,
|
|
||||||
color-mix(in srgb, var(--v2-background-bg-base) 72.9394%, transparent) 22.85%,
|
|
||||||
color-mix(in srgb, var(--v2-background-bg-base) 67.6028%, transparent) 26.05%,
|
|
||||||
color-mix(in srgb, var(--v2-background-bg-base) 62.5055%, transparent) 28.95%,
|
|
||||||
color-mix(in srgb, var(--v2-background-bg-base) 57.4815%, transparent) 31.91%,
|
|
||||||
color-mix(in srgb, var(--v2-background-bg-base) 52.3647%, transparent) 35.27%,
|
|
||||||
color-mix(in srgb, var(--v2-background-bg-base) 46.989%, transparent) 39.4%,
|
|
||||||
color-mix(in srgb, var(--v2-background-bg-base) 41.1884%, transparent) 44.65%,
|
|
||||||
color-mix(in srgb, var(--v2-background-bg-base) 34.7969%, transparent) 51.36%,
|
|
||||||
color-mix(in srgb, var(--v2-background-bg-base) 27.6484%, transparent) 59.9%,
|
|
||||||
color-mix(in srgb, var(--v2-background-bg-base) 19.5767%, transparent) 70.62%,
|
|
||||||
color-mix(in srgb, var(--v2-background-bg-base) 10.416%, transparent) 83.87%,
|
|
||||||
transparent 100%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="titlebar-update-loader"] {
|
[data-slot="titlebar-update-loader"] {
|
||||||
display: block;
|
display: block;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -172,65 +132,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes home-projects-fade-top {
|
@keyframes fade-in {
|
||||||
from {
|
from {
|
||||||
visibility: hidden;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
visibility: visible;
|
opacity: 1;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes home-projects-fade-bottom {
|
|
||||||
from {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="home-projects-scroll"] {
|
|
||||||
timeline-scope: --home-projects-scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="home-projects-scroll"]::before,
|
|
||||||
[data-slot="home-projects-scroll"]::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 10;
|
|
||||||
height: 16px;
|
|
||||||
pointer-events: none;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="home-projects-scroll"]::before {
|
|
||||||
top: 0;
|
|
||||||
background: linear-gradient(to bottom, var(--v2-background-bg-base), transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="home-projects-scroll"]::after {
|
|
||||||
bottom: 0;
|
|
||||||
background: linear-gradient(to top, var(--v2-background-bg-base), transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@supports (animation-timeline: --home-projects-scroll) and (timeline-scope: --home-projects-scroll) {
|
|
||||||
[data-slot="home-projects-scroll"] .scroll-view__viewport {
|
|
||||||
scroll-timeline: --home-projects-scroll y;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="home-projects-scroll"]::before {
|
|
||||||
animation: home-projects-fade-top linear both;
|
|
||||||
animation-timeline: --home-projects-scroll;
|
|
||||||
animation-range: 0 0.1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="home-projects-scroll"]::after {
|
|
||||||
animation: home-projects-fade-bottom linear both;
|
|
||||||
animation-timeline: --home-projects-scroll;
|
|
||||||
animation-range: calc(100% - 1.1px) calc(100% - 1px);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -276,10 +276,7 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div class="relative flex-1 h-screen w-screen min-h-0 flex flex-col items-center justify-center bg-background-base font-sans">
|
||||||
class="relative flex-1 h-screen w-screen min-h-0 flex flex-col items-center justify-center bg-background-base font-sans"
|
|
||||||
data-tauri-drag-region
|
|
||||||
>
|
|
||||||
<div class="w-2/3 max-w-3xl flex flex-col items-center justify-center gap-8">
|
<div class="w-2/3 max-w-3xl flex flex-col items-center justify-center gap-8">
|
||||||
<Logo class="w-58.5 opacity-12 shrink-0" />
|
<Logo class="w-58.5 opacity-12 shrink-0" />
|
||||||
<div class="flex flex-col items-center gap-2 text-center">
|
<div class="flex flex-col items-center gap-2 text-center">
|
||||||
|
|||||||
+123
-277
@@ -1,9 +1,7 @@
|
|||||||
import type { Session } from "@opencode-ai/sdk/v2/client"
|
import type { Session } from "@opencode-ai/sdk/v2/client"
|
||||||
import {
|
import {
|
||||||
type ComponentProps,
|
|
||||||
createEffect,
|
createEffect,
|
||||||
createMemo,
|
createMemo,
|
||||||
createResource,
|
|
||||||
createRoot,
|
createRoot,
|
||||||
For,
|
For,
|
||||||
Match,
|
Match,
|
||||||
@@ -69,10 +67,6 @@ import { archiveHomeSession } from "./home-session-archive"
|
|||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
|
|
||||||
const HOME_SESSION_LIMIT = 64
|
const HOME_SESSION_LIMIT = 64
|
||||||
const HOME_SESSION_HEADER_STICKY_TOP = 12
|
|
||||||
const HOME_SESSION_HEADER_TEXT_HEIGHT = 16
|
|
||||||
const HOME_SESSION_HEADER_FADE_DISTANCE = 16
|
|
||||||
const SHOW_HOME_SESSION_ARCHIVE = false
|
|
||||||
const HOME_ROW_LAYOUT =
|
const HOME_ROW_LAYOUT =
|
||||||
"flex min-w-0 w-full shrink-0 cursor-default items-center rounded-[6px] bg-transparent text-left transition-[background-color,color,box-shadow] duration-[120ms] ease-in-out focus-visible:outline-none"
|
"flex min-w-0 w-full shrink-0 cursor-default items-center rounded-[6px] bg-transparent text-left transition-[background-color,color,box-shadow] duration-[120ms] ease-in-out focus-visible:outline-none"
|
||||||
const HOME_ROW_BASE = `${HOME_ROW_LAYOUT} border-0`
|
const HOME_ROW_BASE = `${HOME_ROW_LAYOUT} border-0`
|
||||||
@@ -137,107 +131,6 @@ function homeSessionSearchKey(record: HomeSessionRecord) {
|
|||||||
return `${pathKey(record.session.directory)}:${record.session.id}`
|
return `${pathKey(record.session.directory)}:${record.session.id}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function useHomeSessionHeaderOpacity(groups: () => HomeSessionGroup[]) {
|
|
||||||
let viewport: HTMLDivElement | undefined
|
|
||||||
let content: HTMLDivElement | undefined
|
|
||||||
let positionFrame: number | undefined
|
|
||||||
let resizeObserver: ResizeObserver | undefined
|
|
||||||
const headerRefs = new Map<HomeSessionGroup["id"], HTMLDivElement>()
|
|
||||||
const headerOffsets = new Map<HomeSessionGroup["id"], number>()
|
|
||||||
const [state, setState] = createStore({
|
|
||||||
titleOpacity: {} as Partial<Record<HomeSessionGroup["id"], number>>,
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const items = groups()
|
|
||||||
const ids = new Set(items.map((group) => group.id))
|
|
||||||
headerRefs.forEach((_, id) => {
|
|
||||||
if (!ids.has(id)) headerRefs.delete(id)
|
|
||||||
})
|
|
||||||
headerOffsets.forEach((_, id) => {
|
|
||||||
if (!ids.has(id)) headerOffsets.delete(id)
|
|
||||||
})
|
|
||||||
if (items.length === 0) {
|
|
||||||
content = undefined
|
|
||||||
bindResizeObserver()
|
|
||||||
}
|
|
||||||
queuePositionUpdate()
|
|
||||||
})
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
if (positionFrame !== undefined) cancelAnimationFrame(positionFrame)
|
|
||||||
resizeObserver?.disconnect()
|
|
||||||
})
|
|
||||||
|
|
||||||
function setViewport(el: HTMLDivElement) {
|
|
||||||
viewport = el
|
|
||||||
bindResizeObserver()
|
|
||||||
queuePositionUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setContentRef(el: HTMLDivElement) {
|
|
||||||
content = el
|
|
||||||
bindResizeObserver()
|
|
||||||
queuePositionUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setHeaderRef(id: HomeSessionGroup["id"], el: HTMLDivElement) {
|
|
||||||
headerRefs.set(id, el)
|
|
||||||
queuePositionUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
function queuePositionUpdate() {
|
|
||||||
if (typeof requestAnimationFrame === "undefined") {
|
|
||||||
updatePositionCache()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (positionFrame !== undefined) return
|
|
||||||
positionFrame = requestAnimationFrame(() => {
|
|
||||||
positionFrame = undefined
|
|
||||||
updatePositionCache()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function updatePositionCache() {
|
|
||||||
if (!viewport) return
|
|
||||||
groups().forEach((group) => {
|
|
||||||
const el = headerRefs.get(group.id)
|
|
||||||
if (!el) return
|
|
||||||
headerOffsets.set(group.id, el.offsetTop)
|
|
||||||
})
|
|
||||||
update(viewport.scrollTop)
|
|
||||||
}
|
|
||||||
|
|
||||||
function update(scrollTop: number) {
|
|
||||||
const items = groups()
|
|
||||||
items.forEach((group, index) => {
|
|
||||||
const nextOffset = items
|
|
||||||
.slice(index + 1)
|
|
||||||
.map((item) => headerOffsets.get(item.id))
|
|
||||||
.find((offset) => offset !== undefined)
|
|
||||||
const fadeEnd = HOME_SESSION_HEADER_STICKY_TOP + HOME_SESSION_HEADER_TEXT_HEIGHT
|
|
||||||
const nextTop = nextOffset === undefined ? undefined : nextOffset - scrollTop
|
|
||||||
const opacity =
|
|
||||||
nextTop === undefined ? 1 : Math.max(0, Math.min(1, (nextTop - fadeEnd) / HOME_SESSION_HEADER_FADE_DISTANCE))
|
|
||||||
setState("titleOpacity", group.id, Math.round(opacity * 1000) / 1000)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function titleOpacity(id: HomeSessionGroup["id"]) {
|
|
||||||
return state.titleOpacity[id] ?? 1
|
|
||||||
}
|
|
||||||
|
|
||||||
function bindResizeObserver() {
|
|
||||||
resizeObserver?.disconnect()
|
|
||||||
if (typeof ResizeObserver === "undefined") return
|
|
||||||
resizeObserver = new ResizeObserver(() => queuePositionUpdate())
|
|
||||||
if (viewport) resizeObserver.observe(viewport)
|
|
||||||
if (content) resizeObserver.observe(content)
|
|
||||||
}
|
|
||||||
|
|
||||||
return { setViewport, setContentRef, setHeaderRef, update, titleOpacity }
|
|
||||||
}
|
|
||||||
|
|
||||||
export function NewHome() {
|
export function NewHome() {
|
||||||
const sync = useServerSync()
|
const sync = useServerSync()
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
@@ -328,7 +221,6 @@ export function NewHome() {
|
|||||||
})
|
})
|
||||||
const searchOpen = createMemo(() => state.searchFocused && search().length > 0)
|
const searchOpen = createMemo(() => state.searchFocused && search().length > 0)
|
||||||
const groups = createMemo(() => groupSessions(records(), language))
|
const groups = createMemo(() => groupSessions(records(), language))
|
||||||
const sessionHeaderOpacity = useHomeSessionHeaderOpacity(groups)
|
|
||||||
const prefetched = new Set<string>()
|
const prefetched = new Set<string>()
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
@@ -408,7 +300,6 @@ export function NewHome() {
|
|||||||
|
|
||||||
function selectProject(conn: ServerConnection.Any, directory: string) {
|
function selectProject(conn: ServerConnection.Any, directory: string) {
|
||||||
const key = ServerConnection.key(conn)
|
const key = ServerConnection.key(conn)
|
||||||
if (global.servers.health[key]?.healthy === false) return
|
|
||||||
if (
|
if (
|
||||||
!global
|
!global
|
||||||
.ensureServerCtx(conn)
|
.ensureServerCtx(conn)
|
||||||
@@ -449,15 +340,15 @@ export function NewHome() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function unseenCount(conn: ServerConnection.Any, project: LocalProject) {
|
function unseenCount(conn: ServerConnection.Any, project: LocalProject) {
|
||||||
const state = notification.ensureServerState(ServerConnection.key(conn))
|
if (ServerConnection.key(conn) !== server.key) return 0
|
||||||
return directories(project).reduce((total, directory) => total + state.project.unseenCount(directory), 0)
|
return directories(project).reduce((total, directory) => total + notification.project.unseenCount(directory), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearNotifications(conn: ServerConnection.Any, project: LocalProject) {
|
function clearNotifications(conn: ServerConnection.Any, project: LocalProject) {
|
||||||
const state = notification.ensureServerState(ServerConnection.key(conn))
|
if (ServerConnection.key(conn) !== server.key) return
|
||||||
directories(project)
|
directories(project)
|
||||||
.filter((directory) => state.project.unseenCount(directory) > 0)
|
.filter((directory) => notification.project.unseenCount(directory) > 0)
|
||||||
.forEach((directory) => state.project.markViewed(directory))
|
.forEach((directory) => notification.project.markViewed(directory))
|
||||||
}
|
}
|
||||||
|
|
||||||
function openSession(session: Session) {
|
function openSession(session: Session) {
|
||||||
@@ -515,7 +406,7 @@ export function NewHome() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="rounded-[10px] shadow-[var(--v2-elevation-raised)] m-2 min-h-0 lg:overflow-hidden bg-v2-background-bg-base self-stretch flex-1">
|
<div class="rounded-[10px] shadow-[var(--v2-elevation-raised)] m-2 min-h-0 lg:overflow-hidden bg-v2-background-bg-base self-stretch flex-1">
|
||||||
<div class="mx-auto grid h-full w-full max-w-[1080px] grid-rows-[auto_minmax(0,1fr)_auto] gap-4 px-3 lg:grid-cols-[280px_minmax(0,720px)] lg:grid-rows-1 lg:gap-8 lg:px-6">
|
<div class="mx-auto grid h-full w-full max-w-[1080px] grid-rows-[auto_minmax(0,1fr)_auto] gap-4 px-3 pb-3 lg:grid-cols-[280px_minmax(0,720px)] lg:grid-rows-1 lg:gap-8 lg:px-6 lg:pb-16">
|
||||||
<HomeProjectColumn
|
<HomeProjectColumn
|
||||||
projects={projects()}
|
projects={projects()}
|
||||||
selected={selection()}
|
selected={selection()}
|
||||||
@@ -541,7 +432,7 @@ export function NewHome() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
class="min-h-0 min-w-0 flex-1 flex flex-col pt-6 lg:pt-12 relative"
|
class="min-h-0 min-w-0 flex-1 flex flex-col pt-6 lg:pt-12"
|
||||||
aria-label={language.t("sidebar.project.recentSessions")}
|
aria-label={language.t("sidebar.project.recentSessions")}
|
||||||
>
|
>
|
||||||
<HomeSessionSearch
|
<HomeSessionSearch
|
||||||
@@ -562,25 +453,7 @@ export function NewHome() {
|
|||||||
onClose={closeSearch}
|
onClose={closeSearch}
|
||||||
onSelect={selectSearchSession}
|
onSelect={selectSearchSession}
|
||||||
/>
|
/>
|
||||||
<ScrollView
|
<ScrollView class="mt-3 min-h-0 flex-1">
|
||||||
class="mt-3 -mr-3 min-h-0 flex-1 relative"
|
|
||||||
viewportRef={sessionHeaderOpacity.setViewport}
|
|
||||||
onScroll={(event) => sessionHeaderOpacity.update(event.currentTarget.scrollTop)}
|
|
||||||
>
|
|
||||||
<Show when={groups().length > 0 && newSessionProject()}>
|
|
||||||
<div class="pointer-events-none absolute top-3 right-3 z-20 flex">
|
|
||||||
<ButtonV2
|
|
||||||
data-action="home-new-session"
|
|
||||||
variant="ghost-muted"
|
|
||||||
size="normal"
|
|
||||||
icon="edit"
|
|
||||||
class="pointer-events-auto h-7 px-2 [font-weight:530]"
|
|
||||||
onClick={openNewSession}
|
|
||||||
>
|
|
||||||
{language.t("command.session.new")}
|
|
||||||
</ButtonV2>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<Show
|
<Show
|
||||||
when={!sessionLoad.isLoading}
|
when={!sessionLoad.isLoading}
|
||||||
fallback={
|
fallback={
|
||||||
@@ -593,19 +466,15 @@ export function NewHome() {
|
|||||||
when={groups().length > 0}
|
when={groups().length > 0}
|
||||||
fallback={<HomeSessionsEmpty onNewSession={newSessionProject() ? openNewSession : undefined} />}
|
fallback={<HomeSessionsEmpty onNewSession={newSessionProject() ? openNewSession : undefined} />}
|
||||||
>
|
>
|
||||||
<div ref={sessionHeaderOpacity.setContentRef} class="flex flex-col pt-3 pr-3 pb-16">
|
<div class="pt-3 flex flex-col gap-6">
|
||||||
<For each={groups()}>
|
<For each={groups()}>
|
||||||
{(group, index) => (
|
{(group, index) => (
|
||||||
<>
|
<div class="flex min-w-0 flex-col gap-4">
|
||||||
<HomeSessionGroupHeader
|
<HomeSessionGroupHeader
|
||||||
title={group.title}
|
title={group.title}
|
||||||
titleOpacity={sessionHeaderOpacity.titleOpacity(group.id)}
|
onNewSession={index() === 0 && newSessionProject() ? openNewSession : undefined}
|
||||||
ref={(el) => sessionHeaderOpacity.setHeaderRef(group.id, el)}
|
|
||||||
elevated={index() === 0}
|
|
||||||
/>
|
/>
|
||||||
<div
|
<div class="flex min-w-0 flex-col gap-px">
|
||||||
class={`flex min-w-0 flex-col gap-px pt-4 ${index() === groups().length - 1 ? "" : "mb-6"}`}
|
|
||||||
>
|
|
||||||
<For each={group.sessions}>
|
<For each={group.sessions}>
|
||||||
{(record) => (
|
{(record) => (
|
||||||
<HomeSessionRow
|
<HomeSessionRow
|
||||||
@@ -619,7 +488,7 @@ export function NewHome() {
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
@@ -656,22 +525,16 @@ function HomeProjectColumn(props: {
|
|||||||
const global = useGlobal()
|
const global = useGlobal()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const controller = useServerManagementController({ navigateOnAdd: false })
|
const controller = useServerManagementController({ navigateOnAdd: false })
|
||||||
const [_state, setState, _, ready] = persisted(
|
const [state, setState] = persisted(
|
||||||
Persist.global("home.servers", ["home.servers.v1"]),
|
Persist.global("home.servers", ["home.servers.v1"]),
|
||||||
createStore({ collapsed: {} as Record<string, boolean> }),
|
createStore({ collapsed: {} as Record<string, boolean> }),
|
||||||
)
|
)
|
||||||
const [state] = createResource(
|
|
||||||
() => ready.promise ?? Promise.resolve(),
|
|
||||||
(p) => p.then(() => _state),
|
|
||||||
{ initialValue: _state },
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
class="mt-6 flex min-h-0 min-w-0 flex-col gap-4 overflow-hidden lg:mt-14 lg:pt-[52px]"
|
class="mt-6 flex min-w-0 flex-col gap-4 lg:mt-14 lg:pt-[52px]"
|
||||||
aria-label={props.language.t("home.projects")}
|
aria-label={props.language.t("home.projects")}
|
||||||
>
|
>
|
||||||
<div class="flex h-7 min-w-0 shrink-0 items-center justify-between pl-1.5">
|
<div class="flex h-7 min-w-0 items-center justify-between pl-1.5">
|
||||||
<div class={HOME_SECTION_LABEL}>{props.language.t("home.projects")}</div>
|
<div class={HOME_SECTION_LABEL}>{props.language.t("home.projects")}</div>
|
||||||
<Show when={global.servers.list().length === 1}>
|
<Show when={global.servers.list().length === 1}>
|
||||||
<TooltipV2 placement="bottom" value={props.language.t("home.project.add")}>
|
<TooltipV2 placement="bottom" value={props.language.t("home.project.add")}>
|
||||||
@@ -688,51 +551,42 @@ function HomeProjectColumn(props: {
|
|||||||
</TooltipV2>
|
</TooltipV2>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<ScrollView data-slot="home-projects-scroll" class="min-h-0 min-w-0 shrink">
|
<Show
|
||||||
<Show
|
when={global.servers.list().length > 1}
|
||||||
when={global.servers.list().length > 1}
|
fallback={<HomeProjectList {...props} server={global.servers.list()[0]!} />}
|
||||||
fallback={
|
>
|
||||||
<div class="pr-3">
|
<For each={global.servers.list()}>
|
||||||
<HomeProjectList {...props} server={global.servers.list()[0]!} />
|
{(item) => {
|
||||||
</div>
|
const key = ServerConnection.key(item)
|
||||||
}
|
const healthy = () => !!global.servers.health[key]?.healthy
|
||||||
>
|
const serverCtx = global.ensureServerCtx(item)
|
||||||
<div class="flex min-w-0 flex-col gap-1 pr-3">
|
const collapsed = () => !!state.collapsed[key]
|
||||||
<For each={global.servers.list()}>
|
return (
|
||||||
{(item) => {
|
<div class="flex max-h-[min(572px,calc(100vh_-_300px))] min-w-0 flex-col gap-1 overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||||
const key = ServerConnection.key(item)
|
<HomeServerRow
|
||||||
const healthy = () => !!global.servers.health[key]?.healthy
|
server={item}
|
||||||
const serverCtx = global.ensureServerCtx(item)
|
selected={props.selected.server === key && !props.selected.directory}
|
||||||
const projects = () => serverCtx.projects.list()
|
healthy={healthy()}
|
||||||
const hasProjects = () => projects().length > 0
|
collapsed={collapsed()}
|
||||||
const collapsed = () => !!state().collapsed[key]
|
health={global.servers.health[key]}
|
||||||
return (
|
controller={controller}
|
||||||
<div class="flex min-w-0 flex-col gap-1">
|
focusServer={props.focusServer}
|
||||||
<HomeServerRow
|
chooseProject={props.chooseProject}
|
||||||
server={item}
|
openEdit={(server) => dialog.show(() => <DialogServerV2 mode="edit" server={server} />)}
|
||||||
selected={props.selected.server === key && !props.selected.directory}
|
toggleCollapsed={() => setState("collapsed", key, !state.collapsed[key])}
|
||||||
collapsed={collapsed()}
|
language={props.language}
|
||||||
health={global.servers.health[key]}
|
/>
|
||||||
controller={controller}
|
<Show when={healthy() && !collapsed()}>
|
||||||
focusServer={props.focusServer}
|
<div class="mx-3 h-px bg-v2-border-border-base" />
|
||||||
chooseProject={props.chooseProject}
|
<HomeProjectList {...props} server={item} projects={serverCtx.projects.list()} />
|
||||||
openEdit={(server) => dialog.show(() => <DialogServerV2 mode="edit" server={server} />)}
|
</Show>
|
||||||
toggleCollapsed={() => setState("collapsed", key, !state().collapsed[key])}
|
</div>
|
||||||
language={props.language}
|
)
|
||||||
/>
|
}}
|
||||||
<Show when={healthy() && hasProjects() && !collapsed()}>
|
</For>
|
||||||
<div class="mx-3 h-px bg-v2-border-border-base" />
|
</Show>
|
||||||
<HomeProjectList {...props} server={item} projects={projects()} />
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</ScrollView>
|
|
||||||
<HomeUtilityNav
|
<HomeUtilityNav
|
||||||
class="mb-8 mt-4 hidden shrink-0 lg:flex"
|
class="mt-4 hidden lg:flex"
|
||||||
openSettings={props.openSettings}
|
openSettings={props.openSettings}
|
||||||
openHelp={props.openHelp}
|
openHelp={props.openHelp}
|
||||||
language={props.language}
|
language={props.language}
|
||||||
@@ -772,6 +626,7 @@ function HomeUtilityNav(props: {
|
|||||||
function HomeServerRow(props: {
|
function HomeServerRow(props: {
|
||||||
server: ServerConnection.Any
|
server: ServerConnection.Any
|
||||||
selected: boolean
|
selected: boolean
|
||||||
|
healthy: boolean
|
||||||
collapsed: boolean
|
collapsed: boolean
|
||||||
health: ServerHealth | undefined
|
health: ServerHealth | undefined
|
||||||
controller: ReturnType<typeof useServerManagementController>
|
controller: ReturnType<typeof useServerManagementController>
|
||||||
@@ -781,46 +636,39 @@ function HomeServerRow(props: {
|
|||||||
toggleCollapsed: () => void
|
toggleCollapsed: () => void
|
||||||
language: ReturnType<typeof useLanguage>
|
language: ReturnType<typeof useLanguage>
|
||||||
}) {
|
}) {
|
||||||
const global = useGlobal()
|
|
||||||
const [state, setState] = createStore({ menuOpen: false })
|
const [state, setState] = createStore({ menuOpen: false })
|
||||||
const healthy = () => !!props.health?.healthy
|
|
||||||
const canToggle = () => healthy() && global.ensureServerCtx(props.server).projects.list().length > 0
|
|
||||||
return (
|
return (
|
||||||
<div class="group/server relative flex h-7 min-w-0 items-center rounded-[6px]">
|
<div class="group/server relative flex h-7 min-w-0 items-center rounded-[6px]">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`${HOME_PROJECT_NAV_ROW} pr-16 disabled:opacity-60`}
|
class={`${HOME_PROJECT_NAV_ROW} pr-16 disabled:opacity-60`}
|
||||||
data-selected={props.selected ? "" : undefined}
|
data-selected={props.selected ? "" : undefined}
|
||||||
disabled={!healthy()}
|
disabled={!props.healthy}
|
||||||
onClick={() => props.focusServer(props.server)}
|
onClick={() => props.focusServer(props.server)}
|
||||||
>
|
>
|
||||||
<span
|
<Show when={props.healthy}>
|
||||||
data-action="home-server-collapse"
|
<span
|
||||||
class="inline-flex -ml-0.5 -mr-1.5 size-5 shrink-0 items-center justify-center rounded-[4px] text-v2-icon-icon-muted"
|
data-action="home-server-collapse"
|
||||||
classList={{
|
class="inline-flex -ml-0.5 -mr-1.5 size-5 shrink-0 items-center justify-center rounded-[4px] text-v2-icon-icon-muted hover:bg-v2-overlay-simple-overlay-hover"
|
||||||
"hover:bg-v2-overlay-simple-overlay-hover": canToggle(),
|
aria-label={
|
||||||
"cursor-default opacity-40": !canToggle(),
|
props.collapsed ? props.language.t("home.server.expand") : props.language.t("home.server.collapse")
|
||||||
}}
|
}
|
||||||
aria-label={
|
aria-expanded={!props.collapsed}
|
||||||
props.collapsed ? props.language.t("home.server.expand") : props.language.t("home.server.collapse")
|
onClick={(event) => {
|
||||||
}
|
event.preventDefault()
|
||||||
aria-disabled={!canToggle()}
|
event.stopPropagation()
|
||||||
aria-expanded={canToggle() ? !props.collapsed : undefined}
|
props.toggleCollapsed()
|
||||||
onClick={(event) => {
|
}}
|
||||||
event.preventDefault()
|
onPointerDown={(event) => event.preventDefault()}
|
||||||
event.stopPropagation()
|
>
|
||||||
if (!canToggle()) return
|
<IconV2
|
||||||
props.toggleCollapsed()
|
name="chevron-down"
|
||||||
}}
|
size="small"
|
||||||
onPointerDown={(event) => event.preventDefault()}
|
class="transition-transform duration-150 ease-in-out"
|
||||||
>
|
style={{ transform: `rotate(${props.collapsed ? -90 : 0}deg)` }}
|
||||||
<IconV2
|
/>
|
||||||
name="chevron-down"
|
</span>
|
||||||
size="small"
|
</Show>
|
||||||
class="transition-transform duration-150 ease-in-out"
|
|
||||||
style={{ transform: `rotate(${props.collapsed ? -90 : 0}deg)` }}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<div class="flex size-4 shrink-0 items-center justify-center -mr-0.5">
|
<div class="flex size-4 shrink-0 items-center justify-center -mr-0.5">
|
||||||
<ServerHealthIndicator health={props.health} />
|
<ServerHealthIndicator health={props.health} />
|
||||||
</div>
|
</div>
|
||||||
@@ -911,18 +759,15 @@ function HomeProjectRow(props: {
|
|||||||
clearNotifications: (server: ServerConnection.Any, project: LocalProject) => void
|
clearNotifications: (server: ServerConnection.Any, project: LocalProject) => void
|
||||||
language: ReturnType<typeof useLanguage>
|
language: ReturnType<typeof useLanguage>
|
||||||
}) {
|
}) {
|
||||||
const global = useGlobal()
|
|
||||||
const serverUnreachable = () => global.servers.health[ServerConnection.key(props.server)]?.healthy === false
|
|
||||||
const [state, setState] = createStore({ menuOpen: false })
|
const [state, setState] = createStore({ menuOpen: false })
|
||||||
return (
|
return (
|
||||||
<div class="group/project relative flex h-7 min-w-0 items-center rounded-[6px]">
|
<div class="group/project relative flex h-7 min-w-0 items-center rounded-[6px]">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-component="home-project-row"
|
data-component="home-project-row"
|
||||||
class={`${HOME_PROJECT_NAV_ROW} pr-16 disabled:opacity-60`}
|
class={`${HOME_PROJECT_NAV_ROW} pr-16`}
|
||||||
data-selected={props.selected ? "" : undefined}
|
data-selected={props.selected ? "" : undefined}
|
||||||
aria-current={props.selected ? "page" : undefined}
|
aria-current={props.selected ? "page" : undefined}
|
||||||
disabled={serverUnreachable()}
|
|
||||||
onClick={() => props.selectProject(props.server, props.project.worktree)}
|
onClick={() => props.selectProject(props.server, props.project.worktree)}
|
||||||
>
|
>
|
||||||
<HomeProjectAvatar project={props.project} />
|
<HomeProjectAvatar project={props.project} />
|
||||||
@@ -997,7 +842,6 @@ function HomeSessionLeading(props: {
|
|||||||
session: Session
|
session: Session
|
||||||
server: ServerConnection.Key
|
server: ServerConnection.Key
|
||||||
activeServer: boolean
|
activeServer: boolean
|
||||||
revealProjectOnHover: boolean
|
|
||||||
}) {
|
}) {
|
||||||
const tabs = useTabs()
|
const tabs = useTabs()
|
||||||
const hasOpenTab = createMemo(() => sessionHasOpenTab(tabs.store, props.server, props.session))
|
const hasOpenTab = createMemo(() => sessionHasOpenTab(tabs.store, props.server, props.session))
|
||||||
@@ -1006,8 +850,8 @@ function HomeSessionLeading(props: {
|
|||||||
<Show when={hasOpenTab()}>
|
<Show when={hasOpenTab()}>
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="pointer-events-none absolute top-1/2 h-3 w-0.5 -translate-y-1/2 rounded-[2px] bg-v2-background-bg-layer-04"
|
class="pointer-events-none absolute top-1/2 h-[7px] w-[3px] -translate-y-1/2 rounded-[2px] bg-v2-background-bg-layer-04"
|
||||||
style={{ right: "calc(100% + 4px)" }}
|
style={{ right: "calc(100% + 5px)" }}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
<SessionTabAvatar
|
<SessionTabAvatar
|
||||||
@@ -1015,7 +859,6 @@ function HomeSessionLeading(props: {
|
|||||||
directory={props.session.directory}
|
directory={props.session.directory}
|
||||||
sessionId={props.session.id}
|
sessionId={props.session.id}
|
||||||
activeServer={props.activeServer}
|
activeServer={props.activeServer}
|
||||||
revealProjectOnHover={props.revealProjectOnHover}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -1106,7 +949,7 @@ function HomeSessionSearch(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<div ref={root} data-component="home-session-search" class="relative z-30 w-full">
|
<div ref={root} data-component="home-session-search" class="relative z-10 w-full">
|
||||||
<Show when={props.open}>
|
<Show when={props.open}>
|
||||||
<div
|
<div
|
||||||
data-component="home-session-search-panel"
|
data-component="home-session-search-panel"
|
||||||
@@ -1114,7 +957,7 @@ function HomeSessionSearch(props: {
|
|||||||
style={{
|
style={{
|
||||||
top: "-6px",
|
top: "-6px",
|
||||||
left: "-6px",
|
left: "-6px",
|
||||||
width: "calc(100% + 12px)",
|
width: "calc(100% + 14px)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="flex flex-col pt-9">
|
<div class="flex flex-col pt-9">
|
||||||
@@ -1163,7 +1006,13 @@ function HomeSessionSearch(props: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<label class="relative z-20 flex h-9 w-full items-center gap-2 rounded-[6px] bg-v2-background-bg-layer-02/60 py-1 pl-3 pr-2 text-v2-icon-icon-muted transition-[background-color,box-shadow] duration-[120ms] ease-in-out hover:bg-v2-background-bg-layer-02">
|
<label
|
||||||
|
class="relative z-20 flex h-9 w-full items-center gap-2 rounded-[6px] bg-v2-background-bg-layer-02 py-1 pl-3 pr-2 text-v2-icon-icon-muted transition-[background-color,box-shadow] duration-[120ms] ease-in-out"
|
||||||
|
classList={{
|
||||||
|
"focus-within:shadow-[0_0_0_0.5px_var(--v2-border-border-focus),var(--v2-elevation-raised)]": !props.open,
|
||||||
|
"shadow-[0_0_0_0.5px_var(--v2-border-border-focus)]": props.open,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<IconV2 name="magnifying-glass" />
|
<IconV2 name="magnifying-glass" />
|
||||||
<input
|
<input
|
||||||
ref={input}
|
ref={input}
|
||||||
@@ -1249,7 +1098,6 @@ function HomeSessionSearchResultRow(props: {
|
|||||||
classList={{
|
classList={{
|
||||||
[HOME_SEARCH_RESULT_ROW]: true,
|
[HOME_SEARCH_RESULT_ROW]: true,
|
||||||
"bg-v2-overlay-simple-overlay-hover": props.selected,
|
"bg-v2-overlay-simple-overlay-hover": props.selected,
|
||||||
group: !!showProjectName(),
|
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() => props.onHighlight()}
|
onMouseEnter={() => props.onHighlight()}
|
||||||
onClick={() => props.onSelect(props.record.session)}
|
onClick={() => props.onSelect(props.record.session)}
|
||||||
@@ -1259,7 +1107,6 @@ function HomeSessionSearchResultRow(props: {
|
|||||||
session={props.record.session}
|
session={props.record.session}
|
||||||
server={props.server}
|
server={props.server}
|
||||||
activeServer={props.activeServer}
|
activeServer={props.activeServer}
|
||||||
revealProjectOnHover={!!showProjectName()}
|
|
||||||
/>
|
/>
|
||||||
<div class="flex min-w-0 flex-1 items-center gap-1.5">
|
<div class="flex min-w-0 flex-1 items-center gap-1.5">
|
||||||
<span
|
<span
|
||||||
@@ -1275,20 +1122,25 @@ function HomeSessionSearchResultRow(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function HomeSessionGroupHeader(props: {
|
function HomeSessionGroupHeader(props: { title: string; onNewSession?: () => void }) {
|
||||||
title: string
|
const language = useLanguage()
|
||||||
titleOpacity: number
|
|
||||||
ref: ComponentProps<"div">["ref"]
|
|
||||||
elevated?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div class="flex h-7 min-w-0 items-center justify-between pl-[18px]">
|
||||||
ref={props.ref}
|
<div class={HOME_SECTION_LABEL}>{props.title}</div>
|
||||||
class={`pointer-events-none sticky top-3 flex h-7 min-w-0 items-center justify-between pl-3 bg-v2-background-bg-base ${props.elevated ? "home-session-group-header z-[5]" : "z-10"}`}
|
<Show when={props.onNewSession}>
|
||||||
>
|
{(onNewSession) => (
|
||||||
<div class={HOME_SECTION_LABEL} style={{ opacity: props.titleOpacity }}>
|
<ButtonV2
|
||||||
{props.title}
|
data-action="home-new-session"
|
||||||
</div>
|
variant="ghost-muted"
|
||||||
|
size="normal"
|
||||||
|
icon="edit"
|
||||||
|
class="h-7 px-2 [font-weight:530]"
|
||||||
|
onClick={onNewSession()}
|
||||||
|
>
|
||||||
|
{language.t("command.session.new")}
|
||||||
|
</ButtonV2>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1306,10 +1158,7 @@ function HomeSessionRow(props: {
|
|||||||
const showProjectName = () => props.showProjectName && props.record.projectName
|
const showProjectName = () => props.showProjectName && props.record.projectName
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div class="group/session relative flex h-10 min-w-0 items-center rounded-[6px]">
|
||||||
class="group/session relative flex h-10 min-w-0 items-center rounded-[6px]"
|
|
||||||
classList={{ group: !!showProjectName() }}
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-component="home-session-row"
|
data-component="home-session-row"
|
||||||
@@ -1321,7 +1170,6 @@ function HomeSessionRow(props: {
|
|||||||
session={props.record.session}
|
session={props.record.session}
|
||||||
server={props.server}
|
server={props.server}
|
||||||
activeServer={props.activeServer}
|
activeServer={props.activeServer}
|
||||||
revealProjectOnHover={!!showProjectName()}
|
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
class={`min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-v2-text-text-base [font-weight:530] ${showProjectName() ? "max-w-[min(70%,480px)] flex-[0_1_auto]" : "flex-[1_1_auto]"}`}
|
class={`min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-v2-text-text-base [font-weight:530] ${showProjectName() ? "max-w-[min(70%,480px)] flex-[0_1_auto]" : "flex-[1_1_auto]"}`}
|
||||||
@@ -1334,24 +1182,22 @@ function HomeSessionRow(props: {
|
|||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
</button>
|
</button>
|
||||||
<Show when={SHOW_HOME_SESSION_ARCHIVE}>
|
<div class="hover-reveal absolute right-1.5 top-1/2 flex -translate-y-1/2 items-center gap-1 group-hover/session:opacity-100 focus-within:opacity-100">
|
||||||
<div class="hover-reveal absolute right-1.5 top-1/2 flex -translate-y-1/2 items-center gap-1 group-hover/session:opacity-100 focus-within:opacity-100">
|
<TooltipV2 class="flex shrink-0 items-center" placement="bottom" value={language.t("common.archive")}>
|
||||||
<TooltipV2 class="flex shrink-0 items-center" placement="bottom" value={language.t("common.archive")}>
|
<IconButtonV2
|
||||||
<IconButtonV2
|
data-action="home-session-archive"
|
||||||
data-action="home-session-archive"
|
variant="ghost-muted"
|
||||||
variant="ghost-muted"
|
size="large"
|
||||||
size="large"
|
icon={<IconV2 name="archive" />}
|
||||||
icon={<IconV2 name="archive" />}
|
aria-label={language.t("common.archive")}
|
||||||
aria-label={language.t("common.archive")}
|
onClick={(event) => {
|
||||||
onClick={(event) => {
|
event.preventDefault()
|
||||||
event.preventDefault()
|
event.stopPropagation()
|
||||||
event.stopPropagation()
|
void props.archiveSession(props.record.session)
|
||||||
void props.archiveSession(props.record.session)
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
</TooltipV2>
|
||||||
</TooltipV2>
|
</div>
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
import { createEffect, Suspense, type ParentProps } from "solid-js"
|
import { createEffect, Suspense, type ParentProps } from "solid-js"
|
||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate, useParams } from "@solidjs/router"
|
||||||
import { DebugBar } from "@/components/debug-bar"
|
import { DebugBar } from "@/components/debug-bar"
|
||||||
import { HelpButton } from "@/components/help-button"
|
import { HelpButton } from "@/components/help-button"
|
||||||
import { Titlebar, type TitlebarUpdate } from "@/components/titlebar"
|
import { Titlebar, type TitlebarUpdate } from "@/components/titlebar"
|
||||||
|
import { useNotification } from "@/context/notification"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { setNavigate } from "@/utils/notification-click"
|
import { setNavigate } from "@/utils/notification-click"
|
||||||
import { setV2Toast, ToastRegion } from "@/utils/toast"
|
import { setV2Toast, ToastRegion } from "@/utils/toast"
|
||||||
|
|
||||||
export default function NewLayout(props: ParentProps) {
|
export default function NewLayout(props: ParentProps) {
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
|
const notification = useNotification()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const params = useParams<{ id?: string }>()
|
||||||
setNavigate(navigate)
|
setNavigate(navigate)
|
||||||
|
|
||||||
createEffect(() => setV2Toast(true))
|
createEffect(() => setV2Toast(true))
|
||||||
|
createEffect(() => {
|
||||||
|
if (!notification.ready() || !params.id) return
|
||||||
|
if (notification.session.unseenCount(params.id) === 0) return
|
||||||
|
notification.session.markViewed(params.id)
|
||||||
|
})
|
||||||
|
|
||||||
const update: TitlebarUpdate = {
|
const update: TitlebarUpdate = {
|
||||||
version: () => {
|
version: () => {
|
||||||
@@ -36,7 +44,7 @@ export default function NewLayout(props: ParentProps) {
|
|||||||
<main class="flex-1 min-h-0 min-w-0 overflow-x-hidden flex flex-col items-start contain-strict">
|
<main class="flex-1 min-h-0 min-w-0 overflow-x-hidden flex flex-col items-start contain-strict">
|
||||||
<Suspense>{props.children}</Suspense>
|
<Suspense>{props.children}</Suspense>
|
||||||
</main>
|
</main>
|
||||||
{import.meta.env.DEV && <DebugBar inline />}
|
{import.meta.env.DEV && <DebugBar />}
|
||||||
<HelpButton />
|
<HelpButton />
|
||||||
<ToastRegion v2 />
|
<ToastRegion v2 />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -943,6 +943,18 @@ export default function LegacyLayout(props: ParentProps) {
|
|||||||
keybind: "mod+comma",
|
keybind: "mod+comma",
|
||||||
onSelect: () => openSettings(),
|
onSelect: () => openSettings(),
|
||||||
},
|
},
|
||||||
|
...(platform.platform === "desktop" && platform.exportDebugLogs
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: "logs.export",
|
||||||
|
title: "Export logs",
|
||||||
|
category: language.t("command.category.settings"),
|
||||||
|
onSelect: () => {
|
||||||
|
void platform.exportDebugLogs?.()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
id: "session.previous",
|
id: "session.previous",
|
||||||
title: language.t("command.session.previous"),
|
title: language.t("command.session.previous"),
|
||||||
|
|||||||
@@ -11,28 +11,32 @@ export function SessionTabAvatar(props: {
|
|||||||
directory: string
|
directory: string
|
||||||
sessionId: string
|
sessionId: string
|
||||||
activeServer: boolean
|
activeServer: boolean
|
||||||
revealProjectOnHover?: boolean
|
|
||||||
}) {
|
}) {
|
||||||
const directory = () => props.directory
|
const directory = () => props.directory
|
||||||
const sessionId = () => props.sessionId
|
const sessionId = () => props.sessionId
|
||||||
const state = useSessionTabAvatarState(directory, sessionId, () => props.activeServer)
|
const state = useSessionTabAvatarState(directory, sessionId, () => props.activeServer)
|
||||||
const projectAvatar = () => (
|
|
||||||
<ProjectAvatar
|
|
||||||
fallback={displayName(props.project ?? { worktree: props.directory })}
|
|
||||||
src={getProjectAvatarSource(props.project?.id, props.project?.icon)}
|
|
||||||
variant={getProjectAvatarVariant(props.project?.icon?.color)}
|
|
||||||
unread={state.unread()}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
return (
|
return (
|
||||||
<Show when={state.loading()} fallback={projectAvatar()}>
|
<Show
|
||||||
<span class="relative block size-4 shrink-0">
|
when={state.loading()}
|
||||||
<SessionProgressIndicatorV2
|
fallback={
|
||||||
class={`absolute inset-0 ${props.revealProjectOnHover === false ? "" : "group-hover:invisible"}`}
|
<ProjectAvatar
|
||||||
|
fallback={displayName(props.project ?? { worktree: props.directory })}
|
||||||
|
src={getProjectAvatarSource(props.project?.id, props.project?.icon)}
|
||||||
|
variant={getProjectAvatarVariant(props.project?.icon?.color)}
|
||||||
|
unread={state.unread()}
|
||||||
/>
|
/>
|
||||||
<Show when={props.revealProjectOnHover !== false}>
|
}
|
||||||
<span class="invisible absolute inset-0 group-hover:visible">{projectAvatar()}</span>
|
>
|
||||||
</Show>
|
<span class="relative block size-4 shrink-0">
|
||||||
|
<SessionProgressIndicatorV2 class="absolute inset-0 group-hover:invisible" />
|
||||||
|
<span class="invisible absolute inset-0 group-hover:visible">
|
||||||
|
<ProjectAvatar
|
||||||
|
fallback={displayName(props.project ?? { worktree: props.directory })}
|
||||||
|
src={getProjectAvatarSource(props.project?.id, props.project?.icon)}
|
||||||
|
variant={getProjectAvatarVariant(props.project?.icon?.color)}
|
||||||
|
unread={state.unread()}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,9 +19,6 @@ import { createPromptInputController, createPromptProjectControls } from "@/page
|
|||||||
import { useSessionKey } from "@/pages/session/session-layout"
|
import { useSessionKey } from "@/pages/session/session-layout"
|
||||||
import { useComposerCommands } from "@/pages/session/use-composer-commands"
|
import { useComposerCommands } from "@/pages/session/use-composer-commands"
|
||||||
import { NEW_SESSION_CONTENT_WIDTH } from "@/pages/session/new-session-layout"
|
import { NEW_SESSION_CONTENT_WIDTH } from "@/pages/session/new-session-layout"
|
||||||
import { PromptWorkspaceSelector } from "@/components/prompt-workspace-selector"
|
|
||||||
|
|
||||||
const showWorkspaceBar = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `/new-session` draft page. Unlike `session.tsx`, this only renders the prompt
|
* The `/new-session` draft page. Unlike `session.tsx`, this only renders the prompt
|
||||||
@@ -54,21 +51,16 @@ export default function NewSessionPage() {
|
|||||||
onDone: () => inputRef?.focus(),
|
onDone: () => inputRef?.focus(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const [store, setStore] = createStore<{ worktree?: string }>({})
|
const [store, setStore] = createStore({
|
||||||
|
worktree: "main",
|
||||||
|
})
|
||||||
|
|
||||||
const newSessionWorktree = createMemo(() => {
|
const newSessionWorktree = createMemo(() => {
|
||||||
if (store.worktree) return store.worktree
|
if (store.worktree === "create") return "create"
|
||||||
const project = sync().project
|
const project = sync().project
|
||||||
if (project && sdk().directory !== project.worktree) return sdk().directory
|
if (project && sdk().directory !== project.worktree) return sdk().directory
|
||||||
return "main"
|
return "main"
|
||||||
})
|
})
|
||||||
const projectRoot = createMemo(() => sync().project?.worktree ?? sdk().directory)
|
|
||||||
const localBranch = createMemo(() => serverSync().child(projectRoot())[0].vcs?.branch)
|
|
||||||
const selectedBranch = createMemo(() => {
|
|
||||||
const worktree = newSessionWorktree()
|
|
||||||
if (worktree === "main" || worktree === "create") return localBranch()
|
|
||||||
return serverSync().child(worktree)[0].vcs?.branch ?? localBranch()
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!prompt.ready()) return
|
if (!prompt.ready()) return
|
||||||
@@ -105,7 +97,7 @@ export default function NewSessionPage() {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class="flex flex-col" classList={{ "gap-8": showWorkspaceBar, "gap-3": !showWorkspaceBar }}>
|
<div class="flex flex-col gap-3">
|
||||||
<PromptInput
|
<PromptInput
|
||||||
controls={inputController()}
|
controls={inputController()}
|
||||||
variant="new-session"
|
variant="new-session"
|
||||||
@@ -113,7 +105,7 @@ export default function NewSessionPage() {
|
|||||||
inputRef = el
|
inputRef = el
|
||||||
}}
|
}}
|
||||||
newSessionWorktree={newSessionWorktree()}
|
newSessionWorktree={newSessionWorktree()}
|
||||||
onNewSessionWorktreeReset={() => setStore("worktree", undefined)}
|
onNewSessionWorktreeReset={() => setStore("worktree", "main")}
|
||||||
onSubmit={() => comments.clear()}
|
onSubmit={() => comments.clear()}
|
||||||
toolbar={
|
toolbar={
|
||||||
<Show when={!projectController.selected()}>
|
<Show when={!projectController.selected()}>
|
||||||
@@ -122,34 +114,8 @@ export default function NewSessionPage() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Show when={projectController.selected()}>
|
<Show when={projectController.selected()}>
|
||||||
<div
|
<div class="flex h-7 min-w-0 items-center gap-0 px-2">
|
||||||
class="flex min-h-7 min-w-0 items-center gap-0 text-v2-text-text-faint"
|
<PromptProjectSelector controller={projectController} />
|
||||||
classList={{
|
|
||||||
"flex-col justify-center sm:flex-row": showWorkspaceBar,
|
|
||||||
"justify-start": !showWorkspaceBar,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PromptProjectSelector
|
|
||||||
controller={projectController}
|
|
||||||
placement={showWorkspaceBar ? "bottom" : "bottom-start"}
|
|
||||||
/>
|
|
||||||
<Show when={showWorkspaceBar}>
|
|
||||||
<PromptWorkspaceSelector
|
|
||||||
value={newSessionWorktree()}
|
|
||||||
projectRoot={projectRoot()}
|
|
||||||
workspaces={sync().project?.sandboxes ?? []}
|
|
||||||
branch={selectedBranch()}
|
|
||||||
onChange={(value) =>
|
|
||||||
setStore(
|
|
||||||
"worktree",
|
|
||||||
value === "main" && sync().project?.worktree !== sdk().directory
|
|
||||||
? sync().project?.worktree
|
|
||||||
: value,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onDone={() => inputRef?.focus()}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export function SessionComposerRegion(props: {
|
|||||||
</Show>
|
</Show>
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
"relative z-[70]": true,
|
"relative z-30": true,
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
"margin-top": `${-controller.lift()}px`,
|
"margin-top": `${-controller.lift()}px`,
|
||||||
|
|||||||
@@ -31,14 +31,9 @@ import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
|||||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
|
||||||
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
|
||||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||||
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { DialogFooter, DialogHeader, DialogTitleGroup, DialogV2 } from "@opencode-ai/ui/v2/dialog-v2"
|
|
||||||
import { InlineInput } from "@opencode-ai/ui/inline-input"
|
import { InlineInput } from "@opencode-ai/ui/inline-input"
|
||||||
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
|
||||||
import { SessionRetry } from "@opencode-ai/session-ui/session-retry"
|
import { SessionRetry } from "@opencode-ai/session-ui/session-retry"
|
||||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||||
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
|
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
|
||||||
@@ -676,34 +671,6 @@ export function MessageTimeline(props: {
|
|||||||
if (!shareEnabled()) return
|
if (!shareEnabled()) return
|
||||||
unshareMutation.mutate(id)
|
unshareMutation.mutate(id)
|
||||||
}
|
}
|
||||||
const copyShareUrl = () => {
|
|
||||||
const url = shareUrl()
|
|
||||||
if (!url) return
|
|
||||||
void navigator.clipboard
|
|
||||||
.writeText(url)
|
|
||||||
.then(() =>
|
|
||||||
showToast({
|
|
||||||
variant: "success",
|
|
||||||
icon: "circle-check",
|
|
||||||
title: language.t("session.share.copy.copied"),
|
|
||||||
description: url,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.catch((err: unknown) =>
|
|
||||||
showToast({
|
|
||||||
title: language.t("common.requestFailed"),
|
|
||||||
description: errorMessage(err),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const selectShareUrlText: JSX.EventHandler<HTMLDivElement, MouseEvent> = (event) => {
|
|
||||||
const selection = window.getSelection()
|
|
||||||
if (!selection) return
|
|
||||||
const range = document.createRange()
|
|
||||||
range.selectNodeContents(event.currentTarget)
|
|
||||||
selection.removeAllRanges()
|
|
||||||
selection.addRange(range)
|
|
||||||
}
|
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
@@ -736,9 +703,8 @@ export function MessageTimeline(props: {
|
|||||||
if (!sessionID() || parentID()) return
|
if (!sessionID() || parentID()) return
|
||||||
setTitle({ editing: true, draft: titleLabel() ?? "" })
|
setTitle({ editing: true, draft: titleLabel() ?? "" })
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (!titleRef) return
|
titleRef?.focus()
|
||||||
titleRef.focus()
|
titleRef?.select()
|
||||||
titleRef.select()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -890,26 +856,6 @@ export function MessageTimeline(props: {
|
|||||||
dialog.close()
|
dialog.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings.general.newLayoutDesigns())
|
|
||||||
return (
|
|
||||||
<DialogV2 fit>
|
|
||||||
<DialogHeader hideClose>
|
|
||||||
<DialogTitleGroup
|
|
||||||
title={language.t("session.delete.title")}
|
|
||||||
description={language.t("session.delete.confirm", { name: name() })}
|
|
||||||
/>
|
|
||||||
</DialogHeader>
|
|
||||||
<DialogFooter>
|
|
||||||
<ButtonV2 variant="ghost" onClick={() => dialog.close()}>
|
|
||||||
{language.t("common.cancel")}
|
|
||||||
</ButtonV2>
|
|
||||||
<ButtonV2 variant="danger" onClick={handleDelete}>
|
|
||||||
{language.t("session.delete.button")}
|
|
||||||
</ButtonV2>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogV2>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog title={language.t("session.delete.title")} fit>
|
<Dialog title={language.t("session.delete.title")} fit>
|
||||||
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
|
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
|
||||||
@@ -1014,7 +960,6 @@ export function MessageTimeline(props: {
|
|||||||
message={message()}
|
message={message()}
|
||||||
showAssistantCopyPartID={assistantCopyPartID(row().userMessageID)}
|
showAssistantCopyPartID={assistantCopyPartID(row().userMessageID)}
|
||||||
turnDurationMs={turnDurationMs(row().userMessageID)}
|
turnDurationMs={turnDurationMs(row().userMessageID)}
|
||||||
useV2Actions={settings.general.newLayoutDesigns()}
|
|
||||||
defaultOpen={defaultOpen()}
|
defaultOpen={defaultOpen()}
|
||||||
toolOpen={toolOpen[part().id] ?? defaultOpen()}
|
toolOpen={toolOpen[part().id] ?? defaultOpen()}
|
||||||
onToolOpenChange={(open) => setToolOpen(part().id, open)}
|
onToolOpenChange={(open) => setToolOpen(part().id, open)}
|
||||||
@@ -1122,7 +1067,6 @@ export function MessageTimeline(props: {
|
|||||||
message={message()}
|
message={message()}
|
||||||
parts={getMsgParts(userMessageRow().userMessageID)}
|
parts={getMsgParts(userMessageRow().userMessageID)}
|
||||||
actions={props.actions}
|
actions={props.actions}
|
||||||
useV2Actions={settings.general.newLayoutDesigns()}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1358,19 +1302,14 @@ export function MessageTimeline(props: {
|
|||||||
"w-full": true,
|
"w-full": true,
|
||||||
"pb-4": true,
|
"pb-4": true,
|
||||||
"pr-3": true,
|
"pr-3": true,
|
||||||
"pl-2": settings.general.newLayoutDesigns(),
|
"pl-4": settings.general.newLayoutDesigns(),
|
||||||
"pl-2 md:pl-4": !settings.general.newLayoutDesigns(),
|
"pl-2 md:pl-4": !settings.general.newLayoutDesigns(),
|
||||||
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered && !settings.general.newLayoutDesigns(),
|
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="h-12 w-full flex items-center justify-between gap-2">
|
<div class="h-12 w-full flex items-center justify-between gap-2">
|
||||||
<div
|
<div class="flex items-center gap-1 min-w-0 flex-1 pr-3">
|
||||||
classList={{
|
<div class="flex items-center min-w-0 grow-1">
|
||||||
"flex items-center gap-1 min-w-0 flex-1": true,
|
|
||||||
"pr-3": !settings.general.newLayoutDesigns(),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="flex items-center min-w-0 flex-1 w-full">
|
|
||||||
<Show when={parentID()}>
|
<Show when={parentID()}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1394,13 +1333,8 @@ export function MessageTimeline(props: {
|
|||||||
fallback={
|
fallback={
|
||||||
<h1
|
<h1
|
||||||
data-slot="session-title-child"
|
data-slot="session-title-child"
|
||||||
classList={{
|
class="text-14-medium text-text-strong truncate grow-1 min-w-0"
|
||||||
"text-14-medium text-text-strong truncate": true,
|
onDblClick={openTitleEditor}
|
||||||
"w-fit rounded-[6px] px-2 py-1 hover:bg-v2-overlay-simple-overlay-hover":
|
|
||||||
settings.general.newLayoutDesigns(),
|
|
||||||
"grow-1 min-w-0": !settings.general.newLayoutDesigns(),
|
|
||||||
}}
|
|
||||||
onClick={openTitleEditor}
|
|
||||||
>
|
>
|
||||||
{childTitle()}
|
{childTitle()}
|
||||||
</h1>
|
</h1>
|
||||||
@@ -1413,17 +1347,8 @@ export function MessageTimeline(props: {
|
|||||||
data-slot="session-title-child"
|
data-slot="session-title-child"
|
||||||
value={title.draft}
|
value={title.draft}
|
||||||
disabled={titleMutation.isPending}
|
disabled={titleMutation.isPending}
|
||||||
classList={{
|
class="text-14-medium text-text-strong grow-1 min-w-0 rounded-[6px] pl-1 -ml-1"
|
||||||
"text-14-medium text-text-strong block": true,
|
style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }}
|
||||||
"w-full flex-1 grow-1 min-w-0 pl-1 -ml-1 rounded-[6px]": !settings.general.newLayoutDesigns(),
|
|
||||||
"field-sizing-content self-start rounded-[6px] px-2 py-1 ":
|
|
||||||
settings.general.newLayoutDesigns(),
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
"--inline-input-shadow": settings.general.newLayoutDesigns()
|
|
||||||
? "none"
|
|
||||||
: "var(--shadow-xs-border-select)",
|
|
||||||
}}
|
|
||||||
onInput={(event) => setTitle("draft", event.currentTarget.value)}
|
onInput={(event) => setTitle("draft", event.currentTarget.value)}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
@@ -1445,170 +1370,88 @@ export function MessageTimeline(props: {
|
|||||||
</div>
|
</div>
|
||||||
<Show when={sessionID()} keyed>
|
<Show when={sessionID()} keyed>
|
||||||
{(id) => (
|
{(id) => (
|
||||||
<div
|
<div class="shrink-0 flex items-center gap-3">
|
||||||
classList={{
|
<SessionContextUsage placement="bottom" />
|
||||||
"shrink-0 flex items-center": true,
|
|
||||||
"gap-2": settings.general.newLayoutDesigns(),
|
|
||||||
"gap-3": !settings.general.newLayoutDesigns(),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SessionContextUsage
|
|
||||||
placement="bottom"
|
|
||||||
buttonAppearance={settings.general.newLayoutDesigns() ? "v2" : "default"}
|
|
||||||
/>
|
|
||||||
<Show when={!parentID()}>
|
<Show when={!parentID()}>
|
||||||
<Show
|
<DropdownMenu
|
||||||
when={settings.general.newLayoutDesigns()}
|
gutter={4}
|
||||||
fallback={
|
placement="bottom-end"
|
||||||
<DropdownMenu
|
open={title.menuOpen}
|
||||||
gutter={4}
|
onOpenChange={(open) => {
|
||||||
placement="bottom-end"
|
setTitle("menuOpen", open)
|
||||||
open={title.menuOpen}
|
if (open) return
|
||||||
onOpenChange={(open) => {
|
}}
|
||||||
setTitle("menuOpen", open)
|
>
|
||||||
if (open) return
|
<DropdownMenu.Trigger
|
||||||
|
as={IconButton}
|
||||||
|
icon="dot-grid"
|
||||||
|
variant="ghost"
|
||||||
|
class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
|
||||||
|
classList={{
|
||||||
|
"bg-surface-base-active": share.open || title.pendingShare,
|
||||||
|
}}
|
||||||
|
aria-label={language.t("common.moreOptions")}
|
||||||
|
aria-expanded={title.menuOpen || share.open || title.pendingShare}
|
||||||
|
ref={(el: HTMLButtonElement) => {
|
||||||
|
more = el
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<DropdownMenu.Portal>
|
||||||
|
<DropdownMenu.Content
|
||||||
|
style={{ "min-width": "104px" }}
|
||||||
|
onCloseAutoFocus={(event) => {
|
||||||
|
if (title.pendingRename) {
|
||||||
|
event.preventDefault()
|
||||||
|
setTitle("pendingRename", false)
|
||||||
|
openTitleEditor()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (title.pendingShare) {
|
||||||
|
event.preventDefault()
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setShare({ open: true, dismiss: null })
|
||||||
|
setTitle("pendingShare", false)
|
||||||
|
})
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Trigger
|
<DropdownMenu.Item
|
||||||
as={IconButton}
|
onSelect={() => {
|
||||||
icon="dot-grid"
|
setTitle("pendingRename", true)
|
||||||
variant="ghost"
|
setTitle("menuOpen", false)
|
||||||
class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
|
|
||||||
classList={{
|
|
||||||
"bg-surface-base-active": share.open || title.pendingShare,
|
|
||||||
}}
|
|
||||||
aria-label={language.t("common.moreOptions")}
|
|
||||||
aria-expanded={title.menuOpen || share.open || title.pendingShare}
|
|
||||||
ref={(el: HTMLButtonElement) => {
|
|
||||||
more = el
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<DropdownMenu.Portal>
|
|
||||||
<DropdownMenu.Content
|
|
||||||
style={{ "min-width": "104px" }}
|
|
||||||
onCloseAutoFocus={(event) => {
|
|
||||||
if (title.pendingRename) {
|
|
||||||
event.preventDefault()
|
|
||||||
setTitle("pendingRename", false)
|
|
||||||
openTitleEditor()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (title.pendingShare) {
|
|
||||||
event.preventDefault()
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
setShare({ open: true, dismiss: null })
|
|
||||||
setTitle("pendingShare", false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DropdownMenu.Item
|
|
||||||
onSelect={() => {
|
|
||||||
setTitle("pendingRename", true)
|
|
||||||
setTitle("menuOpen", false)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<Show when={shareEnabled()}>
|
|
||||||
<DropdownMenu.Item
|
|
||||||
onSelect={() => {
|
|
||||||
setTitle({ pendingShare: true, menuOpen: false })
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DropdownMenu.ItemLabel>
|
|
||||||
{language.t("session.share.action.share")}
|
|
||||||
</DropdownMenu.ItemLabel>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
</Show>
|
|
||||||
<DropdownMenu.Item onSelect={() => void archiveSession(id)}>
|
|
||||||
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Separator />
|
|
||||||
<DropdownMenu.Item
|
|
||||||
onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id} />)}
|
|
||||||
>
|
|
||||||
<DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu.Portal>
|
|
||||||
</DropdownMenu>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<MenuV2
|
|
||||||
gutter={6}
|
|
||||||
placement="bottom-end"
|
|
||||||
open={title.menuOpen}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
setTitle("menuOpen", open)
|
|
||||||
if (open) return
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MenuV2.Trigger
|
|
||||||
as={IconButtonV2}
|
|
||||||
icon={<IconV2 name="outline-dots" />}
|
|
||||||
variant="ghost-muted"
|
|
||||||
size="large"
|
|
||||||
state={share.open || title.pendingShare ? "pressed" : undefined}
|
|
||||||
aria-label={language.t("common.moreOptions")}
|
|
||||||
aria-expanded={title.menuOpen || share.open || title.pendingShare}
|
|
||||||
ref={(el: HTMLButtonElement) => {
|
|
||||||
more = el
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<MenuV2.Portal>
|
|
||||||
<MenuV2.Content
|
|
||||||
style={{ width: "120px", "min-width": "120px" }}
|
|
||||||
onCloseAutoFocus={(event) => {
|
|
||||||
if (title.pendingRename) {
|
|
||||||
event.preventDefault()
|
|
||||||
setTitle("pendingRename", false)
|
|
||||||
openTitleEditor()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (title.pendingShare) {
|
|
||||||
event.preventDefault()
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
setShare({ open: true, dismiss: null })
|
|
||||||
setTitle("pendingShare", false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MenuV2.Item
|
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<Show when={shareEnabled()}>
|
||||||
|
<DropdownMenu.Item
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
setTitle("pendingRename", true)
|
setTitle({ pendingShare: true, menuOpen: false })
|
||||||
setTitle("menuOpen", false)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{language.t("common.rename")}
|
<DropdownMenu.ItemLabel>
|
||||||
</MenuV2.Item>
|
{language.t("session.share.action.share")}
|
||||||
<Show when={shareEnabled()}>
|
</DropdownMenu.ItemLabel>
|
||||||
<MenuV2.Item
|
</DropdownMenu.Item>
|
||||||
onSelect={() => {
|
</Show>
|
||||||
setTitle({ pendingShare: true, menuOpen: false })
|
<DropdownMenu.Item onSelect={() => void archiveSession(id)}>
|
||||||
}}
|
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
|
||||||
>
|
</DropdownMenu.Item>
|
||||||
{language.t("session.share.action.share")}...
|
<DropdownMenu.Separator />
|
||||||
</MenuV2.Item>
|
<DropdownMenu.Item
|
||||||
</Show>
|
onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id} />)}
|
||||||
<MenuV2.Item onSelect={() => void archiveSession(id)}>
|
>
|
||||||
{language.t("common.archive")}
|
<DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
|
||||||
</MenuV2.Item>
|
</DropdownMenu.Item>
|
||||||
<MenuV2.Separator />
|
</DropdownMenu.Content>
|
||||||
<MenuV2.Item onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id} />)}>
|
</DropdownMenu.Portal>
|
||||||
{language.t("common.delete")}...
|
</DropdownMenu>
|
||||||
</MenuV2.Item>
|
|
||||||
</MenuV2.Content>
|
|
||||||
</MenuV2.Portal>
|
|
||||||
</MenuV2>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<KobaltePopover
|
<KobaltePopover
|
||||||
open={share.open}
|
open={share.open}
|
||||||
anchorRef={() => more}
|
anchorRef={() => more}
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
gutter={settings.general.newLayoutDesigns() ? 6 : 4}
|
gutter={4}
|
||||||
modal={false}
|
modal={false}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (open) setShare("dismiss", null)
|
if (open) setShare("dismiss", null)
|
||||||
@@ -1618,10 +1461,6 @@ export function MessageTimeline(props: {
|
|||||||
<KobaltePopover.Portal>
|
<KobaltePopover.Portal>
|
||||||
<KobaltePopover.Content
|
<KobaltePopover.Content
|
||||||
data-component="popover-content"
|
data-component="popover-content"
|
||||||
classList={{
|
|
||||||
"flex w-80 max-w-none flex-col items-start gap-3 rounded-[10px] border-0 bg-v2-background-bg-layer-01 p-3 shadow-[var(--v2-elevation-floating)]":
|
|
||||||
settings.general.newLayoutDesigns(),
|
|
||||||
}}
|
|
||||||
style={{ "min-width": "320px" }}
|
style={{ "min-width": "320px" }}
|
||||||
onEscapeKeyDown={(event) => {
|
onEscapeKeyDown={(event) => {
|
||||||
setShare({ dismiss: "escape", open: false })
|
setShare({ dismiss: "escape", open: false })
|
||||||
@@ -1639,90 +1478,24 @@ export function MessageTimeline(props: {
|
|||||||
setShare("dismiss", null)
|
setShare("dismiss", null)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show
|
<div class="flex flex-col p-3">
|
||||||
when={settings.general.newLayoutDesigns()}
|
<div class="flex flex-col gap-1">
|
||||||
fallback={
|
<div class="text-13-medium text-text-strong">
|
||||||
<div class="flex flex-col p-3">
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<div class="text-13-medium text-text-strong">
|
|
||||||
{language.t("session.share.popover.title")}
|
|
||||||
</div>
|
|
||||||
<div class="text-12-regular text-text-weak">
|
|
||||||
{shareUrl()
|
|
||||||
? language.t("session.share.popover.description.shared")
|
|
||||||
: language.t("session.share.popover.description.unshared")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-3 flex flex-col gap-2">
|
|
||||||
<Show
|
|
||||||
when={shareUrl()}
|
|
||||||
fallback={
|
|
||||||
<Button
|
|
||||||
size="large"
|
|
||||||
variant="primary"
|
|
||||||
class="w-full"
|
|
||||||
onClick={shareSession}
|
|
||||||
disabled={shareMutation.isPending}
|
|
||||||
>
|
|
||||||
{shareMutation.isPending
|
|
||||||
? language.t("session.share.action.publishing")
|
|
||||||
: language.t("session.share.action.publish")}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<TextField
|
|
||||||
value={shareUrl() ?? ""}
|
|
||||||
readOnly
|
|
||||||
copyable
|
|
||||||
copyKind="link"
|
|
||||||
tabIndex={-1}
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
<div class="grid grid-cols-2 gap-2">
|
|
||||||
<Button
|
|
||||||
size="large"
|
|
||||||
variant="secondary"
|
|
||||||
class="w-full shadow-none border border-border-weak-base"
|
|
||||||
onClick={unshareSession}
|
|
||||||
disabled={unshareMutation.isPending}
|
|
||||||
>
|
|
||||||
{unshareMutation.isPending
|
|
||||||
? language.t("session.share.action.unpublishing")
|
|
||||||
: language.t("session.share.action.unpublish")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="large"
|
|
||||||
variant="primary"
|
|
||||||
class="w-full"
|
|
||||||
onClick={viewShare}
|
|
||||||
disabled={unshareMutation.isPending}
|
|
||||||
>
|
|
||||||
{language.t("session.share.action.view")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="flex w-full flex-col gap-1.5 px-0.5 pt-0.5">
|
|
||||||
<div class="select-none text-[13px] font-[530] leading-none tracking-[-0.04px] text-v2-text-text-base [font-variation-settings:'slnt'_0]">
|
|
||||||
{language.t("session.share.popover.title")}
|
{language.t("session.share.popover.title")}
|
||||||
</div>
|
</div>
|
||||||
<div class="select-none text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-muted [font-variation-settings:'slnt'_0]">
|
<div class="text-12-regular text-text-weak">
|
||||||
{shareUrl()
|
{shareUrl()
|
||||||
? language.t("session.share.popover.description.shared")
|
? language.t("session.share.popover.description.shared")
|
||||||
: language.t("session.share.popover.description.unshared")}
|
: language.t("session.share.popover.description.unshared")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full flex-col gap-2">
|
<div class="mt-3 flex flex-col gap-2">
|
||||||
<Show
|
<Show
|
||||||
when={shareUrl()}
|
when={shareUrl()}
|
||||||
fallback={
|
fallback={
|
||||||
<ButtonV2
|
<Button
|
||||||
variant="contrast"
|
size="large"
|
||||||
|
variant="primary"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
onClick={shareSession}
|
onClick={shareSession}
|
||||||
disabled={shareMutation.isPending}
|
disabled={shareMutation.isPending}
|
||||||
@@ -1730,57 +1503,48 @@ export function MessageTimeline(props: {
|
|||||||
{shareMutation.isPending
|
{shareMutation.isPending
|
||||||
? language.t("session.share.action.publishing")
|
? language.t("session.share.action.publishing")
|
||||||
: language.t("session.share.action.publish")}
|
: language.t("session.share.action.publish")}
|
||||||
</ButtonV2>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div
|
<TextField
|
||||||
class="flex h-8 w-full items-center gap-1.5 rounded-[6px] py-1 pl-2.5 pr-1.5 shadow-[var(--v2-elevation-button-neutral)]"
|
value={shareUrl() ?? ""}
|
||||||
style={{
|
readOnly
|
||||||
background:
|
copyable
|
||||||
"linear-gradient(180deg, var(--v2-alpha-light-2) 0%, var(--v2-alpha-light-0) 100%), var(--v2-background-bg-button-neutral)",
|
copyKind="link"
|
||||||
}}
|
tabIndex={-1}
|
||||||
>
|
class="w-full"
|
||||||
<div
|
/>
|
||||||
class="min-w-0 flex-1 truncate select-text cursor-text text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base [font-variation-settings:'slnt'_0]"
|
<div class="grid grid-cols-2 gap-2">
|
||||||
onClick={selectShareUrlText}
|
<Button
|
||||||
>
|
size="large"
|
||||||
{shareUrl()}
|
variant="secondary"
|
||||||
</div>
|
class={
|
||||||
<IconButtonV2
|
settings.general.newLayoutDesigns()
|
||||||
type="button"
|
? "w-full shadow-none border-[0.5px] border-border-weak-base"
|
||||||
size="small"
|
: "w-full shadow-none border border-border-weak-base"
|
||||||
variant="ghost-muted"
|
}
|
||||||
icon={<IconV2 name="outline-copy" />}
|
|
||||||
aria-label={language.t("session.share.copy.copyLink")}
|
|
||||||
onClick={copyShareUrl}
|
|
||||||
/>
|
|
||||||
<IconButtonV2
|
|
||||||
type="button"
|
|
||||||
size="small"
|
|
||||||
variant="ghost-muted"
|
|
||||||
icon={<IconV2 name="outline-square-arrow" />}
|
|
||||||
aria-label={language.t("session.share.action.view")}
|
|
||||||
onClick={viewShare}
|
|
||||||
disabled={unshareMutation.isPending}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex w-full">
|
|
||||||
<ButtonV2
|
|
||||||
variant="outline"
|
|
||||||
class="w-full"
|
|
||||||
onClick={unshareSession}
|
onClick={unshareSession}
|
||||||
disabled={unshareMutation.isPending}
|
disabled={unshareMutation.isPending}
|
||||||
>
|
>
|
||||||
{unshareMutation.isPending
|
{unshareMutation.isPending
|
||||||
? language.t("session.share.action.unpublishing")
|
? language.t("session.share.action.unpublishing")
|
||||||
: language.t("session.share.action.unpublish")}
|
: language.t("session.share.action.unpublish")}
|
||||||
</ButtonV2>
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
variant="primary"
|
||||||
|
class="w-full"
|
||||||
|
onClick={viewShare}
|
||||||
|
disabled={unshareMutation.isPending}
|
||||||
|
>
|
||||||
|
{language.t("session.share.action.view")}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</div>
|
||||||
</KobaltePopover.Content>
|
</KobaltePopover.Content>
|
||||||
</KobaltePopover.Portal>
|
</KobaltePopover.Portal>
|
||||||
</KobaltePopover>
|
</KobaltePopover>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { Tag } from "@opencode-ai/ui/v2/badge-v2"
|
import { Tag } from "@opencode-ai/ui/v2/badge-v2"
|
||||||
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||||
import { Dialog, DialogBody, DialogHeader, DialogTitle } from "@opencode-ai/ui/v2/dialog-v2"
|
import { Dialog } from "@opencode-ai/ui/v2/dialog-v2"
|
||||||
import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2"
|
|
||||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
||||||
@@ -31,14 +30,8 @@ export function AddServerMenu(props: { onAddServer: () => void }) {
|
|||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const openAddWsl = () => {
|
const openAddWsl = () => {
|
||||||
dialog.push(() => (
|
dialog.push(() => (
|
||||||
<Dialog size="large" fit class="settings-v2-wsl-dialog">
|
<Dialog title={language.t("wsl.server.add")} size="large" fit class="settings-v2-wsl-dialog">
|
||||||
<DialogHeader hideClose={true}>
|
<DialogAddWslServer />
|
||||||
<DialogTitle>{language.t("wsl.server.add")}</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<DividerV2 />
|
|
||||||
<DialogBody>
|
|
||||||
<DialogAddWslServer />
|
|
||||||
</DialogBody>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/cli",
|
"name": "@opencode-ai/cli",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { NodeHttpServer } from "@effect/platform-node"
|
import { NodeHttpServer } from "@effect/platform-node"
|
||||||
import { Credential } from "@opencode-ai/core/credential"
|
import { Credential } from "@opencode-ai/core/credential"
|
||||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
|
||||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||||
import { Context, Layer, Option } from "effect"
|
import { Context, Layer, Option } from "effect"
|
||||||
import * as Effect from "effect/Effect"
|
import * as Effect from "effect/Effect"
|
||||||
@@ -40,7 +38,8 @@ function bind(hostname: string, port: number, password: string) {
|
|||||||
return Layer.build(
|
return Layer.build(
|
||||||
HttpRouter.serve(createRoutes(password), { disableListenLog: true, disableLogger: true }).pipe(
|
HttpRouter.serve(createRoutes(password), { disableListenLog: true, disableLogger: true }).pipe(
|
||||||
Layer.provideMerge(NodeHttpServer.layer(() => createServer(), { port, host: hostname })),
|
Layer.provideMerge(NodeHttpServer.layer(() => createServer(), { port, host: hostname })),
|
||||||
Layer.provide(AppNodeBuilder.build(LayerNode.group([Credential.node, PermissionSaved.node]))),
|
Layer.provide(Credential.defaultLayer),
|
||||||
|
Layer.provide(PermissionSaved.defaultLayer),
|
||||||
),
|
),
|
||||||
).pipe(Effect.map((context) => Context.get(context, HttpServer.HttpServer).address))
|
).pipe(Effect.map((context) => Context.get(context, HttpServer.HttpServer).address))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const Handlers = Runtime.handlers(Commands, {
|
|||||||
})
|
})
|
||||||
|
|
||||||
Runtime.run(Commands, Handlers, { version: "local" }).pipe(
|
Runtime.run(Commands, Handlers, { version: "local" }).pipe(
|
||||||
Effect.provide(Daemon.layer),
|
Effect.provide(Daemon.defaultLayer),
|
||||||
Effect.provide(NodeServices.layer),
|
Effect.provide(NodeServices.layer),
|
||||||
Effect.scoped,
|
Effect.scoped,
|
||||||
NodeRuntime.runMain,
|
NodeRuntime.runMain,
|
||||||
|
|||||||
@@ -189,4 +189,6 @@ export const layer = Layer.effect(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const defaultLayer = layer
|
||||||
|
|
||||||
export * as Daemon from "./daemon"
|
export * as Daemon from "./daemon"
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { run } from "@opencode-ai/tui"
|
import { run } from "@opencode-ai/tui"
|
||||||
import { TuiConfig } from "@opencode-ai/tui/config"
|
import { TuiConfig } from "@opencode-ai/tui/config"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
|
|
||||||
export function runTui(transport: { url: string; headers: RequestInit["headers"] }) {
|
export function runTui(transport: { url: string; headers: RequestInit["headers"] }) {
|
||||||
@@ -15,7 +14,7 @@ export function runTui(transport: { url: string; headers: RequestInit["headers"]
|
|||||||
async start() {},
|
async start() {},
|
||||||
async dispose() {},
|
async dispose() {},
|
||||||
},
|
},
|
||||||
}).pipe(Effect.provide(AppNodeBuilder.build(Global.node)))
|
}).pipe(Effect.provide(Global.defaultLayer))
|
||||||
}
|
}
|
||||||
|
|
||||||
const legacyDefaults: Record<string, unknown> = {
|
const legacyDefaults: Record<string, unknown> = {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Private generation target for clients derived directly from OpenCode's authorita
|
|||||||
- `@opencode-ai/client`: zero-Effect Promise client using `fetch`.
|
- `@opencode-ai/client`: zero-Effect Promise client using `fetch`.
|
||||||
- `@opencode-ai/client/effect`: rich Effect network client using an environment-provided `HttpClient`.
|
- `@opencode-ai/client/effect`: rich Effect network client using an environment-provided `HttpClient`.
|
||||||
|
|
||||||
The generated surface includes every standard HTTP group from Server's concrete API. The build compiler reads `@opencode-ai/server/api`; the generated Effect runtime imports a client-local projection built from Protocol, with a generation-equivalence test preventing transport drift. Custom transports such as the PTY WebSocket connection remain outside the generic HTTP client. Run `bun run generate` after changing the contract and `bun run check:generated` to detect committed-output drift.
|
The generated surface starts with the Session group from Server's concrete API. The build compiler reads `@opencode-ai/server/api`; the generated Effect runtime imports a client-local projection built from Protocol, with a generation-equivalence test preventing transport drift. Run `bun run generate` after changing the contract and `bun run check:generated` to detect committed-output drift.
|
||||||
|
|
||||||
The Effect entrypoint uses canonical decoded values such as `Session.ID`, `Location.Ref`, and `Prompt`. These datatypes come from the lightweight `@opencode-ai/schema` package and are re-exported so callers depend only on the client surface. Protocol owns endpoint construction and middleware placement; Server supplies the concrete middleware keys used by the build-time API.
|
The Effect entrypoint uses canonical decoded values such as `Session.ID`, `Location.Ref`, and `Prompt`. These datatypes come from the lightweight `@opencode-ai/schema` package and are re-exported so callers depend only on the client surface. Protocol owns endpoint construction and middleware placement; Server supplies the concrete middleware keys used by the build-time API.
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,20 @@
|
|||||||
import { NodeFileSystem } from "@effect/platform-node"
|
import { NodeFileSystem } from "@effect/platform-node"
|
||||||
import { compile, emitEffectImported, emitPromise, write } from "@opencode-ai/httpapi-codegen"
|
import { compile, emitEffectImported, emitPromise, write } from "@opencode-ai/httpapi-codegen"
|
||||||
import { ClientApi, endpointNames, groupNames, omitEndpoints } from "../src/contract"
|
import { Api } from "@opencode-ai/server/api"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
|
import { HttpApi } from "effect/unstable/httpapi"
|
||||||
import { fileURLToPath } from "url"
|
import { fileURLToPath } from "url"
|
||||||
|
|
||||||
const contract = compile(ClientApi, { groupNames, endpointNames, omitEndpoints })
|
const contract = compile(HttpApi.make("opencode-client").add(Api.groups["server.session"]), {
|
||||||
|
groupNames: { "server.session": "sessions" },
|
||||||
|
})
|
||||||
|
|
||||||
await Effect.runPromise(
|
await Effect.runPromise(
|
||||||
Effect.all(
|
Effect.all(
|
||||||
[
|
[
|
||||||
|
write(emitPromise(contract), fileURLToPath(new URL("../src/generated", import.meta.url))),
|
||||||
write(
|
write(
|
||||||
emitPromise(contract, {
|
emitEffectImported(contract, { module: "../contract", group: "SessionGroup" }),
|
||||||
outputTypes: {
|
|
||||||
"events.subscribe": {
|
|
||||||
name: "OpenCodeEventEncoded",
|
|
||||||
import: 'import type { OpenCodeEventEncoded } from "@opencode-ai/protocol/groups/event"',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
fileURLToPath(new URL("../src/generated", import.meta.url)),
|
|
||||||
),
|
|
||||||
write(
|
|
||||||
emitEffectImported(contract, { module: "../contract", api: "ClientApi" }),
|
|
||||||
fileURLToPath(new URL("../src/generated-effect", import.meta.url)),
|
fileURLToPath(new URL("../src/generated-effect", import.meta.url)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -11,43 +11,9 @@ class SessionLocationMiddleware extends HttpApiMiddleware.Service<SessionLocatio
|
|||||||
{ error: [InvalidRequestError, SessionNotFoundError] },
|
{ error: [InvalidRequestError, SessionNotFoundError] },
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
export const ClientApi = makeDefaultApi({
|
const Api = makeDefaultApi({
|
||||||
locationMiddleware: LocationMiddleware,
|
locationMiddleware: LocationMiddleware,
|
||||||
sessionLocationMiddleware: SessionLocationMiddleware,
|
sessionLocationMiddleware: SessionLocationMiddleware,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const groupNames = {
|
export const SessionGroup = Api.groups["server.session"]
|
||||||
"server.health": "health",
|
|
||||||
"server.location": "location",
|
|
||||||
"server.agent": "agents",
|
|
||||||
"server.session": "sessions",
|
|
||||||
"server.message": "messages",
|
|
||||||
"server.model": "models",
|
|
||||||
"server.provider": "providers",
|
|
||||||
"server.integration": "integrations",
|
|
||||||
"server.credential": "credentials",
|
|
||||||
"server.permission": "permissions",
|
|
||||||
"server.fs": "files",
|
|
||||||
"server.command": "commands",
|
|
||||||
"server.skill": "skills",
|
|
||||||
"server.event": "events",
|
|
||||||
"server.pty": "ptys",
|
|
||||||
"server.question": "questions",
|
|
||||||
"server.reference": "references",
|
|
||||||
"server.projectCopy": "projectCopies",
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export const endpointNames = {
|
|
||||||
"session.messages": "list",
|
|
||||||
"integration.connect.key": "connectKey",
|
|
||||||
"integration.connect.oauth": "connectOauth",
|
|
||||||
"integration.attempt.status": "attemptStatus",
|
|
||||||
"integration.attempt.complete": "attemptComplete",
|
|
||||||
"integration.attempt.cancel": "attemptCancel",
|
|
||||||
"permission.request.list": "listRequests",
|
|
||||||
"permission.saved.list": "listSaved",
|
|
||||||
"permission.saved.remove": "removeSaved",
|
|
||||||
"question.request.list": "listRequests",
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export const omitEndpoints = new Set(["fs.read", "pty.connect", "pty.connectToken"])
|
|
||||||
|
|||||||
@@ -2,24 +2,11 @@
|
|||||||
// Core or Server. Preserve these datatype exports so internal model reorganizations do not require caller migrations.
|
// Core or Server. Preserve these datatype exports so internal model reorganizations do not require caller migrations.
|
||||||
export * from "./generated-effect/index"
|
export * from "./generated-effect/index"
|
||||||
export { Agent } from "@opencode-ai/schema/agent"
|
export { Agent } from "@opencode-ai/schema/agent"
|
||||||
export { Command } from "@opencode-ai/schema/command"
|
|
||||||
export { Credential } from "@opencode-ai/schema/credential"
|
|
||||||
export { FileSystem } from "@opencode-ai/schema/filesystem"
|
|
||||||
export { Integration } from "@opencode-ai/schema/integration"
|
|
||||||
export { Location } from "@opencode-ai/schema/location"
|
export { Location } from "@opencode-ai/schema/location"
|
||||||
export { Model } from "@opencode-ai/schema/model"
|
export { Model } from "@opencode-ai/schema/model"
|
||||||
export { Permission } from "@opencode-ai/schema/permission"
|
|
||||||
export { PermissionSaved } from "@opencode-ai/schema/permission-saved"
|
|
||||||
export { Project } from "@opencode-ai/schema/project"
|
|
||||||
export { ProjectCopy } from "@opencode-ai/schema/project-copy"
|
|
||||||
export { Provider } from "@opencode-ai/schema/provider"
|
export { Provider } from "@opencode-ai/schema/provider"
|
||||||
export { Pty } from "@opencode-ai/schema/pty"
|
|
||||||
export { Question } from "@opencode-ai/schema/question"
|
|
||||||
export { Reference } from "@opencode-ai/schema/reference"
|
|
||||||
export { AbsolutePath, RelativePath } from "@opencode-ai/schema/schema"
|
export { AbsolutePath, RelativePath } from "@opencode-ai/schema/schema"
|
||||||
export { Session } from "@opencode-ai/schema/session"
|
export { Session } from "@opencode-ai/schema/session"
|
||||||
export { SessionInput } from "@opencode-ai/schema/session-input"
|
export { SessionInput } from "@opencode-ai/schema/session-input"
|
||||||
export { SessionMessage } from "@opencode-ai/schema/session-message"
|
export { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||||
export { Skill } from "@opencode-ai/schema/skill"
|
|
||||||
export { Prompt } from "@opencode-ai/schema/prompt"
|
export { Prompt } from "@opencode-ai/schema/prompt"
|
||||||
export type { OpenCodeEvent } from "@opencode-ai/protocol/groups/event"
|
|
||||||
|
|||||||
@@ -2,705 +2,202 @@
|
|||||||
import { Effect, Stream, Schema } from "effect"
|
import { Effect, Stream, Schema } from "effect"
|
||||||
import { Sse } from "effect/unstable/encoding"
|
import { Sse } from "effect/unstable/encoding"
|
||||||
import { HttpClientError } from "effect/unstable/http"
|
import { HttpClientError } from "effect/unstable/http"
|
||||||
import { HttpApiClient } from "effect/unstable/httpapi"
|
import { HttpApi, HttpApiClient } from "effect/unstable/httpapi"
|
||||||
import { ClientApi } from "../contract"
|
import { SessionGroup } from "../contract"
|
||||||
import { ClientError } from "./client-error"
|
import { ClientError } from "./client-error"
|
||||||
|
|
||||||
type RawClient = HttpApiClient.ForApi<typeof ClientApi>
|
const Api = HttpApi.make("generated").add(SessionGroup)
|
||||||
|
|
||||||
|
type RawClient = HttpApiClient.ForApi<typeof Api>
|
||||||
|
|
||||||
const mapClientError = <E>(error: E) =>
|
const mapClientError = <E>(error: E) =>
|
||||||
HttpClientError.isHttpClientError(error) || Schema.isSchemaError(error) || Sse.Retry.is(error)
|
HttpClientError.isHttpClientError(error) || Schema.isSchemaError(error) || Sse.Retry.is(error)
|
||||||
? new ClientError({ cause: error })
|
? new ClientError({ cause: error })
|
||||||
: error
|
: error
|
||||||
|
|
||||||
const Endpoint0_0 = (raw: RawClient["server.health"]) => () =>
|
type Endpoint0_0Request = Parameters<RawClient["server.session"]["session.list"]>[0]
|
||||||
raw["health.get"]({}).pipe(Effect.mapError(mapClientError))
|
type Endpoint0_0Input = {
|
||||||
|
readonly workspace?: Endpoint0_0Request["query"]["workspace"]
|
||||||
const adaptGroup0 = (raw: RawClient["server.health"]) => ({ get: Endpoint0_0(raw) })
|
readonly limit?: Endpoint0_0Request["query"]["limit"]
|
||||||
|
readonly order?: Endpoint0_0Request["query"]["order"]
|
||||||
type Endpoint1_0Request = Parameters<RawClient["server.location"]["location.get"]>[0]
|
readonly search?: Endpoint0_0Request["query"]["search"]
|
||||||
type Endpoint1_0Input = { readonly location?: Endpoint1_0Request["query"]["location"] }
|
readonly directory?: Endpoint0_0Request["query"]["directory"]
|
||||||
const Endpoint1_0 = (raw: RawClient["server.location"]) => (input?: Endpoint1_0Input) =>
|
readonly project?: Endpoint0_0Request["query"]["project"]
|
||||||
raw["location.get"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
readonly subpath?: Endpoint0_0Request["query"]["subpath"]
|
||||||
|
readonly cursor?: Endpoint0_0Request["query"]["cursor"]
|
||||||
const adaptGroup1 = (raw: RawClient["server.location"]) => ({ get: Endpoint1_0(raw) })
|
|
||||||
|
|
||||||
type Endpoint2_0Request = Parameters<RawClient["server.agent"]["agent.list"]>[0]
|
|
||||||
type Endpoint2_0Input = { readonly location?: Endpoint2_0Request["query"]["location"] }
|
|
||||||
const Endpoint2_0 = (raw: RawClient["server.agent"]) => (input?: Endpoint2_0Input) =>
|
|
||||||
raw["agent.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
const adaptGroup2 = (raw: RawClient["server.agent"]) => ({ list: Endpoint2_0(raw) })
|
|
||||||
|
|
||||||
type Endpoint3_0Request = Parameters<RawClient["server.session"]["session.list"]>[0]
|
|
||||||
type Endpoint3_0Input = {
|
|
||||||
readonly workspace?: Endpoint3_0Request["query"]["workspace"]
|
|
||||||
readonly limit?: Endpoint3_0Request["query"]["limit"]
|
|
||||||
readonly order?: Endpoint3_0Request["query"]["order"]
|
|
||||||
readonly search?: Endpoint3_0Request["query"]["search"]
|
|
||||||
readonly directory?: Endpoint3_0Request["query"]["directory"]
|
|
||||||
readonly project?: Endpoint3_0Request["query"]["project"]
|
|
||||||
readonly subpath?: Endpoint3_0Request["query"]["subpath"]
|
|
||||||
readonly cursor?: Endpoint3_0Request["query"]["cursor"]
|
|
||||||
}
|
}
|
||||||
const Endpoint3_0 = (raw: RawClient["server.session"]) => (input?: Endpoint3_0Input) =>
|
const Endpoint0_0 = (raw: RawClient["server.session"]) => (input?: Endpoint0_0Input) =>
|
||||||
raw["session.list"]({
|
raw["session.list"]({
|
||||||
query: {
|
query: {
|
||||||
workspace: input?.["workspace"],
|
workspace: input?.workspace,
|
||||||
limit: input?.["limit"],
|
limit: input?.limit,
|
||||||
order: input?.["order"],
|
order: input?.order,
|
||||||
search: input?.["search"],
|
search: input?.search,
|
||||||
directory: input?.["directory"],
|
directory: input?.directory,
|
||||||
project: input?.["project"],
|
project: input?.project,
|
||||||
subpath: input?.["subpath"],
|
subpath: input?.subpath,
|
||||||
cursor: input?.["cursor"],
|
cursor: input?.cursor,
|
||||||
},
|
},
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint3_1Request = Parameters<RawClient["server.session"]["session.create"]>[0]
|
type Endpoint0_1Request = Parameters<RawClient["server.session"]["session.create"]>[0]
|
||||||
type Endpoint3_1Input = {
|
type Endpoint0_1Input = {
|
||||||
readonly id?: Endpoint3_1Request["payload"]["id"]
|
readonly id?: Endpoint0_1Request["payload"]["id"]
|
||||||
readonly agent?: Endpoint3_1Request["payload"]["agent"]
|
readonly agent?: Endpoint0_1Request["payload"]["agent"]
|
||||||
readonly model?: Endpoint3_1Request["payload"]["model"]
|
readonly model?: Endpoint0_1Request["payload"]["model"]
|
||||||
readonly location?: Endpoint3_1Request["payload"]["location"]
|
readonly location?: Endpoint0_1Request["payload"]["location"]
|
||||||
}
|
}
|
||||||
const Endpoint3_1 = (raw: RawClient["server.session"]) => (input?: Endpoint3_1Input) =>
|
const Endpoint0_1 = (raw: RawClient["server.session"]) => (input?: Endpoint0_1Input) =>
|
||||||
raw["session.create"]({
|
raw["session.create"]({
|
||||||
payload: { id: input?.["id"], agent: input?.["agent"], model: input?.["model"], location: input?.["location"] },
|
payload: { id: input?.id, agent: input?.agent, model: input?.model, location: input?.location },
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
const Endpoint3_2 = (raw: RawClient["server.session"]) => () =>
|
const Endpoint0_2 = (raw: RawClient["server.session"]) => () =>
|
||||||
raw["session.active"]({}).pipe(
|
raw["session.active"]({}).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint3_3Request = Parameters<RawClient["server.session"]["session.get"]>[0]
|
type Endpoint0_3Request = Parameters<RawClient["server.session"]["session.get"]>[0]
|
||||||
type Endpoint3_3Input = { readonly sessionID: Endpoint3_3Request["params"]["sessionID"] }
|
type Endpoint0_3Input = { readonly sessionID: Endpoint0_3Request["params"]["sessionID"] }
|
||||||
const Endpoint3_3 = (raw: RawClient["server.session"]) => (input: Endpoint3_3Input) =>
|
const Endpoint0_3 = (raw: RawClient["server.session"]) => (input: Endpoint0_3Input) =>
|
||||||
raw["session.get"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
raw["session.get"]({ params: { sessionID: input.sessionID } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint3_4Request = Parameters<RawClient["server.session"]["session.switchAgent"]>[0]
|
type Endpoint0_4Request = Parameters<RawClient["server.session"]["session.switchAgent"]>[0]
|
||||||
type Endpoint3_4Input = {
|
type Endpoint0_4Input = {
|
||||||
readonly sessionID: Endpoint3_4Request["params"]["sessionID"]
|
readonly sessionID: Endpoint0_4Request["params"]["sessionID"]
|
||||||
readonly agent: Endpoint3_4Request["payload"]["agent"]
|
readonly agent: Endpoint0_4Request["payload"]["agent"]
|
||||||
}
|
}
|
||||||
const Endpoint3_4 = (raw: RawClient["server.session"]) => (input: Endpoint3_4Input) =>
|
const Endpoint0_4 = (raw: RawClient["server.session"]) => (input: Endpoint0_4Input) =>
|
||||||
raw["session.switchAgent"]({ params: { sessionID: input["sessionID"] }, payload: { agent: input["agent"] } }).pipe(
|
raw["session.switchAgent"]({ params: { sessionID: input.sessionID }, payload: { agent: input.agent } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint3_5Request = Parameters<RawClient["server.session"]["session.switchModel"]>[0]
|
type Endpoint0_5Request = Parameters<RawClient["server.session"]["session.switchModel"]>[0]
|
||||||
type Endpoint3_5Input = {
|
type Endpoint0_5Input = {
|
||||||
readonly sessionID: Endpoint3_5Request["params"]["sessionID"]
|
readonly sessionID: Endpoint0_5Request["params"]["sessionID"]
|
||||||
readonly model: Endpoint3_5Request["payload"]["model"]
|
readonly model: Endpoint0_5Request["payload"]["model"]
|
||||||
}
|
}
|
||||||
const Endpoint3_5 = (raw: RawClient["server.session"]) => (input: Endpoint3_5Input) =>
|
const Endpoint0_5 = (raw: RawClient["server.session"]) => (input: Endpoint0_5Input) =>
|
||||||
raw["session.switchModel"]({ params: { sessionID: input["sessionID"] }, payload: { model: input["model"] } }).pipe(
|
raw["session.switchModel"]({ params: { sessionID: input.sessionID }, payload: { model: input.model } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint3_6Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
|
type Endpoint0_6Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
|
||||||
type Endpoint3_6Input = {
|
type Endpoint0_6Input = {
|
||||||
readonly sessionID: Endpoint3_6Request["params"]["sessionID"]
|
readonly sessionID: Endpoint0_6Request["params"]["sessionID"]
|
||||||
readonly id?: Endpoint3_6Request["payload"]["id"]
|
readonly id?: Endpoint0_6Request["payload"]["id"]
|
||||||
readonly prompt: Endpoint3_6Request["payload"]["prompt"]
|
readonly prompt: Endpoint0_6Request["payload"]["prompt"]
|
||||||
readonly delivery?: Endpoint3_6Request["payload"]["delivery"]
|
readonly delivery?: Endpoint0_6Request["payload"]["delivery"]
|
||||||
readonly resume?: Endpoint3_6Request["payload"]["resume"]
|
readonly resume?: Endpoint0_6Request["payload"]["resume"]
|
||||||
}
|
}
|
||||||
const Endpoint3_6 = (raw: RawClient["server.session"]) => (input: Endpoint3_6Input) =>
|
const Endpoint0_6 = (raw: RawClient["server.session"]) => (input: Endpoint0_6Input) =>
|
||||||
raw["session.prompt"]({
|
raw["session.prompt"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input.sessionID },
|
||||||
payload: { id: input["id"], prompt: input["prompt"], delivery: input["delivery"], resume: input["resume"] },
|
payload: { id: input.id, prompt: input.prompt, delivery: input.delivery, resume: input.resume },
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint3_7Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
|
type Endpoint0_7Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
|
||||||
type Endpoint3_7Input = { readonly sessionID: Endpoint3_7Request["params"]["sessionID"] }
|
type Endpoint0_7Input = { readonly sessionID: Endpoint0_7Request["params"]["sessionID"] }
|
||||||
const Endpoint3_7 = (raw: RawClient["server.session"]) => (input: Endpoint3_7Input) =>
|
const Endpoint0_7 = (raw: RawClient["server.session"]) => (input: Endpoint0_7Input) =>
|
||||||
raw["session.compact"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
raw["session.compact"]({ params: { sessionID: input.sessionID } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint3_8Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
|
type Endpoint0_8Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
|
||||||
type Endpoint3_8Input = { readonly sessionID: Endpoint3_8Request["params"]["sessionID"] }
|
type Endpoint0_8Input = { readonly sessionID: Endpoint0_8Request["params"]["sessionID"] }
|
||||||
const Endpoint3_8 = (raw: RawClient["server.session"]) => (input: Endpoint3_8Input) =>
|
const Endpoint0_8 = (raw: RawClient["server.session"]) => (input: Endpoint0_8Input) =>
|
||||||
raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
raw["session.wait"]({ params: { sessionID: input.sessionID } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint3_9Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
|
type Endpoint0_9Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
|
||||||
type Endpoint3_9Input = {
|
type Endpoint0_9Input = {
|
||||||
readonly sessionID: Endpoint3_9Request["params"]["sessionID"]
|
readonly sessionID: Endpoint0_9Request["params"]["sessionID"]
|
||||||
readonly messageID: Endpoint3_9Request["payload"]["messageID"]
|
readonly messageID: Endpoint0_9Request["payload"]["messageID"]
|
||||||
readonly files?: Endpoint3_9Request["payload"]["files"]
|
readonly files?: Endpoint0_9Request["payload"]["files"]
|
||||||
}
|
}
|
||||||
const Endpoint3_9 = (raw: RawClient["server.session"]) => (input: Endpoint3_9Input) =>
|
const Endpoint0_9 = (raw: RawClient["server.session"]) => (input: Endpoint0_9Input) =>
|
||||||
raw["session.revert.stage"]({
|
raw["session.revert.stage"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input.sessionID },
|
||||||
payload: { messageID: input["messageID"], files: input["files"] },
|
payload: { messageID: input.messageID, files: input.files },
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint3_10Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
|
type Endpoint0_10Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
|
||||||
type Endpoint3_10Input = { readonly sessionID: Endpoint3_10Request["params"]["sessionID"] }
|
type Endpoint0_10Input = { readonly sessionID: Endpoint0_10Request["params"]["sessionID"] }
|
||||||
const Endpoint3_10 = (raw: RawClient["server.session"]) => (input: Endpoint3_10Input) =>
|
const Endpoint0_10 = (raw: RawClient["server.session"]) => (input: Endpoint0_10Input) =>
|
||||||
raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
raw["session.revert.clear"]({ params: { sessionID: input.sessionID } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint3_11Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
|
type Endpoint0_11Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
|
||||||
type Endpoint3_11Input = { readonly sessionID: Endpoint3_11Request["params"]["sessionID"] }
|
type Endpoint0_11Input = { readonly sessionID: Endpoint0_11Request["params"]["sessionID"] }
|
||||||
const Endpoint3_11 = (raw: RawClient["server.session"]) => (input: Endpoint3_11Input) =>
|
const Endpoint0_11 = (raw: RawClient["server.session"]) => (input: Endpoint0_11Input) =>
|
||||||
raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
raw["session.revert.commit"]({ params: { sessionID: input.sessionID } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint3_12Request = Parameters<RawClient["server.session"]["session.context"]>[0]
|
type Endpoint0_12Request = Parameters<RawClient["server.session"]["session.context"]>[0]
|
||||||
type Endpoint3_12Input = { readonly sessionID: Endpoint3_12Request["params"]["sessionID"] }
|
type Endpoint0_12Input = { readonly sessionID: Endpoint0_12Request["params"]["sessionID"] }
|
||||||
const Endpoint3_12 = (raw: RawClient["server.session"]) => (input: Endpoint3_12Input) =>
|
const Endpoint0_12 = (raw: RawClient["server.session"]) => (input: Endpoint0_12Input) =>
|
||||||
raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
raw["session.context"]({ params: { sessionID: input.sessionID } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint3_13Request = Parameters<RawClient["server.session"]["session.history"]>[0]
|
type Endpoint0_13Request = Parameters<RawClient["server.session"]["session.events"]>[0]
|
||||||
type Endpoint3_13Input = {
|
type Endpoint0_13Input = {
|
||||||
readonly sessionID: Endpoint3_13Request["params"]["sessionID"]
|
readonly sessionID: Endpoint0_13Request["params"]["sessionID"]
|
||||||
readonly limit?: Endpoint3_13Request["query"]["limit"]
|
readonly after?: Endpoint0_13Request["query"]["after"]
|
||||||
readonly after?: Endpoint3_13Request["query"]["after"]
|
|
||||||
}
|
}
|
||||||
const Endpoint3_13 = (raw: RawClient["server.session"]) => (input: Endpoint3_13Input) =>
|
const Endpoint0_13 = (raw: RawClient["server.session"]) => (input: Endpoint0_13Input) =>
|
||||||
raw["session.history"]({
|
|
||||||
params: { sessionID: input["sessionID"] },
|
|
||||||
query: { limit: input["limit"], after: input["after"] },
|
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
type Endpoint3_14Request = Parameters<RawClient["server.session"]["session.events"]>[0]
|
|
||||||
type Endpoint3_14Input = {
|
|
||||||
readonly sessionID: Endpoint3_14Request["params"]["sessionID"]
|
|
||||||
readonly after?: Endpoint3_14Request["query"]["after"]
|
|
||||||
}
|
|
||||||
const Endpoint3_14 = (raw: RawClient["server.session"]) => (input: Endpoint3_14Input) =>
|
|
||||||
Stream.unwrap(
|
Stream.unwrap(
|
||||||
raw["session.events"]({ params: { sessionID: input["sessionID"] }, query: { after: input["after"] } }).pipe(
|
raw["session.events"]({ params: { sessionID: input.sessionID }, query: { after: input.after } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError))),
|
Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError))),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint3_15Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
type Endpoint0_14Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
||||||
type Endpoint3_15Input = { readonly sessionID: Endpoint3_15Request["params"]["sessionID"] }
|
type Endpoint0_14Input = { readonly sessionID: Endpoint0_14Request["params"]["sessionID"] }
|
||||||
const Endpoint3_15 = (raw: RawClient["server.session"]) => (input: Endpoint3_15Input) =>
|
const Endpoint0_14 = (raw: RawClient["server.session"]) => (input: Endpoint0_14Input) =>
|
||||||
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
raw["session.interrupt"]({ params: { sessionID: input.sessionID } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint3_16Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
type Endpoint0_15Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
||||||
type Endpoint3_16Input = {
|
type Endpoint0_15Input = {
|
||||||
readonly sessionID: Endpoint3_16Request["params"]["sessionID"]
|
readonly sessionID: Endpoint0_15Request["params"]["sessionID"]
|
||||||
readonly messageID: Endpoint3_16Request["params"]["messageID"]
|
readonly messageID: Endpoint0_15Request["params"]["messageID"]
|
||||||
}
|
}
|
||||||
const Endpoint3_16 = (raw: RawClient["server.session"]) => (input: Endpoint3_16Input) =>
|
const Endpoint0_15 = (raw: RawClient["server.session"]) => (input: Endpoint0_15Input) =>
|
||||||
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
|
raw["session.message"]({ params: { sessionID: input.sessionID, messageID: input.messageID } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
const adaptGroup3 = (raw: RawClient["server.session"]) => ({
|
const adaptGroup0 = (raw: RawClient["server.session"]) => ({
|
||||||
list: Endpoint3_0(raw),
|
list: Endpoint0_0(raw),
|
||||||
create: Endpoint3_1(raw),
|
create: Endpoint0_1(raw),
|
||||||
active: Endpoint3_2(raw),
|
active: Endpoint0_2(raw),
|
||||||
get: Endpoint3_3(raw),
|
get: Endpoint0_3(raw),
|
||||||
switchAgent: Endpoint3_4(raw),
|
switchAgent: Endpoint0_4(raw),
|
||||||
switchModel: Endpoint3_5(raw),
|
switchModel: Endpoint0_5(raw),
|
||||||
prompt: Endpoint3_6(raw),
|
prompt: Endpoint0_6(raw),
|
||||||
compact: Endpoint3_7(raw),
|
compact: Endpoint0_7(raw),
|
||||||
wait: Endpoint3_8(raw),
|
wait: Endpoint0_8(raw),
|
||||||
stage: Endpoint3_9(raw),
|
stage: Endpoint0_9(raw),
|
||||||
clear: Endpoint3_10(raw),
|
clear: Endpoint0_10(raw),
|
||||||
commit: Endpoint3_11(raw),
|
commit: Endpoint0_11(raw),
|
||||||
context: Endpoint3_12(raw),
|
context: Endpoint0_12(raw),
|
||||||
history: Endpoint3_13(raw),
|
events: Endpoint0_13(raw),
|
||||||
events: Endpoint3_14(raw),
|
interrupt: Endpoint0_14(raw),
|
||||||
interrupt: Endpoint3_15(raw),
|
message: Endpoint0_15(raw),
|
||||||
message: Endpoint3_16(raw),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
type Endpoint4_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0]
|
const adaptClient = (raw: RawClient) => ({ sessions: adaptGroup0(raw["server.session"]) })
|
||||||
type Endpoint4_0Input = {
|
|
||||||
readonly sessionID: Endpoint4_0Request["params"]["sessionID"]
|
|
||||||
readonly limit?: Endpoint4_0Request["query"]["limit"]
|
|
||||||
readonly order?: Endpoint4_0Request["query"]["order"]
|
|
||||||
readonly cursor?: Endpoint4_0Request["query"]["cursor"]
|
|
||||||
}
|
|
||||||
const Endpoint4_0 = (raw: RawClient["server.message"]) => (input: Endpoint4_0Input) =>
|
|
||||||
raw["session.messages"]({
|
|
||||||
params: { sessionID: input["sessionID"] },
|
|
||||||
query: { limit: input["limit"], order: input["order"], cursor: input["cursor"] },
|
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
const adaptGroup4 = (raw: RawClient["server.message"]) => ({ list: Endpoint4_0(raw) })
|
|
||||||
|
|
||||||
type Endpoint5_0Request = Parameters<RawClient["server.model"]["model.list"]>[0]
|
|
||||||
type Endpoint5_0Input = { readonly location?: Endpoint5_0Request["query"]["location"] }
|
|
||||||
const Endpoint5_0 = (raw: RawClient["server.model"]) => (input?: Endpoint5_0Input) =>
|
|
||||||
raw["model.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
const adaptGroup5 = (raw: RawClient["server.model"]) => ({ list: Endpoint5_0(raw) })
|
|
||||||
|
|
||||||
type Endpoint6_0Request = Parameters<RawClient["server.provider"]["provider.list"]>[0]
|
|
||||||
type Endpoint6_0Input = { readonly location?: Endpoint6_0Request["query"]["location"] }
|
|
||||||
const Endpoint6_0 = (raw: RawClient["server.provider"]) => (input?: Endpoint6_0Input) =>
|
|
||||||
raw["provider.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
type Endpoint6_1Request = Parameters<RawClient["server.provider"]["provider.get"]>[0]
|
|
||||||
type Endpoint6_1Input = {
|
|
||||||
readonly providerID: Endpoint6_1Request["params"]["providerID"]
|
|
||||||
readonly location?: Endpoint6_1Request["query"]["location"]
|
|
||||||
}
|
|
||||||
const Endpoint6_1 = (raw: RawClient["server.provider"]) => (input: Endpoint6_1Input) =>
|
|
||||||
raw["provider.get"]({ params: { providerID: input["providerID"] }, query: { location: input["location"] } }).pipe(
|
|
||||||
Effect.mapError(mapClientError),
|
|
||||||
)
|
|
||||||
|
|
||||||
const adaptGroup6 = (raw: RawClient["server.provider"]) => ({ list: Endpoint6_0(raw), get: Endpoint6_1(raw) })
|
|
||||||
|
|
||||||
type Endpoint7_0Request = Parameters<RawClient["server.integration"]["integration.list"]>[0]
|
|
||||||
type Endpoint7_0Input = { readonly location?: Endpoint7_0Request["query"]["location"] }
|
|
||||||
const Endpoint7_0 = (raw: RawClient["server.integration"]) => (input?: Endpoint7_0Input) =>
|
|
||||||
raw["integration.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
type Endpoint7_1Request = Parameters<RawClient["server.integration"]["integration.get"]>[0]
|
|
||||||
type Endpoint7_1Input = {
|
|
||||||
readonly integrationID: Endpoint7_1Request["params"]["integrationID"]
|
|
||||||
readonly location?: Endpoint7_1Request["query"]["location"]
|
|
||||||
}
|
|
||||||
const Endpoint7_1 = (raw: RawClient["server.integration"]) => (input: Endpoint7_1Input) =>
|
|
||||||
raw["integration.get"]({
|
|
||||||
params: { integrationID: input["integrationID"] },
|
|
||||||
query: { location: input["location"] },
|
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
type Endpoint7_2Request = Parameters<RawClient["server.integration"]["integration.connect.key"]>[0]
|
|
||||||
type Endpoint7_2Input = {
|
|
||||||
readonly integrationID: Endpoint7_2Request["params"]["integrationID"]
|
|
||||||
readonly location?: Endpoint7_2Request["query"]["location"]
|
|
||||||
readonly key: Endpoint7_2Request["payload"]["key"]
|
|
||||||
readonly label?: Endpoint7_2Request["payload"]["label"]
|
|
||||||
}
|
|
||||||
const Endpoint7_2 = (raw: RawClient["server.integration"]) => (input: Endpoint7_2Input) =>
|
|
||||||
raw["integration.connect.key"]({
|
|
||||||
params: { integrationID: input["integrationID"] },
|
|
||||||
query: { location: input["location"] },
|
|
||||||
payload: { key: input["key"], label: input["label"] },
|
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
type Endpoint7_3Request = Parameters<RawClient["server.integration"]["integration.connect.oauth"]>[0]
|
|
||||||
type Endpoint7_3Input = {
|
|
||||||
readonly integrationID: Endpoint7_3Request["params"]["integrationID"]
|
|
||||||
readonly location?: Endpoint7_3Request["query"]["location"]
|
|
||||||
readonly methodID: Endpoint7_3Request["payload"]["methodID"]
|
|
||||||
readonly inputs: Endpoint7_3Request["payload"]["inputs"]
|
|
||||||
readonly label?: Endpoint7_3Request["payload"]["label"]
|
|
||||||
}
|
|
||||||
const Endpoint7_3 = (raw: RawClient["server.integration"]) => (input: Endpoint7_3Input) =>
|
|
||||||
raw["integration.connect.oauth"]({
|
|
||||||
params: { integrationID: input["integrationID"] },
|
|
||||||
query: { location: input["location"] },
|
|
||||||
payload: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] },
|
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
type Endpoint7_4Request = Parameters<RawClient["server.integration"]["integration.attempt.status"]>[0]
|
|
||||||
type Endpoint7_4Input = {
|
|
||||||
readonly attemptID: Endpoint7_4Request["params"]["attemptID"]
|
|
||||||
readonly location?: Endpoint7_4Request["query"]["location"]
|
|
||||||
}
|
|
||||||
const Endpoint7_4 = (raw: RawClient["server.integration"]) => (input: Endpoint7_4Input) =>
|
|
||||||
raw["integration.attempt.status"]({
|
|
||||||
params: { attemptID: input["attemptID"] },
|
|
||||||
query: { location: input["location"] },
|
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
type Endpoint7_5Request = Parameters<RawClient["server.integration"]["integration.attempt.complete"]>[0]
|
|
||||||
type Endpoint7_5Input = {
|
|
||||||
readonly attemptID: Endpoint7_5Request["params"]["attemptID"]
|
|
||||||
readonly location?: Endpoint7_5Request["query"]["location"]
|
|
||||||
readonly code?: Endpoint7_5Request["payload"]["code"]
|
|
||||||
}
|
|
||||||
const Endpoint7_5 = (raw: RawClient["server.integration"]) => (input: Endpoint7_5Input) =>
|
|
||||||
raw["integration.attempt.complete"]({
|
|
||||||
params: { attemptID: input["attemptID"] },
|
|
||||||
query: { location: input["location"] },
|
|
||||||
payload: { code: input["code"] },
|
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
type Endpoint7_6Request = Parameters<RawClient["server.integration"]["integration.attempt.cancel"]>[0]
|
|
||||||
type Endpoint7_6Input = {
|
|
||||||
readonly attemptID: Endpoint7_6Request["params"]["attemptID"]
|
|
||||||
readonly location?: Endpoint7_6Request["query"]["location"]
|
|
||||||
}
|
|
||||||
const Endpoint7_6 = (raw: RawClient["server.integration"]) => (input: Endpoint7_6Input) =>
|
|
||||||
raw["integration.attempt.cancel"]({
|
|
||||||
params: { attemptID: input["attemptID"] },
|
|
||||||
query: { location: input["location"] },
|
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
const adaptGroup7 = (raw: RawClient["server.integration"]) => ({
|
|
||||||
list: Endpoint7_0(raw),
|
|
||||||
get: Endpoint7_1(raw),
|
|
||||||
connectKey: Endpoint7_2(raw),
|
|
||||||
connectOauth: Endpoint7_3(raw),
|
|
||||||
attemptStatus: Endpoint7_4(raw),
|
|
||||||
attemptComplete: Endpoint7_5(raw),
|
|
||||||
attemptCancel: Endpoint7_6(raw),
|
|
||||||
})
|
|
||||||
|
|
||||||
type Endpoint8_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
|
|
||||||
type Endpoint8_0Input = {
|
|
||||||
readonly credentialID: Endpoint8_0Request["params"]["credentialID"]
|
|
||||||
readonly location?: Endpoint8_0Request["query"]["location"]
|
|
||||||
readonly label: Endpoint8_0Request["payload"]["label"]
|
|
||||||
}
|
|
||||||
const Endpoint8_0 = (raw: RawClient["server.credential"]) => (input: Endpoint8_0Input) =>
|
|
||||||
raw["credential.update"]({
|
|
||||||
params: { credentialID: input["credentialID"] },
|
|
||||||
query: { location: input["location"] },
|
|
||||||
payload: { label: input["label"] },
|
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
type Endpoint8_1Request = Parameters<RawClient["server.credential"]["credential.remove"]>[0]
|
|
||||||
type Endpoint8_1Input = {
|
|
||||||
readonly credentialID: Endpoint8_1Request["params"]["credentialID"]
|
|
||||||
readonly location?: Endpoint8_1Request["query"]["location"]
|
|
||||||
}
|
|
||||||
const Endpoint8_1 = (raw: RawClient["server.credential"]) => (input: Endpoint8_1Input) =>
|
|
||||||
raw["credential.remove"]({
|
|
||||||
params: { credentialID: input["credentialID"] },
|
|
||||||
query: { location: input["location"] },
|
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
const adaptGroup8 = (raw: RawClient["server.credential"]) => ({ update: Endpoint8_0(raw), remove: Endpoint8_1(raw) })
|
|
||||||
|
|
||||||
type Endpoint9_0Request = Parameters<RawClient["server.permission"]["permission.request.list"]>[0]
|
|
||||||
type Endpoint9_0Input = { readonly location?: Endpoint9_0Request["query"]["location"] }
|
|
||||||
const Endpoint9_0 = (raw: RawClient["server.permission"]) => (input?: Endpoint9_0Input) =>
|
|
||||||
raw["permission.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
type Endpoint9_1Request = Parameters<RawClient["server.permission"]["permission.saved.list"]>[0]
|
|
||||||
type Endpoint9_1Input = { readonly projectID?: Endpoint9_1Request["query"]["projectID"] }
|
|
||||||
const Endpoint9_1 = (raw: RawClient["server.permission"]) => (input?: Endpoint9_1Input) =>
|
|
||||||
raw["permission.saved.list"]({ query: { projectID: input?.["projectID"] } }).pipe(
|
|
||||||
Effect.mapError(mapClientError),
|
|
||||||
Effect.map((value) => value.data),
|
|
||||||
)
|
|
||||||
|
|
||||||
type Endpoint9_2Request = Parameters<RawClient["server.permission"]["permission.saved.remove"]>[0]
|
|
||||||
type Endpoint9_2Input = { readonly id: Endpoint9_2Request["params"]["id"] }
|
|
||||||
const Endpoint9_2 = (raw: RawClient["server.permission"]) => (input: Endpoint9_2Input) =>
|
|
||||||
raw["permission.saved.remove"]({ params: { id: input["id"] } }).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
type Endpoint9_3Request = Parameters<RawClient["server.permission"]["session.permission.create"]>[0]
|
|
||||||
type Endpoint9_3Input = {
|
|
||||||
readonly sessionID: Endpoint9_3Request["params"]["sessionID"]
|
|
||||||
readonly id?: Endpoint9_3Request["payload"]["id"]
|
|
||||||
readonly action: Endpoint9_3Request["payload"]["action"]
|
|
||||||
readonly resources: Endpoint9_3Request["payload"]["resources"]
|
|
||||||
readonly save?: Endpoint9_3Request["payload"]["save"]
|
|
||||||
readonly metadata?: Endpoint9_3Request["payload"]["metadata"]
|
|
||||||
readonly source?: Endpoint9_3Request["payload"]["source"]
|
|
||||||
readonly agent?: Endpoint9_3Request["payload"]["agent"]
|
|
||||||
}
|
|
||||||
const Endpoint9_3 = (raw: RawClient["server.permission"]) => (input: Endpoint9_3Input) =>
|
|
||||||
raw["session.permission.create"]({
|
|
||||||
params: { sessionID: input["sessionID"] },
|
|
||||||
payload: {
|
|
||||||
id: input["id"],
|
|
||||||
action: input["action"],
|
|
||||||
resources: input["resources"],
|
|
||||||
save: input["save"],
|
|
||||||
metadata: input["metadata"],
|
|
||||||
source: input["source"],
|
|
||||||
agent: input["agent"],
|
|
||||||
},
|
|
||||||
}).pipe(
|
|
||||||
Effect.mapError(mapClientError),
|
|
||||||
Effect.map((value) => value.data),
|
|
||||||
)
|
|
||||||
|
|
||||||
type Endpoint9_4Request = Parameters<RawClient["server.permission"]["session.permission.list"]>[0]
|
|
||||||
type Endpoint9_4Input = { readonly sessionID: Endpoint9_4Request["params"]["sessionID"] }
|
|
||||||
const Endpoint9_4 = (raw: RawClient["server.permission"]) => (input: Endpoint9_4Input) =>
|
|
||||||
raw["session.permission.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
|
||||||
Effect.mapError(mapClientError),
|
|
||||||
Effect.map((value) => value.data),
|
|
||||||
)
|
|
||||||
|
|
||||||
type Endpoint9_5Request = Parameters<RawClient["server.permission"]["session.permission.get"]>[0]
|
|
||||||
type Endpoint9_5Input = {
|
|
||||||
readonly sessionID: Endpoint9_5Request["params"]["sessionID"]
|
|
||||||
readonly requestID: Endpoint9_5Request["params"]["requestID"]
|
|
||||||
}
|
|
||||||
const Endpoint9_5 = (raw: RawClient["server.permission"]) => (input: Endpoint9_5Input) =>
|
|
||||||
raw["session.permission.get"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe(
|
|
||||||
Effect.mapError(mapClientError),
|
|
||||||
Effect.map((value) => value.data),
|
|
||||||
)
|
|
||||||
|
|
||||||
type Endpoint9_6Request = Parameters<RawClient["server.permission"]["session.permission.reply"]>[0]
|
|
||||||
type Endpoint9_6Input = {
|
|
||||||
readonly sessionID: Endpoint9_6Request["params"]["sessionID"]
|
|
||||||
readonly requestID: Endpoint9_6Request["params"]["requestID"]
|
|
||||||
readonly reply: Endpoint9_6Request["payload"]["reply"]
|
|
||||||
readonly message?: Endpoint9_6Request["payload"]["message"]
|
|
||||||
}
|
|
||||||
const Endpoint9_6 = (raw: RawClient["server.permission"]) => (input: Endpoint9_6Input) =>
|
|
||||||
raw["session.permission.reply"]({
|
|
||||||
params: { sessionID: input["sessionID"], requestID: input["requestID"] },
|
|
||||||
payload: { reply: input["reply"], message: input["message"] },
|
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
const adaptGroup9 = (raw: RawClient["server.permission"]) => ({
|
|
||||||
listRequests: Endpoint9_0(raw),
|
|
||||||
listSaved: Endpoint9_1(raw),
|
|
||||||
removeSaved: Endpoint9_2(raw),
|
|
||||||
create: Endpoint9_3(raw),
|
|
||||||
list: Endpoint9_4(raw),
|
|
||||||
get: Endpoint9_5(raw),
|
|
||||||
reply: Endpoint9_6(raw),
|
|
||||||
})
|
|
||||||
|
|
||||||
type Endpoint10_0Request = Parameters<RawClient["server.fs"]["fs.list"]>[0]
|
|
||||||
type Endpoint10_0Input = {
|
|
||||||
readonly location?: Endpoint10_0Request["query"]["location"]
|
|
||||||
readonly path?: Endpoint10_0Request["query"]["path"]
|
|
||||||
}
|
|
||||||
const Endpoint10_0 = (raw: RawClient["server.fs"]) => (input?: Endpoint10_0Input) =>
|
|
||||||
raw["fs.list"]({ query: { location: input?.["location"], path: input?.["path"] } }).pipe(
|
|
||||||
Effect.mapError(mapClientError),
|
|
||||||
)
|
|
||||||
|
|
||||||
type Endpoint10_1Request = Parameters<RawClient["server.fs"]["fs.find"]>[0]
|
|
||||||
type Endpoint10_1Input = {
|
|
||||||
readonly location?: Endpoint10_1Request["query"]["location"]
|
|
||||||
readonly query: Endpoint10_1Request["query"]["query"]
|
|
||||||
readonly type?: Endpoint10_1Request["query"]["type"]
|
|
||||||
readonly limit?: Endpoint10_1Request["query"]["limit"]
|
|
||||||
}
|
|
||||||
const Endpoint10_1 = (raw: RawClient["server.fs"]) => (input: Endpoint10_1Input) =>
|
|
||||||
raw["fs.find"]({
|
|
||||||
query: { location: input["location"], query: input["query"], type: input["type"], limit: input["limit"] },
|
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
const adaptGroup10 = (raw: RawClient["server.fs"]) => ({ list: Endpoint10_0(raw), find: Endpoint10_1(raw) })
|
|
||||||
|
|
||||||
type Endpoint11_0Request = Parameters<RawClient["server.command"]["command.list"]>[0]
|
|
||||||
type Endpoint11_0Input = { readonly location?: Endpoint11_0Request["query"]["location"] }
|
|
||||||
const Endpoint11_0 = (raw: RawClient["server.command"]) => (input?: Endpoint11_0Input) =>
|
|
||||||
raw["command.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
const adaptGroup11 = (raw: RawClient["server.command"]) => ({ list: Endpoint11_0(raw) })
|
|
||||||
|
|
||||||
type Endpoint12_0Request = Parameters<RawClient["server.skill"]["skill.list"]>[0]
|
|
||||||
type Endpoint12_0Input = { readonly location?: Endpoint12_0Request["query"]["location"] }
|
|
||||||
const Endpoint12_0 = (raw: RawClient["server.skill"]) => (input?: Endpoint12_0Input) =>
|
|
||||||
raw["skill.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
const adaptGroup12 = (raw: RawClient["server.skill"]) => ({ list: Endpoint12_0(raw) })
|
|
||||||
|
|
||||||
const Endpoint13_0 = (raw: RawClient["server.event"]) => () =>
|
|
||||||
Stream.unwrap(
|
|
||||||
raw["event.subscribe"]({}).pipe(
|
|
||||||
Effect.mapError(mapClientError),
|
|
||||||
Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError))),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const adaptGroup13 = (raw: RawClient["server.event"]) => ({ subscribe: Endpoint13_0(raw) })
|
|
||||||
|
|
||||||
type Endpoint14_0Request = Parameters<RawClient["server.pty"]["pty.list"]>[0]
|
|
||||||
type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] }
|
|
||||||
const Endpoint14_0 = (raw: RawClient["server.pty"]) => (input?: Endpoint14_0Input) =>
|
|
||||||
raw["pty.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
type Endpoint14_1Request = Parameters<RawClient["server.pty"]["pty.create"]>[0]
|
|
||||||
type Endpoint14_1Input = {
|
|
||||||
readonly location?: Endpoint14_1Request["query"]["location"]
|
|
||||||
readonly command?: Endpoint14_1Request["payload"]["command"]
|
|
||||||
readonly args?: Endpoint14_1Request["payload"]["args"]
|
|
||||||
readonly cwd?: Endpoint14_1Request["payload"]["cwd"]
|
|
||||||
readonly title?: Endpoint14_1Request["payload"]["title"]
|
|
||||||
readonly env?: Endpoint14_1Request["payload"]["env"]
|
|
||||||
}
|
|
||||||
const Endpoint14_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint14_1Input) =>
|
|
||||||
raw["pty.create"]({
|
|
||||||
query: { location: input?.["location"] },
|
|
||||||
payload: {
|
|
||||||
command: input?.["command"],
|
|
||||||
args: input?.["args"],
|
|
||||||
cwd: input?.["cwd"],
|
|
||||||
title: input?.["title"],
|
|
||||||
env: input?.["env"],
|
|
||||||
},
|
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
type Endpoint14_2Request = Parameters<RawClient["server.pty"]["pty.get"]>[0]
|
|
||||||
type Endpoint14_2Input = {
|
|
||||||
readonly ptyID: Endpoint14_2Request["params"]["ptyID"]
|
|
||||||
readonly location?: Endpoint14_2Request["query"]["location"]
|
|
||||||
}
|
|
||||||
const Endpoint14_2 = (raw: RawClient["server.pty"]) => (input: Endpoint14_2Input) =>
|
|
||||||
raw["pty.get"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe(
|
|
||||||
Effect.mapError(mapClientError),
|
|
||||||
)
|
|
||||||
|
|
||||||
type Endpoint14_3Request = Parameters<RawClient["server.pty"]["pty.update"]>[0]
|
|
||||||
type Endpoint14_3Input = {
|
|
||||||
readonly ptyID: Endpoint14_3Request["params"]["ptyID"]
|
|
||||||
readonly location?: Endpoint14_3Request["query"]["location"]
|
|
||||||
readonly title?: Endpoint14_3Request["payload"]["title"]
|
|
||||||
readonly size?: Endpoint14_3Request["payload"]["size"]
|
|
||||||
}
|
|
||||||
const Endpoint14_3 = (raw: RawClient["server.pty"]) => (input: Endpoint14_3Input) =>
|
|
||||||
raw["pty.update"]({
|
|
||||||
params: { ptyID: input["ptyID"] },
|
|
||||||
query: { location: input["location"] },
|
|
||||||
payload: { title: input["title"], size: input["size"] },
|
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
type Endpoint14_4Request = Parameters<RawClient["server.pty"]["pty.remove"]>[0]
|
|
||||||
type Endpoint14_4Input = {
|
|
||||||
readonly ptyID: Endpoint14_4Request["params"]["ptyID"]
|
|
||||||
readonly location?: Endpoint14_4Request["query"]["location"]
|
|
||||||
}
|
|
||||||
const Endpoint14_4 = (raw: RawClient["server.pty"]) => (input: Endpoint14_4Input) =>
|
|
||||||
raw["pty.remove"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe(
|
|
||||||
Effect.mapError(mapClientError),
|
|
||||||
)
|
|
||||||
|
|
||||||
const adaptGroup14 = (raw: RawClient["server.pty"]) => ({
|
|
||||||
list: Endpoint14_0(raw),
|
|
||||||
create: Endpoint14_1(raw),
|
|
||||||
get: Endpoint14_2(raw),
|
|
||||||
update: Endpoint14_3(raw),
|
|
||||||
remove: Endpoint14_4(raw),
|
|
||||||
})
|
|
||||||
|
|
||||||
type Endpoint15_0Request = Parameters<RawClient["server.question"]["question.request.list"]>[0]
|
|
||||||
type Endpoint15_0Input = { readonly location?: Endpoint15_0Request["query"]["location"] }
|
|
||||||
const Endpoint15_0 = (raw: RawClient["server.question"]) => (input?: Endpoint15_0Input) =>
|
|
||||||
raw["question.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
type Endpoint15_1Request = Parameters<RawClient["server.question"]["session.question.list"]>[0]
|
|
||||||
type Endpoint15_1Input = { readonly sessionID: Endpoint15_1Request["params"]["sessionID"] }
|
|
||||||
const Endpoint15_1 = (raw: RawClient["server.question"]) => (input: Endpoint15_1Input) =>
|
|
||||||
raw["session.question.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
|
||||||
Effect.mapError(mapClientError),
|
|
||||||
Effect.map((value) => value.data),
|
|
||||||
)
|
|
||||||
|
|
||||||
type Endpoint15_2Request = Parameters<RawClient["server.question"]["session.question.reply"]>[0]
|
|
||||||
type Endpoint15_2Input = {
|
|
||||||
readonly sessionID: Endpoint15_2Request["params"]["sessionID"]
|
|
||||||
readonly requestID: Endpoint15_2Request["params"]["requestID"]
|
|
||||||
readonly answers: Endpoint15_2Request["payload"]["answers"]
|
|
||||||
}
|
|
||||||
const Endpoint15_2 = (raw: RawClient["server.question"]) => (input: Endpoint15_2Input) =>
|
|
||||||
raw["session.question.reply"]({
|
|
||||||
params: { sessionID: input["sessionID"], requestID: input["requestID"] },
|
|
||||||
payload: { answers: input["answers"] },
|
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
type Endpoint15_3Request = Parameters<RawClient["server.question"]["session.question.reject"]>[0]
|
|
||||||
type Endpoint15_3Input = {
|
|
||||||
readonly sessionID: Endpoint15_3Request["params"]["sessionID"]
|
|
||||||
readonly requestID: Endpoint15_3Request["params"]["requestID"]
|
|
||||||
}
|
|
||||||
const Endpoint15_3 = (raw: RawClient["server.question"]) => (input: Endpoint15_3Input) =>
|
|
||||||
raw["session.question.reject"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe(
|
|
||||||
Effect.mapError(mapClientError),
|
|
||||||
)
|
|
||||||
|
|
||||||
const adaptGroup15 = (raw: RawClient["server.question"]) => ({
|
|
||||||
listRequests: Endpoint15_0(raw),
|
|
||||||
list: Endpoint15_1(raw),
|
|
||||||
reply: Endpoint15_2(raw),
|
|
||||||
reject: Endpoint15_3(raw),
|
|
||||||
})
|
|
||||||
|
|
||||||
type Endpoint16_0Request = Parameters<RawClient["server.reference"]["reference.list"]>[0]
|
|
||||||
type Endpoint16_0Input = { readonly location?: Endpoint16_0Request["query"]["location"] }
|
|
||||||
const Endpoint16_0 = (raw: RawClient["server.reference"]) => (input?: Endpoint16_0Input) =>
|
|
||||||
raw["reference.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
const adaptGroup16 = (raw: RawClient["server.reference"]) => ({ list: Endpoint16_0(raw) })
|
|
||||||
|
|
||||||
type Endpoint17_0Request = Parameters<RawClient["server.projectCopy"]["projectCopy.create"]>[0]
|
|
||||||
type Endpoint17_0Input = {
|
|
||||||
readonly projectID: Endpoint17_0Request["params"]["projectID"]
|
|
||||||
readonly location?: Endpoint17_0Request["query"]["location"]
|
|
||||||
readonly strategy: Endpoint17_0Request["payload"]["strategy"]
|
|
||||||
readonly directory: Endpoint17_0Request["payload"]["directory"]
|
|
||||||
readonly name?: Endpoint17_0Request["payload"]["name"]
|
|
||||||
}
|
|
||||||
const Endpoint17_0 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint17_0Input) =>
|
|
||||||
raw["projectCopy.create"]({
|
|
||||||
params: { projectID: input["projectID"] },
|
|
||||||
query: { location: input["location"] },
|
|
||||||
payload: { strategy: input["strategy"], directory: input["directory"], name: input["name"] },
|
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
type Endpoint17_1Request = Parameters<RawClient["server.projectCopy"]["projectCopy.remove"]>[0]
|
|
||||||
type Endpoint17_1Input = {
|
|
||||||
readonly projectID: Endpoint17_1Request["params"]["projectID"]
|
|
||||||
readonly location?: Endpoint17_1Request["query"]["location"]
|
|
||||||
readonly directory: Endpoint17_1Request["payload"]["directory"]
|
|
||||||
readonly force: Endpoint17_1Request["payload"]["force"]
|
|
||||||
}
|
|
||||||
const Endpoint17_1 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint17_1Input) =>
|
|
||||||
raw["projectCopy.remove"]({
|
|
||||||
params: { projectID: input["projectID"] },
|
|
||||||
query: { location: input["location"] },
|
|
||||||
payload: { directory: input["directory"], force: input["force"] },
|
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
type Endpoint17_2Request = Parameters<RawClient["server.projectCopy"]["projectCopy.refresh"]>[0]
|
|
||||||
type Endpoint17_2Input = {
|
|
||||||
readonly projectID: Endpoint17_2Request["params"]["projectID"]
|
|
||||||
readonly location?: Endpoint17_2Request["query"]["location"]
|
|
||||||
}
|
|
||||||
const Endpoint17_2 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint17_2Input) =>
|
|
||||||
raw["projectCopy.refresh"]({
|
|
||||||
params: { projectID: input["projectID"] },
|
|
||||||
query: { location: input["location"] },
|
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
const adaptGroup17 = (raw: RawClient["server.projectCopy"]) => ({
|
|
||||||
create: Endpoint17_0(raw),
|
|
||||||
remove: Endpoint17_1(raw),
|
|
||||||
refresh: Endpoint17_2(raw),
|
|
||||||
})
|
|
||||||
|
|
||||||
const adaptClient = (raw: RawClient) => ({
|
|
||||||
health: adaptGroup0(raw["server.health"]),
|
|
||||||
location: adaptGroup1(raw["server.location"]),
|
|
||||||
agents: adaptGroup2(raw["server.agent"]),
|
|
||||||
sessions: adaptGroup3(raw["server.session"]),
|
|
||||||
messages: adaptGroup4(raw["server.message"]),
|
|
||||||
models: adaptGroup5(raw["server.model"]),
|
|
||||||
providers: adaptGroup6(raw["server.provider"]),
|
|
||||||
integrations: adaptGroup7(raw["server.integration"]),
|
|
||||||
credentials: adaptGroup8(raw["server.credential"]),
|
|
||||||
permissions: adaptGroup9(raw["server.permission"]),
|
|
||||||
files: adaptGroup10(raw["server.fs"]),
|
|
||||||
commands: adaptGroup11(raw["server.command"]),
|
|
||||||
skills: adaptGroup12(raw["server.skill"]),
|
|
||||||
events: adaptGroup13(raw["server.event"]),
|
|
||||||
ptys: adaptGroup14(raw["server.pty"]),
|
|
||||||
questions: adaptGroup15(raw["server.question"]),
|
|
||||||
references: adaptGroup16(raw["server.reference"]),
|
|
||||||
projectCopies: adaptGroup17(raw["server.projectCopy"]),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const make = (options?: { readonly baseUrl?: URL | string }) =>
|
export const make = (options?: { readonly baseUrl?: URL | string }) =>
|
||||||
HttpApiClient.make(ClientApi, options).pipe(Effect.map(adaptClient))
|
HttpApiClient.make(Api, options).pipe(Effect.map(adaptClient))
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
import type {
|
import type {
|
||||||
HealthGetOutput,
|
|
||||||
LocationGetInput,
|
|
||||||
LocationGetOutput,
|
|
||||||
AgentsListInput,
|
|
||||||
AgentsListOutput,
|
|
||||||
SessionsListInput,
|
SessionsListInput,
|
||||||
SessionsListOutput,
|
SessionsListOutput,
|
||||||
SessionsCreateInput,
|
SessionsCreateInput,
|
||||||
@@ -29,89 +24,12 @@ import type {
|
|||||||
SessionsCommitOutput,
|
SessionsCommitOutput,
|
||||||
SessionsContextInput,
|
SessionsContextInput,
|
||||||
SessionsContextOutput,
|
SessionsContextOutput,
|
||||||
SessionsHistoryInput,
|
|
||||||
SessionsHistoryOutput,
|
|
||||||
SessionsEventsInput,
|
SessionsEventsInput,
|
||||||
SessionsEventsOutput,
|
SessionsEventsOutput,
|
||||||
SessionsInterruptInput,
|
SessionsInterruptInput,
|
||||||
SessionsInterruptOutput,
|
SessionsInterruptOutput,
|
||||||
SessionsMessageInput,
|
SessionsMessageInput,
|
||||||
SessionsMessageOutput,
|
SessionsMessageOutput,
|
||||||
MessagesListInput,
|
|
||||||
MessagesListOutput,
|
|
||||||
ModelsListInput,
|
|
||||||
ModelsListOutput,
|
|
||||||
ProvidersListInput,
|
|
||||||
ProvidersListOutput,
|
|
||||||
ProvidersGetInput,
|
|
||||||
ProvidersGetOutput,
|
|
||||||
IntegrationsListInput,
|
|
||||||
IntegrationsListOutput,
|
|
||||||
IntegrationsGetInput,
|
|
||||||
IntegrationsGetOutput,
|
|
||||||
IntegrationsConnectKeyInput,
|
|
||||||
IntegrationsConnectKeyOutput,
|
|
||||||
IntegrationsConnectOauthInput,
|
|
||||||
IntegrationsConnectOauthOutput,
|
|
||||||
IntegrationsAttemptStatusInput,
|
|
||||||
IntegrationsAttemptStatusOutput,
|
|
||||||
IntegrationsAttemptCompleteInput,
|
|
||||||
IntegrationsAttemptCompleteOutput,
|
|
||||||
IntegrationsAttemptCancelInput,
|
|
||||||
IntegrationsAttemptCancelOutput,
|
|
||||||
CredentialsUpdateInput,
|
|
||||||
CredentialsUpdateOutput,
|
|
||||||
CredentialsRemoveInput,
|
|
||||||
CredentialsRemoveOutput,
|
|
||||||
PermissionsListRequestsInput,
|
|
||||||
PermissionsListRequestsOutput,
|
|
||||||
PermissionsListSavedInput,
|
|
||||||
PermissionsListSavedOutput,
|
|
||||||
PermissionsRemoveSavedInput,
|
|
||||||
PermissionsRemoveSavedOutput,
|
|
||||||
PermissionsCreateInput,
|
|
||||||
PermissionsCreateOutput,
|
|
||||||
PermissionsListInput,
|
|
||||||
PermissionsListOutput,
|
|
||||||
PermissionsGetInput,
|
|
||||||
PermissionsGetOutput,
|
|
||||||
PermissionsReplyInput,
|
|
||||||
PermissionsReplyOutput,
|
|
||||||
FilesListInput,
|
|
||||||
FilesListOutput,
|
|
||||||
FilesFindInput,
|
|
||||||
FilesFindOutput,
|
|
||||||
CommandsListInput,
|
|
||||||
CommandsListOutput,
|
|
||||||
SkillsListInput,
|
|
||||||
SkillsListOutput,
|
|
||||||
EventsSubscribeOutput,
|
|
||||||
PtysListInput,
|
|
||||||
PtysListOutput,
|
|
||||||
PtysCreateInput,
|
|
||||||
PtysCreateOutput,
|
|
||||||
PtysGetInput,
|
|
||||||
PtysGetOutput,
|
|
||||||
PtysUpdateInput,
|
|
||||||
PtysUpdateOutput,
|
|
||||||
PtysRemoveInput,
|
|
||||||
PtysRemoveOutput,
|
|
||||||
QuestionsListRequestsInput,
|
|
||||||
QuestionsListRequestsOutput,
|
|
||||||
QuestionsListInput,
|
|
||||||
QuestionsListOutput,
|
|
||||||
QuestionsReplyInput,
|
|
||||||
QuestionsReplyOutput,
|
|
||||||
QuestionsRejectInput,
|
|
||||||
QuestionsRejectOutput,
|
|
||||||
ReferencesListInput,
|
|
||||||
ReferencesListOutput,
|
|
||||||
ProjectCopiesCreateInput,
|
|
||||||
ProjectCopiesCreateOutput,
|
|
||||||
ProjectCopiesRemoveInput,
|
|
||||||
ProjectCopiesRemoveOutput,
|
|
||||||
ProjectCopiesRefreshInput,
|
|
||||||
ProjectCopiesRefreshOutput,
|
|
||||||
} from "./types"
|
} from "./types"
|
||||||
import { ClientError } from "./client-error"
|
import { ClientError } from "./client-error"
|
||||||
|
|
||||||
@@ -247,41 +165,6 @@ export function make(options: ClientOptions) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
health: {
|
|
||||||
get: (requestOptions?: RequestOptions) =>
|
|
||||||
request<HealthGetOutput>(
|
|
||||||
{ method: "GET", path: `/api/health`, successStatus: 200, declaredStatuses: [401, 400], empty: false },
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
location: {
|
|
||||||
get: (input?: LocationGetInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<LocationGetOutput>(
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: `/api/location`,
|
|
||||||
query: { location: input?.["location"] },
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [401, 400],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
agents: {
|
|
||||||
list: (input?: AgentsListInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<AgentsListOutput>(
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: `/api/agent`,
|
|
||||||
query: { location: input?.["location"] },
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [401, 400],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
sessions: {
|
sessions: {
|
||||||
list: (input?: SessionsListInput, requestOptions?: RequestOptions) =>
|
list: (input?: SessionsListInput, requestOptions?: RequestOptions) =>
|
||||||
request<SessionsListOutput>(
|
request<SessionsListOutput>(
|
||||||
@@ -289,14 +172,14 @@ export function make(options: ClientOptions) {
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
path: `/api/session`,
|
path: `/api/session`,
|
||||||
query: {
|
query: {
|
||||||
workspace: input?.["workspace"],
|
workspace: input?.workspace,
|
||||||
limit: input?.["limit"],
|
limit: input?.limit,
|
||||||
order: input?.["order"],
|
order: input?.order,
|
||||||
search: input?.["search"],
|
search: input?.search,
|
||||||
directory: input?.["directory"],
|
directory: input?.directory,
|
||||||
project: input?.["project"],
|
project: input?.project,
|
||||||
subpath: input?.["subpath"],
|
subpath: input?.subpath,
|
||||||
cursor: input?.["cursor"],
|
cursor: input?.cursor,
|
||||||
},
|
},
|
||||||
successStatus: 200,
|
successStatus: 200,
|
||||||
declaredStatuses: [400, 401],
|
declaredStatuses: [400, 401],
|
||||||
@@ -309,12 +192,7 @@ export function make(options: ClientOptions) {
|
|||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: `/api/session`,
|
path: `/api/session`,
|
||||||
body: {
|
body: { id: input?.id, agent: input?.agent, model: input?.model, location: input?.location },
|
||||||
id: input?.["id"],
|
|
||||||
agent: input?.["agent"],
|
|
||||||
model: input?.["model"],
|
|
||||||
location: input?.["location"],
|
|
||||||
},
|
|
||||||
successStatus: 200,
|
successStatus: 200,
|
||||||
declaredStatuses: [401, 400],
|
declaredStatuses: [401, 400],
|
||||||
empty: false,
|
empty: false,
|
||||||
@@ -348,7 +226,7 @@ export function make(options: ClientOptions) {
|
|||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/agent`,
|
path: `/api/session/${encodeURIComponent(input.sessionID)}/agent`,
|
||||||
body: { agent: input["agent"] },
|
body: { agent: input.agent },
|
||||||
successStatus: 204,
|
successStatus: 204,
|
||||||
declaredStatuses: [404, 400, 401],
|
declaredStatuses: [404, 400, 401],
|
||||||
empty: true,
|
empty: true,
|
||||||
@@ -360,7 +238,7 @@ export function make(options: ClientOptions) {
|
|||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/model`,
|
path: `/api/session/${encodeURIComponent(input.sessionID)}/model`,
|
||||||
body: { model: input["model"] },
|
body: { model: input.model },
|
||||||
successStatus: 204,
|
successStatus: 204,
|
||||||
declaredStatuses: [404, 400, 401],
|
declaredStatuses: [404, 400, 401],
|
||||||
empty: true,
|
empty: true,
|
||||||
@@ -372,7 +250,7 @@ export function make(options: ClientOptions) {
|
|||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/prompt`,
|
path: `/api/session/${encodeURIComponent(input.sessionID)}/prompt`,
|
||||||
body: { id: input["id"], prompt: input["prompt"], delivery: input["delivery"], resume: input["resume"] },
|
body: { id: input.id, prompt: input.prompt, delivery: input.delivery, resume: input.resume },
|
||||||
successStatus: 200,
|
successStatus: 200,
|
||||||
declaredStatuses: [409, 404, 400, 401],
|
declaredStatuses: [409, 404, 400, 401],
|
||||||
empty: false,
|
empty: false,
|
||||||
@@ -406,7 +284,7 @@ export function make(options: ClientOptions) {
|
|||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/stage`,
|
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/stage`,
|
||||||
body: { messageID: input["messageID"], files: input["files"] },
|
body: { messageID: input.messageID, files: input.files },
|
||||||
successStatus: 200,
|
successStatus: 200,
|
||||||
declaredStatuses: [404, 500, 400, 401],
|
declaredStatuses: [404, 500, 400, 401],
|
||||||
empty: false,
|
empty: false,
|
||||||
@@ -446,24 +324,12 @@ export function make(options: ClientOptions) {
|
|||||||
},
|
},
|
||||||
requestOptions,
|
requestOptions,
|
||||||
).then((value) => value.data),
|
).then((value) => value.data),
|
||||||
history: (input: SessionsHistoryInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<SessionsHistoryOutput>(
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/history`,
|
|
||||||
query: { limit: input["limit"], after: input["after"] },
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [404, 400, 401],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
events: (input: SessionsEventsInput, requestOptions?: RequestOptions): AsyncIterable<SessionsEventsOutput> =>
|
events: (input: SessionsEventsInput, requestOptions?: RequestOptions): AsyncIterable<SessionsEventsOutput> =>
|
||||||
sse<SessionsEventsOutput>(
|
sse<SessionsEventsOutput>(
|
||||||
{
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/event`,
|
path: `/api/session/${encodeURIComponent(input.sessionID)}/event`,
|
||||||
query: { after: input["after"] },
|
query: { after: input.after },
|
||||||
successStatus: 200,
|
successStatus: 200,
|
||||||
declaredStatuses: [404, 400, 401],
|
declaredStatuses: [404, 400, 401],
|
||||||
empty: false,
|
empty: false,
|
||||||
@@ -493,500 +359,6 @@ export function make(options: ClientOptions) {
|
|||||||
requestOptions,
|
requestOptions,
|
||||||
).then((value) => value.data),
|
).then((value) => value.data),
|
||||||
},
|
},
|
||||||
messages: {
|
|
||||||
list: (input: MessagesListInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<MessagesListOutput>(
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/message`,
|
|
||||||
query: { limit: input["limit"], order: input["order"], cursor: input["cursor"] },
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [400, 404, 500, 401],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
models: {
|
|
||||||
list: (input?: ModelsListInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<ModelsListOutput>(
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: `/api/model`,
|
|
||||||
query: { location: input?.["location"] },
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [503, 401, 400],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
providers: {
|
|
||||||
list: (input?: ProvidersListInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<ProvidersListOutput>(
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: `/api/provider`,
|
|
||||||
query: { location: input?.["location"] },
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [503, 401, 400],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
get: (input: ProvidersGetInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<ProvidersGetOutput>(
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: `/api/provider/${encodeURIComponent(input.providerID)}`,
|
|
||||||
query: { location: input["location"] },
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [404, 503, 401, 400],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
integrations: {
|
|
||||||
list: (input?: IntegrationsListInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<IntegrationsListOutput>(
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: `/api/integration`,
|
|
||||||
query: { location: input?.["location"] },
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [401, 400],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
get: (input: IntegrationsGetInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<IntegrationsGetOutput>(
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: `/api/integration/${encodeURIComponent(input.integrationID)}`,
|
|
||||||
query: { location: input["location"] },
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [401, 400],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
connectKey: (input: IntegrationsConnectKeyInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<IntegrationsConnectKeyOutput>(
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/key`,
|
|
||||||
query: { location: input["location"] },
|
|
||||||
body: { key: input["key"], label: input["label"] },
|
|
||||||
successStatus: 204,
|
|
||||||
declaredStatuses: [400, 401],
|
|
||||||
empty: true,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
connectOauth: (input: IntegrationsConnectOauthInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<IntegrationsConnectOauthOutput>(
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth`,
|
|
||||||
query: { location: input["location"] },
|
|
||||||
body: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] },
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [400, 401],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
attemptStatus: (input: IntegrationsAttemptStatusInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<IntegrationsAttemptStatusOutput>(
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
|
|
||||||
query: { location: input["location"] },
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [401, 400],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
attemptComplete: (input: IntegrationsAttemptCompleteInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<IntegrationsAttemptCompleteOutput>(
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}/complete`,
|
|
||||||
query: { location: input["location"] },
|
|
||||||
body: { code: input["code"] },
|
|
||||||
successStatus: 204,
|
|
||||||
declaredStatuses: [400, 401],
|
|
||||||
empty: true,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
attemptCancel: (input: IntegrationsAttemptCancelInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<IntegrationsAttemptCancelOutput>(
|
|
||||||
{
|
|
||||||
method: "DELETE",
|
|
||||||
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
|
|
||||||
query: { location: input["location"] },
|
|
||||||
successStatus: 204,
|
|
||||||
declaredStatuses: [401, 400],
|
|
||||||
empty: true,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
credentials: {
|
|
||||||
update: (input: CredentialsUpdateInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<CredentialsUpdateOutput>(
|
|
||||||
{
|
|
||||||
method: "PATCH",
|
|
||||||
path: `/api/credential/${encodeURIComponent(input.credentialID)}`,
|
|
||||||
query: { location: input["location"] },
|
|
||||||
body: { label: input["label"] },
|
|
||||||
successStatus: 204,
|
|
||||||
declaredStatuses: [401, 400],
|
|
||||||
empty: true,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
remove: (input: CredentialsRemoveInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<CredentialsRemoveOutput>(
|
|
||||||
{
|
|
||||||
method: "DELETE",
|
|
||||||
path: `/api/credential/${encodeURIComponent(input.credentialID)}`,
|
|
||||||
query: { location: input["location"] },
|
|
||||||
successStatus: 204,
|
|
||||||
declaredStatuses: [401, 400],
|
|
||||||
empty: true,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
permissions: {
|
|
||||||
listRequests: (input?: PermissionsListRequestsInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<PermissionsListRequestsOutput>(
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: `/api/permission/request`,
|
|
||||||
query: { location: input?.["location"] },
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [401, 400],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
listSaved: (input?: PermissionsListSavedInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<{ readonly data: PermissionsListSavedOutput }>(
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: `/api/permission/saved`,
|
|
||||||
query: { projectID: input?.["projectID"] },
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [401, 400],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
).then((value) => value.data),
|
|
||||||
removeSaved: (input: PermissionsRemoveSavedInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<PermissionsRemoveSavedOutput>(
|
|
||||||
{
|
|
||||||
method: "DELETE",
|
|
||||||
path: `/api/permission/saved/${encodeURIComponent(input.id)}`,
|
|
||||||
successStatus: 204,
|
|
||||||
declaredStatuses: [401, 400],
|
|
||||||
empty: true,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
create: (input: PermissionsCreateInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<{ readonly data: PermissionsCreateOutput }>(
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/permission`,
|
|
||||||
body: {
|
|
||||||
id: input["id"],
|
|
||||||
action: input["action"],
|
|
||||||
resources: input["resources"],
|
|
||||||
save: input["save"],
|
|
||||||
metadata: input["metadata"],
|
|
||||||
source: input["source"],
|
|
||||||
agent: input["agent"],
|
|
||||||
},
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [404, 400, 401],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
).then((value) => value.data),
|
|
||||||
list: (input: PermissionsListInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<{ readonly data: PermissionsListOutput }>(
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/permission`,
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [404, 400, 401],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
).then((value) => value.data),
|
|
||||||
get: (input: PermissionsGetInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<{ readonly data: PermissionsGetOutput }>(
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/permission/${encodeURIComponent(input.requestID)}`,
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [404, 400, 401],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
).then((value) => value.data),
|
|
||||||
reply: (input: PermissionsReplyInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<PermissionsReplyOutput>(
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/permission/${encodeURIComponent(input.requestID)}/reply`,
|
|
||||||
body: { reply: input["reply"], message: input["message"] },
|
|
||||||
successStatus: 204,
|
|
||||||
declaredStatuses: [404, 400, 401],
|
|
||||||
empty: true,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
files: {
|
|
||||||
list: (input?: FilesListInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<FilesListOutput>(
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: `/api/fs/list`,
|
|
||||||
query: { location: input?.["location"], path: input?.["path"] },
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [401, 400],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
find: (input: FilesFindInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<FilesFindOutput>(
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: `/api/fs/find`,
|
|
||||||
query: { location: input["location"], query: input["query"], type: input["type"], limit: input["limit"] },
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [401, 400],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
commands: {
|
|
||||||
list: (input?: CommandsListInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<CommandsListOutput>(
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: `/api/command`,
|
|
||||||
query: { location: input?.["location"] },
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [401, 400],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
skills: {
|
|
||||||
list: (input?: SkillsListInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<SkillsListOutput>(
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: `/api/skill`,
|
|
||||||
query: { location: input?.["location"] },
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [401, 400],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
events: {
|
|
||||||
subscribe: (requestOptions?: RequestOptions): AsyncIterable<EventsSubscribeOutput> =>
|
|
||||||
sse<EventsSubscribeOutput>(
|
|
||||||
{ method: "GET", path: `/api/event`, successStatus: 200, declaredStatuses: [401, 400], empty: false },
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
ptys: {
|
|
||||||
list: (input?: PtysListInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<PtysListOutput>(
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: `/api/pty`,
|
|
||||||
query: { location: input?.["location"] },
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [401, 400],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
create: (input?: PtysCreateInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<PtysCreateOutput>(
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
path: `/api/pty`,
|
|
||||||
query: { location: input?.["location"] },
|
|
||||||
body: {
|
|
||||||
command: input?.["command"],
|
|
||||||
args: input?.["args"],
|
|
||||||
cwd: input?.["cwd"],
|
|
||||||
title: input?.["title"],
|
|
||||||
env: input?.["env"],
|
|
||||||
},
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [401, 400],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
get: (input: PtysGetInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<PtysGetOutput>(
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: `/api/pty/${encodeURIComponent(input.ptyID)}`,
|
|
||||||
query: { location: input["location"] },
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [404, 401, 400],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
update: (input: PtysUpdateInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<PtysUpdateOutput>(
|
|
||||||
{
|
|
||||||
method: "PUT",
|
|
||||||
path: `/api/pty/${encodeURIComponent(input.ptyID)}`,
|
|
||||||
query: { location: input["location"] },
|
|
||||||
body: { title: input["title"], size: input["size"] },
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [404, 401, 400],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
remove: (input: PtysRemoveInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<PtysRemoveOutput>(
|
|
||||||
{
|
|
||||||
method: "DELETE",
|
|
||||||
path: `/api/pty/${encodeURIComponent(input.ptyID)}`,
|
|
||||||
query: { location: input["location"] },
|
|
||||||
successStatus: 204,
|
|
||||||
declaredStatuses: [404, 401, 400],
|
|
||||||
empty: true,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
questions: {
|
|
||||||
listRequests: (input?: QuestionsListRequestsInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<QuestionsListRequestsOutput>(
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: `/api/question/request`,
|
|
||||||
query: { location: input?.["location"] },
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [401, 400],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
list: (input: QuestionsListInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<{ readonly data: QuestionsListOutput }>(
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/question`,
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [404, 400, 401],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
).then((value) => value.data),
|
|
||||||
reply: (input: QuestionsReplyInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<QuestionsReplyOutput>(
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/question/${encodeURIComponent(input.requestID)}/reply`,
|
|
||||||
body: { answers: input["answers"] },
|
|
||||||
successStatus: 204,
|
|
||||||
declaredStatuses: [404, 400, 401],
|
|
||||||
empty: true,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
reject: (input: QuestionsRejectInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<QuestionsRejectOutput>(
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/question/${encodeURIComponent(input.requestID)}/reject`,
|
|
||||||
successStatus: 204,
|
|
||||||
declaredStatuses: [404, 400, 401],
|
|
||||||
empty: true,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
references: {
|
|
||||||
list: (input?: ReferencesListInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<ReferencesListOutput>(
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: `/api/reference`,
|
|
||||||
query: { location: input?.["location"] },
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [401, 400],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
projectCopies: {
|
|
||||||
create: (input: ProjectCopiesCreateInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<ProjectCopiesCreateOutput>(
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
path: `/experimental/project/${encodeURIComponent(input.projectID)}/copy`,
|
|
||||||
query: { location: input["location"] },
|
|
||||||
body: { strategy: input["strategy"], directory: input["directory"], name: input["name"] },
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [400, 401],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
remove: (input: ProjectCopiesRemoveInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<ProjectCopiesRemoveOutput>(
|
|
||||||
{
|
|
||||||
method: "DELETE",
|
|
||||||
path: `/experimental/project/${encodeURIComponent(input.projectID)}/copy`,
|
|
||||||
query: { location: input["location"] },
|
|
||||||
body: { directory: input["directory"], force: input["force"] },
|
|
||||||
successStatus: 204,
|
|
||||||
declaredStatuses: [400, 401],
|
|
||||||
empty: true,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
refresh: (input: ProjectCopiesRefreshInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<ProjectCopiesRefreshOutput>(
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
path: `/experimental/project/${encodeURIComponent(input.projectID)}/copy/refresh`,
|
|
||||||
query: { location: input["location"] },
|
|
||||||
successStatus: 204,
|
|
||||||
declaredStatuses: [400, 401],
|
|
||||||
empty: true,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1 @@
|
|||||||
export * from "./generated/index"
|
export * from "./generated/index"
|
||||||
export type { EventsSubscribeOutput as OpenCodeEvent } from "./generated/types"
|
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ import { SessionMessage } from "@opencode-ai/schema/session-message"
|
|||||||
import { Workspace } from "@opencode-ai/schema/workspace"
|
import { Workspace } from "@opencode-ai/schema/workspace"
|
||||||
import { Api } from "@opencode-ai/server/api"
|
import { Api } from "@opencode-ai/server/api"
|
||||||
import { compile, emitPromise } from "@opencode-ai/httpapi-codegen"
|
import { compile, emitPromise } from "@opencode-ai/httpapi-codegen"
|
||||||
import { ClientApi, endpointNames, groupNames, omitEndpoints } from "../src/contract"
|
import { HttpApi } from "effect/unstable/httpapi"
|
||||||
|
import { SessionGroup } from "../src/contract"
|
||||||
|
|
||||||
test("Core and Server reuse the authoritative Schema and Protocol values", () => {
|
test("Core and Server reuse the authoritative Schema and Protocol values", () => {
|
||||||
expect(AgentV2.ID).toBe(Agent.ID)
|
expect(AgentV2.ID).toBe(Agent.ID)
|
||||||
@@ -30,16 +31,17 @@ test("Core and Server reuse the authoritative Schema and Protocol values", () =>
|
|||||||
expect(CoreSessionMessage.Message).toBe(SessionMessage.Message)
|
expect(CoreSessionMessage.Message).toBe(SessionMessage.Message)
|
||||||
expect(CorePrompt).toBe(Prompt)
|
expect(CorePrompt).toBe(Prompt)
|
||||||
expect(Api.groups["server.session"].identifier).toBe("server.session")
|
expect(Api.groups["server.session"].identifier).toBe("server.session")
|
||||||
expect(Object.keys(ClientApi.groups)).toEqual(Object.keys(Api.groups))
|
expect(SessionGroup.identifier).toBe(Api.groups["server.session"].identifier)
|
||||||
expect(Session.ID.create()).toStartWith("ses_")
|
expect(Session.ID.create()).toStartWith("ses_")
|
||||||
expect(Project.ID.global).toBe("global")
|
expect(Project.ID.global).toBe("global")
|
||||||
expect(Provider.ID.anthropic).toBe("anthropic")
|
expect(Provider.ID.anthropic).toBe("anthropic")
|
||||||
expect(Workspace.ID.create()).toStartWith("wrk_")
|
expect(Workspace.ID.create()).toStartWith("wrk_")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("client and Server contracts generate identically", () => {
|
test("client and Server Session contracts generate identically", () => {
|
||||||
const server = compile(Api, { groupNames, endpointNames, omitEndpoints })
|
const options = { groupNames: { "server.session": "sessions" } }
|
||||||
const client = compile(ClientApi, { groupNames, endpointNames, omitEndpoints })
|
const server = compile(HttpApi.make("server").add(Api.groups["server.session"]), options)
|
||||||
|
const client = compile(HttpApi.make("client").add(SessionGroup), options)
|
||||||
|
|
||||||
expect(emitPromise(client)).toEqual(emitPromise(server))
|
expect(emitPromise(client)).toEqual(emitPromise(server))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,53 +15,7 @@ test("sessions.get returns the decoded Effect projection", async () => {
|
|||||||
expect(DateTime.toEpochMillis(result.time.created)).toBe(1_717_171_717_000)
|
expect(DateTime.toEpochMillis(result.time.created)).toBe(1_717_171_717_000)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("events.subscribe exposes and decodes the native Effect event stream", async () => {
|
|
||||||
const httpClient = HttpClient.make((request) =>
|
|
||||||
Effect.succeed(
|
|
||||||
HttpClientResponse.fromWeb(
|
|
||||||
request,
|
|
||||||
new Response(
|
|
||||||
`data: ${JSON.stringify({ id: "evt_connected", type: "server.connected", data: {} })}\n\n` +
|
|
||||||
`data: ${JSON.stringify(modelSwitchedEvent)}\n\n`,
|
|
||||||
{ headers: { "content-type": "text/event-stream" } },
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
const events = await Effect.gen(function* () {
|
|
||||||
const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" })
|
|
||||||
return yield* client.events.subscribe().pipe(Stream.runCollect)
|
|
||||||
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
|
|
||||||
|
|
||||||
expect(Array.from(events).map((event) => event.type)).toEqual(["server.connected", "session.next.model.switched"])
|
|
||||||
const durable = events[1]
|
|
||||||
if (durable?.type !== "session.next.model.switched") throw new Error("Expected model event")
|
|
||||||
expect(DateTime.toEpochMillis(durable.data.timestamp)).toBe(1_717_171_717_000)
|
|
||||||
expect(durable.durable).toEqual({ aggregateID: "ses_test", seq: 1, version: 1 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("events.subscribe terminates on Effect protocol decode failures", async () => {
|
|
||||||
const httpClient = HttpClient.make((request) =>
|
|
||||||
Effect.succeed(
|
|
||||||
HttpClientResponse.fromWeb(
|
|
||||||
request,
|
|
||||||
new Response(`data: {"type":"server.connected"}\n\n`, {
|
|
||||||
headers: { "content-type": "text/event-stream" },
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
const error = await Effect.gen(function* () {
|
|
||||||
const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" })
|
|
||||||
return yield* client.events.subscribe().pipe(Stream.runCollect, Effect.flip)
|
|
||||||
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
|
|
||||||
|
|
||||||
expect(error._tag).toBe("ClientError")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("session methods retain decoded Effect inputs and outputs", async () => {
|
test("session methods retain decoded Effect inputs and outputs", async () => {
|
||||||
const historyQueries: Array<Record<string, string>> = []
|
|
||||||
let historyPage = 0
|
|
||||||
const httpClient = HttpClient.make((request) => {
|
const httpClient = HttpClient.make((request) => {
|
||||||
const url = request.url
|
const url = request.url
|
||||||
if (url.includes("/event")) {
|
if (url.includes("/event")) {
|
||||||
@@ -74,18 +28,6 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (url.includes("/history")) {
|
|
||||||
historyPage++
|
|
||||||
historyQueries.push(Object.fromEntries(request.urlParams.params))
|
|
||||||
return Effect.succeed(
|
|
||||||
HttpClientResponse.fromWeb(
|
|
||||||
request,
|
|
||||||
Response.json(
|
|
||||||
historyPage === 1 ? { data: [modelSwitchedEvent], hasMore: true } : { data: [], hasMore: false },
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (url.includes("/prompt")) {
|
if (url.includes("/prompt")) {
|
||||||
return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(admission)))
|
return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(admission)))
|
||||||
}
|
}
|
||||||
@@ -130,18 +72,6 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
|
|||||||
yield* client.sessions.compact({ sessionID: Session.ID.make("ses_test") })
|
yield* client.sessions.compact({ sessionID: Session.ID.make("ses_test") })
|
||||||
yield* client.sessions.wait({ sessionID: Session.ID.make("ses_test") })
|
yield* client.sessions.wait({ sessionID: Session.ID.make("ses_test") })
|
||||||
const context = yield* client.sessions.context({ sessionID: Session.ID.make("ses_test") })
|
const context = yield* client.sessions.context({ sessionID: Session.ID.make("ses_test") })
|
||||||
const history = yield* client.sessions.history({
|
|
||||||
sessionID: Session.ID.make("ses_test"),
|
|
||||||
after: 0,
|
|
||||||
limit: 1,
|
|
||||||
})
|
|
||||||
const historyNext = history.hasMore
|
|
||||||
? yield* client.sessions.history({
|
|
||||||
sessionID: Session.ID.make("ses_test"),
|
|
||||||
after: history.data.at(-1)?.durable?.seq,
|
|
||||||
limit: 2,
|
|
||||||
})
|
|
||||||
: undefined
|
|
||||||
const events = yield* client.sessions
|
const events = yield* client.sessions
|
||||||
.events({ sessionID: Session.ID.make("ses_test"), after: 0 })
|
.events({ sessionID: Session.ID.make("ses_test"), after: 0 })
|
||||||
.pipe(Stream.runCollect)
|
.pipe(Stream.runCollect)
|
||||||
@@ -150,7 +80,7 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
|
|||||||
sessionID: Session.ID.make("ses_test"),
|
sessionID: Session.ID.make("ses_test"),
|
||||||
messageID: SessionMessage.ID.make("msg_model"),
|
messageID: SessionMessage.ID.make("msg_model"),
|
||||||
})
|
})
|
||||||
return { page, active, created, admitted, context, history, historyNext, events, message }
|
return { page, active, created, admitted, context, events, message }
|
||||||
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
|
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
|
||||||
|
|
||||||
expect(DateTime.toEpochMillis(result.page.data[0].time.created)).toBe(1_717_171_717_000)
|
expect(DateTime.toEpochMillis(result.page.data[0].time.created)).toBe(1_717_171_717_000)
|
||||||
@@ -162,39 +92,10 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
|
|||||||
expect(Object.getPrototypeOf(result.admitted.prompt)).toBe(Object.prototype)
|
expect(Object.getPrototypeOf(result.admitted.prompt)).toBe(Object.prototype)
|
||||||
expect(DateTime.toEpochMillis(result.admitted.timeCreated)).toBe(1_717_171_717_000)
|
expect(DateTime.toEpochMillis(result.admitted.timeCreated)).toBe(1_717_171_717_000)
|
||||||
expect(result.context).toEqual([])
|
expect(result.context).toEqual([])
|
||||||
expect(DateTime.toEpochMillis(result.history.data[0].data.timestamp)).toBe(1_717_171_717_000)
|
|
||||||
expect(result.history).toEqual(expect.objectContaining({ hasMore: true }))
|
|
||||||
expect(result.historyNext).toEqual({ data: [], hasMore: false })
|
|
||||||
expect(historyQueries[0]).toEqual({ limit: "1", after: "0" })
|
|
||||||
expect(historyQueries[1]).toEqual({ limit: "2", after: "1" })
|
|
||||||
expect(DateTime.toEpochMillis(result.events[0].data.timestamp)).toBe(1_717_171_717_000)
|
expect(DateTime.toEpochMillis(result.events[0].data.timestamp)).toBe(1_717_171_717_000)
|
||||||
expect(result.message).toEqual(expect.objectContaining({ id: "msg_model", type: "model-switched" }))
|
expect(result.message).toEqual(expect.objectContaining({ id: "msg_model", type: "model-switched" }))
|
||||||
})
|
})
|
||||||
|
|
||||||
test("sessions.history retains the typed SessionNotFoundError", async () => {
|
|
||||||
const httpClient = HttpClient.make((request) =>
|
|
||||||
Effect.succeed(
|
|
||||||
HttpClientResponse.fromWeb(
|
|
||||||
request,
|
|
||||||
Response.json(
|
|
||||||
{ _tag: "SessionNotFoundError", sessionID: "ses_missing", message: "Session not found" },
|
|
||||||
{ status: 404 },
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
const error = await Effect.gen(function* () {
|
|
||||||
const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" })
|
|
||||||
return yield* client.sessions
|
|
||||||
.history({
|
|
||||||
sessionID: Session.ID.make("ses_missing"),
|
|
||||||
})
|
|
||||||
.pipe(Effect.flip)
|
|
||||||
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
|
|
||||||
|
|
||||||
expect(error._tag).toBe("SessionNotFoundError")
|
|
||||||
})
|
|
||||||
|
|
||||||
const session = {
|
const session = {
|
||||||
data: {
|
data: {
|
||||||
id: "ses_test",
|
id: "ses_test",
|
||||||
|
|||||||
@@ -1,42 +1,5 @@
|
|||||||
import { expect, test } from "bun:test"
|
import { expect, test } from "bun:test"
|
||||||
import { isSessionNotFoundError, isUnauthorizedError, OpenCode } from "../src"
|
import { isUnauthorizedError, OpenCode } from "../src"
|
||||||
|
|
||||||
test("exposes every standard HTTP API group", () => {
|
|
||||||
const client = OpenCode.make({ baseUrl: "http://localhost:3000" })
|
|
||||||
|
|
||||||
expect(Object.keys(client)).toEqual([
|
|
||||||
"health",
|
|
||||||
"location",
|
|
||||||
"agents",
|
|
||||||
"sessions",
|
|
||||||
"messages",
|
|
||||||
"models",
|
|
||||||
"providers",
|
|
||||||
"integrations",
|
|
||||||
"credentials",
|
|
||||||
"permissions",
|
|
||||||
"files",
|
|
||||||
"commands",
|
|
||||||
"skills",
|
|
||||||
"events",
|
|
||||||
"ptys",
|
|
||||||
"questions",
|
|
||||||
"references",
|
|
||||||
"projectCopies",
|
|
||||||
])
|
|
||||||
expect(Object.keys(client.messages)).toEqual(["list"])
|
|
||||||
expect(Object.keys(client.integrations)).toEqual([
|
|
||||||
"list",
|
|
||||||
"get",
|
|
||||||
"connectKey",
|
|
||||||
"connectOauth",
|
|
||||||
"attemptStatus",
|
|
||||||
"attemptComplete",
|
|
||||||
"attemptCancel",
|
|
||||||
])
|
|
||||||
expect(Object.keys(client.files)).toEqual(["list", "find"])
|
|
||||||
expect(Object.keys(client.ptys)).toEqual(["list", "create", "get", "update", "remove"])
|
|
||||||
})
|
|
||||||
|
|
||||||
test("sessions.get returns the wire projection", async () => {
|
test("sessions.get returns the wire projection", async () => {
|
||||||
const client = OpenCode.make({
|
const client = OpenCode.make({
|
||||||
@@ -54,38 +17,8 @@ test("sessions.get returns the wire projection", async () => {
|
|||||||
expect(result.time.created).toBe(1_717_171_717_000)
|
expect(result.time.created).toBe(1_717_171_717_000)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("events.subscribe exposes the Promise event stream wire projection", async () => {
|
|
||||||
const client = OpenCode.make({
|
|
||||||
baseUrl: "http://localhost:3000",
|
|
||||||
fetch: async () =>
|
|
||||||
new Response(
|
|
||||||
`: heartbeat\n\ndata: ${JSON.stringify({ id: "evt_connected", type: "server.connected", data: {} })}\n\n` +
|
|
||||||
`data: ${JSON.stringify(modelSwitchedEvent)}\n\n`,
|
|
||||||
{ headers: { "content-type": "text/event-stream" } },
|
|
||||||
),
|
|
||||||
})
|
|
||||||
const events = []
|
|
||||||
for await (const event of client.events.subscribe()) events.push(event)
|
|
||||||
|
|
||||||
expect(events).toEqual([{ id: "evt_connected", type: "server.connected", data: {} }, modelSwitchedEvent])
|
|
||||||
expect(events[1]?.type === "session.next.model.switched" && events[1].data.timestamp).toBe(1_717_171_717_000)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("events.subscribe terminates on malformed Promise SSE data", async () => {
|
|
||||||
const client = OpenCode.make({
|
|
||||||
baseUrl: "http://localhost:3000",
|
|
||||||
fetch: async () => new Response("data: {not-json}\n\n", { headers: { "content-type": "text/event-stream" } }),
|
|
||||||
})
|
|
||||||
|
|
||||||
await expect(client.events.subscribe()[Symbol.asyncIterator]().next()).rejects.toMatchObject({
|
|
||||||
name: "ClientError",
|
|
||||||
reason: "MalformedResponse",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("session methods use the public HTTP contract", async () => {
|
test("session methods use the public HTTP contract", async () => {
|
||||||
const requests: Array<{ url: string; init?: RequestInit }> = []
|
const requests: Array<{ url: string; init?: RequestInit }> = []
|
||||||
let historyPage = 0
|
|
||||||
const client = OpenCode.make({
|
const client = OpenCode.make({
|
||||||
baseUrl: "http://localhost:3000",
|
baseUrl: "http://localhost:3000",
|
||||||
fetch: async (input, init) => {
|
fetch: async (input, init) => {
|
||||||
@@ -96,12 +29,6 @@ test("session methods use the public HTTP contract", async () => {
|
|||||||
headers: { "content-type": "text/event-stream" },
|
headers: { "content-type": "text/event-stream" },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (url.includes("/history")) {
|
|
||||||
historyPage++
|
|
||||||
return Response.json(
|
|
||||||
historyPage === 1 ? { data: [modelSwitchedEvent], hasMore: true } : { data: [], hasMore: false },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (url.includes("/prompt")) return Response.json(admission)
|
if (url.includes("/prompt")) return Response.json(admission)
|
||||||
if (url.includes("/context")) return Response.json({ data: [] })
|
if (url.includes("/context")) return Response.json({ data: [] })
|
||||||
if (url.includes("/message/")) return Response.json({ data: modelSwitchedMessage })
|
if (url.includes("/message/")) return Response.json({ data: modelSwitchedMessage })
|
||||||
@@ -112,7 +39,7 @@ test("session methods use the public HTTP contract", async () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const page = await client.sessions.list({ limit: 10, order: "desc" })
|
const page = await client.sessions.list({ limit: "10", order: "desc" })
|
||||||
const active = await client.sessions.active()
|
const active = await client.sessions.active()
|
||||||
const created = await client.sessions.create({ location: { directory: "/tmp/project" } })
|
const created = await client.sessions.create({ location: { directory: "/tmp/project" } })
|
||||||
await client.sessions.switchAgent({ sessionID: "ses_test", agent: "build" })
|
await client.sessions.switchAgent({ sessionID: "ses_test", agent: "build" })
|
||||||
@@ -128,13 +55,8 @@ test("session methods use the public HTTP contract", async () => {
|
|||||||
await client.sessions.compact({ sessionID: "ses_test" })
|
await client.sessions.compact({ sessionID: "ses_test" })
|
||||||
await client.sessions.wait({ sessionID: "ses_test" })
|
await client.sessions.wait({ sessionID: "ses_test" })
|
||||||
const context = await client.sessions.context({ sessionID: "ses_test" })
|
const context = await client.sessions.context({ sessionID: "ses_test" })
|
||||||
const history = await client.sessions.history({ sessionID: "ses_test", after: 0, limit: 1 })
|
|
||||||
const historyAfter = history.data.at(-1)?.durable?.seq
|
|
||||||
const historyNext = history.hasMore
|
|
||||||
? await client.sessions.history({ sessionID: "ses_test", after: historyAfter, limit: 2 })
|
|
||||||
: undefined
|
|
||||||
const events = []
|
const events = []
|
||||||
for await (const event of client.sessions.events({ sessionID: "ses_test", after: 0 })) events.push(event)
|
for await (const event of client.sessions.events({ sessionID: "ses_test", after: "0" })) events.push(event)
|
||||||
await client.sessions.interrupt({ sessionID: "ses_test" })
|
await client.sessions.interrupt({ sessionID: "ses_test" })
|
||||||
const message = await client.sessions.message({ sessionID: "ses_test", messageID: "msg_model" })
|
const message = await client.sessions.message({ sessionID: "ses_test", messageID: "msg_model" })
|
||||||
|
|
||||||
@@ -143,8 +65,6 @@ test("session methods use the public HTTP contract", async () => {
|
|||||||
expect(created.id).toBe("ses_test")
|
expect(created.id).toBe("ses_test")
|
||||||
expect(admitted.id).toBe("msg_test")
|
expect(admitted.id).toBe("msg_test")
|
||||||
expect(context).toEqual([])
|
expect(context).toEqual([])
|
||||||
expect(history).toEqual({ data: [modelSwitchedEvent], hasMore: true })
|
|
||||||
expect(historyNext).toEqual({ data: [], hasMore: false })
|
|
||||||
expect(events).toEqual([modelSwitchedEvent])
|
expect(events).toEqual([modelSwitchedEvent])
|
||||||
expect(message).toEqual(modelSwitchedMessage)
|
expect(message).toEqual(modelSwitchedMessage)
|
||||||
expect(requests.map((request) => [request.init?.method, request.url])).toEqual([
|
expect(requests.map((request) => [request.init?.method, request.url])).toEqual([
|
||||||
@@ -157,8 +77,6 @@ test("session methods use the public HTTP contract", async () => {
|
|||||||
["POST", "http://localhost:3000/api/session/ses_test/compact"],
|
["POST", "http://localhost:3000/api/session/ses_test/compact"],
|
||||||
["POST", "http://localhost:3000/api/session/ses_test/wait"],
|
["POST", "http://localhost:3000/api/session/ses_test/wait"],
|
||||||
["GET", "http://localhost:3000/api/session/ses_test/context"],
|
["GET", "http://localhost:3000/api/session/ses_test/context"],
|
||||||
["GET", "http://localhost:3000/api/session/ses_test/history?limit=1&after=0"],
|
|
||||||
["GET", "http://localhost:3000/api/session/ses_test/history?limit=2&after=1"],
|
|
||||||
["GET", "http://localhost:3000/api/session/ses_test/event?after=0"],
|
["GET", "http://localhost:3000/api/session/ses_test/event?after=0"],
|
||||||
["POST", "http://localhost:3000/api/session/ses_test/interrupt"],
|
["POST", "http://localhost:3000/api/session/ses_test/interrupt"],
|
||||||
["GET", "http://localhost:3000/api/session/ses_test/message/msg_model"],
|
["GET", "http://localhost:3000/api/session/ses_test/message/msg_model"],
|
||||||
@@ -186,24 +104,6 @@ test("middleware errors remain declared client errors", async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test("sessions.history decodes SessionNotFoundError", async () => {
|
|
||||||
const client = OpenCode.make({
|
|
||||||
baseUrl: "http://localhost:3000",
|
|
||||||
fetch: async () =>
|
|
||||||
Response.json(
|
|
||||||
{ _tag: "SessionNotFoundError", sessionID: "ses_missing", message: "Session not found" },
|
|
||||||
{ status: 404 },
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
await client.sessions.history({ sessionID: "ses_missing" })
|
|
||||||
throw new Error("Expected request to fail")
|
|
||||||
} catch (error) {
|
|
||||||
expect(isSessionNotFoundError(error)).toBe(true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const session = {
|
const session = {
|
||||||
data: {
|
data: {
|
||||||
id: "ses_test",
|
id: "ses_test",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.17.12",
|
"version": "1.17.11",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "الاستثناءات التالية",
|
"zen.privacy.exceptionsLink": "الاستثناءات التالية",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع",
|
"go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع",
|
||||||
|
"go.banner.text": "MiniMax M3: حد استخدام أكبر 3 مرات لفترة محدودة",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ GLM-5.2 وGLM-5.1 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash.",
|
"يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ GLM-5.2 وGLM-5.1 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash.",
|
||||||
"go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع",
|
"go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع",
|
||||||
@@ -368,8 +369,6 @@ export const dict = {
|
|||||||
"zen.api.error.userMonthlyLimitReached":
|
"zen.api.error.userMonthlyLimitReached":
|
||||||
"لقد وصلت إلى حد الإنفاق الشهري البالغ ${{amount}}. إدارة حدودك هنا: {{membersUrl}}",
|
"لقد وصلت إلى حد الإنفاق الشهري البالغ ${{amount}}. إدارة حدودك هنا: {{membersUrl}}",
|
||||||
"zen.api.error.modelDisabled": "النموذج معطل",
|
"zen.api.error.modelDisabled": "النموذج معطل",
|
||||||
"zen.api.error.regionNotAllowed":
|
|
||||||
"هذا النموذج مستضاف في الصين. إذا كنت ترغب في استخدام هذا النموذج، فعّله في إعداداتك: {{consoleGoUrl}}",
|
|
||||||
"zen.api.error.trialEnded":
|
"zen.api.error.trialEnded":
|
||||||
"انتهى العرض المجاني لـ {{model}}. يمكنك مواصلة استخدام النموذج بالاشتراك في OpenCode Go - {{link}}",
|
"انتهى العرض المجاني لـ {{model}}. يمكنك مواصلة استخدام النموذج بالاشتراك في OpenCode Go - {{link}}",
|
||||||
|
|
||||||
@@ -647,9 +646,6 @@ export const dict = {
|
|||||||
"workspace.lite.subscription.useBalance": "استخدم رصيدك المتوفر بعد الوصول إلى حدود الاستخدام",
|
"workspace.lite.subscription.useBalance": "استخدم رصيدك المتوفر بعد الوصول إلى حدود الاستخدام",
|
||||||
"workspace.lite.subscription.selectProvider":
|
"workspace.lite.subscription.selectProvider":
|
||||||
'اختر "OpenCode Go" كمزود في إعدادات opencode الخاصة بك لاستخدام نماذج Go.',
|
'اختر "OpenCode Go" كمزود في إعدادات opencode الخاصة بك لاستخدام نماذج Go.',
|
||||||
"workspace.lite.providers.title": "المزودون",
|
|
||||||
"workspace.lite.providers.description": "تحكم في المزودين المستخدمين للتوجيه.",
|
|
||||||
"workspace.lite.providers.useChina": "تفعيل النماذج المستضافة في الصين",
|
|
||||||
"workspace.lite.black.message":
|
"workspace.lite.black.message":
|
||||||
"أنت مشترك حاليًا في OpenCode Black أو في قائمة الانتظار. يرجى إلغاء الاشتراك أولاً إذا كنت ترغب في التبديل إلى Go.",
|
"أنت مشترك حاليًا في OpenCode Black أو في قائمة الانتظار. يرجى إلغاء الاشتراك أولاً إذا كنت ترغب في التبديل إلى Go.",
|
||||||
"workspace.lite.other.message":
|
"workspace.lite.other.message":
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "seguintes exceções",
|
"zen.privacy.exceptionsLink": "seguintes exceções",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos",
|
"go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos",
|
||||||
|
"go.banner.text": "MiniMax M3 tem limite de uso 3x maior por tempo limitado",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de solicitação de 5 horas para GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash.",
|
"O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de solicitação de 5 horas para GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Modelos de codificação de baixo custo para todos",
|
"go.hero.title": "Modelos de codificação de baixo custo para todos",
|
||||||
@@ -376,8 +377,6 @@ export const dict = {
|
|||||||
"zen.api.error.userMonthlyLimitReached":
|
"zen.api.error.userMonthlyLimitReached":
|
||||||
"Você atingiu seu limite de gastos mensais de ${{amount}}. Gerencie seus limites aqui: {{membersUrl}}",
|
"Você atingiu seu limite de gastos mensais de ${{amount}}. Gerencie seus limites aqui: {{membersUrl}}",
|
||||||
"zen.api.error.modelDisabled": "O modelo está desabilitado",
|
"zen.api.error.modelDisabled": "O modelo está desabilitado",
|
||||||
"zen.api.error.regionNotAllowed":
|
|
||||||
"Este modelo está hospedado na China. Se você quiser usar este modelo, ative-o nas suas configurações: {{consoleGoUrl}}",
|
|
||||||
"zen.api.error.trialEnded":
|
"zen.api.error.trialEnded":
|
||||||
"A promoção gratuita do {{model}} terminou. Você pode continuar usando o modelo assinando o OpenCode Go - {{link}}",
|
"A promoção gratuita do {{model}} terminou. Você pode continuar usando o modelo assinando o OpenCode Go - {{link}}",
|
||||||
|
|
||||||
@@ -657,9 +656,6 @@ export const dict = {
|
|||||||
"workspace.lite.subscription.useBalance": "Use seu saldo disponível após atingir os limites de uso",
|
"workspace.lite.subscription.useBalance": "Use seu saldo disponível após atingir os limites de uso",
|
||||||
"workspace.lite.subscription.selectProvider":
|
"workspace.lite.subscription.selectProvider":
|
||||||
'Selecione "OpenCode Go" como provedor na sua configuração do opencode para usar os modelos Go.',
|
'Selecione "OpenCode Go" como provedor na sua configuração do opencode para usar os modelos Go.',
|
||||||
"workspace.lite.providers.title": "Provedores",
|
|
||||||
"workspace.lite.providers.description": "Controle quais provedores são usados para roteamento.",
|
|
||||||
"workspace.lite.providers.useChina": "Ativar modelos hospedados na China",
|
|
||||||
"workspace.lite.black.message":
|
"workspace.lite.black.message":
|
||||||
"Você está atualmente inscrito no OpenCode Black ou na lista de espera. Por favor, cancele a assinatura primeiro se desejar mudar para o Go.",
|
"Você está atualmente inscrito no OpenCode Black ou na lista de espera. Por favor, cancele a assinatura primeiro se desejar mudar para o Go.",
|
||||||
"workspace.lite.other.message":
|
"workspace.lite.other.message":
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "følgende undtagelser",
|
"zen.privacy.exceptionsLink": "følgende undtagelser",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle",
|
"go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle",
|
||||||
|
"go.banner.text": "MiniMax M3 får tredoblet brugsgrænse i en begrænset periode",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go starter ved $5 for den første måned, derefter $10/måned, med generøse 5-timers anmodningsgrænser for GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash.",
|
"Go starter ved $5 for den første måned, derefter $10/måned, med generøse 5-timers anmodningsgrænser for GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Kodningsmodeller til lav pris for alle",
|
"go.hero.title": "Kodningsmodeller til lav pris for alle",
|
||||||
@@ -372,8 +373,6 @@ export const dict = {
|
|||||||
"zen.api.error.userMonthlyLimitReached":
|
"zen.api.error.userMonthlyLimitReached":
|
||||||
"Du har nået din månedlige forbrugsgrænse på ${{amount}}. Administrer dine grænser her: {{membersUrl}}",
|
"Du har nået din månedlige forbrugsgrænse på ${{amount}}. Administrer dine grænser her: {{membersUrl}}",
|
||||||
"zen.api.error.modelDisabled": "Modellen er deaktiveret",
|
"zen.api.error.modelDisabled": "Modellen er deaktiveret",
|
||||||
"zen.api.error.regionNotAllowed":
|
|
||||||
"Denne model hostes i Kina. Hvis du vil bruge denne model, skal du aktivere den i dine indstillinger: {{consoleGoUrl}}",
|
|
||||||
"zen.api.error.trialEnded":
|
"zen.api.error.trialEnded":
|
||||||
"Den gratis kampagne for {{model}} er afsluttet. Du kan fortsætte med at bruge modellen ved at abonnere på OpenCode Go - {{link}}",
|
"Den gratis kampagne for {{model}} er afsluttet. Du kan fortsætte med at bruge modellen ved at abonnere på OpenCode Go - {{link}}",
|
||||||
|
|
||||||
@@ -653,9 +652,6 @@ export const dict = {
|
|||||||
"workspace.lite.subscription.useBalance": "Brug din tilgængelige saldo, når du har nået forbrugsgrænserne",
|
"workspace.lite.subscription.useBalance": "Brug din tilgængelige saldo, når du har nået forbrugsgrænserne",
|
||||||
"workspace.lite.subscription.selectProvider":
|
"workspace.lite.subscription.selectProvider":
|
||||||
'Vælg "OpenCode Go" som udbyder i din opencode-konfiguration for at bruge Go-modeller.',
|
'Vælg "OpenCode Go" som udbyder i din opencode-konfiguration for at bruge Go-modeller.',
|
||||||
"workspace.lite.providers.title": "Udbydere",
|
|
||||||
"workspace.lite.providers.description": "Styr, hvilke udbydere der bruges til routing.",
|
|
||||||
"workspace.lite.providers.useChina": "Aktivér modeller hostet i Kina",
|
|
||||||
"workspace.lite.black.message":
|
"workspace.lite.black.message":
|
||||||
"Du abonnerer i øjeblikket på OpenCode Black eller er på venteliste. Afmeld venligst først, hvis du vil skifte til Go.",
|
"Du abonnerer i øjeblikket på OpenCode Black eller er på venteliste. Afmeld venligst først, hvis du vil skifte til Go.",
|
||||||
"workspace.lite.other.message":
|
"workspace.lite.other.message":
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "folgenden Ausnahmen",
|
"zen.privacy.exceptionsLink": "folgenden Ausnahmen",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle",
|
"go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle",
|
||||||
|
"go.banner.text": "MiniMax M3 erhält für begrenzte Zeit 3x Nutzungslimits",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go beginnt bei $5 für den ersten Monat, danach $10/Monat, mit großzügigen 5-Stunden-Anfragelimits für GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro und DeepSeek V4 Flash.",
|
"Go beginnt bei $5 für den ersten Monat, danach $10/Monat, mit großzügigen 5-Stunden-Anfragelimits für GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro und DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Kostengünstige Coding-Modelle für alle",
|
"go.hero.title": "Kostengünstige Coding-Modelle für alle",
|
||||||
@@ -375,8 +376,6 @@ export const dict = {
|
|||||||
"zen.api.error.userMonthlyLimitReached":
|
"zen.api.error.userMonthlyLimitReached":
|
||||||
"Du hast dein monatliches Ausgabenlimit von ${{amount}} erreicht. Verwalte deine Limits hier: {{membersUrl}}",
|
"Du hast dein monatliches Ausgabenlimit von ${{amount}} erreicht. Verwalte deine Limits hier: {{membersUrl}}",
|
||||||
"zen.api.error.modelDisabled": "Modell ist deaktiviert",
|
"zen.api.error.modelDisabled": "Modell ist deaktiviert",
|
||||||
"zen.api.error.regionNotAllowed":
|
|
||||||
"Dieses Modell wird in China gehostet. Wenn du dieses Modell verwenden möchtest, aktiviere es in deinen Einstellungen: {{consoleGoUrl}}",
|
|
||||||
"zen.api.error.trialEnded":
|
"zen.api.error.trialEnded":
|
||||||
"Die kostenlose Aktion für {{model}} ist beendet. Du kannst das Modell weiterhin nutzen, indem du OpenCode Go abonnierst - {{link}}",
|
"Die kostenlose Aktion für {{model}} ist beendet. Du kannst das Modell weiterhin nutzen, indem du OpenCode Go abonnierst - {{link}}",
|
||||||
|
|
||||||
@@ -656,9 +655,6 @@ export const dict = {
|
|||||||
"workspace.lite.subscription.useBalance": "Nutze dein verfügbares Guthaben, nachdem die Nutzungslimits erreicht sind",
|
"workspace.lite.subscription.useBalance": "Nutze dein verfügbares Guthaben, nachdem die Nutzungslimits erreicht sind",
|
||||||
"workspace.lite.subscription.selectProvider":
|
"workspace.lite.subscription.selectProvider":
|
||||||
'Wähle "OpenCode Go" als Anbieter in deiner opencode-Konfiguration, um Go-Modelle zu verwenden.',
|
'Wähle "OpenCode Go" als Anbieter in deiner opencode-Konfiguration, um Go-Modelle zu verwenden.',
|
||||||
"workspace.lite.providers.title": "Anbieter",
|
|
||||||
"workspace.lite.providers.description": "Steuere, welche Anbieter für das Routing verwendet werden.",
|
|
||||||
"workspace.lite.providers.useChina": "In China gehostete Modelle aktivieren",
|
|
||||||
"workspace.lite.black.message":
|
"workspace.lite.black.message":
|
||||||
"Du hast derzeit OpenCode Black abonniert oder stehst auf der Warteliste. Bitte kündige zuerst, wenn du zu Go wechseln möchtest.",
|
"Du hast derzeit OpenCode Black abonniert oder stehst auf der Warteliste. Bitte kündige zuerst, wenn du zu Go wechseln möchtest.",
|
||||||
"workspace.lite.other.message":
|
"workspace.lite.other.message":
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "following exceptions",
|
"zen.privacy.exceptionsLink": "following exceptions",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Low cost coding models for everyone",
|
"go.title": "OpenCode Go | Low cost coding models for everyone",
|
||||||
|
"go.banner.text": "MiniMax M3 gets 3× usage limits for a limited time",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go starts at $5 for your first month, then $10/month, with generous 5-hour request limits for GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, and DeepSeek V4 Flash.",
|
"Go starts at $5 for your first month, then $10/month, with generous 5-hour request limits for GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, and DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Low cost coding models for everyone",
|
"go.hero.title": "Low cost coding models for everyone",
|
||||||
@@ -369,8 +370,6 @@ export const dict = {
|
|||||||
"zen.api.error.userMonthlyLimitReached":
|
"zen.api.error.userMonthlyLimitReached":
|
||||||
"You have reached your monthly spending limit of ${{amount}}. Manage your limits here: {{membersUrl}}",
|
"You have reached your monthly spending limit of ${{amount}}. Manage your limits here: {{membersUrl}}",
|
||||||
"zen.api.error.modelDisabled": "Model is disabled",
|
"zen.api.error.modelDisabled": "Model is disabled",
|
||||||
"zen.api.error.regionNotAllowed":
|
|
||||||
"This model is hosted in China. If you would like to use this model, enable it in your settings: {{consoleGoUrl}}",
|
|
||||||
"zen.api.error.trialEnded":
|
"zen.api.error.trialEnded":
|
||||||
"Free promotion has ended for {{model}}. You can continue using the model by subscribing to OpenCode Go - {{link}}",
|
"Free promotion has ended for {{model}}. You can continue using the model by subscribing to OpenCode Go - {{link}}",
|
||||||
|
|
||||||
@@ -650,9 +649,6 @@ export const dict = {
|
|||||||
"workspace.lite.subscription.useBalance": "Use your available balance after reaching the usage limits",
|
"workspace.lite.subscription.useBalance": "Use your available balance after reaching the usage limits",
|
||||||
"workspace.lite.subscription.selectProvider":
|
"workspace.lite.subscription.selectProvider":
|
||||||
'Select "OpenCode Go" as the provider in your opencode configuration to use Go models.',
|
'Select "OpenCode Go" as the provider in your opencode configuration to use Go models.',
|
||||||
"workspace.lite.providers.title": "Providers",
|
|
||||||
"workspace.lite.providers.description": "Control which providers are used for routing.",
|
|
||||||
"workspace.lite.providers.useChina": "Enable models hosted in China",
|
|
||||||
"workspace.lite.black.message":
|
"workspace.lite.black.message":
|
||||||
"You're currently subscribed to OpenCode Black or on the waitlist. Please unsubscribe first if you'd like to switch to Go.",
|
"You're currently subscribed to OpenCode Black or on the waitlist. Please unsubscribe first if you'd like to switch to Go.",
|
||||||
"workspace.lite.other.message":
|
"workspace.lite.other.message":
|
||||||
|
|||||||
@@ -253,6 +253,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "siguientes excepciones",
|
"zen.privacy.exceptionsLink": "siguientes excepciones",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Modelos de programación de bajo coste para todos",
|
"go.title": "OpenCode Go | Modelos de programación de bajo coste para todos",
|
||||||
|
"go.banner.text": "MiniMax M3 tiene límites de uso 3x mayores por tiempo limitado",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go comienza en $5 el primer mes, luego 10 $/mes, con generosos límites de solicitudes de 5 horas para GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro y DeepSeek V4 Flash.",
|
"Go comienza en $5 el primer mes, luego 10 $/mes, con generosos límites de solicitudes de 5 horas para GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro y DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Modelos de programación de bajo coste para todos",
|
"go.hero.title": "Modelos de programación de bajo coste para todos",
|
||||||
@@ -376,8 +377,6 @@ export const dict = {
|
|||||||
"zen.api.error.userMonthlyLimitReached":
|
"zen.api.error.userMonthlyLimitReached":
|
||||||
"Has alcanzado tu límite de gasto mensual de ${{amount}}. Gestiona tus límites aquí: {{membersUrl}}",
|
"Has alcanzado tu límite de gasto mensual de ${{amount}}. Gestiona tus límites aquí: {{membersUrl}}",
|
||||||
"zen.api.error.modelDisabled": "El modelo está deshabilitado",
|
"zen.api.error.modelDisabled": "El modelo está deshabilitado",
|
||||||
"zen.api.error.regionNotAllowed":
|
|
||||||
"Este modelo está alojado en China. Si quieres usar este modelo, actívalo en tu configuración: {{consoleGoUrl}}",
|
|
||||||
"zen.api.error.trialEnded":
|
"zen.api.error.trialEnded":
|
||||||
"La promoción gratuita de {{model}} ha finalizado. Puedes seguir usando el modelo suscribiéndote a OpenCode Go - {{link}}",
|
"La promoción gratuita de {{model}} ha finalizado. Puedes seguir usando el modelo suscribiéndote a OpenCode Go - {{link}}",
|
||||||
|
|
||||||
@@ -657,9 +656,6 @@ export const dict = {
|
|||||||
"workspace.lite.subscription.useBalance": "Usa tu saldo disponible después de alcanzar los límites de uso",
|
"workspace.lite.subscription.useBalance": "Usa tu saldo disponible después de alcanzar los límites de uso",
|
||||||
"workspace.lite.subscription.selectProvider":
|
"workspace.lite.subscription.selectProvider":
|
||||||
'Selecciona "OpenCode Go" como proveedor en tu configuración de opencode para usar los modelos Go.',
|
'Selecciona "OpenCode Go" como proveedor en tu configuración de opencode para usar los modelos Go.',
|
||||||
"workspace.lite.providers.title": "Proveedores",
|
|
||||||
"workspace.lite.providers.description": "Controla qué proveedores se usan para el enrutamiento.",
|
|
||||||
"workspace.lite.providers.useChina": "Activar modelos alojados en China",
|
|
||||||
"workspace.lite.black.message":
|
"workspace.lite.black.message":
|
||||||
"Actualmente estás suscrito a OpenCode Black o estás en la lista de espera. Por favor, cancela la suscripción primero si deseas cambiar a Go.",
|
"Actualmente estás suscrito a OpenCode Black o estás en la lista de espera. Por favor, cancela la suscripción primero si deseas cambiar a Go.",
|
||||||
"workspace.lite.other.message":
|
"workspace.lite.other.message":
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "exceptions suivantes",
|
"zen.privacy.exceptionsLink": "exceptions suivantes",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Modèles de code à faible coût pour tous",
|
"go.title": "OpenCode Go | Modèles de code à faible coût pour tous",
|
||||||
|
"go.banner.text": "MiniMax M3 bénéficie de limites d’utilisation 3x supérieures pour une durée limitée",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites de requêtes généreuses sur 5 heures pour GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro et DeepSeek V4 Flash.",
|
"Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites de requêtes généreuses sur 5 heures pour GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro et DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Modèles de code à faible coût pour tous",
|
"go.hero.title": "Modèles de code à faible coût pour tous",
|
||||||
@@ -376,8 +377,6 @@ export const dict = {
|
|||||||
"zen.api.error.userMonthlyLimitReached":
|
"zen.api.error.userMonthlyLimitReached":
|
||||||
"Vous avez atteint votre limite de dépense mensuelle de {{amount}} $. Gérez vos limites ici : {{membersUrl}}",
|
"Vous avez atteint votre limite de dépense mensuelle de {{amount}} $. Gérez vos limites ici : {{membersUrl}}",
|
||||||
"zen.api.error.modelDisabled": "Le modèle est désactivé",
|
"zen.api.error.modelDisabled": "Le modèle est désactivé",
|
||||||
"zen.api.error.regionNotAllowed":
|
|
||||||
"Ce modèle est hébergé en Chine. Si vous souhaitez utiliser ce modèle, activez-le dans vos paramètres : {{consoleGoUrl}}",
|
|
||||||
"zen.api.error.trialEnded":
|
"zen.api.error.trialEnded":
|
||||||
"La promotion gratuite de {{model}} est terminée. Vous pouvez continuer à utiliser le modèle en vous abonnant à OpenCode Go - {{link}}",
|
"La promotion gratuite de {{model}} est terminée. Vous pouvez continuer à utiliser le modèle en vous abonnant à OpenCode Go - {{link}}",
|
||||||
|
|
||||||
@@ -663,9 +662,6 @@ export const dict = {
|
|||||||
"Utilisez votre solde disponible après avoir atteint les limites d'utilisation",
|
"Utilisez votre solde disponible après avoir atteint les limites d'utilisation",
|
||||||
"workspace.lite.subscription.selectProvider":
|
"workspace.lite.subscription.selectProvider":
|
||||||
'Sélectionnez "OpenCode Go" comme fournisseur dans votre configuration opencode pour utiliser les modèles Go.',
|
'Sélectionnez "OpenCode Go" comme fournisseur dans votre configuration opencode pour utiliser les modèles Go.',
|
||||||
"workspace.lite.providers.title": "Fournisseurs",
|
|
||||||
"workspace.lite.providers.description": "Contrôlez les fournisseurs utilisés pour le routage.",
|
|
||||||
"workspace.lite.providers.useChina": "Activer les modèles hébergés en Chine",
|
|
||||||
"workspace.lite.black.message":
|
"workspace.lite.black.message":
|
||||||
"Vous êtes actuellement abonné à OpenCode Black ou sur liste d'attente. Veuillez d'abord vous désabonner si vous souhaitez passer à Go.",
|
"Vous êtes actuellement abonné à OpenCode Black ou sur liste d'attente. Veuillez d'abord vous désabonner si vous souhaitez passer à Go.",
|
||||||
"workspace.lite.other.message":
|
"workspace.lite.other.message":
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "seguenti eccezioni",
|
"zen.privacy.exceptionsLink": "seguenti eccezioni",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Modelli di coding a basso costo per tutti",
|
"go.title": "OpenCode Go | Modelli di coding a basso costo per tutti",
|
||||||
|
"go.banner.text": "MiniMax M3 offre limiti di utilizzo 3x superiori per un periodo limitato",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go inizia a $5 per il primo mese, poi $10/mese, con generosi limiti di richiesta di 5 ore per GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash.",
|
"Go inizia a $5 per il primo mese, poi $10/mese, con generosi limiti di richiesta di 5 ore per GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Modelli di coding a basso costo per tutti",
|
"go.hero.title": "Modelli di coding a basso costo per tutti",
|
||||||
@@ -372,8 +373,6 @@ export const dict = {
|
|||||||
"zen.api.error.userMonthlyLimitReached":
|
"zen.api.error.userMonthlyLimitReached":
|
||||||
"Hai raggiunto il tuo limite di spesa mensile di ${{amount}}. Gestisci i tuoi limiti qui: {{membersUrl}}",
|
"Hai raggiunto il tuo limite di spesa mensile di ${{amount}}. Gestisci i tuoi limiti qui: {{membersUrl}}",
|
||||||
"zen.api.error.modelDisabled": "Il modello è disabilitato",
|
"zen.api.error.modelDisabled": "Il modello è disabilitato",
|
||||||
"zen.api.error.regionNotAllowed":
|
|
||||||
"Questo modello è ospitato in Cina. Se vuoi usare questo modello, abilitalo nelle tue impostazioni: {{consoleGoUrl}}",
|
|
||||||
"zen.api.error.trialEnded":
|
"zen.api.error.trialEnded":
|
||||||
"La promozione gratuita di {{model}} è terminata. Puoi continuare a usare il modello abbonandoti a OpenCode Go - {{link}}",
|
"La promozione gratuita di {{model}} è terminata. Puoi continuare a usare il modello abbonandoti a OpenCode Go - {{link}}",
|
||||||
|
|
||||||
@@ -655,9 +654,6 @@ export const dict = {
|
|||||||
"workspace.lite.subscription.useBalance": "Usa il tuo saldo disponibile dopo aver raggiunto i limiti di utilizzo",
|
"workspace.lite.subscription.useBalance": "Usa il tuo saldo disponibile dopo aver raggiunto i limiti di utilizzo",
|
||||||
"workspace.lite.subscription.selectProvider":
|
"workspace.lite.subscription.selectProvider":
|
||||||
'Seleziona "OpenCode Go" come provider nella tua configurazione opencode per utilizzare i modelli Go.',
|
'Seleziona "OpenCode Go" come provider nella tua configurazione opencode per utilizzare i modelli Go.',
|
||||||
"workspace.lite.providers.title": "Provider",
|
|
||||||
"workspace.lite.providers.description": "Controlla quali provider vengono usati per il routing.",
|
|
||||||
"workspace.lite.providers.useChina": "Abilita modelli ospitati in Cina",
|
|
||||||
"workspace.lite.black.message":
|
"workspace.lite.black.message":
|
||||||
"Attualmente sei abbonato a OpenCode Black o sei in lista d'attesa. Annulla l'iscrizione prima se desideri passare a Go.",
|
"Attualmente sei abbonato a OpenCode Black o sei in lista d'attesa. Annulla l'iscrizione prima se desideri passare a Go.",
|
||||||
"workspace.lite.other.message":
|
"workspace.lite.other.message":
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "以下の例外",
|
"zen.privacy.exceptionsLink": "以下の例外",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル",
|
"go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル",
|
||||||
|
"go.banner.text": "MiniMax M3の利用上限が期間限定で3倍に",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Goは最初の月$5、その後$10/月で、GLM-5.2、GLM-5.1、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flashに対して5時間のゆとりあるリクエスト上限があります。",
|
"Goは最初の月$5、その後$10/月で、GLM-5.2、GLM-5.1、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flashに対して5時間のゆとりあるリクエスト上限があります。",
|
||||||
"go.hero.title": "すべての人のための低価格なコーディングモデル",
|
"go.hero.title": "すべての人のための低価格なコーディングモデル",
|
||||||
@@ -373,8 +374,6 @@ export const dict = {
|
|||||||
"zen.api.error.userMonthlyLimitReached":
|
"zen.api.error.userMonthlyLimitReached":
|
||||||
"月額の利用上限 ${{amount}} に達しました。こちらから上限を管理してください: {{membersUrl}}",
|
"月額の利用上限 ${{amount}} に達しました。こちらから上限を管理してください: {{membersUrl}}",
|
||||||
"zen.api.error.modelDisabled": "モデルが無効です",
|
"zen.api.error.modelDisabled": "モデルが無効です",
|
||||||
"zen.api.error.regionNotAllowed":
|
|
||||||
"このモデルは中国でホストされています。このモデルを使用したい場合は、設定で有効にしてください: {{consoleGoUrl}}",
|
|
||||||
"zen.api.error.trialEnded":
|
"zen.api.error.trialEnded":
|
||||||
"{{model}} の無料プロモーションは終了しました。OpenCode Go を購読するとモデルを引き続き使用できます - {{link}}",
|
"{{model}} の無料プロモーションは終了しました。OpenCode Go を購読するとモデルを引き続き使用できます - {{link}}",
|
||||||
|
|
||||||
@@ -655,9 +654,6 @@ export const dict = {
|
|||||||
"workspace.lite.subscription.useBalance": "利用限度額に達したら利用可能な残高を使用する",
|
"workspace.lite.subscription.useBalance": "利用限度額に達したら利用可能な残高を使用する",
|
||||||
"workspace.lite.subscription.selectProvider":
|
"workspace.lite.subscription.selectProvider":
|
||||||
"Go モデルを使用するには、opencode の設定で「OpenCode Go」をプロバイダーとして選択してください。",
|
"Go モデルを使用するには、opencode の設定で「OpenCode Go」をプロバイダーとして選択してください。",
|
||||||
"workspace.lite.providers.title": "プロバイダー",
|
|
||||||
"workspace.lite.providers.description": "ルーティングに使用するプロバイダーを管理します。",
|
|
||||||
"workspace.lite.providers.useChina": "中国でホストされているモデルを有効にする",
|
|
||||||
"workspace.lite.black.message":
|
"workspace.lite.black.message":
|
||||||
"現在 OpenCode Black を購読中、またはウェイティングリストに登録されています。Go に切り替える場合は、先に登録を解除してください。",
|
"現在 OpenCode Black を購読中、またはウェイティングリストに登録されています。Go に切り替える場合は、先に登録を解除してください。",
|
||||||
"workspace.lite.other.message":
|
"workspace.lite.other.message":
|
||||||
|
|||||||
@@ -246,6 +246,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "다음 예외",
|
"zen.privacy.exceptionsLink": "다음 예외",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델",
|
"go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델",
|
||||||
|
"go.banner.text": "MiniMax M3 사용 한도가 한시적으로 3배 확대됩니다",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash에 대해 넉넉한 5시간 요청 한도를 제공합니다.",
|
"Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash에 대해 넉넉한 5시간 요청 한도를 제공합니다.",
|
||||||
"go.hero.title": "모두를 위한 저비용 코딩 모델",
|
"go.hero.title": "모두를 위한 저비용 코딩 모델",
|
||||||
@@ -367,8 +368,6 @@ export const dict = {
|
|||||||
"zen.api.error.userMonthlyLimitReached":
|
"zen.api.error.userMonthlyLimitReached":
|
||||||
"월간 지출 한도인 ${{amount}}에 도달했습니다. 한도 관리를 여기서 하세요: {{membersUrl}}",
|
"월간 지출 한도인 ${{amount}}에 도달했습니다. 한도 관리를 여기서 하세요: {{membersUrl}}",
|
||||||
"zen.api.error.modelDisabled": "모델이 비활성화되었습니다",
|
"zen.api.error.modelDisabled": "모델이 비활성화되었습니다",
|
||||||
"zen.api.error.regionNotAllowed":
|
|
||||||
"이 모델은 중국에서 호스팅됩니다. 이 모델을 사용하려면 설정에서 활성화하세요: {{consoleGoUrl}}",
|
|
||||||
"zen.api.error.trialEnded":
|
"zen.api.error.trialEnded":
|
||||||
"{{model}}의 무료 프로모션이 종료되었습니다. OpenCode Go를 구독하면 모델을 계속 사용할 수 있습니다 - {{link}}",
|
"{{model}}의 무료 프로모션이 종료되었습니다. OpenCode Go를 구독하면 모델을 계속 사용할 수 있습니다 - {{link}}",
|
||||||
|
|
||||||
@@ -647,9 +646,6 @@ export const dict = {
|
|||||||
"workspace.lite.subscription.useBalance": "사용 한도 도달 후에는 보유 잔액 사용",
|
"workspace.lite.subscription.useBalance": "사용 한도 도달 후에는 보유 잔액 사용",
|
||||||
"workspace.lite.subscription.selectProvider":
|
"workspace.lite.subscription.selectProvider":
|
||||||
'Go 모델을 사용하려면 opencode 설정에서 "OpenCode Go"를 공급자로 선택하세요.',
|
'Go 모델을 사용하려면 opencode 설정에서 "OpenCode Go"를 공급자로 선택하세요.',
|
||||||
"workspace.lite.providers.title": "공급자",
|
|
||||||
"workspace.lite.providers.description": "라우팅에 사용할 공급자를 제어합니다.",
|
|
||||||
"workspace.lite.providers.useChina": "중국에서 호스팅되는 모델 활성화",
|
|
||||||
"workspace.lite.black.message":
|
"workspace.lite.black.message":
|
||||||
"현재 OpenCode Black을 구독 중이거나 대기 명단에 등록되어 있습니다. Go로 전환하려면 먼저 구독을 취소해 주세요.",
|
"현재 OpenCode Black을 구독 중이거나 대기 명단에 등록되어 있습니다. Go로 전환하려면 먼저 구독을 취소해 주세요.",
|
||||||
"workspace.lite.other.message":
|
"workspace.lite.other.message":
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "følgende unntak",
|
"zen.privacy.exceptionsLink": "følgende unntak",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Rimelige kodemodeller for alle",
|
"go.title": "OpenCode Go | Rimelige kodemodeller for alle",
|
||||||
|
"go.banner.text": "MiniMax M3 får 3x bruksgrense i en begrenset periode",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse 5-timers forespørselsgrenser for GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash.",
|
"Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse 5-timers forespørselsgrenser for GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Rimelige kodemodeller for alle",
|
"go.hero.title": "Rimelige kodemodeller for alle",
|
||||||
@@ -373,8 +374,6 @@ export const dict = {
|
|||||||
"zen.api.error.userMonthlyLimitReached":
|
"zen.api.error.userMonthlyLimitReached":
|
||||||
"Du har nådd din månedlige utgiftsgrense på ${{amount}}. Administrer grensene dine her: {{membersUrl}}",
|
"Du har nådd din månedlige utgiftsgrense på ${{amount}}. Administrer grensene dine her: {{membersUrl}}",
|
||||||
"zen.api.error.modelDisabled": "Modellen er deaktivert",
|
"zen.api.error.modelDisabled": "Modellen er deaktivert",
|
||||||
"zen.api.error.regionNotAllowed":
|
|
||||||
"Denne modellen hostes i Kina. Hvis du vil bruke denne modellen, aktiver den i innstillingene dine: {{consoleGoUrl}}",
|
|
||||||
"zen.api.error.trialEnded":
|
"zen.api.error.trialEnded":
|
||||||
"Den gratis kampanjen for {{model}} er avsluttet. Du kan fortsette å bruke modellen ved å abonnere på OpenCode Go - {{link}}",
|
"Den gratis kampanjen for {{model}} er avsluttet. Du kan fortsette å bruke modellen ved å abonnere på OpenCode Go - {{link}}",
|
||||||
|
|
||||||
@@ -654,9 +653,6 @@ export const dict = {
|
|||||||
"workspace.lite.subscription.useBalance": "Bruk din tilgjengelige saldo etter å ha nådd bruksgrensene",
|
"workspace.lite.subscription.useBalance": "Bruk din tilgjengelige saldo etter å ha nådd bruksgrensene",
|
||||||
"workspace.lite.subscription.selectProvider":
|
"workspace.lite.subscription.selectProvider":
|
||||||
'Velg "OpenCode Go" som leverandør i opencode-konfigurasjonen din for å bruke Go-modeller.',
|
'Velg "OpenCode Go" som leverandør i opencode-konfigurasjonen din for å bruke Go-modeller.',
|
||||||
"workspace.lite.providers.title": "Leverandører",
|
|
||||||
"workspace.lite.providers.description": "Kontroller hvilke leverandører som brukes til ruting.",
|
|
||||||
"workspace.lite.providers.useChina": "Aktiver modeller hostet i Kina",
|
|
||||||
"workspace.lite.black.message":
|
"workspace.lite.black.message":
|
||||||
"Du abonnerer for øyeblikket på OpenCode Black eller står på venteliste. Vennligst avslutt abonnementet først hvis du vil bytte til Go.",
|
"Du abonnerer for øyeblikket på OpenCode Black eller står på venteliste. Vennligst avslutt abonnementet først hvis du vil bytte til Go.",
|
||||||
"workspace.lite.other.message":
|
"workspace.lite.other.message":
|
||||||
|
|||||||
@@ -251,6 +251,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "następującymi wyjątkami",
|
"zen.privacy.exceptionsLink": "następującymi wyjątkami",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego",
|
"go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego",
|
||||||
|
"go.banner.text": "MiniMax M3 oferuje 3x wyższe limity użycia przez ograniczony czas",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi 5-godzinnymi limitami zapytań dla GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro i DeepSeek V4 Flash.",
|
"Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi 5-godzinnymi limitami zapytań dla GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro i DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Niskokosztowe modele do kodowania dla każdego",
|
"go.hero.title": "Niskokosztowe modele do kodowania dla każdego",
|
||||||
@@ -374,8 +375,6 @@ export const dict = {
|
|||||||
"zen.api.error.userMonthlyLimitReached":
|
"zen.api.error.userMonthlyLimitReached":
|
||||||
"Osiągnąłeś swój miesięczny limit wydatków w wysokości ${{amount}}. Zarządzaj swoimi limitami tutaj: {{membersUrl}}",
|
"Osiągnąłeś swój miesięczny limit wydatków w wysokości ${{amount}}. Zarządzaj swoimi limitami tutaj: {{membersUrl}}",
|
||||||
"zen.api.error.modelDisabled": "Model jest wyłączony",
|
"zen.api.error.modelDisabled": "Model jest wyłączony",
|
||||||
"zen.api.error.regionNotAllowed":
|
|
||||||
"Ten model jest hostowany w Chinach. Jeśli chcesz korzystać z tego modelu, włącz go w swoich ustawieniach: {{consoleGoUrl}}",
|
|
||||||
"zen.api.error.trialEnded":
|
"zen.api.error.trialEnded":
|
||||||
"Bezpłatna promocja {{model}} dobiegła końca. Możesz dalej korzystać z modelu, subskrybując OpenCode Go - {{link}}",
|
"Bezpłatna promocja {{model}} dobiegła końca. Możesz dalej korzystać z modelu, subskrybując OpenCode Go - {{link}}",
|
||||||
|
|
||||||
@@ -655,9 +654,6 @@ export const dict = {
|
|||||||
"workspace.lite.subscription.useBalance": "Użyj dostępnego salda po osiągnięciu limitów użycia",
|
"workspace.lite.subscription.useBalance": "Użyj dostępnego salda po osiągnięciu limitów użycia",
|
||||||
"workspace.lite.subscription.selectProvider":
|
"workspace.lite.subscription.selectProvider":
|
||||||
'Wybierz "OpenCode Go" jako dostawcę w konfiguracji opencode, aby używać modeli Go.',
|
'Wybierz "OpenCode Go" jako dostawcę w konfiguracji opencode, aby używać modeli Go.',
|
||||||
"workspace.lite.providers.title": "Dostawcy",
|
|
||||||
"workspace.lite.providers.description": "Kontroluj, którzy dostawcy są używani do routingu.",
|
|
||||||
"workspace.lite.providers.useChina": "Włącz modele hostowane w Chinach",
|
|
||||||
"workspace.lite.black.message":
|
"workspace.lite.black.message":
|
||||||
"Obecnie subskrybujesz OpenCode Black lub jesteś na liście oczekujących. Jeśli chcesz przejść na Go, najpierw anuluj subskrypcję.",
|
"Obecnie subskrybujesz OpenCode Black lub jesteś na liście oczekujących. Jeśli chcesz przejść na Go, najpierw anuluj subskrypcję.",
|
||||||
"workspace.lite.other.message":
|
"workspace.lite.other.message":
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "следующими исключениями",
|
"zen.privacy.exceptionsLink": "следующими исключениями",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Недорогие модели для кодинга для всех",
|
"go.title": "OpenCode Go | Недорогие модели для кодинга для всех",
|
||||||
|
"go.banner.text": "MiniMax M3 получает 3x лимиты использования на ограниченное время",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами запросов за 5 часов для GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro и DeepSeek V4 Flash.",
|
"Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами запросов за 5 часов для GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro и DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Недорогие модели для кодинга для всех",
|
"go.hero.title": "Недорогие модели для кодинга для всех",
|
||||||
@@ -378,8 +379,6 @@ export const dict = {
|
|||||||
"zen.api.error.userMonthlyLimitReached":
|
"zen.api.error.userMonthlyLimitReached":
|
||||||
"Вы достигли ежемесячного лимита расходов в ${{amount}}. Управляйте лимитами здесь: {{membersUrl}}",
|
"Вы достигли ежемесячного лимита расходов в ${{amount}}. Управляйте лимитами здесь: {{membersUrl}}",
|
||||||
"zen.api.error.modelDisabled": "Модель отключена",
|
"zen.api.error.modelDisabled": "Модель отключена",
|
||||||
"zen.api.error.regionNotAllowed":
|
|
||||||
"Эта модель размещена в Китае. Если вы хотите использовать эту модель, включите её в настройках: {{consoleGoUrl}}",
|
|
||||||
"zen.api.error.trialEnded":
|
"zen.api.error.trialEnded":
|
||||||
"Бесплатная акция для {{model}} завершена. Вы можете продолжить использование модели, подписавшись на OpenCode Go - {{link}}",
|
"Бесплатная акция для {{model}} завершена. Вы можете продолжить использование модели, подписавшись на OpenCode Go - {{link}}",
|
||||||
|
|
||||||
@@ -661,9 +660,6 @@ export const dict = {
|
|||||||
"workspace.lite.subscription.useBalance": "Использовать доступный баланс после достижения лимитов",
|
"workspace.lite.subscription.useBalance": "Использовать доступный баланс после достижения лимитов",
|
||||||
"workspace.lite.subscription.selectProvider":
|
"workspace.lite.subscription.selectProvider":
|
||||||
'Выберите "OpenCode Go" в качестве провайдера в настройках opencode для использования моделей Go.',
|
'Выберите "OpenCode Go" в качестве провайдера в настройках opencode для использования моделей Go.',
|
||||||
"workspace.lite.providers.title": "Провайдеры",
|
|
||||||
"workspace.lite.providers.description": "Управляйте провайдерами, используемыми для маршрутизации.",
|
|
||||||
"workspace.lite.providers.useChina": "Включить модели, размещенные в Китае",
|
|
||||||
"workspace.lite.black.message":
|
"workspace.lite.black.message":
|
||||||
"Вы подписаны на OpenCode Black или находитесь в списке ожидания. Пожалуйста, сначала отмените подписку, если хотите перейти на Go.",
|
"Вы подписаны на OpenCode Black или находитесь в списке ожидания. Пожалуйста, сначала отмените подписку, если хотите перейти на Go.",
|
||||||
"workspace.lite.other.message":
|
"workspace.lite.other.message":
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "ข้อยกเว้นดังนี้",
|
"zen.privacy.exceptionsLink": "ข้อยกเว้นดังนี้",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
|
"go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
|
||||||
|
"go.banner.text": "MiniMax M3 เพิ่มโควตาการใช้งานเป็น 3 เท่าในช่วงเวลาจำกัด",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดคำขอ 5 ชั่วโมงที่เอื้อเฟื้อสำหรับ GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro และ DeepSeek V4 Flash",
|
"Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดคำขอ 5 ชั่วโมงที่เอื้อเฟื้อสำหรับ GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro และ DeepSeek V4 Flash",
|
||||||
"go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
|
"go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
|
||||||
@@ -369,8 +370,6 @@ export const dict = {
|
|||||||
"zen.api.error.userMonthlyLimitReached":
|
"zen.api.error.userMonthlyLimitReached":
|
||||||
"คุณถึงขีดจำกัดการใช้จ่ายรายเดือนที่ ${{amount}} แล้ว จัดการขีดจำกัดของคุณที่นี่: {{membersUrl}}",
|
"คุณถึงขีดจำกัดการใช้จ่ายรายเดือนที่ ${{amount}} แล้ว จัดการขีดจำกัดของคุณที่นี่: {{membersUrl}}",
|
||||||
"zen.api.error.modelDisabled": "โมเดลถูกปิดใช้งาน",
|
"zen.api.error.modelDisabled": "โมเดลถูกปิดใช้งาน",
|
||||||
"zen.api.error.regionNotAllowed":
|
|
||||||
"โมเดลนี้โฮสต์อยู่ในประเทศจีน หากคุณต้องการใช้โมเดลนี้ ให้เปิดใช้งานในการตั้งค่าของคุณ: {{consoleGoUrl}}",
|
|
||||||
"zen.api.error.trialEnded":
|
"zen.api.error.trialEnded":
|
||||||
"โปรโมชันฟรีสำหรับ {{model}} สิ้นสุดแล้ว คุณสามารถใช้โมเดลต่อได้โดยสมัครสมาชิก OpenCode Go - {{link}}",
|
"โปรโมชันฟรีสำหรับ {{model}} สิ้นสุดแล้ว คุณสามารถใช้โมเดลต่อได้โดยสมัครสมาชิก OpenCode Go - {{link}}",
|
||||||
|
|
||||||
@@ -650,9 +649,6 @@ export const dict = {
|
|||||||
"workspace.lite.subscription.useBalance": "ใช้ยอดคงเหลือของคุณหลังจากถึงขีดจำกัดการใช้งาน",
|
"workspace.lite.subscription.useBalance": "ใช้ยอดคงเหลือของคุณหลังจากถึงขีดจำกัดการใช้งาน",
|
||||||
"workspace.lite.subscription.selectProvider":
|
"workspace.lite.subscription.selectProvider":
|
||||||
'เลือก "OpenCode Go" เป็นผู้ให้บริการในการตั้งค่า opencode ของคุณเพื่อใช้โมเดล Go',
|
'เลือก "OpenCode Go" เป็นผู้ให้บริการในการตั้งค่า opencode ของคุณเพื่อใช้โมเดล Go',
|
||||||
"workspace.lite.providers.title": "ผู้ให้บริการ",
|
|
||||||
"workspace.lite.providers.description": "ควบคุมผู้ให้บริการที่ใช้สำหรับการกำหนดเส้นทาง",
|
|
||||||
"workspace.lite.providers.useChina": "เปิดใช้โมเดลที่โฮสต์ในจีน",
|
|
||||||
"workspace.lite.black.message":
|
"workspace.lite.black.message":
|
||||||
"ขณะนี้คุณสมัครสมาชิก OpenCode Black หรืออยู่ในรายการรอ โปรดยกเลิกการสมัครก่อนหากต้องการเปลี่ยนไปใช้ Go",
|
"ขณะนี้คุณสมัครสมาชิก OpenCode Black หรืออยู่ในรายการรอ โปรดยกเลิกการสมัครก่อนหากต้องการเปลี่ยนไปใช้ Go",
|
||||||
"workspace.lite.other.message":
|
"workspace.lite.other.message":
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "aşağıdaki istisnalar",
|
"zen.privacy.exceptionsLink": "aşağıdaki istisnalar",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri",
|
"go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri",
|
||||||
|
"go.banner.text": "MiniMax M3 sınırlı bir süre için 3x kullanım limiti sunuyor",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro ve DeepSeek V4 Flash için cömert 5 saatlik istek limitleri sunar.",
|
"Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro ve DeepSeek V4 Flash için cömert 5 saatlik istek limitleri sunar.",
|
||||||
"go.hero.title": "Herkes için düşük maliyetli kodlama modelleri",
|
"go.hero.title": "Herkes için düşük maliyetli kodlama modelleri",
|
||||||
@@ -376,8 +377,6 @@ export const dict = {
|
|||||||
"zen.api.error.userMonthlyLimitReached":
|
"zen.api.error.userMonthlyLimitReached":
|
||||||
"Aylık ${{amount}} harcama limitinize ulaştınız. Limitlerinizi buradan yönetin: {{membersUrl}}",
|
"Aylık ${{amount}} harcama limitinize ulaştınız. Limitlerinizi buradan yönetin: {{membersUrl}}",
|
||||||
"zen.api.error.modelDisabled": "Model devre dışı",
|
"zen.api.error.modelDisabled": "Model devre dışı",
|
||||||
"zen.api.error.regionNotAllowed":
|
|
||||||
"Bu model Çin'de barındırılıyor. Bu modeli kullanmak istiyorsanız ayarlarınızdan etkinleştirin: {{consoleGoUrl}}",
|
|
||||||
"zen.api.error.trialEnded":
|
"zen.api.error.trialEnded":
|
||||||
"{{model}} için ücretsiz promosyon sona erdi. OpenCode Go'ya abone olarak modeli kullanmaya devam edebilirsiniz - {{link}}",
|
"{{model}} için ücretsiz promosyon sona erdi. OpenCode Go'ya abone olarak modeli kullanmaya devam edebilirsiniz - {{link}}",
|
||||||
|
|
||||||
@@ -657,9 +656,6 @@ export const dict = {
|
|||||||
"workspace.lite.subscription.useBalance": "Kullanım limitlerine ulaştıktan sonra mevcut bakiyenizi kullanın",
|
"workspace.lite.subscription.useBalance": "Kullanım limitlerine ulaştıktan sonra mevcut bakiyenizi kullanın",
|
||||||
"workspace.lite.subscription.selectProvider":
|
"workspace.lite.subscription.selectProvider":
|
||||||
'Go modellerini kullanmak için opencode yapılandırmanızda "OpenCode Go"\'yu sağlayıcı olarak seçin.',
|
'Go modellerini kullanmak için opencode yapılandırmanızda "OpenCode Go"\'yu sağlayıcı olarak seçin.',
|
||||||
"workspace.lite.providers.title": "Sağlayıcılar",
|
|
||||||
"workspace.lite.providers.description": "Yönlendirme için hangi sağlayıcıların kullanılacağını kontrol edin.",
|
|
||||||
"workspace.lite.providers.useChina": "Çin'de barındırılan modelleri etkinleştir",
|
|
||||||
"workspace.lite.black.message":
|
"workspace.lite.black.message":
|
||||||
"Şu anda OpenCode Black abonesisiniz veya bekleme listesindesiniz. Go'ya geçmek istiyorsanız lütfen önce aboneliğinizi iptal edin.",
|
"Şu anda OpenCode Black abonesisiniz veya bekleme listesindesiniz. Go'ya geçmek istiyorsanız lütfen önce aboneliğinizi iptal edin.",
|
||||||
"workspace.lite.other.message":
|
"workspace.lite.other.message":
|
||||||
|
|||||||
@@ -251,6 +251,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "такими винятками",
|
"zen.privacy.exceptionsLink": "такими винятками",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Недорогі моделі кодування для всіх",
|
"go.title": "OpenCode Go | Недорогі моделі кодування для всіх",
|
||||||
|
"go.banner.text": "MiniMax M3 отримує 3x ліміти використання протягом обмеженого часу",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go починається від $5 за перший місяць, потім $10/місяць, з generous 5-годинними лімітами запитів для GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro та DeepSeek V4 Flash.",
|
"Go починається від $5 за перший місяць, потім $10/місяць, з generous 5-годинними лімітами запитів для GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro та DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Недорогі моделі кодування для всіх",
|
"go.hero.title": "Недорогі моделі кодування для всіх",
|
||||||
@@ -373,8 +374,6 @@ export const dict = {
|
|||||||
"zen.api.error.userMonthlyLimitReached":
|
"zen.api.error.userMonthlyLimitReached":
|
||||||
"Ви досягли місячного ліміту витрат ${{amount}}. Керуйте лімітами: {{membersUrl}}",
|
"Ви досягли місячного ліміту витрат ${{amount}}. Керуйте лімітами: {{membersUrl}}",
|
||||||
"zen.api.error.modelDisabled": "Модель вимкнено",
|
"zen.api.error.modelDisabled": "Модель вимкнено",
|
||||||
"zen.api.error.regionNotAllowed":
|
|
||||||
"Ця модель розміщена в Китаї. Якщо ви хочете використовувати цю модель, увімкніть її в налаштуваннях: {{consoleGoUrl}}",
|
|
||||||
"zen.api.error.trialEnded":
|
"zen.api.error.trialEnded":
|
||||||
"Безкоштовна акція для {{model}} закінчилася. Ви можете продовжити використання, підписавшись на OpenCode Go — {{link}}",
|
"Безкоштовна акція для {{model}} закінчилася. Ви можете продовжити використання, підписавшись на OpenCode Go — {{link}}",
|
||||||
|
|
||||||
@@ -653,9 +652,6 @@ export const dict = {
|
|||||||
"workspace.lite.subscription.resetsIn": "Скидається через",
|
"workspace.lite.subscription.resetsIn": "Скидається через",
|
||||||
"workspace.lite.subscription.useBalance": "Використовуйте доступний баланс після досягнення лімітів",
|
"workspace.lite.subscription.useBalance": "Використовуйте доступний баланс після досягнення лімітів",
|
||||||
"workspace.lite.subscription.selectProvider": 'Виберіть "OpenCode Go" як провайдера в конфігурації opencode.',
|
"workspace.lite.subscription.selectProvider": 'Виберіть "OpenCode Go" як провайдера в конфігурації opencode.',
|
||||||
"workspace.lite.providers.title": "Провайдери",
|
|
||||||
"workspace.lite.providers.description": "Керуйте провайдерами, які використовуються для маршрутизації.",
|
|
||||||
"workspace.lite.providers.useChina": "Увімкнути моделі, розміщені в Китаї",
|
|
||||||
"workspace.lite.black.message":
|
"workspace.lite.black.message":
|
||||||
"Ви вже підписані на OpenCode Black або в списку очікування. Спочатку скасуйте підписку, якщо хочете перейти на Go.",
|
"Ви вже підписані на OpenCode Black або в списку очікування. Спочатку скасуйте підписку, якщо хочете перейти на Go.",
|
||||||
"workspace.lite.other.message": "Інший учасник цього робочого простору вже підписаний на OpenCode Go.",
|
"workspace.lite.other.message": "Інший учасник цього робочого простору вже підписаний на OpenCode Go.",
|
||||||
|
|||||||
@@ -240,6 +240,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "以下例外情况除外",
|
"zen.privacy.exceptionsLink": "以下例外情况除外",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | 人人可用的低成本编程模型",
|
"go.title": "OpenCode Go | 人人可用的低成本编程模型",
|
||||||
|
"go.banner.text": "MiniMax M3 限时享受 3 倍使用额度",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go 首月 $5,之后 $10/月,提供对 GLM-5.2、GLM-5.1、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小时充裕请求额度。",
|
"Go 首月 $5,之后 $10/月,提供对 GLM-5.2、GLM-5.1、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小时充裕请求额度。",
|
||||||
"go.hero.title": "人人可用的低成本编程模型",
|
"go.hero.title": "人人可用的低成本编程模型",
|
||||||
@@ -355,7 +356,6 @@ export const dict = {
|
|||||||
"您的工作区已达到每月支出限额 ${{amount}}。请在此处管理您的限额:{{billingUrl}}",
|
"您的工作区已达到每月支出限额 ${{amount}}。请在此处管理您的限额:{{billingUrl}}",
|
||||||
"zen.api.error.userMonthlyLimitReached": "您已达到每月支出限额 ${{amount}}。请在此处管理您的限额:{{membersUrl}}",
|
"zen.api.error.userMonthlyLimitReached": "您已达到每月支出限额 ${{amount}}。请在此处管理您的限额:{{membersUrl}}",
|
||||||
"zen.api.error.modelDisabled": "模型已禁用",
|
"zen.api.error.modelDisabled": "模型已禁用",
|
||||||
"zen.api.error.regionNotAllowed": "该模型部署在中国。如果你想使用该模型,请在设置中启用它:{{consoleGoUrl}}",
|
|
||||||
"zen.api.error.trialEnded": "{{model}} 的限免活动已结束。您可以订阅 OpenCode Go 继续使用该模型 - {{link}}",
|
"zen.api.error.trialEnded": "{{model}} 的限免活动已结束。您可以订阅 OpenCode Go 继续使用该模型 - {{link}}",
|
||||||
|
|
||||||
"black.meta.title": "OpenCode Black | 访问全球顶尖编程模型",
|
"black.meta.title": "OpenCode Black | 访问全球顶尖编程模型",
|
||||||
@@ -631,9 +631,6 @@ export const dict = {
|
|||||||
"workspace.lite.subscription.useBalance": "达到使用限额后使用您的可用余额",
|
"workspace.lite.subscription.useBalance": "达到使用限额后使用您的可用余额",
|
||||||
"workspace.lite.subscription.selectProvider":
|
"workspace.lite.subscription.selectProvider":
|
||||||
"在你的 opencode 配置中选择「OpenCode Go」作为提供商,即可使用 Go 模型。",
|
"在你的 opencode 配置中选择「OpenCode Go」作为提供商,即可使用 Go 模型。",
|
||||||
"workspace.lite.providers.title": "提供商",
|
|
||||||
"workspace.lite.providers.description": "控制用于路由的提供商。",
|
|
||||||
"workspace.lite.providers.useChina": "启用部署在中国的模型",
|
|
||||||
"workspace.lite.black.message": "您当前已订阅 OpenCode Black 或在候补名单中。如需切换到 Go,请先取消订阅。",
|
"workspace.lite.black.message": "您当前已订阅 OpenCode Black 或在候补名单中。如需切换到 Go,请先取消订阅。",
|
||||||
"workspace.lite.other.message": "此工作区中的另一位成员已经订阅了 OpenCode Go。每个工作区只有一名成员可以订阅。",
|
"workspace.lite.other.message": "此工作区中的另一位成员已经订阅了 OpenCode Go。每个工作区只有一名成员可以订阅。",
|
||||||
"workspace.lite.promo.description":
|
"workspace.lite.promo.description":
|
||||||
|
|||||||
@@ -240,6 +240,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "以下例外情況",
|
"zen.privacy.exceptionsLink": "以下例外情況",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | 低成本全民編碼模型",
|
"go.title": "OpenCode Go | 低成本全民編碼模型",
|
||||||
|
"go.banner.text": "MiniMax M3 限時享有 3 倍使用額度",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go 首月 $5,之後 $10/月,提供對 GLM-5.2、GLM-5.1、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小時充裕請求額度。",
|
"Go 首月 $5,之後 $10/月,提供對 GLM-5.2、GLM-5.1、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小時充裕請求額度。",
|
||||||
"go.hero.title": "低成本全民編碼模型",
|
"go.hero.title": "低成本全民編碼模型",
|
||||||
@@ -355,7 +356,6 @@ export const dict = {
|
|||||||
"你的工作區已達到每月支出限額 ${{amount}}。請在此處管理你的限額:{{billingUrl}}",
|
"你的工作區已達到每月支出限額 ${{amount}}。請在此處管理你的限額:{{billingUrl}}",
|
||||||
"zen.api.error.userMonthlyLimitReached": "你已達到每月支出限額 ${{amount}}。請在此處管理你的限額:{{membersUrl}}",
|
"zen.api.error.userMonthlyLimitReached": "你已達到每月支出限額 ${{amount}}。請在此處管理你的限額:{{membersUrl}}",
|
||||||
"zen.api.error.modelDisabled": "模型已停用",
|
"zen.api.error.modelDisabled": "模型已停用",
|
||||||
"zen.api.error.regionNotAllowed": "此模型部署於中國。如果你想使用此模型,請在設定中啟用它:{{consoleGoUrl}}",
|
|
||||||
"zen.api.error.trialEnded": "{{model}} 的限免活动已結束。您可以訂閱 OpenCode Go 繼續使用該模型 - {{link}}",
|
"zen.api.error.trialEnded": "{{model}} 的限免活动已結束。您可以訂閱 OpenCode Go 繼續使用該模型 - {{link}}",
|
||||||
|
|
||||||
"black.meta.title": "OpenCode Black | 存取全球最佳編碼模型",
|
"black.meta.title": "OpenCode Black | 存取全球最佳編碼模型",
|
||||||
@@ -631,9 +631,6 @@ export const dict = {
|
|||||||
"workspace.lite.subscription.useBalance": "達到使用限制後使用您的可用餘額",
|
"workspace.lite.subscription.useBalance": "達到使用限制後使用您的可用餘額",
|
||||||
"workspace.lite.subscription.selectProvider":
|
"workspace.lite.subscription.selectProvider":
|
||||||
"在您的 opencode 設定中選擇「OpenCode Go」作為提供商,即可使用 Go 模型。",
|
"在您的 opencode 設定中選擇「OpenCode Go」作為提供商,即可使用 Go 模型。",
|
||||||
"workspace.lite.providers.title": "提供商",
|
|
||||||
"workspace.lite.providers.description": "控制用於路由的提供商。",
|
|
||||||
"workspace.lite.providers.useChina": "啟用部署在中國的模型",
|
|
||||||
"workspace.lite.black.message": "您目前已訂閱 OpenCode Black 或在候補名單中。若要切換至 Go,請先取消訂閱。",
|
"workspace.lite.black.message": "您目前已訂閱 OpenCode Black 或在候補名單中。若要切換至 Go,請先取消訂閱。",
|
||||||
"workspace.lite.other.message": "此工作區中的另一位成員已訂閱 OpenCode Go。每個工作區只能有一位成員訂閱。",
|
"workspace.lite.other.message": "此工作區中的另一位成員已訂閱 OpenCode Go。每個工作區只能有一位成員訂閱。",
|
||||||
"workspace.lite.promo.description":
|
"workspace.lite.promo.description":
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
export function countryFromRequest(request: Request | undefined) {
|
|
||||||
if (!request) return undefined
|
|
||||||
const cloudflareRequest = request as Request & { cf?: { country?: string } }
|
|
||||||
return cloudflareRequest.cf?.country ?? request.headers.get("cf-ipcountry") ?? undefined
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,10 @@
|
|||||||
import type { APIEvent } from "@solidjs/start/server"
|
import type { APIEvent } from "@solidjs/start/server"
|
||||||
import { Resource } from "@opencode-ai/console-resource"
|
import { Resource } from "@opencode-ai/console-resource"
|
||||||
import { LOCALE_HEADER, cookie, localeFromRequest, route, tag } from "~/lib/language"
|
|
||||||
|
|
||||||
const dataPath = "/data"
|
const dataPath = "/data"
|
||||||
|
|
||||||
export async function statsProxy(evt: APIEvent) {
|
export async function statsProxy(evt: APIEvent) {
|
||||||
const req = evt.request.clone()
|
const req = evt.request.clone()
|
||||||
const locale = localeFromRequest(req)
|
|
||||||
const redirect = redirectToLocalizedData(req, new URL(req.url), locale)
|
|
||||||
if (redirect) return redirect
|
|
||||||
|
|
||||||
const targetUrl = new URL(req.url)
|
const targetUrl = new URL(req.url)
|
||||||
targetUrl.protocol = "https:"
|
targetUrl.protocol = "https:"
|
||||||
targetUrl.hostname = Resource.App.stage === "production" ? "stats.opencode.ai" : "stats.dev.opencode.ai"
|
targetUrl.hostname = Resource.App.stage === "production" ? "stats.opencode.ai" : "stats.dev.opencode.ai"
|
||||||
@@ -23,13 +18,9 @@ export async function statsProxy(evt: APIEvent) {
|
|||||||
targetUrl.pathname = targetUrl.pathname.slice(dataPath.length)
|
targetUrl.pathname = targetUrl.pathname.slice(dataPath.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestHeaders = new Headers(req.headers)
|
|
||||||
requestHeaders.set(LOCALE_HEADER, locale)
|
|
||||||
requestHeaders.set("accept-language", tag(locale))
|
|
||||||
|
|
||||||
const response = await fetch(targetUrl, {
|
const response = await fetch(targetUrl, {
|
||||||
method: req.method,
|
method: req.method,
|
||||||
headers: requestHeaders,
|
headers: req.headers,
|
||||||
body: req.body,
|
body: req.body,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -39,7 +30,6 @@ export async function statsProxy(evt: APIEvent) {
|
|||||||
headers.delete("content-encoding")
|
headers.delete("content-encoding")
|
||||||
headers.delete("content-length")
|
headers.delete("content-length")
|
||||||
headers.delete("etag")
|
headers.delete("etag")
|
||||||
appendVary(headers, "Accept-Language", "Cookie")
|
|
||||||
|
|
||||||
return new Response(rewriteStatsHtml(await response.text()), {
|
return new Response(rewriteStatsHtml(await response.text()), {
|
||||||
status: response.status,
|
status: response.status,
|
||||||
@@ -62,60 +52,3 @@ export function statsRedirect(evt: APIEvent) {
|
|||||||
function rewriteStatsHtml(html: string) {
|
function rewriteStatsHtml(html: string) {
|
||||||
return html.replaceAll('"/_build/', `"${dataPath}/_build/`).replaceAll("'/_build/", `'${dataPath}/_build/`)
|
return html.replaceAll('"/_build/', `"${dataPath}/_build/`).replaceAll("'/_build/", `'${dataPath}/_build/`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function redirectToLocalizedData(request: Request, url: URL, locale: ReturnType<typeof localeFromRequest>) {
|
|
||||||
if (locale === "en") return null
|
|
||||||
if (request.headers.get(LOCALE_HEADER)) return null
|
|
||||||
if (request.method !== "GET" && request.method !== "HEAD") return null
|
|
||||||
if (!acceptsHtml(request)) return null
|
|
||||||
if (!url.pathname.startsWith(`${dataPath}/`) && url.pathname !== dataPath) return null
|
|
||||||
if (isDataBypassPath(url.pathname)) return null
|
|
||||||
|
|
||||||
const next = new URL(url)
|
|
||||||
next.pathname = route(locale, url.pathname)
|
|
||||||
|
|
||||||
const headers = new Headers({
|
|
||||||
Location: next.toString(),
|
|
||||||
})
|
|
||||||
headers.append("set-cookie", cookie(locale))
|
|
||||||
appendVary(headers, "Accept-Language", "Cookie")
|
|
||||||
|
|
||||||
return new Response(null, {
|
|
||||||
status: 308,
|
|
||||||
headers,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function acceptsHtml(request: Request) {
|
|
||||||
const accept = request.headers.get("accept")
|
|
||||||
return !accept || accept.includes("text/html") || accept.includes("*/*")
|
|
||||||
}
|
|
||||||
|
|
||||||
function isDataBypassPath(pathname: string) {
|
|
||||||
return (
|
|
||||||
pathname.startsWith(`${dataPath}/_build/`) ||
|
|
||||||
pathname.startsWith(`${dataPath}/api/`) ||
|
|
||||||
pathname.startsWith(`${dataPath}/_server`) ||
|
|
||||||
pathname === `${dataPath}/banner.jpg` ||
|
|
||||||
pathname === `${dataPath}/banner.png`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function appendVary(headers: Headers, ...values: string[]) {
|
|
||||||
const existing = headers
|
|
||||||
.get("vary")
|
|
||||||
?.split(",")
|
|
||||||
.map((value) => value.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
|
|
||||||
headers.set(
|
|
||||||
"vary",
|
|
||||||
values
|
|
||||||
.reduce(
|
|
||||||
(result, value) =>
|
|
||||||
result.some((item) => item.toLowerCase() === value.toLowerCase()) ? result : [...result, value],
|
|
||||||
existing ?? [],
|
|
||||||
)
|
|
||||||
.join(", "),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -327,6 +327,37 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-component="desktop-app-banner"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
|
||||||
|
[data-slot="badge"] {
|
||||||
|
background: var(--color-background-strong);
|
||||||
|
color: var(--color-text-inverted);
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 4px 8px;
|
||||||
|
line-height: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="content"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="text"] {
|
||||||
|
color: var(--color-text-strong);
|
||||||
|
line-height: 1.4;
|
||||||
|
|
||||||
|
@media (max-width: 30.625rem) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[data-slot="hero-copy"] {
|
[data-slot="hero-copy"] {
|
||||||
img {
|
img {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
@@ -632,6 +663,10 @@ body {
|
|||||||
fill: var(--color-text-strong);
|
fill: var(--color-text-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-bar][data-kind="promo"] {
|
||||||
|
fill: color-mix(in srgb, var(--bar-go) 50%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
[data-val] {
|
[data-val] {
|
||||||
fill: var(--color-text-strong);
|
fill: var(--color-text-strong);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
|||||||
@@ -64,10 +64,10 @@ function LimitsGraph(props: { href: string }) {
|
|||||||
{ id: "glm-5.2", name: "GLM-5.2", req: 880, d: "100ms" },
|
{ id: "glm-5.2", name: "GLM-5.2", req: 880, d: "100ms" },
|
||||||
{ id: "qwen3.7-max", name: "Qwen3.7 Max", req: 950, d: "110ms" },
|
{ id: "qwen3.7-max", name: "Qwen3.7 Max", req: 950, d: "110ms" },
|
||||||
{ id: "kimi-k2.7-code", name: "Kimi K2.7 Code", req: 1150, d: "150ms" },
|
{ id: "kimi-k2.7-code", name: "Kimi K2.7 Code", req: 1150, d: "150ms" },
|
||||||
{ id: "minimax-m3", name: "MiniMax M3", req: 3200, d: "210ms" },
|
{ id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", req: 3250, d: "210ms" },
|
||||||
{ id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", req: 3250, d: "240ms" },
|
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", req: 3450, d: "240ms" },
|
||||||
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", req: 3450, d: "270ms" },
|
{ id: "qwen3.7-plus", name: "Qwen3.7 Plus", req: 4300, d: "250ms" },
|
||||||
{ id: "qwen3.7-plus", name: "Qwen3.7 Plus", req: 4300, d: "300ms" },
|
{ id: "minimax-m3", name: "MiniMax M3 (3x usage)", req: 9600, baseReq: 3200, d: "280ms" },
|
||||||
{ id: "mimo-v2.5", name: "MiMo-V2.5", req: 30100, d: "340ms" },
|
{ id: "mimo-v2.5", name: "MiMo-V2.5", req: 30100, d: "340ms" },
|
||||||
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", req: 31650, d: "340ms" },
|
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", req: 31650, d: "340ms" },
|
||||||
]
|
]
|
||||||
@@ -157,12 +157,24 @@ function LimitsGraph(props: { href: string }) {
|
|||||||
<rect
|
<rect
|
||||||
x={left}
|
x={left}
|
||||||
y={gy(i()) - bh / 2}
|
y={gy(i()) - bh / 2}
|
||||||
width={Math.max(0, x(ratio(m.req)) - left)}
|
width={Math.max(0, x(ratio(m.baseReq ?? m.req)) - left)}
|
||||||
height={bh}
|
height={bh}
|
||||||
data-bar
|
data-bar
|
||||||
data-kind="go"
|
data-kind="go"
|
||||||
data-model={m.id}
|
data-model={m.id}
|
||||||
|
data-segment={m.baseReq ? "base" : undefined}
|
||||||
/>
|
/>
|
||||||
|
{m.baseReq && (
|
||||||
|
<rect
|
||||||
|
x={x(ratio(m.baseReq)) + 2}
|
||||||
|
y={gy(i()) - bh / 2}
|
||||||
|
width={Math.max(0, x(ratio(m.req)) - x(ratio(m.baseReq)) - 2)}
|
||||||
|
height={bh}
|
||||||
|
data-bar
|
||||||
|
data-kind="promo"
|
||||||
|
data-model={m.id}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</g>
|
</g>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
@@ -252,6 +264,12 @@ export default function Home() {
|
|||||||
|
|
||||||
<div data-component="content">
|
<div data-component="content">
|
||||||
<section data-component="hero">
|
<section data-component="hero">
|
||||||
|
<div data-component="desktop-app-banner">
|
||||||
|
<span data-slot="badge">{i18n.t("home.banner.badge")}</span>
|
||||||
|
<div data-slot="content">
|
||||||
|
<span data-slot="text">{i18n.t("go.banner.text")}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div data-slot="hero-copy">
|
<div data-slot="hero-copy">
|
||||||
<img data-slot="zen logo light" src={goLogoLight} alt="" />
|
<img data-slot="zen logo light" src={goLogoLight} alt="" />
|
||||||
<img data-slot="zen logo dark" src={goLogoDark} alt="" />
|
<img data-slot="zen logo dark" src={goLogoDark} alt="" />
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { z } from "zod"
|
|||||||
import { Resource } from "@opencode-ai/console-resource"
|
import { Resource } from "@opencode-ai/console-resource"
|
||||||
import { safeEqual } from "@opencode-ai/console-core/util/crypto.js"
|
import { safeEqual } from "@opencode-ai/console-core/util/crypto.js"
|
||||||
|
|
||||||
const DISCORD_ALERT_ROLE_ID = "1520924666359713863"
|
const DISCORD_ALERT_ROLE_ID = "1511795723262365887"
|
||||||
|
|
||||||
const basePayload = z.object({
|
const basePayload = z.object({
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
|
|||||||
@@ -75,40 +75,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="providers-section"] {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-3);
|
|
||||||
margin-top: var(--space-6);
|
|
||||||
padding-top: var(--space-6);
|
|
||||||
border-top: 1px solid var(--color-border-muted);
|
|
||||||
|
|
||||||
[data-slot="providers-header"] {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-1);
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
color: var(--color-text);
|
|
||||||
font-size: var(--font-size-lg);
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.2;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
line-height: 1.5;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="setting-row"] {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="toggle-label"] {
|
[data-slot="toggle-label"] {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import { Modal } from "~/component/modal"
|
|||||||
import { Billing } from "@opencode-ai/console-core/billing.js"
|
import { Billing } from "@opencode-ai/console-core/billing.js"
|
||||||
import { Database, eq, and, isNull } from "@opencode-ai/console-core/drizzle/index.js"
|
import { Database, eq, and, isNull } from "@opencode-ai/console-core/drizzle/index.js"
|
||||||
import { BillingTable, LiteTable } from "@opencode-ai/console-core/schema/billing.sql.js"
|
import { BillingTable, LiteTable } from "@opencode-ai/console-core/schema/billing.sql.js"
|
||||||
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
|
|
||||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||||
import { Workspace } from "@opencode-ai/console-core/workspace.js"
|
|
||||||
import { Subscription } from "@opencode-ai/console-core/subscription.js"
|
import { Subscription } from "@opencode-ai/console-core/subscription.js"
|
||||||
import { LiteData } from "@opencode-ai/console-core/lite.js"
|
import { LiteData } from "@opencode-ai/console-core/lite.js"
|
||||||
import { withActor } from "~/context/auth.withActor"
|
import { withActor } from "~/context/auth.withActor"
|
||||||
@@ -18,8 +16,6 @@ import { useLanguage } from "~/context/language"
|
|||||||
import { formError } from "~/lib/form-error"
|
import { formError } from "~/lib/form-error"
|
||||||
import { formatResetTime, liteResetTimeKeys } from "~/lib/format-reset-time"
|
import { formatResetTime, liteResetTimeKeys } from "~/lib/format-reset-time"
|
||||||
import { createReferralFromCookie } from "~/lib/referral-invite"
|
import { createReferralFromCookie } from "~/lib/referral-invite"
|
||||||
import { getRequestEvent } from "solid-js/web"
|
|
||||||
import { countryFromRequest } from "~/lib/request-country"
|
|
||||||
|
|
||||||
import { IconAlipay, IconUpi } from "~/component/icon"
|
import { IconAlipay, IconUpi } from "~/component/icon"
|
||||||
|
|
||||||
@@ -38,11 +34,9 @@ export const queryLiteSubscription = query(async (workspaceID: string) => {
|
|||||||
timeMonthlyUpdated: LiteTable.timeMonthlyUpdated,
|
timeMonthlyUpdated: LiteTable.timeMonthlyUpdated,
|
||||||
timeCreated: LiteTable.timeCreated,
|
timeCreated: LiteTable.timeCreated,
|
||||||
lite: BillingTable.lite,
|
lite: BillingTable.lite,
|
||||||
region: WorkspaceTable.region,
|
|
||||||
})
|
})
|
||||||
.from(BillingTable)
|
.from(BillingTable)
|
||||||
.innerJoin(LiteTable, eq(LiteTable.workspaceID, BillingTable.workspaceID))
|
.innerJoin(LiteTable, eq(LiteTable.workspaceID, BillingTable.workspaceID))
|
||||||
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, BillingTable.workspaceID))
|
|
||||||
.where(and(eq(LiteTable.workspaceID, Actor.workspace()), isNull(LiteTable.timeDeleted)))
|
.where(and(eq(LiteTable.workspaceID, Actor.workspace()), isNull(LiteTable.timeDeleted)))
|
||||||
.then((r) => r[0]),
|
.then((r) => r[0]),
|
||||||
)
|
)
|
||||||
@@ -54,8 +48,6 @@ export const queryLiteSubscription = query(async (workspaceID: string) => {
|
|||||||
return {
|
return {
|
||||||
mine,
|
mine,
|
||||||
useBalance: row.lite?.useBalance ?? false,
|
useBalance: row.lite?.useBalance ?? false,
|
||||||
region:
|
|
||||||
row.region ?? (await Workspace.setDefaultRegion({ country: countryFromRequest(getRequestEvent()?.request) })),
|
|
||||||
rollingUsage: Subscription.analyzeRollingUsage({
|
rollingUsage: Subscription.analyzeRollingUsage({
|
||||||
limit: limits.rollingLimit,
|
limit: limits.rollingLimit,
|
||||||
window: limits.rollingWindow,
|
window: limits.rollingWindow,
|
||||||
@@ -136,24 +128,6 @@ const setLiteUseBalance = action(async (form: FormData) => {
|
|||||||
)
|
)
|
||||||
}, "setLiteUseBalance")
|
}, "setLiteUseBalance")
|
||||||
|
|
||||||
const setGoProviderRouting = action(async (form: FormData) => {
|
|
||||||
"use server"
|
|
||||||
const workspaceID = form.get("workspaceID") as string | null
|
|
||||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
|
||||||
const useChinaProviders = (form.get("useChinaProviders") as string | null) === "true"
|
|
||||||
|
|
||||||
return json(
|
|
||||||
await withActor(
|
|
||||||
() =>
|
|
||||||
Workspace.update({ region: useChinaProviders ? ["us", "eu", "sg"] : ["us", "eu", "sg", "cn"] })
|
|
||||||
.then(() => ({ error: undefined }))
|
|
||||||
.catch((e) => ({ error: e.message as string })),
|
|
||||||
workspaceID,
|
|
||||||
),
|
|
||||||
{ revalidate: queryLiteSubscription.key },
|
|
||||||
)
|
|
||||||
}, "go.providerRouting.set")
|
|
||||||
|
|
||||||
function LiteUsageItem(props: { label: string; usage: { usagePercent: number; resetInSec: number } }) {
|
function LiteUsageItem(props: { label: string; usage: { usagePercent: number; resetInSec: number } }) {
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
|
|
||||||
@@ -185,7 +159,6 @@ export function LiteSection(props: { lite: LiteSubscription | undefined }) {
|
|||||||
const checkoutAction = useAction(createLiteCheckoutUrl)
|
const checkoutAction = useAction(createLiteCheckoutUrl)
|
||||||
const checkoutSubmission = useSubmission(createLiteCheckoutUrl)
|
const checkoutSubmission = useSubmission(createLiteCheckoutUrl)
|
||||||
const useBalanceSubmission = useSubmission(setLiteUseBalance)
|
const useBalanceSubmission = useSubmission(setLiteUseBalance)
|
||||||
const providerRoutingSubmission = useSubmission(setGoProviderRouting)
|
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
loading: undefined as undefined | "session" | "checkout" | "alipay" | "upi",
|
loading: undefined as undefined | "session" | "checkout" | "alipay" | "upi",
|
||||||
showModal: false,
|
showModal: false,
|
||||||
@@ -259,28 +232,6 @@ export function LiteSection(props: { lite: LiteSubscription | undefined }) {
|
|||||||
<span></span>
|
<span></span>
|
||||||
</label>
|
</label>
|
||||||
</form>
|
</form>
|
||||||
{/*
|
|
||||||
<div data-slot="providers-section">
|
|
||||||
<div data-slot="providers-header">
|
|
||||||
<h3>{i18n.t("workspace.lite.providers.title")}</h3>
|
|
||||||
<p>{i18n.t("workspace.lite.providers.description")}</p>
|
|
||||||
</div>
|
|
||||||
<form action={setGoProviderRouting} method="post" data-slot="setting-row">
|
|
||||||
<p>{i18n.t("workspace.lite.providers.useChina")}</p>
|
|
||||||
<input type="hidden" name="workspaceID" value={params.id} />
|
|
||||||
<input type="hidden" name="useChinaProviders" value={sub().region.includes("cn") ? "true" : "false"} />
|
|
||||||
<label data-slot="toggle-label">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={sub().region.includes("cn")}
|
|
||||||
disabled={providerRoutingSubmission.pending}
|
|
||||||
onChange={(e) => e.currentTarget.form?.requestSubmit()}
|
|
||||||
/>
|
|
||||||
<span></span>
|
|
||||||
</label>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
*/}
|
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ const updateWorkspace = action(async (form: FormData) => {
|
|||||||
.catch((e) => ({ error: e.message as string })),
|
.catch((e) => ({ error: e.message as string })),
|
||||||
workspaceID,
|
workspaceID,
|
||||||
),
|
),
|
||||||
{ revalidate: getWorkspaceInfo.key },
|
|
||||||
)
|
)
|
||||||
}, "workspace.update")
|
}, "workspace.update")
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user