mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-15 07:48:24 -04:00
Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 91918ae7ab | |||
| f80624cf17 | |||
| fe59174c23 | |||
| 2fe057324f | |||
| 19a5b5a05d | |||
| ff4cab03c1 | |||
| b2d46ecd7e | |||
| 360d85a521 | |||
| c65a7d50c1 | |||
| fa73546a86 | |||
| 935ac2db91 | |||
| 01edae4a7f | |||
| 381d67572e | |||
| e8ac44430b | |||
| 5ae93092aa | |||
| 595c6bd4a7 | |||
| 7073e8797f | |||
| f7034a35a8 | |||
| 53b93b6991 | |||
| 6067019434 | |||
| 42a3cf9645 | |||
| 04c6bed240 | |||
| 94e3a29d2f | |||
| 41283933ff | |||
| 11bf8d8a42 | |||
| c22973ab9f | |||
| 612009e0f1 | |||
| a0a6c9545e | |||
| bceb6d0a9e | |||
| 49e7dc191f | |||
| f4720be08e | |||
| d1d7ebc2c6 | |||
| cd942d0669 | |||
| 4741e3ee6b | |||
| 4673bfbaa0 | |||
| b458dd8c63 | |||
| 7edd6c3a1d | |||
| f43f066741 | |||
| b6553d14e1 | |||
| ab0042a666 | |||
| 5df049d081 | |||
| 573ab9c24b | |||
| 6a16c41e8f | |||
| a491cbee64 | |||
| 658cbe9caf | |||
| 62b2bc39df | |||
| beb2c52c3f | |||
| 655adbf46e | |||
| ac2a78391f | |||
| 9a9bdaba95 | |||
| 8b682c42b6 | |||
| ad4f1c1018 | |||
| a0afb63ed0 | |||
| df9ecb8f6a | |||
| e6f660fecf |
@@ -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:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- ci
|
- ci
|
||||||
- dev
|
- dev
|
||||||
|
- v2
|
||||||
- beta
|
- beta
|
||||||
- fix/npm-native-binary-install
|
- fix/npm-native-binary-install
|
||||||
- snapshot-*
|
- snapshot-*
|
||||||
@@ -31,6 +32,9 @@ permissions:
|
|||||||
contents: write
|
contents: write
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
OPENCODE_CHANNEL: ${{ (github.ref_name == 'v2' && 'next') || '' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
version:
|
version:
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
@@ -122,7 +126,7 @@ jobs:
|
|||||||
- build-cli
|
- build-cli
|
||||||
- version
|
- version
|
||||||
runs-on: blacksmith-4vcpu-windows-2025
|
runs-on: blacksmith-4vcpu-windows-2025
|
||||||
if: github.repository == 'anomalyco/opencode'
|
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2'
|
||||||
env:
|
env:
|
||||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
@@ -221,7 +225,7 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- build-cli
|
- build-cli
|
||||||
- version
|
- version
|
||||||
if: github.repository == 'anomalyco/opencode'
|
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2'
|
||||||
continue-on-error: false
|
continue-on-error: false
|
||||||
env:
|
env:
|
||||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
@@ -447,6 +451,7 @@ jobs:
|
|||||||
path: packages/opencode/dist
|
path: packages/opencode/dist
|
||||||
|
|
||||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||||
|
if: github.ref_name != 'v2'
|
||||||
with:
|
with:
|
||||||
name: opencode-cli-signed-windows
|
name: opencode-cli-signed-windows
|
||||||
path: packages/opencode/dist
|
path: packages/opencode/dist
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
|
- v2
|
||||||
pull_request:
|
pull_request:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
@@ -74,13 +75,9 @@ jobs:
|
|||||||
working-directory: packages/client
|
working-directory: packages/client
|
||||||
run: bun run check:generated
|
run: bun run check:generated
|
||||||
|
|
||||||
- name: Run HttpApi exerciser gates
|
|
||||||
if: runner.os == 'Linux'
|
|
||||||
working-directory: packages/opencode
|
|
||||||
run: bun run test:httpapi
|
|
||||||
|
|
||||||
e2e:
|
e2e:
|
||||||
name: e2e (${{ matrix.settings.name }})
|
name: e2e (${{ matrix.settings.name }})
|
||||||
|
if: github.ref_name != 'v2' && github.head_ref != 'v2'
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -94,10 +94,11 @@
|
|||||||
"name": "@opencode-ai/cli",
|
"name": "@opencode-ai/cli",
|
||||||
"version": "1.17.11",
|
"version": "1.17.11",
|
||||||
"bin": {
|
"bin": {
|
||||||
"lildax": "./bin/lildax.cjs",
|
"opencode2": "./bin/opencode2.cjs",
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
|
"@opencode-ai/client": "workspace:*",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/server": "workspace:*",
|
"@opencode-ai/server": "workspace:*",
|
||||||
@@ -106,12 +107,15 @@
|
|||||||
"@opentui/solid": "catalog:",
|
"@opentui/solid": "catalog:",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
|
"jsonc-parser": "3.3.1",
|
||||||
|
"semver": "catalog:",
|
||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
"@tsconfig/bun": "catalog:",
|
"@tsconfig/bun": "catalog:",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
|
"@types/semver": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -578,6 +582,7 @@
|
|||||||
"@octokit/graphql": "9.0.2",
|
"@octokit/graphql": "9.0.2",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
"@openauthjs/openauth": "catalog:",
|
"@openauthjs/openauth": "catalog:",
|
||||||
|
"@opencode-ai/client": "workspace:*",
|
||||||
"@opencode-ai/llm": "workspace:*",
|
"@opencode-ai/llm": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
"@opencode-ai/protocol": "workspace:*",
|
"@opencode-ai/protocol": "workspace:*",
|
||||||
@@ -929,6 +934,7 @@
|
|||||||
"name": "@opencode-ai/tui",
|
"name": "@opencode-ai/tui",
|
||||||
"version": "1.17.11",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@opencode-ai/client": "workspace:*",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -936,6 +942,7 @@
|
|||||||
"@opentui/core": "catalog:",
|
"@opentui/core": "catalog:",
|
||||||
"@opentui/keymap": "catalog:",
|
"@opentui/keymap": "catalog:",
|
||||||
"@opentui/solid": "catalog:",
|
"@opentui/solid": "catalog:",
|
||||||
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
"clipboardy": "4.0.0",
|
"clipboardy": "4.0.0",
|
||||||
"diff": "catalog:",
|
"diff": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -5979,6 +5986,8 @@
|
|||||||
|
|
||||||
"@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="],
|
"@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="],
|
||||||
|
|
||||||
|
"@opencode-ai/cli/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||||
|
|
||||||
"@opencode-ai/core/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="],
|
"@opencode-ai/core/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="],
|
||||||
|
|
||||||
"@opencode-ai/core/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
"@opencode-ai/core/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||||
|
|||||||
+2
-1
@@ -2,11 +2,12 @@
|
|||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"description": "AI-powered development tool",
|
"description": "AI-powered development tool",
|
||||||
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "bun@1.3.14",
|
"packageManager": "bun@1.3.14",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
"dev": "bun run --cwd packages/cli --conditions=browser src/index.ts",
|
||||||
"dev:desktop": "bun --cwd packages/desktop dev",
|
"dev:desktop": "bun --cwd packages/desktop dev",
|
||||||
"dev:web": "bun --cwd packages/app dev",
|
"dev:web": "bun --cwd packages/app dev",
|
||||||
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
|
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -40,7 +40,6 @@ import { LayoutProvider } from "@/context/layout"
|
|||||||
import { ModelsProvider } from "@/context/models"
|
import { ModelsProvider } from "@/context/models"
|
||||||
import { NotificationProvider, useNotification } from "@/context/notification"
|
import { NotificationProvider, useNotification } 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
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ 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"
|
|
||||||
|
|
||||||
export type PromptInputState = ReturnType<typeof usePrompt>
|
export type PromptInputState = ReturnType<typeof usePrompt>
|
||||||
|
|
||||||
@@ -215,7 +214,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
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -1396,9 +1287,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
|
||||||
@@ -1490,11 +1378,6 @@ 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"),
|
||||||
@@ -1523,8 +1406,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>
|
||||||
@@ -1578,7 +1459,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 +1477,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,7 +1497,7 @@ 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
|
<TooltipV2
|
||||||
placement="top"
|
placement="top"
|
||||||
@@ -1757,7 +1640,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 +1658,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={{
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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()) },
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -229,8 +229,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()}))`
|
||||||
|
|||||||
@@ -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, useSearchParams } 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"
|
||||||
@@ -18,12 +18,10 @@ type State = {
|
|||||||
agent?: string
|
agent?: string
|
||||||
model?: ModelKey
|
model?: ModelKey
|
||||||
variant?: string | null
|
variant?: string | null
|
||||||
source?: "manual" | "message"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Saved = {
|
type Saved = {
|
||||||
session: Record<string, State | undefined>
|
session: Record<string, State | undefined>
|
||||||
draft?: State
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const WORKSPACE_KEY = "__workspace__"
|
const WORKSPACE_KEY = "__workspace__"
|
||||||
@@ -39,13 +37,11 @@ const migrate = (value: unknown) => {
|
|||||||
pick?: Record<string, State | undefined>
|
pick?: Record<string, State | undefined>
|
||||||
}
|
}
|
||||||
|
|
||||||
const draft = "draft" in item && item.draft && typeof item.draft === "object" ? (item.draft as State) : undefined
|
if (item.session && typeof item.session === "object") return { session: item.session }
|
||||||
if (item.session && typeof item.session === "object") return { session: item.session, draft }
|
|
||||||
if (!item.pick || typeof item.pick !== "object") return { session: {} }
|
if (!item.pick || typeof item.pick !== "object") return { session: {} }
|
||||||
|
|
||||||
return {
|
return {
|
||||||
session: Object.fromEntries(Object.entries(item.pick).filter(([key]) => key !== WORKSPACE_KEY)),
|
session: Object.fromEntries(Object.entries(item.pick).filter(([key]) => key !== WORKSPACE_KEY)),
|
||||||
draft,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +57,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
name: "Local",
|
name: "Local",
|
||||||
init: () => {
|
init: () => {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const [search] = useSearchParams<{ draftId?: string }>()
|
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const serverSDK = useServerSDK()
|
const serverSDK = useServerSDK()
|
||||||
@@ -69,15 +64,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
const models = useModels()
|
const models = useModels()
|
||||||
|
|
||||||
const id = createMemo(() => params.id || undefined)
|
const id = createMemo(() => params.id || undefined)
|
||||||
const draftID = createMemo(() => search.draftId || undefined)
|
|
||||||
const list = createMemo(() => sync().data.agent.filter((item) => item.mode !== "subagent" && !item.hidden))
|
const list = createMemo(() => sync().data.agent.filter((item) => item.mode !== "subagent" && !item.hidden))
|
||||||
const connected = createMemo(() => new Set(providers.connected().map((item) => item.id)))
|
const connected = createMemo(() => new Set(providers.connected().map((item) => item.id)))
|
||||||
|
|
||||||
const [saved, setSaved] = persisted(
|
const [saved, setSaved] = persisted(
|
||||||
{
|
{
|
||||||
...(draftID()
|
...Persist.serverWorkspace(serverSDK().scope, sdk().directory, "model-selection", ["model-selection.v1"]),
|
||||||
? Persist.draft(draftID()!, "model-selection")
|
|
||||||
: Persist.serverWorkspace(serverSDK().scope, sdk().directory, "model-selection", ["model-selection.v1"])),
|
|
||||||
migrate,
|
migrate,
|
||||||
},
|
},
|
||||||
createStore<Saved>({
|
createStore<Saved>({
|
||||||
@@ -88,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
|
||||||
@@ -132,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 saved.draft ?? 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))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -145,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 = () => {
|
||||||
@@ -186,19 +175,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
|
|
||||||
const fallback = createMemo<ModelKey | undefined>(() => configuredModel() ?? recentModel() ?? defaultModel())
|
const fallback = createMemo<ModelKey | undefined>(() => configuredModel() ?? recentModel() ?? defaultModel())
|
||||||
|
|
||||||
const save = (state: State) => {
|
|
||||||
const session = id()
|
|
||||||
if (session) {
|
|
||||||
setSaved("session", session, state)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (draftID()) {
|
|
||||||
setSaved("draft", state)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setStore("draft", state)
|
|
||||||
}
|
|
||||||
|
|
||||||
const agent = {
|
const agent = {
|
||||||
list,
|
list,
|
||||||
current() {
|
current() {
|
||||||
@@ -224,9 +200,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
agent: item.name,
|
agent: item.name,
|
||||||
model: item.model ?? prev?.model,
|
model: item.model ?? prev?.model,
|
||||||
variant: item.variant ?? prev?.variant,
|
variant: item.variant ?? prev?.variant,
|
||||||
source: "manual",
|
|
||||||
} satisfies State
|
} satisfies State
|
||||||
save(next)
|
const session = id()
|
||||||
|
if (session) {
|
||||||
|
setSaved("session", session, next)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setStore("draft", next)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
move(direction: 1 | -1) {
|
move(direction: 1 | -1) {
|
||||||
@@ -280,10 +260,14 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
const state = {
|
const state = {
|
||||||
...(scope() ?? { agent: agent.current()?.name }),
|
...(scope() ?? { agent: agent.current()?.name }),
|
||||||
...next,
|
...next,
|
||||||
source: "manual",
|
|
||||||
} satisfies State
|
} satisfies State
|
||||||
|
|
||||||
save(state)
|
const session = id()
|
||||||
|
if (session) {
|
||||||
|
setSaved("session", session, state)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setStore("draft", state)
|
||||||
}
|
}
|
||||||
|
|
||||||
const recent = createMemo(() => models.recent.list().map(models.find).filter(Boolean))
|
const recent = createMemo(() => models.recent.list().map(models.find).filter(Boolean))
|
||||||
@@ -310,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)
|
||||||
@@ -353,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()
|
||||||
@@ -389,36 +369,32 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
agent,
|
agent,
|
||||||
session: {
|
session: {
|
||||||
reset() {
|
reset() {
|
||||||
setStore({ draft: undefined, promoting: undefined })
|
setStore("draft", undefined)
|
||||||
if (draftID()) setSaved("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
|
||||||
next.source = "manual"
|
|
||||||
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: { id?: string; sessionID: string; agent: string; model: ModelKey }) {
|
restore(msg: { sessionID: string; agent: string; model: ModelKey }) {
|
||||||
const session = id()
|
const session = id()
|
||||||
if (!session) return
|
if (!session) return
|
||||||
if (msg.sessionID !== session) return
|
if (msg.sessionID !== session) return
|
||||||
const current = saved.session[session]
|
if (saved.session[session] !== undefined) return
|
||||||
if (current?.source === "manual") return
|
|
||||||
if (handoff.has(handoffKey(serverSDK().scope, sdk().directory, session))) return
|
if (handoff.has(handoffKey(serverSDK().scope, sdk().directory, session))) return
|
||||||
|
|
||||||
setSaved("session", session, {
|
setSaved("session", session, {
|
||||||
agent: msg.agent,
|
agent: msg.agent,
|
||||||
model: msg.model,
|
model: msg.model,
|
||||||
variant: msg.model?.variant ?? null,
|
variant: msg.model?.variant ?? null,
|
||||||
source: "message",
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,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[]) {
|
||||||
|
|||||||
@@ -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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -108,45 +108,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;
|
||||||
@@ -171,66 +132,4 @@
|
|||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes home-projects-fade-top {
|
|
||||||
from {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+89
-235
@@ -1,6 +1,5 @@
|
|||||||
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,
|
createResource,
|
||||||
@@ -69,9 +68,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 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"
|
||||||
@@ -137,107 +133,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 +223,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(() => {
|
||||||
@@ -541,7 +435,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 +456,7 @@ export function NewHome() {
|
|||||||
onClose={closeSearch}
|
onClose={closeSearch}
|
||||||
onSelect={selectSearchSession}
|
onSelect={selectSearchSession}
|
||||||
/>
|
/>
|
||||||
<ScrollView
|
<ScrollView class="mt-3 -mr-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 +469,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="flex flex-col gap-6 pt-3 pr-3 pb-16">
|
||||||
<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 +491,7 @@ export function NewHome() {
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
@@ -668,10 +540,10 @@ function HomeProjectColumn(props: {
|
|||||||
|
|
||||||
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 +560,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 +635,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 +645,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>
|
||||||
@@ -997,7 +854,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))
|
||||||
@@ -1015,7 +871,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 +961,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"
|
||||||
@@ -1255,7 +1110,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)}
|
||||||
@@ -1265,7 +1119,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
|
||||||
@@ -1281,20 +1134,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-3">
|
||||||
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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1312,10 +1170,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"
|
||||||
@@ -1327,7 +1182,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]"}`}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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(
|
||||||
@@ -889,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">
|
||||||
@@ -1013,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)}
|
||||||
@@ -1121,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>
|
||||||
@@ -1357,18 +1302,13 @@ 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={{
|
|
||||||
"flex items-center gap-1 min-w-0 flex-1": true,
|
|
||||||
"pr-3": !settings.general.newLayoutDesigns(),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="flex items-center min-w-0 grow-1">
|
<div class="flex items-center min-w-0 grow-1">
|
||||||
<Show when={parentID()}>
|
<Show when={parentID()}>
|
||||||
<button
|
<button
|
||||||
@@ -1393,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>
|
||||||
@@ -1412,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 grow-1 min-w-0 pl-1 ml-1": true,
|
style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }}
|
||||||
"grow-1 min-w-0 pl-1 -ml-1 rounded-[6px]": !settings.general.newLayoutDesigns(),
|
|
||||||
"rounded-[6px] -ml-2 px-2 py-1 h-6 leading-4 focus:shadow-none focus:outline focus:outline-1 focus:outline-offset-[-1px] focus:outline-v2-border-border-focus":
|
|
||||||
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()
|
||||||
@@ -1444,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)
|
||||||
@@ -1617,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 })
|
||||||
@@ -1638,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}
|
||||||
@@ -1729,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>
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# V2 CLI and TUI development guide
|
||||||
|
|
||||||
|
## Migration context
|
||||||
|
|
||||||
|
- The TUI is being ported from legacy APIs to the new V2 APIs. New and migrated TUI behavior should use `sdk.client.v2` and the location-scoped data in `packages/tui/src/context/data.tsx` instead of adding dependencies on legacy sync state.
|
||||||
|
- Preserve established TUI behavior unless the task intentionally changes it. When behavior, copy, keyboard interaction, or layout is unclear, compare the local V2 TUI with the latest released legacy TUI.
|
||||||
|
- Run both versions in separate Terminal Control sessions and save PNG-only captures at equivalent states:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From packages/cli: local V2 TUI
|
||||||
|
termctrl start opencode-v2-dev --host opentui --cols 112 --rows 34 -- bun dev
|
||||||
|
|
||||||
|
# Released legacy TUI behavior reference
|
||||||
|
termctrl start opencode-legacy --host opentui --cols 112 --rows 34 -- bunx opencode-ai@latest
|
||||||
|
|
||||||
|
termctrl save opencode-v2-dev --format png --out /tmp/opencode/v2.png
|
||||||
|
termctrl save opencode-legacy --format png --out /tmp/opencode/legacy.png
|
||||||
|
```
|
||||||
|
|
||||||
|
- Use the same viewport and send equivalent inputs to both sessions before comparing screenshots. The released CLI is a behavioral reference, not a source of V2 API design; keep the local implementation on V2 endpoints.
|
||||||
|
- Stop both sessions after comparison: `termctrl stop opencode-v2-dev` and `termctrl stop opencode-legacy`.
|
||||||
|
|
||||||
|
## Interactive debugging
|
||||||
|
|
||||||
|
- This package is the V2 CLI adapter. Run its `dev` script when testing the TUI; do not use the repository-root `bun dev`, which launches the legacy `packages/opencode` CLI.
|
||||||
|
- Run commands from `packages/cli`. Use `bun dev` for most debugging so the TUI starts with a private V2 server.
|
||||||
|
- Use `termctrl` for interactive checks instead of starting the TUI as a blocking foreground process. It provides a real PTY, handles OpenTUI's host handshake, and can save reviewable screenshots.
|
||||||
|
- Use a dedicated session name and do not reuse or kill an unrelated session.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
termctrl start opencode-v2-dev --host opentui --cols 112 --rows 34 -- bun dev
|
||||||
|
termctrl wait opencode-v2-dev "Ask anything" --timeout 20000
|
||||||
|
termctrl show opencode-v2-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
- Wait for visible text before interacting instead of relying on fixed sleeps. Use the text expected from the screen under test, such as `Ask anything` or `Connect a provider`.
|
||||||
|
- Drive the running TUI with `termctrl send`. Prefix typed input with `text:` and send control keys separately so the interaction matches real terminal input.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
termctrl send opencode-v2-dev 'text:example prompt' enter
|
||||||
|
termctrl send opencode-v2-dev ctrl-c
|
||||||
|
```
|
||||||
|
|
||||||
|
- Use `termctrl show` after each meaningful interaction and inspect the full visible screen for rendering errors, stale state, error toasts, and unexpected exits.
|
||||||
|
- Save PNG evidence for every user-visible bug and fix. Do not save text captures; inspect the rendered PNG. Write temporary captures outside the repository unless the artifact is intended to be committed.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
termctrl save opencode-v2-dev --format png --out /tmp/opencode/v2-tui.png
|
||||||
|
```
|
||||||
|
|
||||||
|
- For resize-sensitive changes, resize the viewport, wait for the expected content, and capture the screen again:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
termctrl resize opencode-v2-dev --cols 100 --rows 30
|
||||||
|
termctrl show opencode-v2-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
- Source changes may require restarting the process. Use `termctrl restart opencode-v2-dev` rather than assuming the running TUI reloaded the change.
|
||||||
|
- To exercise background-service behavior, use `bun dev service start`, `bun dev service status`, and `bun dev service stop`.
|
||||||
|
- Always clean up the Terminal Control session when the check is complete:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
termctrl stop opencode-v2-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Server/API debugging
|
||||||
|
|
||||||
|
- Use `bun dev api --help` from `packages/cli` to inspect the API debugging command. It sends one request to the V2 server using the same daemon discovery/auth path as the CLI.
|
||||||
|
- Use `bun dev api` to introspect the server-side data backing the TUI. This is useful when debugging UI bugs: compare what the screen renders with the raw session, message, event, agent, or health data returned by the API to determine whether the bug is in the server state, the client data layer, or the TUI rendering.
|
||||||
|
- `bun dev api` accepts either an OpenAPI operation ID or a raw HTTP method plus path:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun dev api get /health
|
||||||
|
bun dev api get /openapi.json
|
||||||
|
bun dev api <operationId> --param key=value
|
||||||
|
```
|
||||||
|
|
||||||
|
- Pass JSON request bodies with `--data`/`-d`; the command sets `content-type: application/json` automatically unless you provide a header. Add extra headers with `--header`/`-H name:value`.
|
||||||
|
- If no compatible background server is registered, `bun dev api` starts one through the daemon service. Use `bun dev service status`, `bun dev service restart`, and `bun dev service stop` when you need explicit lifecycle control.
|
||||||
|
- Prefer raw method/path calls for quick server debugging and operation IDs when exercising documented OpenAPI routes with path or query parameters.
|
||||||
|
|
||||||
|
## Debugger
|
||||||
|
|
||||||
|
- To debug the V2 CLI or TUI with Bun's inspector, launch the CLI entrypoint through Terminal Control with an inspector URL, then attach a debugger to that URL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
termctrl start opencode-v2-debug --host opentui --cols 112 --rows 34 -- \
|
||||||
|
bun run --inspect=ws://localhost:6499/ src/index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
- Use `--inspect-wait` or `--inspect-brk` when execution must pause until the debugger attaches.
|
||||||
|
- Use `termctrl logs opencode-v2-debug` for inspector output or startup failures emitted before the TUI renderer starts. Use `termctrl show` for the visible full-screen TUI.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Run `bun typecheck` from `packages/cli` after CLI adapter changes.
|
||||||
|
- Run `bun typecheck` and `bun test` from `packages/tui` after shared TUI changes. Do not run tests from the repository root.
|
||||||
|
- Treat automated checks and Terminal Control smoke tests as complementary. For user-visible changes, verify initial render, the changed interaction, Ctrl-C exit behavior, and save a screenshot of the corrected state.
|
||||||
@@ -31,11 +31,11 @@ function run(target) {
|
|||||||
|
|
||||||
const envPath = process.env.OPENCODE_BIN_PATH
|
const envPath = process.env.OPENCODE_BIN_PATH
|
||||||
const scriptDir = path.dirname(fs.realpathSync(__filename))
|
const scriptDir = path.dirname(fs.realpathSync(__filename))
|
||||||
const cached = path.join(scriptDir, ".lildax")
|
const cached = path.join(scriptDir, ".opencode2")
|
||||||
const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[os.platform()] || os.platform()
|
const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[os.platform()] || os.platform()
|
||||||
const arch = { x64: "x64", arm64: "arm64", arm: "arm" }[os.arch()] || os.arch()
|
const arch = { x64: "x64", arm64: "arm64", arm: "arm" }[os.arch()] || os.arch()
|
||||||
const base = "@opencode-ai/cli-" + platform + "-" + arch
|
const base = "@opencode-ai/cli-" + platform + "-" + arch
|
||||||
const binary = platform === "windows" ? "lildax.exe" : "lildax"
|
const binary = platform === "windows" ? "opencode2.exe" : "opencode2"
|
||||||
|
|
||||||
function supportsAvx2() {
|
function supportsAvx2() {
|
||||||
if (arch !== "x64") return false
|
if (arch !== "x64") return false
|
||||||
@@ -121,7 +121,7 @@ function findBinary(startDir) {
|
|||||||
const resolved = envPath || (fs.existsSync(cached) ? cached : findBinary(scriptDir))
|
const resolved = envPath || (fs.existsSync(cached) ? cached : findBinary(scriptDir))
|
||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
console.error(
|
console.error(
|
||||||
"It seems that your package manager failed to install the right lildax CLI package. Try manually installing " +
|
"It seems that your package manager failed to install the right opencode2 CLI package. Try manually installing " +
|
||||||
names.map((name) => `"${name}"`).join(" or ") +
|
names.map((name) => `"${name}"`).join(" or ") +
|
||||||
" package",
|
" package",
|
||||||
)
|
)
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"lildax": "./bin/lildax.cjs"
|
"opencode2": "./bin/opencode2.cjs"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"bin"
|
"bin"
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
|
"@opencode-ai/client": "workspace:*",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/server": "workspace:*",
|
"@opencode-ai/server": "workspace:*",
|
||||||
@@ -25,12 +26,15 @@
|
|||||||
"@opentui/solid": "catalog:",
|
"@opentui/solid": "catalog:",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
|
"jsonc-parser": "3.3.1",
|
||||||
|
"semver": "catalog:",
|
||||||
"solid-js": "catalog:"
|
"solid-js": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
"@tsconfig/bun": "catalog:",
|
"@tsconfig/bun": "catalog:",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
|
"@types/semver": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:"
|
"@typescript/native-preview": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import pkg from "../package.json"
|
|||||||
import { modelsData } from "./generate"
|
import { modelsData } from "./generate"
|
||||||
|
|
||||||
const dir = path.resolve(import.meta.dirname, "..")
|
const dir = path.resolve(import.meta.dirname, "..")
|
||||||
const binary = "lildax"
|
const binary = "opencode2"
|
||||||
process.chdir(dir)
|
process.chdir(dir)
|
||||||
|
|
||||||
await rm("dist", { recursive: true, force: true })
|
await rm("dist", { recursive: true, force: true })
|
||||||
|
|||||||
@@ -25,14 +25,15 @@ for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: "./dist" }
|
|||||||
}
|
}
|
||||||
console.log("binaries", binaries)
|
console.log("binaries", binaries)
|
||||||
const version = Object.values(binaries)[0]
|
const version = Object.values(binaries)[0]
|
||||||
|
const name = pkg.name
|
||||||
|
|
||||||
await $`mkdir -p ./dist/${pkg.name}/bin`
|
await $`mkdir -p ./dist/${name}/bin`
|
||||||
await $`cp ./bin/lildax.cjs ./dist/${pkg.name}/bin/lildax`
|
await $`cp ./bin/opencode2.cjs ./dist/${name}/bin/opencode2`
|
||||||
await Bun.file(`./dist/${pkg.name}/package.json`).write(
|
await Bun.file(`./dist/${name}/package.json`).write(
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
name: pkg.name,
|
name,
|
||||||
bin: { lildax: "./bin/lildax" },
|
bin: { opencode2: "./bin/opencode2" },
|
||||||
version,
|
version,
|
||||||
license: pkg.license,
|
license: pkg.license,
|
||||||
repository: { type: "git", url: "git+https://github.com/anomalyco/opencode.git" },
|
repository: { type: "git", url: "git+https://github.com/anomalyco/opencode.git" },
|
||||||
@@ -50,4 +51,4 @@ await Promise.all(
|
|||||||
publish(`./dist/${name.replace("@opencode-ai/", "")}`, name, version),
|
publish(`./dist/${name.replace("@opencode-ai/", "")}`, name, version),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
await publish(`./dist/${pkg.name}`, pkg.name, version)
|
await publish(`./dist/${name}`, name, version)
|
||||||
|
|||||||
@@ -5,6 +5,16 @@ declare const OPENCODE_CLI_NAME: string | undefined
|
|||||||
|
|
||||||
export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME : "opencode", {
|
export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME : "opencode", {
|
||||||
description: "OpenCode 2.0 preview command line interface",
|
description: "OpenCode 2.0 preview command line interface",
|
||||||
|
params: {
|
||||||
|
directory: Argument.string("directory").pipe(
|
||||||
|
Argument.withDescription("Directory to start OpenCode in"),
|
||||||
|
Argument.optional,
|
||||||
|
),
|
||||||
|
standalone: Flag.boolean("standalone").pipe(
|
||||||
|
Flag.withDescription("Run with a private server instead of the background service"),
|
||||||
|
Flag.withDefault(false),
|
||||||
|
),
|
||||||
|
},
|
||||||
commands: [
|
commands: [
|
||||||
Spec.make("api", {
|
Spec.make("api", {
|
||||||
description: "Make a request to the running server",
|
description: "Make a request to the running server",
|
||||||
@@ -46,6 +56,7 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO
|
|||||||
hostname: Flag.string("hostname").pipe(Flag.withDefault("127.0.0.1")),
|
hostname: Flag.string("hostname").pipe(Flag.withDefault("127.0.0.1")),
|
||||||
port: Flag.integer("port").pipe(Flag.optional),
|
port: Flag.integer("port").pipe(Flag.optional),
|
||||||
register: Flag.boolean("register").pipe(Flag.withDefault(false)),
|
register: Flag.boolean("register").pipe(Flag.withDefault(false)),
|
||||||
|
stdio: Flag.boolean("stdio").pipe(Flag.withDefault(false)),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,13 +1,27 @@
|
|||||||
import { Commands } from "../commands"
|
import { Commands } from "../commands"
|
||||||
import { Runtime } from "../../framework/runtime"
|
import { Runtime } from "../../framework/runtime"
|
||||||
import { Effect } from "effect"
|
import { Effect, Option } from "effect"
|
||||||
import { Daemon } from "../../services/daemon"
|
import { Daemon } from "../../services/daemon"
|
||||||
|
import { Standalone } from "../../services/standalone"
|
||||||
|
import { Updater } from "../../services/updater"
|
||||||
|
|
||||||
export default Runtime.handler(Commands, () =>
|
export default Runtime.handler(Commands, (input) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
const directory = Option.getOrUndefined(input.directory)
|
||||||
|
if (directory !== undefined) process.chdir(directory)
|
||||||
|
const updater = yield* Updater.Service
|
||||||
|
yield* updater.check()
|
||||||
const daemon = yield* Daemon.Service
|
const daemon = yield* Daemon.Service
|
||||||
const transport = yield* daemon.transport()
|
const transport = yield* (input.standalone ? Standalone.transport() : daemon.transport())
|
||||||
const { runTui } = yield* Effect.promise(() => import("../../tui"))
|
const { runTui } = yield* Effect.promise(() => import("../../tui"))
|
||||||
yield* runTui(transport)
|
yield* runTui(
|
||||||
|
transport,
|
||||||
|
input.standalone
|
||||||
|
? undefined
|
||||||
|
: async () => {
|
||||||
|
await Effect.runPromise(daemon.stop())
|
||||||
|
return Effect.runPromise(daemon.transport())
|
||||||
|
},
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
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 { 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, Schedule } from "effect"
|
||||||
import * as Effect from "effect/Effect"
|
import * as Effect from "effect/Effect"
|
||||||
import { HttpRouter, HttpServer } from "effect/unstable/http"
|
import { HttpRouter, HttpServer } from "effect/unstable/http"
|
||||||
import { createServer } from "node:http"
|
import { createServer } from "node:http"
|
||||||
import { createRoutes } from "@opencode-ai/server/routes"
|
import { createRoutes } from "@opencode-ai/server/routes"
|
||||||
|
import { ServerAuth } from "@opencode-ai/server/auth"
|
||||||
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||||
import { Commands } from "../commands"
|
import { Commands } from "../commands"
|
||||||
import { Runtime } from "../../framework/runtime"
|
import { Runtime } from "../../framework/runtime"
|
||||||
import { Daemon } from "../../services/daemon"
|
import { Daemon } from "../../services/daemon"
|
||||||
|
import { Updater } from "../../services/updater"
|
||||||
|
|
||||||
export default Runtime.handler(
|
export default Runtime.handler(
|
||||||
Commands.commands.serve,
|
Commands.commands.serve,
|
||||||
@@ -16,15 +19,43 @@ export default Runtime.handler(
|
|||||||
return yield* Effect.scoped(
|
return yield* Effect.scoped(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const daemon = yield* Daemon.Service
|
const daemon = yield* Daemon.Service
|
||||||
const address = yield* listen(input.hostname, input.port, yield* daemon.password())
|
const standalonePassword = process.env.OPENCODE_SERVER_PASSWORD
|
||||||
|
if (input.stdio) delete process.env.OPENCODE_SERVER_PASSWORD
|
||||||
|
const password = input.stdio ? standalonePassword : yield* daemon.password()
|
||||||
|
if (!password) return yield* Effect.fail(new Error("Missing server password"))
|
||||||
|
const address = yield* listen(input.hostname, input.port, password)
|
||||||
|
yield* Effect.tryPromise(() =>
|
||||||
|
createOpencodeClient({
|
||||||
|
baseUrl: HttpServer.formatAddress(address),
|
||||||
|
headers: ServerAuth.headers({ password }),
|
||||||
|
}).v2.location.get(undefined, { throwOnError: true }),
|
||||||
|
)
|
||||||
if (input.register) yield* daemon.register(address)
|
if (input.register) yield* daemon.register(address)
|
||||||
console.log(`server listening on ${HttpServer.formatAddress(address)}`)
|
const url = HttpServer.formatAddress(address)
|
||||||
return yield* Effect.never
|
console.log(input.stdio ? JSON.stringify({ url }) : `server listening on ${url}`)
|
||||||
}),
|
const updater = yield* Updater.Service
|
||||||
|
yield* updater.check().pipe(Effect.schedule(Schedule.spaced("10 minutes")), Effect.forkScoped)
|
||||||
|
return yield* (input.stdio ? waitForStdinClose() : Effect.never)
|
||||||
|
}).pipe(Effect.annotateLogs({ role: "server" })),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function waitForStdinClose() {
|
||||||
|
return Effect.callback<void>((resume) => {
|
||||||
|
const close = () => resume(Effect.void)
|
||||||
|
process.stdin.once("end", close)
|
||||||
|
process.stdin.once("close", close)
|
||||||
|
process.stdin.resume()
|
||||||
|
if (process.stdin.readableEnded || process.stdin.destroyed) close()
|
||||||
|
return Effect.sync(() => {
|
||||||
|
process.stdin.off("end", close)
|
||||||
|
process.stdin.off("close", close)
|
||||||
|
process.stdin.pause()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function listen(hostname: string, port: Option.Option<number>, password: string) {
|
function listen(hostname: string, port: Option.Option<number>, password: string) {
|
||||||
if (Option.isSome(port)) return bind(hostname, port.value, password)
|
if (Option.isSome(port)) return bind(hostname, port.value, password)
|
||||||
const next = (port: number): ReturnType<typeof bind> =>
|
const next = (port: number): ReturnType<typeof bind> =>
|
||||||
@@ -35,11 +66,15 @@ function listen(hostname: string, port: Option.Option<number>, password: string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bind(hostname: string, port: number, password: string) {
|
function bind(hostname: string, port: number, password: string) {
|
||||||
|
const server = createServer()
|
||||||
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(() => server, { port, host: hostname })),
|
||||||
Layer.provide(Credential.defaultLayer),
|
Layer.provide(Credential.defaultLayer),
|
||||||
Layer.provide(PermissionSaved.defaultLayer),
|
Layer.provide(PermissionSaved.defaultLayer),
|
||||||
),
|
),
|
||||||
).pipe(Effect.map((context) => Context.get(context, HttpServer.HttpServer).address))
|
).pipe(
|
||||||
|
Effect.tap(() => Effect.addFinalizer(() => Effect.sync(() => server.closeAllConnections()))),
|
||||||
|
Effect.map((context) => Context.get(context, HttpServer.HttpServer).address),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ export default Runtime.handler(
|
|||||||
Commands.commands.service.commands.status,
|
Commands.commands.service.commands.status,
|
||||||
Effect.fn("cli.service.status")(function* () {
|
Effect.fn("cli.service.status")(function* () {
|
||||||
const url = yield* (yield* Daemon.Service).status()
|
const url = yield* (yield* Daemon.Service).status()
|
||||||
process.stdout.write((url ? `running ${url}` : "stopped") + EOL)
|
process.stdout.write((url ? url : "stopped") + EOL)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import * as Effect from "effect/Effect"
|
|||||||
import * as Command from "effect/unstable/cli/Command"
|
import * as Command from "effect/unstable/cli/Command"
|
||||||
import { Spec } from "./spec"
|
import { Spec } from "./spec"
|
||||||
import { Daemon } from "../services/daemon"
|
import { Daemon } from "../services/daemon"
|
||||||
|
import { Updater } from "../services/updater"
|
||||||
|
import { Scope } from "effect"
|
||||||
|
|
||||||
export type Input<Value> =
|
export type Input<Value> =
|
||||||
Value extends Spec.Node<infer _Name, infer Command, infer _Commands>
|
Value extends Spec.Node<infer _Name, infer Command, infer _Commands>
|
||||||
@@ -10,11 +12,11 @@ export type Input<Value> =
|
|||||||
? Input
|
? Input
|
||||||
: never
|
: never
|
||||||
|
|
||||||
type RuntimeHandler = (input: unknown) => Effect.Effect<void, unknown, Daemon.Service>
|
type RuntimeHandler = (input: unknown) => Effect.Effect<void, unknown, Daemon.Service | Updater.Service | Scope.Scope>
|
||||||
type Loader<Node extends Spec.Any> = () => Promise<{
|
type Loader<Node extends Spec.Any> = () => Promise<{
|
||||||
default: (input: Input<Node>) => Effect.Effect<void, any, Daemon.Service>
|
default: (input: Input<Node>) => Effect.Effect<void, any, Daemon.Service | Updater.Service | Scope.Scope>
|
||||||
}>
|
}>
|
||||||
type ProvidedCommand = Command.Command<string, unknown, unknown, unknown, Daemon.Service>
|
type ProvidedCommand = Command.Command<string, unknown, unknown, unknown, Daemon.Service | Updater.Service | Scope.Scope>
|
||||||
|
|
||||||
export type Handlers<Node extends Spec.Any> = keyof Node["commands"] extends never
|
export type Handlers<Node extends Spec.Any> = keyof Node["commands"] extends never
|
||||||
? Loader<Node>
|
? Loader<Node>
|
||||||
|
|||||||
@@ -2,10 +2,21 @@
|
|||||||
|
|
||||||
import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
|
import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
|
||||||
import * as NodeServices from "@effect/platform-node/NodeServices"
|
import * as NodeServices from "@effect/platform-node/NodeServices"
|
||||||
|
import { NodeFileSystem } from "@effect/platform-node"
|
||||||
import * as Effect from "effect/Effect"
|
import * as Effect from "effect/Effect"
|
||||||
|
import { Layer, Logger, References } from "effect"
|
||||||
import { Commands } from "./commands/commands"
|
import { Commands } from "./commands/commands"
|
||||||
import { Runtime } from "./framework/runtime"
|
import { Runtime } from "./framework/runtime"
|
||||||
import { Daemon } from "./services/daemon"
|
import { Daemon } from "./services/daemon"
|
||||||
|
import { Logging } from "@opencode-ai/core/observability/logging"
|
||||||
|
import { Updater } from "./services/updater"
|
||||||
|
import { InstallationChannel, InstallationVersion, InstallationLocal } from "@opencode-ai/core/installation/version"
|
||||||
|
|
||||||
|
const LoggingLayer = Logger.layer(Logging.loggers(), { mergeWithExisting: false }).pipe(
|
||||||
|
Layer.provide(NodeFileSystem.layer),
|
||||||
|
Layer.orDie,
|
||||||
|
Layer.merge(Layer.succeed(References.MinimumLogLevel, Logging.minimumLogLevel())),
|
||||||
|
)
|
||||||
|
|
||||||
const Handlers = Runtime.handlers(Commands, {
|
const Handlers = Runtime.handlers(Commands, {
|
||||||
$: () => import("./commands/handlers/default"),
|
$: () => import("./commands/handlers/default"),
|
||||||
@@ -24,9 +35,14 @@ const Handlers = Runtime.handlers(Commands, {
|
|||||||
serve: () => import("./commands/handlers/serve"),
|
serve: () => import("./commands/handlers/serve"),
|
||||||
})
|
})
|
||||||
|
|
||||||
Runtime.run(Commands, Handlers, { version: "local" }).pipe(
|
Effect.logInfo("cli starting", { version: InstallationVersion, channel: InstallationChannel, local: InstallationLocal }).pipe(
|
||||||
|
Effect.flatMap(() => Runtime.run(Commands, Handlers, { version: InstallationVersion })),
|
||||||
|
Effect.annotateLogs({ role: "cli" }),
|
||||||
Effect.provide(Daemon.defaultLayer),
|
Effect.provide(Daemon.defaultLayer),
|
||||||
|
Effect.provide(Updater.defaultLayer),
|
||||||
|
Effect.provide(LoggingLayer),
|
||||||
Effect.provide(NodeServices.layer),
|
Effect.provide(NodeServices.layer),
|
||||||
Effect.scoped,
|
Effect.scoped,
|
||||||
|
Effect.tap(() => Effect.sync(() => process.exit(0))),
|
||||||
NodeRuntime.runMain,
|
NodeRuntime.runMain,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||||
import { ServerAuth } from "@opencode-ai/server/auth"
|
import { ServerAuth } from "@opencode-ai/server/auth"
|
||||||
import { Context, Effect, FileSystem, Layer, Option, Schedule, Schema, Scope } from "effect"
|
import { Context, Effect, FileSystem, Layer, Option, Schedule, Schema, Scope } from "effect"
|
||||||
@@ -28,6 +28,10 @@ const Registration = Schema.Struct({
|
|||||||
})
|
})
|
||||||
type Registration = typeof Registration.Type
|
type Registration = typeof Registration.Type
|
||||||
|
|
||||||
|
const Config = Schema.Struct({
|
||||||
|
password: Schema.optional(Schema.String),
|
||||||
|
})
|
||||||
|
|
||||||
function sameRegistration(left: Registration, right: Registration) {
|
function sameRegistration(left: Registration, right: Registration) {
|
||||||
return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid
|
return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid
|
||||||
}
|
}
|
||||||
@@ -37,22 +41,30 @@ export const layer = Layer.effect(
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const fs = yield* FileSystem.FileSystem
|
const fs = yield* FileSystem.FileSystem
|
||||||
const directory = Global.Path.state
|
const directory = Global.Path.state
|
||||||
const file = path.join(directory, "server.json")
|
const file = path.join(directory, InstallationChannel === "local" ? "server-local.json" : "server.json")
|
||||||
const passwordFile = path.join(directory, "password")
|
const configFile = path.join(Global.Path.config, "service.json")
|
||||||
|
const legacyPasswordFile = path.join(directory, "password")
|
||||||
const decodeRegistration = Schema.decodeUnknownEffect(Schema.fromJsonString(Registration))
|
const decodeRegistration = Schema.decodeUnknownEffect(Schema.fromJsonString(Registration))
|
||||||
|
const decodeConfig = Schema.decodeUnknownEffect(Schema.fromJsonString(Config))
|
||||||
|
|
||||||
const password = Effect.fn("cli.daemon.password")(function* (value?: string) {
|
const password = Effect.fn("cli.daemon.password")(function* (value?: string) {
|
||||||
const existing = yield* fs.readFileString(passwordFile).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
const config = yield* fs
|
||||||
if (value === undefined && existing) return existing
|
.readFileString(configFile)
|
||||||
|
.pipe(Effect.flatMap(decodeConfig), Effect.catch(() => Effect.succeed(undefined)))
|
||||||
|
if (value === undefined && config?.password) return config.password
|
||||||
|
|
||||||
|
const legacy = yield* fs
|
||||||
|
.readFileString(legacyPasswordFile)
|
||||||
|
.pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||||
|
const next = value ?? legacy ?? randomBytes(32).toString("base64url")
|
||||||
|
|
||||||
// Keep one private credential across server restarts so discovered clients
|
// Keep one private credential across server restarts so discovered clients
|
||||||
// can reconnect without exposing a password flag or environment variable.
|
// can reconnect without exposing a password flag or environment variable.
|
||||||
const generated = value ?? randomBytes(32).toString("base64url")
|
const temp = configFile + ".tmp"
|
||||||
const temp = passwordFile + ".tmp"
|
yield* fs.writeFileString(temp, JSON.stringify({ password: next }, null, 2) + "\n", { mode: 0o600 })
|
||||||
yield* fs.makeDirectory(directory, { recursive: true })
|
yield* fs.rename(temp, configFile)
|
||||||
yield* fs.writeFileString(temp, generated, { mode: 0o600 })
|
if (legacy) yield* fs.remove(legacyPasswordFile).pipe(Effect.ignore)
|
||||||
yield* fs.rename(temp, passwordFile)
|
return next
|
||||||
return generated
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const registration = Effect.fnUntraced(function* () {
|
const registration = Effect.fnUntraced(function* () {
|
||||||
@@ -111,7 +123,7 @@ export const layer = Layer.effect(
|
|||||||
const existing = yield* healthy().pipe(Effect.option)
|
const existing = yield* healthy().pipe(Effect.option)
|
||||||
const found = Option.getOrUndefined(existing)
|
const found = Option.getOrUndefined(existing)
|
||||||
const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun"
|
const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun"
|
||||||
if (found?.version === InstallationVersion && compiled) return found.url
|
if (found?.version === InstallationVersion) return found.url
|
||||||
if (found) yield* stopProcess(found).pipe(Effect.ignore)
|
if (found) yield* stopProcess(found).pipe(Effect.ignore)
|
||||||
|
|
||||||
const entrypoint = compiled ? undefined : process.argv[1]
|
const entrypoint = compiled ? undefined : process.argv[1]
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { ServerAuth } from "@opencode-ai/server/auth"
|
||||||
|
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||||
|
import { Effect, Schema, Stream } from "effect"
|
||||||
|
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||||
|
import { randomBytes } from "node:crypto"
|
||||||
|
import path from "node:path"
|
||||||
|
|
||||||
|
const Ready = Schema.Struct({ url: Schema.String })
|
||||||
|
const decodeReady = Schema.decodeUnknownPromise(Schema.fromJsonString(Ready))
|
||||||
|
|
||||||
|
function command(password: string) {
|
||||||
|
const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun"
|
||||||
|
const entrypoint = compiled ? [] : process.argv[1] ? [process.argv[1]] : []
|
||||||
|
if (!compiled && entrypoint.length === 0) throw new Error("Failed to resolve CLI entrypoint")
|
||||||
|
return ChildProcess.make(process.execPath, [...entrypoint, "serve", "--stdio", "--port", "0"], {
|
||||||
|
cwd: process.cwd(),
|
||||||
|
env: { OPENCODE_SERVER_PASSWORD: password },
|
||||||
|
extendEnv: true,
|
||||||
|
// The server treats EOF on this pipe as the end of its ownership lease.
|
||||||
|
// The OS closes it even when the TUI is killed before Effect finalizers run.
|
||||||
|
stdin: "pipe",
|
||||||
|
stderr: "ignore",
|
||||||
|
killSignal: "SIGTERM",
|
||||||
|
forceKillAfter: "3 seconds",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const transport = Effect.fn("cli.standalone.transport")(
|
||||||
|
function* () {
|
||||||
|
const password = randomBytes(32).toString("base64url")
|
||||||
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
||||||
|
const proc = yield* spawner.spawn(command(password))
|
||||||
|
const output = yield* proc.stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.take(1), Stream.mkString)
|
||||||
|
if (!output) return yield* Effect.fail(new Error("Standalone server exited before reporting readiness"))
|
||||||
|
const ready = yield* Effect.tryPromise(() => decodeReady(output))
|
||||||
|
return { url: ready.url, headers: ServerAuth.headers({ password }), pid: proc.pid }
|
||||||
|
},
|
||||||
|
Effect.provide(CrossSpawnSpawner.defaultLayer),
|
||||||
|
)
|
||||||
|
|
||||||
|
export * as Standalone from "./standalone"
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { action, decodePolicy } from "./updater"
|
||||||
|
|
||||||
|
describe("updater", () => {
|
||||||
|
test("reads autoupdate from JSONC", () => {
|
||||||
|
expect(decodePolicy('{ // preference\n "autoupdate": "notify",\n}')).toBe("notify")
|
||||||
|
expect(decodePolicy('{ "autoupdate": false }')).toBe(false)
|
||||||
|
expect(decodePolicy('{ "autoupdate": "invalid" }')).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("automatically updates patches and minors", () => {
|
||||||
|
expect(action("1.2.3", "1.2.4", true)).toBe("upgrade")
|
||||||
|
expect(action("1.2.3", "1.3.0", true)).toBe("upgrade")
|
||||||
|
expect(action("1.2.3", "1.2.4", "notify")).toBe("upgrade")
|
||||||
|
expect(action("1.2.3", "1.3.0", "notify")).toBe("upgrade")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("skips when autoupdate is disabled", () => {
|
||||||
|
expect(action("1.2.3", "1.2.4", false)).toBe("none")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("never automatically updates majors", () => {
|
||||||
|
expect(action("1.2.3", "2.0.0", true)).toBe("none")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("reports up-to-date only when versions match", () => {
|
||||||
|
expect(action("1.2.3", "1.2.3", true)).toBe("none")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("upgrades when latest is lower (rollback)", () => {
|
||||||
|
expect(action("1.2.4", "1.2.3", true)).toBe("upgrade")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import { Global } from "@opencode-ai/core/global"
|
||||||
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
|
import { AppProcess } from "@opencode-ai/core/process"
|
||||||
|
import {
|
||||||
|
InstallationChannel,
|
||||||
|
InstallationLocal,
|
||||||
|
InstallationVersion,
|
||||||
|
} from "@opencode-ai/core/installation/version"
|
||||||
|
import { Context, Duration, Effect, FileSystem, Layer } from "effect"
|
||||||
|
import { ChildProcess } from "effect/unstable/process"
|
||||||
|
import { parse, type ParseError } from "jsonc-parser"
|
||||||
|
import path from "node:path"
|
||||||
|
import semver from "semver"
|
||||||
|
|
||||||
|
export type Policy = boolean | "notify"
|
||||||
|
export type Action = "none" | "upgrade"
|
||||||
|
type Method = "npm" | "pnpm" | "bun" | "yarn"
|
||||||
|
|
||||||
|
const packageName = "@opencode-ai/cli"
|
||||||
|
|
||||||
|
export interface Interface {
|
||||||
|
readonly check: () => Effect.Effect<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Service extends Context.Service<Service, Interface>()("@opencode/cli/Updater") {}
|
||||||
|
|
||||||
|
export function decodePolicy(text: string): Policy | undefined {
|
||||||
|
// The CLI only projects this host-level preference instead of initializing
|
||||||
|
// the location-scoped server configuration graph.
|
||||||
|
const errors: ParseError[] = []
|
||||||
|
const input: unknown = parse(text, errors, { allowTrailingComma: true })
|
||||||
|
if (errors.length || typeof input !== "object" || input === null || !("autoupdate" in input)) return
|
||||||
|
const value = input.autoupdate
|
||||||
|
if (typeof value === "boolean" || value === "notify") return value
|
||||||
|
}
|
||||||
|
|
||||||
|
export function action(current: string, latest: string, policy: Policy): Action {
|
||||||
|
if (policy === false) return "none"
|
||||||
|
if (!semver.valid(current) || !semver.valid(latest) || semver.eq(latest, current)) return "none"
|
||||||
|
// Major upgrades are never installed automatically.
|
||||||
|
if (semver.major(latest) !== semver.major(current)) return "none"
|
||||||
|
return "upgrade"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const layer = Layer.effect(
|
||||||
|
Service,
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const fs = yield* FileSystem.FileSystem
|
||||||
|
const global = yield* Global.Service
|
||||||
|
const appProcess = yield* AppProcess.Service
|
||||||
|
const channel = InstallationChannel.replace(/[^a-zA-Z0-9._-]/g, "-")
|
||||||
|
|
||||||
|
const readPolicy = Effect.fnUntraced(function* () {
|
||||||
|
const values = yield* Effect.forEach(["config.json", "opencode.json", "opencode.jsonc"], (name) =>
|
||||||
|
fs
|
||||||
|
.readFileString(path.join(global.config, name))
|
||||||
|
.pipe(Effect.map(decodePolicy), Effect.catch(() => Effect.succeed(undefined))),
|
||||||
|
)
|
||||||
|
return values.findLast((value) => value !== undefined) ?? true
|
||||||
|
})
|
||||||
|
|
||||||
|
const run = Effect.fnUntraced(function* (command: string[], timeout: Duration.Input = "10 seconds") {
|
||||||
|
return yield* appProcess
|
||||||
|
.run(ChildProcess.make(command[0], command.slice(1)), {
|
||||||
|
timeout,
|
||||||
|
maxOutputBytes: 100_000,
|
||||||
|
maxErrorBytes: 100_000,
|
||||||
|
})
|
||||||
|
.pipe(
|
||||||
|
Effect.map((result) => ({
|
||||||
|
code: result.exitCode,
|
||||||
|
stdout: result.stdout.toString("utf8"),
|
||||||
|
stderr: result.stderr.toString("utf8"),
|
||||||
|
})),
|
||||||
|
Effect.catch(() => Effect.succeed({ code: 1, stdout: "", stderr: "" })),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const method = Effect.fnUntraced(function* () {
|
||||||
|
const checks: ReadonlyArray<{ method: Method; command: string[] }> = [
|
||||||
|
{ method: "npm", command: ["npm", "list", "-g", "--depth=0", packageName] },
|
||||||
|
{ method: "pnpm", command: ["pnpm", "list", "-g", "--depth=0", packageName] },
|
||||||
|
{ method: "bun", command: ["bun", "pm", "ls", "-g"] },
|
||||||
|
{ method: "yarn", command: ["yarn", "global", "list"] },
|
||||||
|
]
|
||||||
|
const results = yield* Effect.forEach(
|
||||||
|
checks,
|
||||||
|
(check) => run(check.command).pipe(Effect.map((result) => ({ check, result }))),
|
||||||
|
{ concurrency: "unbounded" },
|
||||||
|
)
|
||||||
|
return results.find((result) => result.result.stdout.includes(packageName))?.check.method
|
||||||
|
})
|
||||||
|
|
||||||
|
const latest = Effect.fnUntraced(function* () {
|
||||||
|
const response = yield* Effect.tryPromise({
|
||||||
|
try: () =>
|
||||||
|
fetch(
|
||||||
|
`https://registry.npmjs.org/${encodeURIComponent(packageName)}/${encodeURIComponent(InstallationChannel)}`,
|
||||||
|
{ headers: { "User-Agent": `opencode/${InstallationVersion}` }, signal: AbortSignal.timeout(10_000) },
|
||||||
|
),
|
||||||
|
catch: (cause) => new Error("Failed to check for updates", { cause }),
|
||||||
|
})
|
||||||
|
if (!response.ok) return yield* Effect.fail(new Error(`Update check failed with status ${response.status}`))
|
||||||
|
const data = yield* Effect.tryPromise({
|
||||||
|
try: () => response.json(),
|
||||||
|
catch: (cause) => new Error("Failed to read update information", { cause }),
|
||||||
|
})
|
||||||
|
if (typeof data !== "object" || data === null || !("version" in data) || typeof data.version !== "string") {
|
||||||
|
return yield* Effect.fail(new Error("Update information did not include a version"))
|
||||||
|
}
|
||||||
|
return data.version
|
||||||
|
})
|
||||||
|
|
||||||
|
const upgrade = Effect.fnUntraced(function* (method: Method, version: string) {
|
||||||
|
const target = `${packageName}@${version}`
|
||||||
|
const commands: Record<Method, string[]> = {
|
||||||
|
npm: ["npm", "install", "--global", target],
|
||||||
|
pnpm: ["pnpm", "install", "--global", target],
|
||||||
|
bun: ["bun", "install", "--global", target],
|
||||||
|
yarn: ["yarn", "global", "add", target],
|
||||||
|
}
|
||||||
|
const result = yield* run(commands[method], "5 minutes")
|
||||||
|
if (result.code === 0) return
|
||||||
|
return yield* Effect.fail(new Error(result.stderr.trim() || `Failed to update with ${method}`))
|
||||||
|
})
|
||||||
|
|
||||||
|
const check = Effect.fn("cli.updater.check")(function* () {
|
||||||
|
if (InstallationLocal || Flag.OPENCODE_DISABLE_AUTOUPDATE)
|
||||||
|
return yield* Effect.logInfo("update check skipped", {
|
||||||
|
reason: InstallationLocal ? "local-install" : "disabled",
|
||||||
|
version: InstallationVersion,
|
||||||
|
channel: InstallationChannel,
|
||||||
|
})
|
||||||
|
const policy = yield* readPolicy()
|
||||||
|
if (policy === false) return yield* Effect.logInfo("update check skipped", { reason: "policy-disabled" })
|
||||||
|
|
||||||
|
return yield* Effect.gen(function* () {
|
||||||
|
const version = yield* latest()
|
||||||
|
yield* Effect.logInfo("update check", {
|
||||||
|
current: InstallationVersion,
|
||||||
|
latest: version,
|
||||||
|
})
|
||||||
|
const next = action(InstallationVersion, version, policy)
|
||||||
|
if (next === "none") return yield* Effect.logInfo("update check done", { action: "up-to-date" })
|
||||||
|
const detected = yield* method()
|
||||||
|
if (!detected) return yield* Effect.logWarning("automatic update skipped: installation method not found")
|
||||||
|
yield* upgrade(detected, version)
|
||||||
|
yield* Effect.logInfo("updated OpenCode", { from: InstallationVersion, to: version, method: detected })
|
||||||
|
})
|
||||||
|
}, Effect.catchCause((cause) => Effect.logWarning("automatic update failed", { cause })))
|
||||||
|
|
||||||
|
return Service.of({ check })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const defaultLayer = layer.pipe(Layer.provide(AppProcess.defaultLayer), Layer.provide(Global.defaultLayer))
|
||||||
|
|
||||||
|
export * as Updater from "./updater"
|
||||||
+39
-28
@@ -2,35 +2,46 @@ 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 { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
|
import { loadBuiltinPlugins } from "@opencode-ai/tui/builtins"
|
||||||
|
import { OpenCode } from "@opencode-ai/client"
|
||||||
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||||
|
|
||||||
export function runTui(transport: { url: string; headers: RequestInit["headers"] }) {
|
type Transport = { url: string; headers: RequestInit["headers"] }
|
||||||
|
|
||||||
|
export function runTui(transport: Transport, reload?: () => Promise<Transport>) {
|
||||||
const config = TuiConfig.resolve({}, { terminalSuspend: false })
|
const config = TuiConfig.resolve({}, { terminalSuspend: false })
|
||||||
return run({
|
let disposeSlots: (() => void) | undefined
|
||||||
...transport,
|
return Effect.gen(function* () {
|
||||||
args: {},
|
const options = { baseUrl: transport.url, headers: transport.headers }
|
||||||
config,
|
const api = OpenCode.make(options)
|
||||||
fetch: gracefulFetch,
|
const directory = yield* Effect.tryPromise(() => api.files.list({ location: { directory: process.cwd() } })).pipe(
|
||||||
pluginHost: {
|
Effect.map((response) => response.location.directory),
|
||||||
async start() {},
|
Effect.catch(() =>
|
||||||
async dispose() {},
|
Effect.tryPromise(() => api.location.get()).pipe(Effect.map((response) => response.directory)),
|
||||||
},
|
),
|
||||||
|
)
|
||||||
|
return yield* run({
|
||||||
|
client: createOpencodeClient({ ...options, directory }),
|
||||||
|
api,
|
||||||
|
reload: reload
|
||||||
|
? async () => {
|
||||||
|
const next = await reload()
|
||||||
|
return {
|
||||||
|
client: createOpencodeClient({ baseUrl: next.url, headers: next.headers, directory }),
|
||||||
|
api: OpenCode.make({ baseUrl: next.url, headers: next.headers }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
args: {},
|
||||||
|
config,
|
||||||
|
pluginHost: {
|
||||||
|
async start(input) {
|
||||||
|
disposeSlots = await loadBuiltinPlugins(input.api, input.runtime)
|
||||||
|
},
|
||||||
|
async dispose() {
|
||||||
|
disposeSlots?.()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
}).pipe(Effect.provide(Global.defaultLayer))
|
}).pipe(Effect.provide(Global.defaultLayer))
|
||||||
}
|
}
|
||||||
|
|
||||||
const legacyDefaults: Record<string, unknown> = {
|
|
||||||
"/config/providers": { providers: [], default: {} },
|
|
||||||
"/provider": { all: [], default: {}, connected: [] },
|
|
||||||
"/agent": [],
|
|
||||||
"/config": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
const gracefulFetch = Object.assign(
|
|
||||||
async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
||||||
const response = await fetch(input, init)
|
|
||||||
if (response.status !== 404) return response
|
|
||||||
const fallback = legacyDefaults[new URL(input instanceof Request ? input.url : input).pathname]
|
|
||||||
if (fallback === undefined) return response
|
|
||||||
return Response.json(fallback)
|
|
||||||
},
|
|
||||||
{ preconnect: fetch.preconnect },
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Effect } from "effect"
|
||||||
|
import path from "node:path"
|
||||||
|
import { Standalone } from "../../src/services/standalone"
|
||||||
|
|
||||||
|
process.argv[1] = path.join(import.meta.dir, "../../src/index.ts")
|
||||||
|
|
||||||
|
await Effect.runPromise(
|
||||||
|
Effect.scoped(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const transport = yield* Standalone.transport()
|
||||||
|
console.log(`${transport.pid} ${transport.url}`)
|
||||||
|
return yield* Effect.never
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { expect, test } from "bun:test"
|
||||||
|
import path from "node:path"
|
||||||
|
|
||||||
|
test("standalone server exits when its owner is killed", async () => {
|
||||||
|
const owner = Bun.spawn([process.execPath, path.join(import.meta.dir, "fixture/standalone-owner.ts")], {
|
||||||
|
cwd: path.join(import.meta.dir, ".."),
|
||||||
|
env: process.env,
|
||||||
|
stdin: "ignore",
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
})
|
||||||
|
const line = await Promise.race([readLine(owner.stdout), Bun.sleep(10_000).then(() => undefined)])
|
||||||
|
const [rawPID, url] = line?.split(" ") ?? []
|
||||||
|
const pid = Number(rawPID)
|
||||||
|
|
||||||
|
try {
|
||||||
|
expect(pid).toBeGreaterThan(0)
|
||||||
|
expect(url).toStartWith("http://127.0.0.1:")
|
||||||
|
expect(running(pid)).toBe(true)
|
||||||
|
|
||||||
|
owner.kill("SIGKILL")
|
||||||
|
await owner.exited
|
||||||
|
|
||||||
|
expect(await waitForExit(pid)).toBe(true)
|
||||||
|
} finally {
|
||||||
|
owner.kill("SIGKILL")
|
||||||
|
if (running(pid)) process.kill(pid, "SIGKILL")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function readLine(stream: ReadableStream<Uint8Array>) {
|
||||||
|
const reader = stream.getReader()
|
||||||
|
const decoder = new TextDecoder()
|
||||||
|
const chunks: string[] = []
|
||||||
|
while (true) {
|
||||||
|
const result = await reader.read()
|
||||||
|
if (result.done) break
|
||||||
|
chunks.push(decoder.decode(result.value, { stream: true }))
|
||||||
|
const output = chunks.join("")
|
||||||
|
const newline = output.indexOf("\n")
|
||||||
|
if (newline !== -1) {
|
||||||
|
reader.releaseLock()
|
||||||
|
return output.slice(0, newline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.releaseLock()
|
||||||
|
return chunks.join("") + decoder.decode()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForExit(pid: number, attempts = 100): Promise<boolean> {
|
||||||
|
if (!running(pid)) return true
|
||||||
|
if (attempts === 0) return false
|
||||||
|
await Bun.sleep(50)
|
||||||
|
return waitForExit(pid, attempts - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function running(pid: number) {
|
||||||
|
if (!Number.isSafeInteger(pid) || pid <= 0) return false
|
||||||
|
try {
|
||||||
|
process.kill(pid, 0)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ export const groupNames = {
|
|||||||
"server.session": "sessions",
|
"server.session": "sessions",
|
||||||
"server.message": "messages",
|
"server.message": "messages",
|
||||||
"server.model": "models",
|
"server.model": "models",
|
||||||
|
"server.generate": "generate",
|
||||||
"server.provider": "providers",
|
"server.provider": "providers",
|
||||||
"server.integration": "integrations",
|
"server.integration": "integrations",
|
||||||
"server.credential": "credentials",
|
"server.credential": "credentials",
|
||||||
@@ -34,6 +35,7 @@ export const groupNames = {
|
|||||||
"server.pty": "ptys",
|
"server.pty": "ptys",
|
||||||
"server.question": "questions",
|
"server.question": "questions",
|
||||||
"server.reference": "references",
|
"server.reference": "references",
|
||||||
|
"server.project": "project",
|
||||||
"server.projectCopy": "projectCopies",
|
"server.projectCopy": "projectCopies",
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
|||||||
@@ -86,35 +86,56 @@ const Endpoint3_3 = (raw: RawClient["server.session"]) => (input: Endpoint3_3Inp
|
|||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint3_4Request = Parameters<RawClient["server.session"]["session.switchAgent"]>[0]
|
type Endpoint3_4Request = Parameters<RawClient["server.session"]["session.fork"]>[0]
|
||||||
type Endpoint3_4Input = {
|
type Endpoint3_4Input = {
|
||||||
readonly sessionID: Endpoint3_4Request["params"]["sessionID"]
|
readonly sessionID: Endpoint3_4Request["params"]["sessionID"]
|
||||||
readonly agent: Endpoint3_4Request["payload"]["agent"]
|
readonly messageID?: Endpoint3_4Request["payload"]["messageID"]
|
||||||
}
|
}
|
||||||
const Endpoint3_4 = (raw: RawClient["server.session"]) => (input: Endpoint3_4Input) =>
|
const Endpoint3_4 = (raw: RawClient["server.session"]) => (input: Endpoint3_4Input) =>
|
||||||
|
raw["session.fork"]({ params: { sessionID: input["sessionID"] }, payload: { messageID: input["messageID"] } }).pipe(
|
||||||
|
Effect.mapError(mapClientError),
|
||||||
|
Effect.map((value) => value.data),
|
||||||
|
)
|
||||||
|
|
||||||
|
type Endpoint3_5Request = Parameters<RawClient["server.session"]["session.switchAgent"]>[0]
|
||||||
|
type Endpoint3_5Input = {
|
||||||
|
readonly sessionID: Endpoint3_5Request["params"]["sessionID"]
|
||||||
|
readonly agent: Endpoint3_5Request["payload"]["agent"]
|
||||||
|
}
|
||||||
|
const Endpoint3_5 = (raw: RawClient["server.session"]) => (input: Endpoint3_5Input) =>
|
||||||
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 Endpoint3_6Request = Parameters<RawClient["server.session"]["session.switchModel"]>[0]
|
||||||
type Endpoint3_5Input = {
|
type Endpoint3_6Input = {
|
||||||
readonly sessionID: Endpoint3_5Request["params"]["sessionID"]
|
readonly sessionID: Endpoint3_6Request["params"]["sessionID"]
|
||||||
readonly model: Endpoint3_5Request["payload"]["model"]
|
readonly model: Endpoint3_6Request["payload"]["model"]
|
||||||
}
|
}
|
||||||
const Endpoint3_5 = (raw: RawClient["server.session"]) => (input: Endpoint3_5Input) =>
|
const Endpoint3_6 = (raw: RawClient["server.session"]) => (input: Endpoint3_6Input) =>
|
||||||
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 Endpoint3_7Request = Parameters<RawClient["server.session"]["session.rename"]>[0]
|
||||||
type Endpoint3_6Input = {
|
type Endpoint3_7Input = {
|
||||||
readonly sessionID: Endpoint3_6Request["params"]["sessionID"]
|
readonly sessionID: Endpoint3_7Request["params"]["sessionID"]
|
||||||
readonly id?: Endpoint3_6Request["payload"]["id"]
|
readonly title: Endpoint3_7Request["payload"]["title"]
|
||||||
readonly prompt: Endpoint3_6Request["payload"]["prompt"]
|
|
||||||
readonly delivery?: Endpoint3_6Request["payload"]["delivery"]
|
|
||||||
readonly resume?: Endpoint3_6Request["payload"]["resume"]
|
|
||||||
}
|
}
|
||||||
const Endpoint3_6 = (raw: RawClient["server.session"]) => (input: Endpoint3_6Input) =>
|
const Endpoint3_7 = (raw: RawClient["server.session"]) => (input: Endpoint3_7Input) =>
|
||||||
|
raw["session.rename"]({ params: { sessionID: input["sessionID"] }, payload: { title: input["title"] } }).pipe(
|
||||||
|
Effect.mapError(mapClientError),
|
||||||
|
)
|
||||||
|
|
||||||
|
type Endpoint3_8Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
|
||||||
|
type Endpoint3_8Input = {
|
||||||
|
readonly sessionID: Endpoint3_8Request["params"]["sessionID"]
|
||||||
|
readonly id?: Endpoint3_8Request["payload"]["id"]
|
||||||
|
readonly prompt: Endpoint3_8Request["payload"]["prompt"]
|
||||||
|
readonly delivery?: Endpoint3_8Request["payload"]["delivery"]
|
||||||
|
readonly resume?: Endpoint3_8Request["payload"]["resume"]
|
||||||
|
}
|
||||||
|
const Endpoint3_8 = (raw: RawClient["server.session"]) => (input: Endpoint3_8Input) =>
|
||||||
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"] },
|
||||||
@@ -123,23 +144,23 @@ const Endpoint3_6 = (raw: RawClient["server.session"]) => (input: Endpoint3_6Inp
|
|||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint3_7Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
|
type Endpoint3_9Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
|
||||||
type Endpoint3_7Input = { readonly sessionID: Endpoint3_7Request["params"]["sessionID"] }
|
type Endpoint3_9Input = { readonly sessionID: Endpoint3_9Request["params"]["sessionID"] }
|
||||||
const Endpoint3_7 = (raw: RawClient["server.session"]) => (input: Endpoint3_7Input) =>
|
const Endpoint3_9 = (raw: RawClient["server.session"]) => (input: Endpoint3_9Input) =>
|
||||||
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 Endpoint3_10Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
|
||||||
type Endpoint3_8Input = { readonly sessionID: Endpoint3_8Request["params"]["sessionID"] }
|
type Endpoint3_10Input = { readonly sessionID: Endpoint3_10Request["params"]["sessionID"] }
|
||||||
const Endpoint3_8 = (raw: RawClient["server.session"]) => (input: Endpoint3_8Input) =>
|
const Endpoint3_10 = (raw: RawClient["server.session"]) => (input: Endpoint3_10Input) =>
|
||||||
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 Endpoint3_11Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
|
||||||
type Endpoint3_9Input = {
|
type Endpoint3_11Input = {
|
||||||
readonly sessionID: Endpoint3_9Request["params"]["sessionID"]
|
readonly sessionID: Endpoint3_11Request["params"]["sessionID"]
|
||||||
readonly messageID: Endpoint3_9Request["payload"]["messageID"]
|
readonly messageID: Endpoint3_11Request["payload"]["messageID"]
|
||||||
readonly files?: Endpoint3_9Request["payload"]["files"]
|
readonly files?: Endpoint3_11Request["payload"]["files"]
|
||||||
}
|
}
|
||||||
const Endpoint3_9 = (raw: RawClient["server.session"]) => (input: Endpoint3_9Input) =>
|
const Endpoint3_11 = (raw: RawClient["server.session"]) => (input: Endpoint3_11Input) =>
|
||||||
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"] },
|
||||||
@@ -148,42 +169,42 @@ const Endpoint3_9 = (raw: RawClient["server.session"]) => (input: Endpoint3_9Inp
|
|||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint3_10Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
|
type Endpoint3_12Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
|
||||||
type Endpoint3_10Input = { readonly sessionID: Endpoint3_10Request["params"]["sessionID"] }
|
|
||||||
const Endpoint3_10 = (raw: RawClient["server.session"]) => (input: Endpoint3_10Input) =>
|
|
||||||
raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
type Endpoint3_11Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
|
|
||||||
type Endpoint3_11Input = { readonly sessionID: Endpoint3_11Request["params"]["sessionID"] }
|
|
||||||
const Endpoint3_11 = (raw: RawClient["server.session"]) => (input: Endpoint3_11Input) =>
|
|
||||||
raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
type Endpoint3_12Request = Parameters<RawClient["server.session"]["session.context"]>[0]
|
|
||||||
type Endpoint3_12Input = { readonly sessionID: Endpoint3_12Request["params"]["sessionID"] }
|
type Endpoint3_12Input = { readonly sessionID: Endpoint3_12Request["params"]["sessionID"] }
|
||||||
const Endpoint3_12 = (raw: RawClient["server.session"]) => (input: Endpoint3_12Input) =>
|
const Endpoint3_12 = (raw: RawClient["server.session"]) => (input: Endpoint3_12Input) =>
|
||||||
|
raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
|
type Endpoint3_13Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
|
||||||
|
type Endpoint3_13Input = { readonly sessionID: Endpoint3_13Request["params"]["sessionID"] }
|
||||||
|
const Endpoint3_13 = (raw: RawClient["server.session"]) => (input: Endpoint3_13Input) =>
|
||||||
|
raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
|
type Endpoint3_14Request = Parameters<RawClient["server.session"]["session.context"]>[0]
|
||||||
|
type Endpoint3_14Input = { readonly sessionID: Endpoint3_14Request["params"]["sessionID"] }
|
||||||
|
const Endpoint3_14 = (raw: RawClient["server.session"]) => (input: Endpoint3_14Input) =>
|
||||||
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 Endpoint3_15Request = Parameters<RawClient["server.session"]["session.history"]>[0]
|
||||||
type Endpoint3_13Input = {
|
type Endpoint3_15Input = {
|
||||||
readonly sessionID: Endpoint3_13Request["params"]["sessionID"]
|
readonly sessionID: Endpoint3_15Request["params"]["sessionID"]
|
||||||
readonly limit?: Endpoint3_13Request["query"]["limit"]
|
readonly limit?: Endpoint3_15Request["query"]["limit"]
|
||||||
readonly after?: Endpoint3_13Request["query"]["after"]
|
readonly after?: Endpoint3_15Request["query"]["after"]
|
||||||
}
|
}
|
||||||
const Endpoint3_13 = (raw: RawClient["server.session"]) => (input: Endpoint3_13Input) =>
|
const Endpoint3_15 = (raw: RawClient["server.session"]) => (input: Endpoint3_15Input) =>
|
||||||
raw["session.history"]({
|
raw["session.history"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input["sessionID"] },
|
||||||
query: { limit: input["limit"], after: input["after"] },
|
query: { limit: input["limit"], after: input["after"] },
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint3_14Request = Parameters<RawClient["server.session"]["session.events"]>[0]
|
type Endpoint3_16Request = Parameters<RawClient["server.session"]["session.events"]>[0]
|
||||||
type Endpoint3_14Input = {
|
type Endpoint3_16Input = {
|
||||||
readonly sessionID: Endpoint3_14Request["params"]["sessionID"]
|
readonly sessionID: Endpoint3_16Request["params"]["sessionID"]
|
||||||
readonly after?: Endpoint3_14Request["query"]["after"]
|
readonly after?: Endpoint3_16Request["query"]["after"]
|
||||||
}
|
}
|
||||||
const Endpoint3_14 = (raw: RawClient["server.session"]) => (input: Endpoint3_14Input) =>
|
const Endpoint3_16 = (raw: RawClient["server.session"]) => (input: Endpoint3_16Input) =>
|
||||||
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),
|
||||||
@@ -191,17 +212,17 @@ const Endpoint3_14 = (raw: RawClient["server.session"]) => (input: Endpoint3_14I
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint3_15Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
type Endpoint3_17Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
||||||
type Endpoint3_15Input = { readonly sessionID: Endpoint3_15Request["params"]["sessionID"] }
|
type Endpoint3_17Input = { readonly sessionID: Endpoint3_17Request["params"]["sessionID"] }
|
||||||
const Endpoint3_15 = (raw: RawClient["server.session"]) => (input: Endpoint3_15Input) =>
|
const Endpoint3_17 = (raw: RawClient["server.session"]) => (input: Endpoint3_17Input) =>
|
||||||
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 Endpoint3_18Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
||||||
type Endpoint3_16Input = {
|
type Endpoint3_18Input = {
|
||||||
readonly sessionID: Endpoint3_16Request["params"]["sessionID"]
|
readonly sessionID: Endpoint3_18Request["params"]["sessionID"]
|
||||||
readonly messageID: Endpoint3_16Request["params"]["messageID"]
|
readonly messageID: Endpoint3_18Request["params"]["messageID"]
|
||||||
}
|
}
|
||||||
const Endpoint3_16 = (raw: RawClient["server.session"]) => (input: Endpoint3_16Input) =>
|
const Endpoint3_18 = (raw: RawClient["server.session"]) => (input: Endpoint3_18Input) =>
|
||||||
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),
|
||||||
@@ -212,19 +233,21 @@ const adaptGroup3 = (raw: RawClient["server.session"]) => ({
|
|||||||
create: Endpoint3_1(raw),
|
create: Endpoint3_1(raw),
|
||||||
active: Endpoint3_2(raw),
|
active: Endpoint3_2(raw),
|
||||||
get: Endpoint3_3(raw),
|
get: Endpoint3_3(raw),
|
||||||
switchAgent: Endpoint3_4(raw),
|
fork: Endpoint3_4(raw),
|
||||||
switchModel: Endpoint3_5(raw),
|
switchAgent: Endpoint3_5(raw),
|
||||||
prompt: Endpoint3_6(raw),
|
switchModel: Endpoint3_6(raw),
|
||||||
compact: Endpoint3_7(raw),
|
rename: Endpoint3_7(raw),
|
||||||
wait: Endpoint3_8(raw),
|
prompt: Endpoint3_8(raw),
|
||||||
stage: Endpoint3_9(raw),
|
compact: Endpoint3_9(raw),
|
||||||
clear: Endpoint3_10(raw),
|
wait: Endpoint3_10(raw),
|
||||||
commit: Endpoint3_11(raw),
|
stage: Endpoint3_11(raw),
|
||||||
context: Endpoint3_12(raw),
|
clear: Endpoint3_12(raw),
|
||||||
history: Endpoint3_13(raw),
|
commit: Endpoint3_13(raw),
|
||||||
events: Endpoint3_14(raw),
|
context: Endpoint3_14(raw),
|
||||||
interrupt: Endpoint3_15(raw),
|
history: Endpoint3_15(raw),
|
||||||
message: Endpoint3_16(raw),
|
events: Endpoint3_16(raw),
|
||||||
|
interrupt: Endpoint3_17(raw),
|
||||||
|
message: Endpoint3_18(raw),
|
||||||
})
|
})
|
||||||
|
|
||||||
type Endpoint4_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0]
|
type Endpoint4_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0]
|
||||||
@@ -249,169 +272,207 @@ const Endpoint5_0 = (raw: RawClient["server.model"]) => (input?: Endpoint5_0Inpu
|
|||||||
|
|
||||||
const adaptGroup5 = (raw: RawClient["server.model"]) => ({ list: Endpoint5_0(raw) })
|
const adaptGroup5 = (raw: RawClient["server.model"]) => ({ list: Endpoint5_0(raw) })
|
||||||
|
|
||||||
type Endpoint6_0Request = Parameters<RawClient["server.provider"]["provider.list"]>[0]
|
type Endpoint6_0Request = Parameters<RawClient["server.generate"]["generate.text"]>[0]
|
||||||
type Endpoint6_0Input = { readonly location?: Endpoint6_0Request["query"]["location"] }
|
type Endpoint6_0Input = {
|
||||||
const Endpoint6_0 = (raw: RawClient["server.provider"]) => (input?: Endpoint6_0Input) =>
|
readonly location?: Endpoint6_0Request["query"]["location"]
|
||||||
|
readonly prompt: Endpoint6_0Request["payload"]["prompt"]
|
||||||
|
readonly model?: Endpoint6_0Request["payload"]["model"]
|
||||||
|
}
|
||||||
|
const Endpoint6_0 = (raw: RawClient["server.generate"]) => (input: Endpoint6_0Input) =>
|
||||||
|
raw["generate.text"]({
|
||||||
|
query: { location: input["location"] },
|
||||||
|
payload: { prompt: input["prompt"], model: input["model"] },
|
||||||
|
}).pipe(
|
||||||
|
Effect.mapError(mapClientError),
|
||||||
|
Effect.map((value) => value.data),
|
||||||
|
)
|
||||||
|
|
||||||
|
const adaptGroup6 = (raw: RawClient["server.generate"]) => ({ text: Endpoint6_0(raw) })
|
||||||
|
|
||||||
|
type Endpoint7_0Request = Parameters<RawClient["server.provider"]["provider.list"]>[0]
|
||||||
|
type Endpoint7_0Input = { readonly location?: Endpoint7_0Request["query"]["location"] }
|
||||||
|
const Endpoint7_0 = (raw: RawClient["server.provider"]) => (input?: Endpoint7_0Input) =>
|
||||||
raw["provider.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
raw["provider.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint6_1Request = Parameters<RawClient["server.provider"]["provider.get"]>[0]
|
type Endpoint7_1Request = Parameters<RawClient["server.provider"]["provider.get"]>[0]
|
||||||
type Endpoint6_1Input = {
|
type Endpoint7_1Input = {
|
||||||
readonly providerID: Endpoint6_1Request["params"]["providerID"]
|
readonly providerID: Endpoint7_1Request["params"]["providerID"]
|
||||||
readonly location?: Endpoint6_1Request["query"]["location"]
|
readonly location?: Endpoint7_1Request["query"]["location"]
|
||||||
}
|
}
|
||||||
const Endpoint6_1 = (raw: RawClient["server.provider"]) => (input: Endpoint6_1Input) =>
|
const Endpoint7_1 = (raw: RawClient["server.provider"]) => (input: Endpoint7_1Input) =>
|
||||||
raw["provider.get"]({ params: { providerID: input["providerID"] }, query: { location: input["location"] } }).pipe(
|
raw["provider.get"]({ params: { providerID: input["providerID"] }, query: { location: input["location"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
)
|
)
|
||||||
|
|
||||||
const adaptGroup6 = (raw: RawClient["server.provider"]) => ({ list: Endpoint6_0(raw), get: Endpoint6_1(raw) })
|
const adaptGroup7 = (raw: RawClient["server.provider"]) => ({ list: Endpoint7_0(raw), get: Endpoint7_1(raw) })
|
||||||
|
|
||||||
type Endpoint7_0Request = Parameters<RawClient["server.integration"]["integration.list"]>[0]
|
type Endpoint8_0Request = Parameters<RawClient["server.integration"]["integration.list"]>[0]
|
||||||
type Endpoint7_0Input = { readonly location?: Endpoint7_0Request["query"]["location"] }
|
type Endpoint8_0Input = { readonly location?: Endpoint8_0Request["query"]["location"] }
|
||||||
const Endpoint7_0 = (raw: RawClient["server.integration"]) => (input?: Endpoint7_0Input) =>
|
const Endpoint8_0 = (raw: RawClient["server.integration"]) => (input?: Endpoint8_0Input) =>
|
||||||
raw["integration.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
raw["integration.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint7_1Request = Parameters<RawClient["server.integration"]["integration.get"]>[0]
|
type Endpoint8_1Request = Parameters<RawClient["server.integration"]["integration.get"]>[0]
|
||||||
type Endpoint7_1Input = {
|
type Endpoint8_1Input = {
|
||||||
readonly integrationID: Endpoint7_1Request["params"]["integrationID"]
|
readonly integrationID: Endpoint8_1Request["params"]["integrationID"]
|
||||||
readonly location?: Endpoint7_1Request["query"]["location"]
|
readonly location?: Endpoint8_1Request["query"]["location"]
|
||||||
}
|
}
|
||||||
const Endpoint7_1 = (raw: RawClient["server.integration"]) => (input: Endpoint7_1Input) =>
|
const Endpoint8_1 = (raw: RawClient["server.integration"]) => (input: Endpoint8_1Input) =>
|
||||||
raw["integration.get"]({
|
raw["integration.get"]({
|
||||||
params: { integrationID: input["integrationID"] },
|
params: { integrationID: input["integrationID"] },
|
||||||
query: { location: input["location"] },
|
query: { location: input["location"] },
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint7_2Request = Parameters<RawClient["server.integration"]["integration.connect.key"]>[0]
|
type Endpoint8_2Request = Parameters<RawClient["server.integration"]["integration.connect.key"]>[0]
|
||||||
type Endpoint7_2Input = {
|
type Endpoint8_2Input = {
|
||||||
readonly integrationID: Endpoint7_2Request["params"]["integrationID"]
|
readonly integrationID: Endpoint8_2Request["params"]["integrationID"]
|
||||||
readonly location?: Endpoint7_2Request["query"]["location"]
|
readonly location?: Endpoint8_2Request["query"]["location"]
|
||||||
readonly key: Endpoint7_2Request["payload"]["key"]
|
readonly key: Endpoint8_2Request["payload"]["key"]
|
||||||
readonly label?: Endpoint7_2Request["payload"]["label"]
|
readonly label?: Endpoint8_2Request["payload"]["label"]
|
||||||
}
|
}
|
||||||
const Endpoint7_2 = (raw: RawClient["server.integration"]) => (input: Endpoint7_2Input) =>
|
const Endpoint8_2 = (raw: RawClient["server.integration"]) => (input: Endpoint8_2Input) =>
|
||||||
raw["integration.connect.key"]({
|
raw["integration.connect.key"]({
|
||||||
params: { integrationID: input["integrationID"] },
|
params: { integrationID: input["integrationID"] },
|
||||||
query: { location: input["location"] },
|
query: { location: input["location"] },
|
||||||
payload: { key: input["key"], label: input["label"] },
|
payload: { key: input["key"], label: input["label"] },
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint7_3Request = Parameters<RawClient["server.integration"]["integration.connect.oauth"]>[0]
|
type Endpoint8_3Request = Parameters<RawClient["server.integration"]["integration.connect.oauth"]>[0]
|
||||||
type Endpoint7_3Input = {
|
type Endpoint8_3Input = {
|
||||||
readonly integrationID: Endpoint7_3Request["params"]["integrationID"]
|
readonly integrationID: Endpoint8_3Request["params"]["integrationID"]
|
||||||
readonly location?: Endpoint7_3Request["query"]["location"]
|
readonly location?: Endpoint8_3Request["query"]["location"]
|
||||||
readonly methodID: Endpoint7_3Request["payload"]["methodID"]
|
readonly methodID: Endpoint8_3Request["payload"]["methodID"]
|
||||||
readonly inputs: Endpoint7_3Request["payload"]["inputs"]
|
readonly inputs: Endpoint8_3Request["payload"]["inputs"]
|
||||||
readonly label?: Endpoint7_3Request["payload"]["label"]
|
readonly label?: Endpoint8_3Request["payload"]["label"]
|
||||||
}
|
}
|
||||||
const Endpoint7_3 = (raw: RawClient["server.integration"]) => (input: Endpoint7_3Input) =>
|
const Endpoint8_3 = (raw: RawClient["server.integration"]) => (input: Endpoint8_3Input) =>
|
||||||
raw["integration.connect.oauth"]({
|
raw["integration.connect.oauth"]({
|
||||||
params: { integrationID: input["integrationID"] },
|
params: { integrationID: input["integrationID"] },
|
||||||
query: { location: input["location"] },
|
query: { location: input["location"] },
|
||||||
payload: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] },
|
payload: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] },
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint7_4Request = Parameters<RawClient["server.integration"]["integration.attempt.status"]>[0]
|
type Endpoint8_4Request = Parameters<RawClient["server.integration"]["integration.attempt.status"]>[0]
|
||||||
type Endpoint7_4Input = {
|
type Endpoint8_4Input = {
|
||||||
readonly attemptID: Endpoint7_4Request["params"]["attemptID"]
|
readonly attemptID: Endpoint8_4Request["params"]["attemptID"]
|
||||||
readonly location?: Endpoint7_4Request["query"]["location"]
|
readonly location?: Endpoint8_4Request["query"]["location"]
|
||||||
}
|
}
|
||||||
const Endpoint7_4 = (raw: RawClient["server.integration"]) => (input: Endpoint7_4Input) =>
|
const Endpoint8_4 = (raw: RawClient["server.integration"]) => (input: Endpoint8_4Input) =>
|
||||||
raw["integration.attempt.status"]({
|
raw["integration.attempt.status"]({
|
||||||
params: { attemptID: input["attemptID"] },
|
params: { attemptID: input["attemptID"] },
|
||||||
query: { location: input["location"] },
|
query: { location: input["location"] },
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint7_5Request = Parameters<RawClient["server.integration"]["integration.attempt.complete"]>[0]
|
type Endpoint8_5Request = Parameters<RawClient["server.integration"]["integration.attempt.complete"]>[0]
|
||||||
type Endpoint7_5Input = {
|
type Endpoint8_5Input = {
|
||||||
readonly attemptID: Endpoint7_5Request["params"]["attemptID"]
|
readonly attemptID: Endpoint8_5Request["params"]["attemptID"]
|
||||||
readonly location?: Endpoint7_5Request["query"]["location"]
|
readonly location?: Endpoint8_5Request["query"]["location"]
|
||||||
readonly code?: Endpoint7_5Request["payload"]["code"]
|
readonly code?: Endpoint8_5Request["payload"]["code"]
|
||||||
}
|
}
|
||||||
const Endpoint7_5 = (raw: RawClient["server.integration"]) => (input: Endpoint7_5Input) =>
|
const Endpoint8_5 = (raw: RawClient["server.integration"]) => (input: Endpoint8_5Input) =>
|
||||||
raw["integration.attempt.complete"]({
|
raw["integration.attempt.complete"]({
|
||||||
params: { attemptID: input["attemptID"] },
|
params: { attemptID: input["attemptID"] },
|
||||||
query: { location: input["location"] },
|
query: { location: input["location"] },
|
||||||
payload: { code: input["code"] },
|
payload: { code: input["code"] },
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint7_6Request = Parameters<RawClient["server.integration"]["integration.attempt.cancel"]>[0]
|
type Endpoint8_6Request = Parameters<RawClient["server.integration"]["integration.attempt.cancel"]>[0]
|
||||||
type Endpoint7_6Input = {
|
type Endpoint8_6Input = {
|
||||||
readonly attemptID: Endpoint7_6Request["params"]["attemptID"]
|
readonly attemptID: Endpoint8_6Request["params"]["attemptID"]
|
||||||
readonly location?: Endpoint7_6Request["query"]["location"]
|
readonly location?: Endpoint8_6Request["query"]["location"]
|
||||||
}
|
}
|
||||||
const Endpoint7_6 = (raw: RawClient["server.integration"]) => (input: Endpoint7_6Input) =>
|
const Endpoint8_6 = (raw: RawClient["server.integration"]) => (input: Endpoint8_6Input) =>
|
||||||
raw["integration.attempt.cancel"]({
|
raw["integration.attempt.cancel"]({
|
||||||
params: { attemptID: input["attemptID"] },
|
params: { attemptID: input["attemptID"] },
|
||||||
query: { location: input["location"] },
|
query: { location: input["location"] },
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
const adaptGroup7 = (raw: RawClient["server.integration"]) => ({
|
const adaptGroup8 = (raw: RawClient["server.integration"]) => ({
|
||||||
list: Endpoint7_0(raw),
|
list: Endpoint8_0(raw),
|
||||||
get: Endpoint7_1(raw),
|
get: Endpoint8_1(raw),
|
||||||
connectKey: Endpoint7_2(raw),
|
connectKey: Endpoint8_2(raw),
|
||||||
connectOauth: Endpoint7_3(raw),
|
connectOauth: Endpoint8_3(raw),
|
||||||
attemptStatus: Endpoint7_4(raw),
|
attemptStatus: Endpoint8_4(raw),
|
||||||
attemptComplete: Endpoint7_5(raw),
|
attemptComplete: Endpoint8_5(raw),
|
||||||
attemptCancel: Endpoint7_6(raw),
|
attemptCancel: Endpoint8_6(raw),
|
||||||
})
|
})
|
||||||
|
|
||||||
type Endpoint8_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
|
type Endpoint9_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
|
||||||
type Endpoint8_0Input = {
|
type Endpoint9_0Input = {
|
||||||
readonly credentialID: Endpoint8_0Request["params"]["credentialID"]
|
readonly credentialID: Endpoint9_0Request["params"]["credentialID"]
|
||||||
readonly location?: Endpoint8_0Request["query"]["location"]
|
readonly location?: Endpoint9_0Request["query"]["location"]
|
||||||
readonly label: Endpoint8_0Request["payload"]["label"]
|
readonly label: Endpoint9_0Request["payload"]["label"]
|
||||||
}
|
}
|
||||||
const Endpoint8_0 = (raw: RawClient["server.credential"]) => (input: Endpoint8_0Input) =>
|
const Endpoint9_0 = (raw: RawClient["server.credential"]) => (input: Endpoint9_0Input) =>
|
||||||
raw["credential.update"]({
|
raw["credential.update"]({
|
||||||
params: { credentialID: input["credentialID"] },
|
params: { credentialID: input["credentialID"] },
|
||||||
query: { location: input["location"] },
|
query: { location: input["location"] },
|
||||||
payload: { label: input["label"] },
|
payload: { label: input["label"] },
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint8_1Request = Parameters<RawClient["server.credential"]["credential.remove"]>[0]
|
type Endpoint9_1Request = Parameters<RawClient["server.credential"]["credential.remove"]>[0]
|
||||||
type Endpoint8_1Input = {
|
type Endpoint9_1Input = {
|
||||||
readonly credentialID: Endpoint8_1Request["params"]["credentialID"]
|
readonly credentialID: Endpoint9_1Request["params"]["credentialID"]
|
||||||
readonly location?: Endpoint8_1Request["query"]["location"]
|
readonly location?: Endpoint9_1Request["query"]["location"]
|
||||||
}
|
}
|
||||||
const Endpoint8_1 = (raw: RawClient["server.credential"]) => (input: Endpoint8_1Input) =>
|
const Endpoint9_1 = (raw: RawClient["server.credential"]) => (input: Endpoint9_1Input) =>
|
||||||
raw["credential.remove"]({
|
raw["credential.remove"]({
|
||||||
params: { credentialID: input["credentialID"] },
|
params: { credentialID: input["credentialID"] },
|
||||||
query: { location: input["location"] },
|
query: { location: input["location"] },
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
const adaptGroup8 = (raw: RawClient["server.credential"]) => ({ update: Endpoint8_0(raw), remove: Endpoint8_1(raw) })
|
const adaptGroup9 = (raw: RawClient["server.credential"]) => ({ update: Endpoint9_0(raw), remove: Endpoint9_1(raw) })
|
||||||
|
|
||||||
type Endpoint9_0Request = Parameters<RawClient["server.permission"]["permission.request.list"]>[0]
|
type Endpoint10_0Request = Parameters<RawClient["server.project"]["project.current"]>[0]
|
||||||
type Endpoint9_0Input = { readonly location?: Endpoint9_0Request["query"]["location"] }
|
type Endpoint10_0Input = { readonly location?: Endpoint10_0Request["query"]["location"] }
|
||||||
const Endpoint9_0 = (raw: RawClient["server.permission"]) => (input?: Endpoint9_0Input) =>
|
const Endpoint10_0 = (raw: RawClient["server.project"]) => (input?: Endpoint10_0Input) =>
|
||||||
|
raw["project.current"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
|
type Endpoint10_1Request = Parameters<RawClient["server.project"]["project.directories"]>[0]
|
||||||
|
type Endpoint10_1Input = {
|
||||||
|
readonly projectID: Endpoint10_1Request["params"]["projectID"]
|
||||||
|
readonly location?: Endpoint10_1Request["query"]["location"]
|
||||||
|
}
|
||||||
|
const Endpoint10_1 = (raw: RawClient["server.project"]) => (input: Endpoint10_1Input) =>
|
||||||
|
raw["project.directories"]({
|
||||||
|
params: { projectID: input["projectID"] },
|
||||||
|
query: { location: input["location"] },
|
||||||
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
|
const adaptGroup10 = (raw: RawClient["server.project"]) => ({
|
||||||
|
current: Endpoint10_0(raw),
|
||||||
|
directories: Endpoint10_1(raw),
|
||||||
|
})
|
||||||
|
|
||||||
|
type Endpoint11_0Request = Parameters<RawClient["server.permission"]["permission.request.list"]>[0]
|
||||||
|
type Endpoint11_0Input = { readonly location?: Endpoint11_0Request["query"]["location"] }
|
||||||
|
const Endpoint11_0 = (raw: RawClient["server.permission"]) => (input?: Endpoint11_0Input) =>
|
||||||
raw["permission.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
raw["permission.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint9_1Request = Parameters<RawClient["server.permission"]["permission.saved.list"]>[0]
|
type Endpoint11_1Request = Parameters<RawClient["server.permission"]["permission.saved.list"]>[0]
|
||||||
type Endpoint9_1Input = { readonly projectID?: Endpoint9_1Request["query"]["projectID"] }
|
type Endpoint11_1Input = { readonly projectID?: Endpoint11_1Request["query"]["projectID"] }
|
||||||
const Endpoint9_1 = (raw: RawClient["server.permission"]) => (input?: Endpoint9_1Input) =>
|
const Endpoint11_1 = (raw: RawClient["server.permission"]) => (input?: Endpoint11_1Input) =>
|
||||||
raw["permission.saved.list"]({ query: { projectID: input?.["projectID"] } }).pipe(
|
raw["permission.saved.list"]({ query: { projectID: input?.["projectID"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint9_2Request = Parameters<RawClient["server.permission"]["permission.saved.remove"]>[0]
|
type Endpoint11_2Request = Parameters<RawClient["server.permission"]["permission.saved.remove"]>[0]
|
||||||
type Endpoint9_2Input = { readonly id: Endpoint9_2Request["params"]["id"] }
|
type Endpoint11_2Input = { readonly id: Endpoint11_2Request["params"]["id"] }
|
||||||
const Endpoint9_2 = (raw: RawClient["server.permission"]) => (input: Endpoint9_2Input) =>
|
const Endpoint11_2 = (raw: RawClient["server.permission"]) => (input: Endpoint11_2Input) =>
|
||||||
raw["permission.saved.remove"]({ params: { id: input["id"] } }).pipe(Effect.mapError(mapClientError))
|
raw["permission.saved.remove"]({ params: { id: input["id"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint9_3Request = Parameters<RawClient["server.permission"]["session.permission.create"]>[0]
|
type Endpoint11_3Request = Parameters<RawClient["server.permission"]["session.permission.create"]>[0]
|
||||||
type Endpoint9_3Input = {
|
type Endpoint11_3Input = {
|
||||||
readonly sessionID: Endpoint9_3Request["params"]["sessionID"]
|
readonly sessionID: Endpoint11_3Request["params"]["sessionID"]
|
||||||
readonly id?: Endpoint9_3Request["payload"]["id"]
|
readonly id?: Endpoint11_3Request["payload"]["id"]
|
||||||
readonly action: Endpoint9_3Request["payload"]["action"]
|
readonly action: Endpoint11_3Request["payload"]["action"]
|
||||||
readonly resources: Endpoint9_3Request["payload"]["resources"]
|
readonly resources: Endpoint11_3Request["payload"]["resources"]
|
||||||
readonly save?: Endpoint9_3Request["payload"]["save"]
|
readonly save?: Endpoint11_3Request["payload"]["save"]
|
||||||
readonly metadata?: Endpoint9_3Request["payload"]["metadata"]
|
readonly metadata?: Endpoint11_3Request["payload"]["metadata"]
|
||||||
readonly source?: Endpoint9_3Request["payload"]["source"]
|
readonly source?: Endpoint11_3Request["payload"]["source"]
|
||||||
readonly agent?: Endpoint9_3Request["payload"]["agent"]
|
readonly agent?: Endpoint11_3Request["payload"]["agent"]
|
||||||
}
|
}
|
||||||
const Endpoint9_3 = (raw: RawClient["server.permission"]) => (input: Endpoint9_3Input) =>
|
const Endpoint11_3 = (raw: RawClient["server.permission"]) => (input: Endpoint11_3Input) =>
|
||||||
raw["session.permission.create"]({
|
raw["session.permission.create"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input["sessionID"] },
|
||||||
payload: {
|
payload: {
|
||||||
@@ -428,87 +489,87 @@ const Endpoint9_3 = (raw: RawClient["server.permission"]) => (input: Endpoint9_3
|
|||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint9_4Request = Parameters<RawClient["server.permission"]["session.permission.list"]>[0]
|
type Endpoint11_4Request = Parameters<RawClient["server.permission"]["session.permission.list"]>[0]
|
||||||
type Endpoint9_4Input = { readonly sessionID: Endpoint9_4Request["params"]["sessionID"] }
|
type Endpoint11_4Input = { readonly sessionID: Endpoint11_4Request["params"]["sessionID"] }
|
||||||
const Endpoint9_4 = (raw: RawClient["server.permission"]) => (input: Endpoint9_4Input) =>
|
const Endpoint11_4 = (raw: RawClient["server.permission"]) => (input: Endpoint11_4Input) =>
|
||||||
raw["session.permission.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
raw["session.permission.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint9_5Request = Parameters<RawClient["server.permission"]["session.permission.get"]>[0]
|
type Endpoint11_5Request = Parameters<RawClient["server.permission"]["session.permission.get"]>[0]
|
||||||
type Endpoint9_5Input = {
|
type Endpoint11_5Input = {
|
||||||
readonly sessionID: Endpoint9_5Request["params"]["sessionID"]
|
readonly sessionID: Endpoint11_5Request["params"]["sessionID"]
|
||||||
readonly requestID: Endpoint9_5Request["params"]["requestID"]
|
readonly requestID: Endpoint11_5Request["params"]["requestID"]
|
||||||
}
|
}
|
||||||
const Endpoint9_5 = (raw: RawClient["server.permission"]) => (input: Endpoint9_5Input) =>
|
const Endpoint11_5 = (raw: RawClient["server.permission"]) => (input: Endpoint11_5Input) =>
|
||||||
raw["session.permission.get"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe(
|
raw["session.permission.get"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint9_6Request = Parameters<RawClient["server.permission"]["session.permission.reply"]>[0]
|
type Endpoint11_6Request = Parameters<RawClient["server.permission"]["session.permission.reply"]>[0]
|
||||||
type Endpoint9_6Input = {
|
type Endpoint11_6Input = {
|
||||||
readonly sessionID: Endpoint9_6Request["params"]["sessionID"]
|
readonly sessionID: Endpoint11_6Request["params"]["sessionID"]
|
||||||
readonly requestID: Endpoint9_6Request["params"]["requestID"]
|
readonly requestID: Endpoint11_6Request["params"]["requestID"]
|
||||||
readonly reply: Endpoint9_6Request["payload"]["reply"]
|
readonly reply: Endpoint11_6Request["payload"]["reply"]
|
||||||
readonly message?: Endpoint9_6Request["payload"]["message"]
|
readonly message?: Endpoint11_6Request["payload"]["message"]
|
||||||
}
|
}
|
||||||
const Endpoint9_6 = (raw: RawClient["server.permission"]) => (input: Endpoint9_6Input) =>
|
const Endpoint11_6 = (raw: RawClient["server.permission"]) => (input: Endpoint11_6Input) =>
|
||||||
raw["session.permission.reply"]({
|
raw["session.permission.reply"]({
|
||||||
params: { sessionID: input["sessionID"], requestID: input["requestID"] },
|
params: { sessionID: input["sessionID"], requestID: input["requestID"] },
|
||||||
payload: { reply: input["reply"], message: input["message"] },
|
payload: { reply: input["reply"], message: input["message"] },
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
const adaptGroup9 = (raw: RawClient["server.permission"]) => ({
|
const adaptGroup11 = (raw: RawClient["server.permission"]) => ({
|
||||||
listRequests: Endpoint9_0(raw),
|
listRequests: Endpoint11_0(raw),
|
||||||
listSaved: Endpoint9_1(raw),
|
listSaved: Endpoint11_1(raw),
|
||||||
removeSaved: Endpoint9_2(raw),
|
removeSaved: Endpoint11_2(raw),
|
||||||
create: Endpoint9_3(raw),
|
create: Endpoint11_3(raw),
|
||||||
list: Endpoint9_4(raw),
|
list: Endpoint11_4(raw),
|
||||||
get: Endpoint9_5(raw),
|
get: Endpoint11_5(raw),
|
||||||
reply: Endpoint9_6(raw),
|
reply: Endpoint11_6(raw),
|
||||||
})
|
})
|
||||||
|
|
||||||
type Endpoint10_0Request = Parameters<RawClient["server.fs"]["fs.list"]>[0]
|
type Endpoint12_0Request = Parameters<RawClient["server.fs"]["fs.list"]>[0]
|
||||||
type Endpoint10_0Input = {
|
type Endpoint12_0Input = {
|
||||||
readonly location?: Endpoint10_0Request["query"]["location"]
|
readonly location?: Endpoint12_0Request["query"]["location"]
|
||||||
readonly path?: Endpoint10_0Request["query"]["path"]
|
readonly path?: Endpoint12_0Request["query"]["path"]
|
||||||
}
|
}
|
||||||
const Endpoint10_0 = (raw: RawClient["server.fs"]) => (input?: Endpoint10_0Input) =>
|
const Endpoint12_0 = (raw: RawClient["server.fs"]) => (input?: Endpoint12_0Input) =>
|
||||||
raw["fs.list"]({ query: { location: input?.["location"], path: input?.["path"] } }).pipe(
|
raw["fs.list"]({ query: { location: input?.["location"], path: input?.["path"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint10_1Request = Parameters<RawClient["server.fs"]["fs.find"]>[0]
|
type Endpoint12_1Request = Parameters<RawClient["server.fs"]["fs.find"]>[0]
|
||||||
type Endpoint10_1Input = {
|
type Endpoint12_1Input = {
|
||||||
readonly location?: Endpoint10_1Request["query"]["location"]
|
readonly location?: Endpoint12_1Request["query"]["location"]
|
||||||
readonly query: Endpoint10_1Request["query"]["query"]
|
readonly query: Endpoint12_1Request["query"]["query"]
|
||||||
readonly type?: Endpoint10_1Request["query"]["type"]
|
readonly type?: Endpoint12_1Request["query"]["type"]
|
||||||
readonly limit?: Endpoint10_1Request["query"]["limit"]
|
readonly limit?: Endpoint12_1Request["query"]["limit"]
|
||||||
}
|
}
|
||||||
const Endpoint10_1 = (raw: RawClient["server.fs"]) => (input: Endpoint10_1Input) =>
|
const Endpoint12_1 = (raw: RawClient["server.fs"]) => (input: Endpoint12_1Input) =>
|
||||||
raw["fs.find"]({
|
raw["fs.find"]({
|
||||||
query: { location: input["location"], query: input["query"], type: input["type"], limit: input["limit"] },
|
query: { location: input["location"], query: input["query"], type: input["type"], limit: input["limit"] },
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
const adaptGroup10 = (raw: RawClient["server.fs"]) => ({ list: Endpoint10_0(raw), find: Endpoint10_1(raw) })
|
const adaptGroup12 = (raw: RawClient["server.fs"]) => ({ list: Endpoint12_0(raw), find: Endpoint12_1(raw) })
|
||||||
|
|
||||||
type Endpoint11_0Request = Parameters<RawClient["server.command"]["command.list"]>[0]
|
type Endpoint13_0Request = Parameters<RawClient["server.command"]["command.list"]>[0]
|
||||||
type Endpoint11_0Input = { readonly location?: Endpoint11_0Request["query"]["location"] }
|
type Endpoint13_0Input = { readonly location?: Endpoint13_0Request["query"]["location"] }
|
||||||
const Endpoint11_0 = (raw: RawClient["server.command"]) => (input?: Endpoint11_0Input) =>
|
const Endpoint13_0 = (raw: RawClient["server.command"]) => (input?: Endpoint13_0Input) =>
|
||||||
raw["command.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
raw["command.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
const adaptGroup11 = (raw: RawClient["server.command"]) => ({ list: Endpoint11_0(raw) })
|
const adaptGroup13 = (raw: RawClient["server.command"]) => ({ list: Endpoint13_0(raw) })
|
||||||
|
|
||||||
type Endpoint12_0Request = Parameters<RawClient["server.skill"]["skill.list"]>[0]
|
type Endpoint14_0Request = Parameters<RawClient["server.skill"]["skill.list"]>[0]
|
||||||
type Endpoint12_0Input = { readonly location?: Endpoint12_0Request["query"]["location"] }
|
type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] }
|
||||||
const Endpoint12_0 = (raw: RawClient["server.skill"]) => (input?: Endpoint12_0Input) =>
|
const Endpoint14_0 = (raw: RawClient["server.skill"]) => (input?: Endpoint14_0Input) =>
|
||||||
raw["skill.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
raw["skill.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
const adaptGroup12 = (raw: RawClient["server.skill"]) => ({ list: Endpoint12_0(raw) })
|
const adaptGroup14 = (raw: RawClient["server.skill"]) => ({ list: Endpoint14_0(raw) })
|
||||||
|
|
||||||
const Endpoint13_0 = (raw: RawClient["server.event"]) => () =>
|
const Endpoint15_0 = (raw: RawClient["server.event"]) => () =>
|
||||||
Stream.unwrap(
|
Stream.unwrap(
|
||||||
raw["event.subscribe"]({}).pipe(
|
raw["event.subscribe"]({}).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
@@ -516,23 +577,23 @@ const Endpoint13_0 = (raw: RawClient["server.event"]) => () =>
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const adaptGroup13 = (raw: RawClient["server.event"]) => ({ subscribe: Endpoint13_0(raw) })
|
const adaptGroup15 = (raw: RawClient["server.event"]) => ({ subscribe: Endpoint15_0(raw) })
|
||||||
|
|
||||||
type Endpoint14_0Request = Parameters<RawClient["server.pty"]["pty.list"]>[0]
|
type Endpoint16_0Request = Parameters<RawClient["server.pty"]["pty.list"]>[0]
|
||||||
type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] }
|
type Endpoint16_0Input = { readonly location?: Endpoint16_0Request["query"]["location"] }
|
||||||
const Endpoint14_0 = (raw: RawClient["server.pty"]) => (input?: Endpoint14_0Input) =>
|
const Endpoint16_0 = (raw: RawClient["server.pty"]) => (input?: Endpoint16_0Input) =>
|
||||||
raw["pty.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
raw["pty.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint14_1Request = Parameters<RawClient["server.pty"]["pty.create"]>[0]
|
type Endpoint16_1Request = Parameters<RawClient["server.pty"]["pty.create"]>[0]
|
||||||
type Endpoint14_1Input = {
|
type Endpoint16_1Input = {
|
||||||
readonly location?: Endpoint14_1Request["query"]["location"]
|
readonly location?: Endpoint16_1Request["query"]["location"]
|
||||||
readonly command?: Endpoint14_1Request["payload"]["command"]
|
readonly command?: Endpoint16_1Request["payload"]["command"]
|
||||||
readonly args?: Endpoint14_1Request["payload"]["args"]
|
readonly args?: Endpoint16_1Request["payload"]["args"]
|
||||||
readonly cwd?: Endpoint14_1Request["payload"]["cwd"]
|
readonly cwd?: Endpoint16_1Request["payload"]["cwd"]
|
||||||
readonly title?: Endpoint14_1Request["payload"]["title"]
|
readonly title?: Endpoint16_1Request["payload"]["title"]
|
||||||
readonly env?: Endpoint14_1Request["payload"]["env"]
|
readonly env?: Endpoint16_1Request["payload"]["env"]
|
||||||
}
|
}
|
||||||
const Endpoint14_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint14_1Input) =>
|
const Endpoint16_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint16_1Input) =>
|
||||||
raw["pty.create"]({
|
raw["pty.create"]({
|
||||||
query: { location: input?.["location"] },
|
query: { location: input?.["location"] },
|
||||||
payload: {
|
payload: {
|
||||||
@@ -544,141 +605,201 @@ const Endpoint14_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint14_1Inpu
|
|||||||
},
|
},
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint14_2Request = Parameters<RawClient["server.pty"]["pty.get"]>[0]
|
type Endpoint16_2Request = Parameters<RawClient["server.pty"]["pty.get"]>[0]
|
||||||
type Endpoint14_2Input = {
|
type Endpoint16_2Input = {
|
||||||
readonly ptyID: Endpoint14_2Request["params"]["ptyID"]
|
readonly ptyID: Endpoint16_2Request["params"]["ptyID"]
|
||||||
readonly location?: Endpoint14_2Request["query"]["location"]
|
readonly location?: Endpoint16_2Request["query"]["location"]
|
||||||
}
|
}
|
||||||
const Endpoint14_2 = (raw: RawClient["server.pty"]) => (input: Endpoint14_2Input) =>
|
const Endpoint16_2 = (raw: RawClient["server.pty"]) => (input: Endpoint16_2Input) =>
|
||||||
raw["pty.get"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe(
|
raw["pty.get"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint14_3Request = Parameters<RawClient["server.pty"]["pty.update"]>[0]
|
type Endpoint16_3Request = Parameters<RawClient["server.pty"]["pty.update"]>[0]
|
||||||
type Endpoint14_3Input = {
|
type Endpoint16_3Input = {
|
||||||
readonly ptyID: Endpoint14_3Request["params"]["ptyID"]
|
readonly ptyID: Endpoint16_3Request["params"]["ptyID"]
|
||||||
readonly location?: Endpoint14_3Request["query"]["location"]
|
readonly location?: Endpoint16_3Request["query"]["location"]
|
||||||
readonly title?: Endpoint14_3Request["payload"]["title"]
|
readonly title?: Endpoint16_3Request["payload"]["title"]
|
||||||
readonly size?: Endpoint14_3Request["payload"]["size"]
|
readonly size?: Endpoint16_3Request["payload"]["size"]
|
||||||
}
|
}
|
||||||
const Endpoint14_3 = (raw: RawClient["server.pty"]) => (input: Endpoint14_3Input) =>
|
const Endpoint16_3 = (raw: RawClient["server.pty"]) => (input: Endpoint16_3Input) =>
|
||||||
raw["pty.update"]({
|
raw["pty.update"]({
|
||||||
params: { ptyID: input["ptyID"] },
|
params: { ptyID: input["ptyID"] },
|
||||||
query: { location: input["location"] },
|
query: { location: input["location"] },
|
||||||
payload: { title: input["title"], size: input["size"] },
|
payload: { title: input["title"], size: input["size"] },
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint14_4Request = Parameters<RawClient["server.pty"]["pty.remove"]>[0]
|
type Endpoint16_4Request = Parameters<RawClient["server.pty"]["pty.remove"]>[0]
|
||||||
type Endpoint14_4Input = {
|
type Endpoint16_4Input = {
|
||||||
readonly ptyID: Endpoint14_4Request["params"]["ptyID"]
|
readonly ptyID: Endpoint16_4Request["params"]["ptyID"]
|
||||||
readonly location?: Endpoint14_4Request["query"]["location"]
|
readonly location?: Endpoint16_4Request["query"]["location"]
|
||||||
}
|
}
|
||||||
const Endpoint14_4 = (raw: RawClient["server.pty"]) => (input: Endpoint14_4Input) =>
|
const Endpoint16_4 = (raw: RawClient["server.pty"]) => (input: Endpoint16_4Input) =>
|
||||||
raw["pty.remove"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe(
|
raw["pty.remove"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
)
|
)
|
||||||
|
|
||||||
const adaptGroup14 = (raw: RawClient["server.pty"]) => ({
|
const adaptGroup16 = (raw: RawClient["server.pty"]) => ({
|
||||||
list: Endpoint14_0(raw),
|
list: Endpoint16_0(raw),
|
||||||
create: Endpoint14_1(raw),
|
create: Endpoint16_1(raw),
|
||||||
get: Endpoint14_2(raw),
|
get: Endpoint16_2(raw),
|
||||||
update: Endpoint14_3(raw),
|
update: Endpoint16_3(raw),
|
||||||
remove: Endpoint14_4(raw),
|
remove: Endpoint16_4(raw),
|
||||||
})
|
})
|
||||||
|
|
||||||
type Endpoint15_0Request = Parameters<RawClient["server.question"]["question.request.list"]>[0]
|
type Endpoint17_0Request = Parameters<RawClient["server.shell"]["shell.list"]>[0]
|
||||||
type Endpoint15_0Input = { readonly location?: Endpoint15_0Request["query"]["location"] }
|
type Endpoint17_0Input = { readonly location?: Endpoint17_0Request["query"]["location"] }
|
||||||
const Endpoint15_0 = (raw: RawClient["server.question"]) => (input?: Endpoint15_0Input) =>
|
const Endpoint17_0 = (raw: RawClient["server.shell"]) => (input?: Endpoint17_0Input) =>
|
||||||
|
raw["shell.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
|
type Endpoint17_1Request = Parameters<RawClient["server.shell"]["shell.create"]>[0]
|
||||||
|
type Endpoint17_1Input = {
|
||||||
|
readonly location?: Endpoint17_1Request["query"]["location"]
|
||||||
|
readonly command: Endpoint17_1Request["payload"]["command"]
|
||||||
|
readonly cwd?: Endpoint17_1Request["payload"]["cwd"]
|
||||||
|
readonly timeout?: Endpoint17_1Request["payload"]["timeout"]
|
||||||
|
readonly metadata?: Endpoint17_1Request["payload"]["metadata"]
|
||||||
|
}
|
||||||
|
const Endpoint17_1 = (raw: RawClient["server.shell"]) => (input: Endpoint17_1Input) =>
|
||||||
|
raw["shell.create"]({
|
||||||
|
query: { location: input["location"] },
|
||||||
|
payload: { command: input["command"], cwd: input["cwd"], timeout: input["timeout"], metadata: input["metadata"] },
|
||||||
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
|
type Endpoint17_2Request = Parameters<RawClient["server.shell"]["shell.get"]>[0]
|
||||||
|
type Endpoint17_2Input = {
|
||||||
|
readonly id: Endpoint17_2Request["params"]["id"]
|
||||||
|
readonly location?: Endpoint17_2Request["query"]["location"]
|
||||||
|
}
|
||||||
|
const Endpoint17_2 = (raw: RawClient["server.shell"]) => (input: Endpoint17_2Input) =>
|
||||||
|
raw["shell.get"]({ params: { id: input["id"] }, query: { location: input["location"] } }).pipe(
|
||||||
|
Effect.mapError(mapClientError),
|
||||||
|
)
|
||||||
|
|
||||||
|
type Endpoint17_3Request = Parameters<RawClient["server.shell"]["shell.output"]>[0]
|
||||||
|
type Endpoint17_3Input = {
|
||||||
|
readonly id: Endpoint17_3Request["params"]["id"]
|
||||||
|
readonly location?: Endpoint17_3Request["query"]["location"]
|
||||||
|
readonly cursor?: Endpoint17_3Request["query"]["cursor"]
|
||||||
|
readonly limit?: Endpoint17_3Request["query"]["limit"]
|
||||||
|
}
|
||||||
|
const Endpoint17_3 = (raw: RawClient["server.shell"]) => (input: Endpoint17_3Input) =>
|
||||||
|
raw["shell.output"]({
|
||||||
|
params: { id: input["id"] },
|
||||||
|
query: { location: input["location"], cursor: input["cursor"], limit: input["limit"] },
|
||||||
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
|
type Endpoint17_4Request = Parameters<RawClient["server.shell"]["shell.remove"]>[0]
|
||||||
|
type Endpoint17_4Input = {
|
||||||
|
readonly id: Endpoint17_4Request["params"]["id"]
|
||||||
|
readonly location?: Endpoint17_4Request["query"]["location"]
|
||||||
|
}
|
||||||
|
const Endpoint17_4 = (raw: RawClient["server.shell"]) => (input: Endpoint17_4Input) =>
|
||||||
|
raw["shell.remove"]({ params: { id: input["id"] }, query: { location: input["location"] } }).pipe(
|
||||||
|
Effect.mapError(mapClientError),
|
||||||
|
)
|
||||||
|
|
||||||
|
const adaptGroup17 = (raw: RawClient["server.shell"]) => ({
|
||||||
|
list: Endpoint17_0(raw),
|
||||||
|
create: Endpoint17_1(raw),
|
||||||
|
get: Endpoint17_2(raw),
|
||||||
|
output: Endpoint17_3(raw),
|
||||||
|
remove: Endpoint17_4(raw),
|
||||||
|
})
|
||||||
|
|
||||||
|
type Endpoint18_0Request = Parameters<RawClient["server.question"]["question.request.list"]>[0]
|
||||||
|
type Endpoint18_0Input = { readonly location?: Endpoint18_0Request["query"]["location"] }
|
||||||
|
const Endpoint18_0 = (raw: RawClient["server.question"]) => (input?: Endpoint18_0Input) =>
|
||||||
raw["question.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
raw["question.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint15_1Request = Parameters<RawClient["server.question"]["session.question.list"]>[0]
|
type Endpoint18_1Request = Parameters<RawClient["server.question"]["session.question.list"]>[0]
|
||||||
type Endpoint15_1Input = { readonly sessionID: Endpoint15_1Request["params"]["sessionID"] }
|
type Endpoint18_1Input = { readonly sessionID: Endpoint18_1Request["params"]["sessionID"] }
|
||||||
const Endpoint15_1 = (raw: RawClient["server.question"]) => (input: Endpoint15_1Input) =>
|
const Endpoint18_1 = (raw: RawClient["server.question"]) => (input: Endpoint18_1Input) =>
|
||||||
raw["session.question.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
raw["session.question.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint15_2Request = Parameters<RawClient["server.question"]["session.question.reply"]>[0]
|
type Endpoint18_2Request = Parameters<RawClient["server.question"]["session.question.reply"]>[0]
|
||||||
type Endpoint15_2Input = {
|
type Endpoint18_2Input = {
|
||||||
readonly sessionID: Endpoint15_2Request["params"]["sessionID"]
|
readonly sessionID: Endpoint18_2Request["params"]["sessionID"]
|
||||||
readonly requestID: Endpoint15_2Request["params"]["requestID"]
|
readonly requestID: Endpoint18_2Request["params"]["requestID"]
|
||||||
readonly answers: Endpoint15_2Request["payload"]["answers"]
|
readonly answers: Endpoint18_2Request["payload"]["answers"]
|
||||||
}
|
}
|
||||||
const Endpoint15_2 = (raw: RawClient["server.question"]) => (input: Endpoint15_2Input) =>
|
const Endpoint18_2 = (raw: RawClient["server.question"]) => (input: Endpoint18_2Input) =>
|
||||||
raw["session.question.reply"]({
|
raw["session.question.reply"]({
|
||||||
params: { sessionID: input["sessionID"], requestID: input["requestID"] },
|
params: { sessionID: input["sessionID"], requestID: input["requestID"] },
|
||||||
payload: { answers: input["answers"] },
|
payload: { answers: input["answers"] },
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint15_3Request = Parameters<RawClient["server.question"]["session.question.reject"]>[0]
|
type Endpoint18_3Request = Parameters<RawClient["server.question"]["session.question.reject"]>[0]
|
||||||
type Endpoint15_3Input = {
|
type Endpoint18_3Input = {
|
||||||
readonly sessionID: Endpoint15_3Request["params"]["sessionID"]
|
readonly sessionID: Endpoint18_3Request["params"]["sessionID"]
|
||||||
readonly requestID: Endpoint15_3Request["params"]["requestID"]
|
readonly requestID: Endpoint18_3Request["params"]["requestID"]
|
||||||
}
|
}
|
||||||
const Endpoint15_3 = (raw: RawClient["server.question"]) => (input: Endpoint15_3Input) =>
|
const Endpoint18_3 = (raw: RawClient["server.question"]) => (input: Endpoint18_3Input) =>
|
||||||
raw["session.question.reject"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe(
|
raw["session.question.reject"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
)
|
)
|
||||||
|
|
||||||
const adaptGroup15 = (raw: RawClient["server.question"]) => ({
|
const adaptGroup18 = (raw: RawClient["server.question"]) => ({
|
||||||
listRequests: Endpoint15_0(raw),
|
listRequests: Endpoint18_0(raw),
|
||||||
list: Endpoint15_1(raw),
|
list: Endpoint18_1(raw),
|
||||||
reply: Endpoint15_2(raw),
|
reply: Endpoint18_2(raw),
|
||||||
reject: Endpoint15_3(raw),
|
reject: Endpoint18_3(raw),
|
||||||
})
|
})
|
||||||
|
|
||||||
type Endpoint16_0Request = Parameters<RawClient["server.reference"]["reference.list"]>[0]
|
type Endpoint19_0Request = Parameters<RawClient["server.reference"]["reference.list"]>[0]
|
||||||
type Endpoint16_0Input = { readonly location?: Endpoint16_0Request["query"]["location"] }
|
type Endpoint19_0Input = { readonly location?: Endpoint19_0Request["query"]["location"] }
|
||||||
const Endpoint16_0 = (raw: RawClient["server.reference"]) => (input?: Endpoint16_0Input) =>
|
const Endpoint19_0 = (raw: RawClient["server.reference"]) => (input?: Endpoint19_0Input) =>
|
||||||
raw["reference.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
raw["reference.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
const adaptGroup16 = (raw: RawClient["server.reference"]) => ({ list: Endpoint16_0(raw) })
|
const adaptGroup19 = (raw: RawClient["server.reference"]) => ({ list: Endpoint19_0(raw) })
|
||||||
|
|
||||||
type Endpoint17_0Request = Parameters<RawClient["server.projectCopy"]["projectCopy.create"]>[0]
|
type Endpoint20_0Request = Parameters<RawClient["server.projectCopy"]["projectCopy.create"]>[0]
|
||||||
type Endpoint17_0Input = {
|
type Endpoint20_0Input = {
|
||||||
readonly projectID: Endpoint17_0Request["params"]["projectID"]
|
readonly projectID: Endpoint20_0Request["params"]["projectID"]
|
||||||
readonly location?: Endpoint17_0Request["query"]["location"]
|
readonly location?: Endpoint20_0Request["query"]["location"]
|
||||||
readonly strategy: Endpoint17_0Request["payload"]["strategy"]
|
readonly strategy: Endpoint20_0Request["payload"]["strategy"]
|
||||||
readonly directory: Endpoint17_0Request["payload"]["directory"]
|
readonly directory: Endpoint20_0Request["payload"]["directory"]
|
||||||
readonly name?: Endpoint17_0Request["payload"]["name"]
|
readonly name?: Endpoint20_0Request["payload"]["name"]
|
||||||
}
|
}
|
||||||
const Endpoint17_0 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint17_0Input) =>
|
const Endpoint20_0 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint20_0Input) =>
|
||||||
raw["projectCopy.create"]({
|
raw["projectCopy.create"]({
|
||||||
params: { projectID: input["projectID"] },
|
params: { projectID: input["projectID"] },
|
||||||
query: { location: input["location"] },
|
query: { location: input["location"] },
|
||||||
payload: { strategy: input["strategy"], directory: input["directory"], name: input["name"] },
|
payload: { strategy: input["strategy"], directory: input["directory"], name: input["name"] },
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint17_1Request = Parameters<RawClient["server.projectCopy"]["projectCopy.remove"]>[0]
|
type Endpoint20_1Request = Parameters<RawClient["server.projectCopy"]["projectCopy.remove"]>[0]
|
||||||
type Endpoint17_1Input = {
|
type Endpoint20_1Input = {
|
||||||
readonly projectID: Endpoint17_1Request["params"]["projectID"]
|
readonly projectID: Endpoint20_1Request["params"]["projectID"]
|
||||||
readonly location?: Endpoint17_1Request["query"]["location"]
|
readonly location?: Endpoint20_1Request["query"]["location"]
|
||||||
readonly directory: Endpoint17_1Request["payload"]["directory"]
|
readonly directory: Endpoint20_1Request["payload"]["directory"]
|
||||||
readonly force: Endpoint17_1Request["payload"]["force"]
|
readonly force: Endpoint20_1Request["payload"]["force"]
|
||||||
}
|
}
|
||||||
const Endpoint17_1 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint17_1Input) =>
|
const Endpoint20_1 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint20_1Input) =>
|
||||||
raw["projectCopy.remove"]({
|
raw["projectCopy.remove"]({
|
||||||
params: { projectID: input["projectID"] },
|
params: { projectID: input["projectID"] },
|
||||||
query: { location: input["location"] },
|
query: { location: input["location"] },
|
||||||
payload: { directory: input["directory"], force: input["force"] },
|
payload: { directory: input["directory"], force: input["force"] },
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint17_2Request = Parameters<RawClient["server.projectCopy"]["projectCopy.refresh"]>[0]
|
type Endpoint20_2Request = Parameters<RawClient["server.projectCopy"]["projectCopy.refresh"]>[0]
|
||||||
type Endpoint17_2Input = {
|
type Endpoint20_2Input = {
|
||||||
readonly projectID: Endpoint17_2Request["params"]["projectID"]
|
readonly projectID: Endpoint20_2Request["params"]["projectID"]
|
||||||
readonly location?: Endpoint17_2Request["query"]["location"]
|
readonly location?: Endpoint20_2Request["query"]["location"]
|
||||||
}
|
}
|
||||||
const Endpoint17_2 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint17_2Input) =>
|
const Endpoint20_2 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint20_2Input) =>
|
||||||
raw["projectCopy.refresh"]({
|
raw["projectCopy.refresh"]({
|
||||||
params: { projectID: input["projectID"] },
|
params: { projectID: input["projectID"] },
|
||||||
query: { location: input["location"] },
|
query: { location: input["location"] },
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
const adaptGroup17 = (raw: RawClient["server.projectCopy"]) => ({
|
const adaptGroup20 = (raw: RawClient["server.projectCopy"]) => ({
|
||||||
create: Endpoint17_0(raw),
|
create: Endpoint20_0(raw),
|
||||||
remove: Endpoint17_1(raw),
|
remove: Endpoint20_1(raw),
|
||||||
refresh: Endpoint17_2(raw),
|
refresh: Endpoint20_2(raw),
|
||||||
})
|
})
|
||||||
|
|
||||||
const adaptClient = (raw: RawClient) => ({
|
const adaptClient = (raw: RawClient) => ({
|
||||||
@@ -688,18 +809,21 @@ const adaptClient = (raw: RawClient) => ({
|
|||||||
sessions: adaptGroup3(raw["server.session"]),
|
sessions: adaptGroup3(raw["server.session"]),
|
||||||
messages: adaptGroup4(raw["server.message"]),
|
messages: adaptGroup4(raw["server.message"]),
|
||||||
models: adaptGroup5(raw["server.model"]),
|
models: adaptGroup5(raw["server.model"]),
|
||||||
providers: adaptGroup6(raw["server.provider"]),
|
generate: adaptGroup6(raw["server.generate"]),
|
||||||
integrations: adaptGroup7(raw["server.integration"]),
|
providers: adaptGroup7(raw["server.provider"]),
|
||||||
credentials: adaptGroup8(raw["server.credential"]),
|
integrations: adaptGroup8(raw["server.integration"]),
|
||||||
permissions: adaptGroup9(raw["server.permission"]),
|
credentials: adaptGroup9(raw["server.credential"]),
|
||||||
files: adaptGroup10(raw["server.fs"]),
|
project: adaptGroup10(raw["server.project"]),
|
||||||
commands: adaptGroup11(raw["server.command"]),
|
permissions: adaptGroup11(raw["server.permission"]),
|
||||||
skills: adaptGroup12(raw["server.skill"]),
|
files: adaptGroup12(raw["server.fs"]),
|
||||||
events: adaptGroup13(raw["server.event"]),
|
commands: adaptGroup13(raw["server.command"]),
|
||||||
ptys: adaptGroup14(raw["server.pty"]),
|
skills: adaptGroup14(raw["server.skill"]),
|
||||||
questions: adaptGroup15(raw["server.question"]),
|
events: adaptGroup15(raw["server.event"]),
|
||||||
references: adaptGroup16(raw["server.reference"]),
|
ptys: adaptGroup16(raw["server.pty"]),
|
||||||
projectCopies: adaptGroup17(raw["server.projectCopy"]),
|
"server.shell": adaptGroup17(raw["server.shell"]),
|
||||||
|
questions: adaptGroup18(raw["server.question"]),
|
||||||
|
references: adaptGroup19(raw["server.reference"]),
|
||||||
|
projectCopies: adaptGroup20(raw["server.projectCopy"]),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const make = (options?: { readonly baseUrl?: URL | string }) =>
|
export const make = (options?: { readonly baseUrl?: URL | string }) =>
|
||||||
|
|||||||
@@ -11,10 +11,14 @@ import type {
|
|||||||
SessionsActiveOutput,
|
SessionsActiveOutput,
|
||||||
SessionsGetInput,
|
SessionsGetInput,
|
||||||
SessionsGetOutput,
|
SessionsGetOutput,
|
||||||
|
SessionsForkInput,
|
||||||
|
SessionsForkOutput,
|
||||||
SessionsSwitchAgentInput,
|
SessionsSwitchAgentInput,
|
||||||
SessionsSwitchAgentOutput,
|
SessionsSwitchAgentOutput,
|
||||||
SessionsSwitchModelInput,
|
SessionsSwitchModelInput,
|
||||||
SessionsSwitchModelOutput,
|
SessionsSwitchModelOutput,
|
||||||
|
SessionsRenameInput,
|
||||||
|
SessionsRenameOutput,
|
||||||
SessionsPromptInput,
|
SessionsPromptInput,
|
||||||
SessionsPromptOutput,
|
SessionsPromptOutput,
|
||||||
SessionsCompactInput,
|
SessionsCompactInput,
|
||||||
@@ -41,6 +45,8 @@ import type {
|
|||||||
MessagesListOutput,
|
MessagesListOutput,
|
||||||
ModelsListInput,
|
ModelsListInput,
|
||||||
ModelsListOutput,
|
ModelsListOutput,
|
||||||
|
GenerateTextInput,
|
||||||
|
GenerateTextOutput,
|
||||||
ProvidersListInput,
|
ProvidersListInput,
|
||||||
ProvidersListOutput,
|
ProvidersListOutput,
|
||||||
ProvidersGetInput,
|
ProvidersGetInput,
|
||||||
@@ -63,6 +69,10 @@ import type {
|
|||||||
CredentialsUpdateOutput,
|
CredentialsUpdateOutput,
|
||||||
CredentialsRemoveInput,
|
CredentialsRemoveInput,
|
||||||
CredentialsRemoveOutput,
|
CredentialsRemoveOutput,
|
||||||
|
ProjectCurrentInput,
|
||||||
|
ProjectCurrentOutput,
|
||||||
|
ProjectDirectoriesInput,
|
||||||
|
ProjectDirectoriesOutput,
|
||||||
PermissionsListRequestsInput,
|
PermissionsListRequestsInput,
|
||||||
PermissionsListRequestsOutput,
|
PermissionsListRequestsOutput,
|
||||||
PermissionsListSavedInput,
|
PermissionsListSavedInput,
|
||||||
@@ -96,6 +106,16 @@ import type {
|
|||||||
PtysUpdateOutput,
|
PtysUpdateOutput,
|
||||||
PtysRemoveInput,
|
PtysRemoveInput,
|
||||||
PtysRemoveOutput,
|
PtysRemoveOutput,
|
||||||
|
ServerShellListInput,
|
||||||
|
ServerShellListOutput,
|
||||||
|
ServerShellCreateInput,
|
||||||
|
ServerShellCreateOutput,
|
||||||
|
ServerShellGetInput,
|
||||||
|
ServerShellGetOutput,
|
||||||
|
ServerShellOutputInput,
|
||||||
|
ServerShellOutputOutput,
|
||||||
|
ServerShellRemoveInput,
|
||||||
|
ServerShellRemoveOutput,
|
||||||
QuestionsListRequestsInput,
|
QuestionsListRequestsInput,
|
||||||
QuestionsListRequestsOutput,
|
QuestionsListRequestsOutput,
|
||||||
QuestionsListInput,
|
QuestionsListInput,
|
||||||
@@ -343,6 +363,18 @@ export function make(options: ClientOptions) {
|
|||||||
},
|
},
|
||||||
requestOptions,
|
requestOptions,
|
||||||
).then((value) => value.data),
|
).then((value) => value.data),
|
||||||
|
fork: (input: SessionsForkInput, requestOptions?: RequestOptions) =>
|
||||||
|
request<{ readonly data: SessionsForkOutput }>(
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
path: `/api/session/${encodeURIComponent(input.sessionID)}/fork`,
|
||||||
|
body: { messageID: input["messageID"] },
|
||||||
|
successStatus: 200,
|
||||||
|
declaredStatuses: [404, 400, 401],
|
||||||
|
empty: false,
|
||||||
|
},
|
||||||
|
requestOptions,
|
||||||
|
).then((value) => value.data),
|
||||||
switchAgent: (input: SessionsSwitchAgentInput, requestOptions?: RequestOptions) =>
|
switchAgent: (input: SessionsSwitchAgentInput, requestOptions?: RequestOptions) =>
|
||||||
request<SessionsSwitchAgentOutput>(
|
request<SessionsSwitchAgentOutput>(
|
||||||
{
|
{
|
||||||
@@ -367,6 +399,18 @@ export function make(options: ClientOptions) {
|
|||||||
},
|
},
|
||||||
requestOptions,
|
requestOptions,
|
||||||
),
|
),
|
||||||
|
rename: (input: SessionsRenameInput, requestOptions?: RequestOptions) =>
|
||||||
|
request<SessionsRenameOutput>(
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
path: `/api/session/${encodeURIComponent(input.sessionID)}/rename`,
|
||||||
|
body: { title: input["title"] },
|
||||||
|
successStatus: 204,
|
||||||
|
declaredStatuses: [404, 400, 401],
|
||||||
|
empty: true,
|
||||||
|
},
|
||||||
|
requestOptions,
|
||||||
|
),
|
||||||
prompt: (input: SessionsPromptInput, requestOptions?: RequestOptions) =>
|
prompt: (input: SessionsPromptInput, requestOptions?: RequestOptions) =>
|
||||||
request<{ readonly data: SessionsPromptOutput }>(
|
request<{ readonly data: SessionsPromptOutput }>(
|
||||||
{
|
{
|
||||||
@@ -385,7 +429,7 @@ export function make(options: ClientOptions) {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/compact`,
|
path: `/api/session/${encodeURIComponent(input.sessionID)}/compact`,
|
||||||
successStatus: 204,
|
successStatus: 204,
|
||||||
declaredStatuses: [404, 503, 400, 401],
|
declaredStatuses: [404, 409, 503, 500, 400, 401],
|
||||||
empty: true,
|
empty: true,
|
||||||
},
|
},
|
||||||
requestOptions,
|
requestOptions,
|
||||||
@@ -408,7 +452,7 @@ export function make(options: ClientOptions) {
|
|||||||
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, 409, 500, 400, 401],
|
||||||
empty: false,
|
empty: false,
|
||||||
},
|
},
|
||||||
requestOptions,
|
requestOptions,
|
||||||
@@ -419,7 +463,7 @@ export function make(options: ClientOptions) {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/clear`,
|
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/clear`,
|
||||||
successStatus: 204,
|
successStatus: 204,
|
||||||
declaredStatuses: [404, 500, 400, 401],
|
declaredStatuses: [404, 409, 500, 400, 401],
|
||||||
empty: true,
|
empty: true,
|
||||||
},
|
},
|
||||||
requestOptions,
|
requestOptions,
|
||||||
@@ -430,7 +474,7 @@ export function make(options: ClientOptions) {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/commit`,
|
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/commit`,
|
||||||
successStatus: 204,
|
successStatus: 204,
|
||||||
declaredStatuses: [404, 400, 401],
|
declaredStatuses: [404, 409, 400, 401],
|
||||||
empty: true,
|
empty: true,
|
||||||
},
|
},
|
||||||
requestOptions,
|
requestOptions,
|
||||||
@@ -521,6 +565,21 @@ export function make(options: ClientOptions) {
|
|||||||
requestOptions,
|
requestOptions,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
generate: {
|
||||||
|
text: (input: GenerateTextInput, requestOptions?: RequestOptions) =>
|
||||||
|
request<{ readonly data: GenerateTextOutput }>(
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
path: `/api/generate`,
|
||||||
|
query: { location: input["location"] },
|
||||||
|
body: { prompt: input["prompt"], model: input["model"] },
|
||||||
|
successStatus: 200,
|
||||||
|
declaredStatuses: [400, 503, 401],
|
||||||
|
empty: false,
|
||||||
|
},
|
||||||
|
requestOptions,
|
||||||
|
).then((value) => value.data),
|
||||||
|
},
|
||||||
providers: {
|
providers: {
|
||||||
list: (input?: ProvidersListInput, requestOptions?: RequestOptions) =>
|
list: (input?: ProvidersListInput, requestOptions?: RequestOptions) =>
|
||||||
request<ProvidersListOutput>(
|
request<ProvidersListOutput>(
|
||||||
@@ -663,6 +722,32 @@ export function make(options: ClientOptions) {
|
|||||||
requestOptions,
|
requestOptions,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
project: {
|
||||||
|
current: (input?: ProjectCurrentInput, requestOptions?: RequestOptions) =>
|
||||||
|
request<ProjectCurrentOutput>(
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
path: `/api/project/current`,
|
||||||
|
query: { location: input?.["location"] },
|
||||||
|
successStatus: 200,
|
||||||
|
declaredStatuses: [401, 400],
|
||||||
|
empty: false,
|
||||||
|
},
|
||||||
|
requestOptions,
|
||||||
|
),
|
||||||
|
directories: (input: ProjectDirectoriesInput, requestOptions?: RequestOptions) =>
|
||||||
|
request<ProjectDirectoriesOutput>(
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
path: `/api/project/${encodeURIComponent(input.projectID)}/directories`,
|
||||||
|
query: { location: input["location"] },
|
||||||
|
successStatus: 200,
|
||||||
|
declaredStatuses: [401, 400],
|
||||||
|
empty: false,
|
||||||
|
},
|
||||||
|
requestOptions,
|
||||||
|
),
|
||||||
|
},
|
||||||
permissions: {
|
permissions: {
|
||||||
listRequests: (input?: PermissionsListRequestsInput, requestOptions?: RequestOptions) =>
|
listRequests: (input?: PermissionsListRequestsInput, requestOptions?: RequestOptions) =>
|
||||||
request<PermissionsListRequestsOutput>(
|
request<PermissionsListRequestsOutput>(
|
||||||
@@ -885,6 +970,74 @@ export function make(options: ClientOptions) {
|
|||||||
requestOptions,
|
requestOptions,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
"server.shell": {
|
||||||
|
list: (input?: ServerShellListInput, requestOptions?: RequestOptions) =>
|
||||||
|
request<ServerShellListOutput>(
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
path: `/api/shell`,
|
||||||
|
query: { location: input?.["location"] },
|
||||||
|
successStatus: 200,
|
||||||
|
declaredStatuses: [401, 400],
|
||||||
|
empty: false,
|
||||||
|
},
|
||||||
|
requestOptions,
|
||||||
|
),
|
||||||
|
create: (input: ServerShellCreateInput, requestOptions?: RequestOptions) =>
|
||||||
|
request<ServerShellCreateOutput>(
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
path: `/api/shell`,
|
||||||
|
query: { location: input["location"] },
|
||||||
|
body: {
|
||||||
|
command: input["command"],
|
||||||
|
cwd: input["cwd"],
|
||||||
|
timeout: input["timeout"],
|
||||||
|
metadata: input["metadata"],
|
||||||
|
},
|
||||||
|
successStatus: 200,
|
||||||
|
declaredStatuses: [401, 400],
|
||||||
|
empty: false,
|
||||||
|
},
|
||||||
|
requestOptions,
|
||||||
|
),
|
||||||
|
get: (input: ServerShellGetInput, requestOptions?: RequestOptions) =>
|
||||||
|
request<ServerShellGetOutput>(
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
path: `/api/shell/${encodeURIComponent(input.id)}`,
|
||||||
|
query: { location: input["location"] },
|
||||||
|
successStatus: 200,
|
||||||
|
declaredStatuses: [404, 401, 400],
|
||||||
|
empty: false,
|
||||||
|
},
|
||||||
|
requestOptions,
|
||||||
|
),
|
||||||
|
output: (input: ServerShellOutputInput, requestOptions?: RequestOptions) =>
|
||||||
|
request<ServerShellOutputOutput>(
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
path: `/api/shell/${encodeURIComponent(input.id)}/output`,
|
||||||
|
query: { location: input["location"], cursor: input["cursor"], limit: input["limit"] },
|
||||||
|
successStatus: 200,
|
||||||
|
declaredStatuses: [404, 401, 400],
|
||||||
|
empty: false,
|
||||||
|
},
|
||||||
|
requestOptions,
|
||||||
|
),
|
||||||
|
remove: (input: ServerShellRemoveInput, requestOptions?: RequestOptions) =>
|
||||||
|
request<ServerShellRemoveOutput>(
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
path: `/api/shell/${encodeURIComponent(input.id)}`,
|
||||||
|
query: { location: input["location"] },
|
||||||
|
successStatus: 204,
|
||||||
|
declaredStatuses: [404, 401, 400],
|
||||||
|
empty: true,
|
||||||
|
},
|
||||||
|
requestOptions,
|
||||||
|
),
|
||||||
|
},
|
||||||
questions: {
|
questions: {
|
||||||
listRequests: (input?: QuestionsListRequestsInput, requestOptions?: RequestOptions) =>
|
listRequests: (input?: QuestionsListRequestsInput, requestOptions?: RequestOptions) =>
|
||||||
request<QuestionsListRequestsOutput>(
|
request<QuestionsListRequestsOutput>(
|
||||||
|
|||||||
@@ -33,22 +33,6 @@ export type SessionNotFoundError = {
|
|||||||
export const isSessionNotFoundError = (value: unknown): value is SessionNotFoundError =>
|
export const isSessionNotFoundError = (value: unknown): value is SessionNotFoundError =>
|
||||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "SessionNotFoundError"
|
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "SessionNotFoundError"
|
||||||
|
|
||||||
export type ConflictError = {
|
|
||||||
readonly _tag: "ConflictError"
|
|
||||||
readonly message: string
|
|
||||||
readonly resource?: string | undefined
|
|
||||||
}
|
|
||||||
export const isConflictError = (value: unknown): value is ConflictError =>
|
|
||||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ConflictError"
|
|
||||||
|
|
||||||
export type ServiceUnavailableError = {
|
|
||||||
readonly _tag: "ServiceUnavailableError"
|
|
||||||
readonly message: string
|
|
||||||
readonly service?: string | undefined
|
|
||||||
}
|
|
||||||
export const isServiceUnavailableError = (value: unknown): value is ServiceUnavailableError =>
|
|
||||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ServiceUnavailableError"
|
|
||||||
|
|
||||||
export type MessageNotFoundError = {
|
export type MessageNotFoundError = {
|
||||||
readonly _tag: "MessageNotFoundError"
|
readonly _tag: "MessageNotFoundError"
|
||||||
readonly sessionID: string
|
readonly sessionID: string
|
||||||
@@ -58,6 +42,30 @@ export type MessageNotFoundError = {
|
|||||||
export const isMessageNotFoundError = (value: unknown): value is MessageNotFoundError =>
|
export const isMessageNotFoundError = (value: unknown): value is MessageNotFoundError =>
|
||||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "MessageNotFoundError"
|
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "MessageNotFoundError"
|
||||||
|
|
||||||
|
export type ConflictError = {
|
||||||
|
readonly _tag: "ConflictError"
|
||||||
|
readonly message: string
|
||||||
|
readonly resource?: string | undefined
|
||||||
|
}
|
||||||
|
export const isConflictError = (value: unknown): value is ConflictError =>
|
||||||
|
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ConflictError"
|
||||||
|
|
||||||
|
export type SessionBusyError = {
|
||||||
|
readonly _tag: "SessionBusyError"
|
||||||
|
readonly sessionID: string
|
||||||
|
readonly message: string
|
||||||
|
}
|
||||||
|
export const isSessionBusyError = (value: unknown): value is SessionBusyError =>
|
||||||
|
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "SessionBusyError"
|
||||||
|
|
||||||
|
export type ServiceUnavailableError = {
|
||||||
|
readonly _tag: "ServiceUnavailableError"
|
||||||
|
readonly message: string
|
||||||
|
readonly service?: string | undefined
|
||||||
|
}
|
||||||
|
export const isServiceUnavailableError = (value: unknown): value is ServiceUnavailableError =>
|
||||||
|
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ServiceUnavailableError"
|
||||||
|
|
||||||
export type UnknownError = {
|
export type UnknownError = {
|
||||||
readonly _tag: "UnknownError"
|
readonly _tag: "UnknownError"
|
||||||
readonly message: string
|
readonly message: string
|
||||||
@@ -86,6 +94,10 @@ export type PtyNotFoundError = { readonly _tag: "PtyNotFoundError"; readonly pty
|
|||||||
export const isPtyNotFoundError = (value: unknown): value is PtyNotFoundError =>
|
export const isPtyNotFoundError = (value: unknown): value is PtyNotFoundError =>
|
||||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "PtyNotFoundError"
|
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "PtyNotFoundError"
|
||||||
|
|
||||||
|
export type ShellNotFoundError = { readonly _tag: "ShellNotFoundError"; readonly id: string; readonly message: string }
|
||||||
|
export const isShellNotFoundError = (value: unknown): value is ShellNotFoundError =>
|
||||||
|
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ShellNotFoundError"
|
||||||
|
|
||||||
export type QuestionNotFoundError = {
|
export type QuestionNotFoundError = {
|
||||||
readonly _tag: "QuestionNotFoundError"
|
readonly _tag: "QuestionNotFoundError"
|
||||||
readonly requestID: string
|
readonly requestID: string
|
||||||
@@ -365,6 +377,45 @@ export type SessionsGetOutput = {
|
|||||||
}
|
}
|
||||||
}["data"]
|
}["data"]
|
||||||
|
|
||||||
|
export type SessionsForkInput = {
|
||||||
|
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||||
|
readonly messageID?: { readonly messageID?: string | undefined }["messageID"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SessionsForkOutput = {
|
||||||
|
readonly data: {
|
||||||
|
readonly id: string
|
||||||
|
readonly parentID?: string
|
||||||
|
readonly projectID: string
|
||||||
|
readonly agent?: string
|
||||||
|
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
||||||
|
readonly cost: number
|
||||||
|
readonly tokens: {
|
||||||
|
readonly input: number
|
||||||
|
readonly output: number
|
||||||
|
readonly reasoning: number
|
||||||
|
readonly cache: { readonly read: number; readonly write: number }
|
||||||
|
}
|
||||||
|
readonly time: { readonly created: number; readonly updated: number; readonly archived?: number }
|
||||||
|
readonly title: string
|
||||||
|
readonly location: { readonly directory: string; readonly workspaceID?: string }
|
||||||
|
readonly subpath?: string
|
||||||
|
readonly revert?: {
|
||||||
|
readonly messageID: string
|
||||||
|
readonly partID?: string
|
||||||
|
readonly snapshot?: string
|
||||||
|
readonly diff?: string
|
||||||
|
readonly files?: ReadonlyArray<{
|
||||||
|
readonly path: string
|
||||||
|
readonly status: "added" | "modified" | "deleted"
|
||||||
|
readonly additions: number
|
||||||
|
readonly deletions: number
|
||||||
|
readonly patch: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}["data"]
|
||||||
|
|
||||||
export type SessionsSwitchAgentInput = {
|
export type SessionsSwitchAgentInput = {
|
||||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||||
readonly agent: { readonly agent: string }["agent"]
|
readonly agent: { readonly agent: string }["agent"]
|
||||||
@@ -381,6 +432,13 @@ export type SessionsSwitchModelInput = {
|
|||||||
|
|
||||||
export type SessionsSwitchModelOutput = void
|
export type SessionsSwitchModelOutput = void
|
||||||
|
|
||||||
|
export type SessionsRenameInput = {
|
||||||
|
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||||
|
readonly title: { readonly title: string }["title"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SessionsRenameOutput = void
|
||||||
|
|
||||||
export type SessionsPromptInput = {
|
export type SessionsPromptInput = {
|
||||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||||
readonly id?: {
|
readonly id?: {
|
||||||
@@ -723,6 +781,27 @@ export type SessionsHistoryOutput = {
|
|||||||
readonly subdirectory?: string
|
readonly subdirectory?: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
readonly id: string
|
||||||
|
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||||
|
readonly type: "session.next.renamed"
|
||||||
|
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||||
|
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||||
|
readonly data: { readonly timestamp: number; readonly sessionID: string; readonly title: string }
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
readonly id: string
|
||||||
|
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||||
|
readonly type: "session.next.forked"
|
||||||
|
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||||
|
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||||
|
readonly data: {
|
||||||
|
readonly timestamp: number
|
||||||
|
readonly sessionID: string
|
||||||
|
readonly parentID: string
|
||||||
|
readonly messageID?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
readonly id: string
|
readonly id: string
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||||
@@ -1181,6 +1260,27 @@ export type SessionsEventsOutput =
|
|||||||
readonly subdirectory?: string
|
readonly subdirectory?: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
readonly id: string
|
||||||
|
readonly metadata?: { readonly [x: string]: unknown }
|
||||||
|
readonly type: "session.next.renamed"
|
||||||
|
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||||
|
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||||
|
readonly data: { readonly timestamp: number; readonly sessionID: string; readonly title: string }
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
readonly id: string
|
||||||
|
readonly metadata?: { readonly [x: string]: unknown }
|
||||||
|
readonly type: "session.next.forked"
|
||||||
|
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||||
|
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||||
|
readonly data: {
|
||||||
|
readonly timestamp: number
|
||||||
|
readonly sessionID: string
|
||||||
|
readonly parentID: string
|
||||||
|
readonly messageID?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
readonly id: string
|
readonly id: string
|
||||||
readonly metadata?: { readonly [x: string]: unknown }
|
readonly metadata?: { readonly [x: string]: unknown }
|
||||||
@@ -1986,6 +2086,22 @@ export type ModelsListOutput = {
|
|||||||
}>
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type GenerateTextInput = {
|
||||||
|
readonly location?: {
|
||||||
|
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||||
|
}["location"]
|
||||||
|
readonly prompt: {
|
||||||
|
readonly prompt: string
|
||||||
|
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||||
|
}["prompt"]
|
||||||
|
readonly model?: {
|
||||||
|
readonly prompt: string
|
||||||
|
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||||
|
}["model"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GenerateTextOutput = { readonly data: { readonly text: string } }["data"]
|
||||||
|
|
||||||
export type ProvidersListInput = {
|
export type ProvidersListInput = {
|
||||||
readonly location?: {
|
readonly location?: {
|
||||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||||
@@ -2288,6 +2404,23 @@ export type CredentialsRemoveInput = {
|
|||||||
|
|
||||||
export type CredentialsRemoveOutput = void
|
export type CredentialsRemoveOutput = void
|
||||||
|
|
||||||
|
export type ProjectCurrentInput = {
|
||||||
|
readonly location?: {
|
||||||
|
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||||
|
}["location"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProjectCurrentOutput = { readonly id: string; readonly directory: string }
|
||||||
|
|
||||||
|
export type ProjectDirectoriesInput = {
|
||||||
|
readonly projectID: { readonly projectID: string }["projectID"]
|
||||||
|
readonly location?: {
|
||||||
|
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||||
|
}["location"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProjectDirectoriesOutput = ReadonlyArray<{ readonly directory: string; readonly strategy?: string }>
|
||||||
|
|
||||||
export type PermissionsListRequestsInput = {
|
export type PermissionsListRequestsInput = {
|
||||||
readonly location?: {
|
readonly location?: {
|
||||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||||
@@ -2687,6 +2820,160 @@ export type PtysRemoveInput = {
|
|||||||
|
|
||||||
export type PtysRemoveOutput = void
|
export type PtysRemoveOutput = void
|
||||||
|
|
||||||
|
export type ServerShellListInput = {
|
||||||
|
readonly location?: {
|
||||||
|
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||||
|
}["location"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ServerShellListOutput = {
|
||||||
|
readonly location: {
|
||||||
|
readonly directory: string
|
||||||
|
readonly workspaceID?: string
|
||||||
|
readonly project: { readonly id: string; readonly directory: string }
|
||||||
|
}
|
||||||
|
readonly data: ReadonlyArray<{
|
||||||
|
readonly id: string
|
||||||
|
readonly status: "running" | "exited" | "timeout" | "killed"
|
||||||
|
readonly command: string
|
||||||
|
readonly cwd: string
|
||||||
|
readonly shell: string
|
||||||
|
readonly file: string
|
||||||
|
readonly pid?: number
|
||||||
|
readonly exit?: number | "Infinity" | "-Infinity" | "NaN"
|
||||||
|
readonly metadata: { readonly [x: string]: JsonValue }
|
||||||
|
readonly time: {
|
||||||
|
readonly started: number | "Infinity" | "-Infinity" | "NaN"
|
||||||
|
readonly completed?: number | "Infinity" | "-Infinity" | "NaN"
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ServerShellCreateInput = {
|
||||||
|
readonly location?: {
|
||||||
|
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||||
|
}["location"]
|
||||||
|
readonly command: {
|
||||||
|
readonly command: string
|
||||||
|
readonly cwd?: string
|
||||||
|
readonly timeout?: number
|
||||||
|
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||||
|
}["command"]
|
||||||
|
readonly cwd?: {
|
||||||
|
readonly command: string
|
||||||
|
readonly cwd?: string
|
||||||
|
readonly timeout?: number
|
||||||
|
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||||
|
}["cwd"]
|
||||||
|
readonly timeout?: {
|
||||||
|
readonly command: string
|
||||||
|
readonly cwd?: string
|
||||||
|
readonly timeout?: number
|
||||||
|
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||||
|
}["timeout"]
|
||||||
|
readonly metadata?: {
|
||||||
|
readonly command: string
|
||||||
|
readonly cwd?: string
|
||||||
|
readonly timeout?: number
|
||||||
|
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||||
|
}["metadata"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ServerShellCreateOutput = {
|
||||||
|
readonly location: {
|
||||||
|
readonly directory: string
|
||||||
|
readonly workspaceID?: string
|
||||||
|
readonly project: { readonly id: string; readonly directory: string }
|
||||||
|
}
|
||||||
|
readonly data: {
|
||||||
|
readonly id: string
|
||||||
|
readonly status: "running" | "exited" | "timeout" | "killed"
|
||||||
|
readonly command: string
|
||||||
|
readonly cwd: string
|
||||||
|
readonly shell: string
|
||||||
|
readonly file: string
|
||||||
|
readonly pid?: number
|
||||||
|
readonly exit?: number | "Infinity" | "-Infinity" | "NaN"
|
||||||
|
readonly metadata: { readonly [x: string]: JsonValue }
|
||||||
|
readonly time: {
|
||||||
|
readonly started: number | "Infinity" | "-Infinity" | "NaN"
|
||||||
|
readonly completed?: number | "Infinity" | "-Infinity" | "NaN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ServerShellGetInput = {
|
||||||
|
readonly id: { readonly id: string }["id"]
|
||||||
|
readonly location?: {
|
||||||
|
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||||
|
}["location"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ServerShellGetOutput = {
|
||||||
|
readonly location: {
|
||||||
|
readonly directory: string
|
||||||
|
readonly workspaceID?: string
|
||||||
|
readonly project: { readonly id: string; readonly directory: string }
|
||||||
|
}
|
||||||
|
readonly data: {
|
||||||
|
readonly id: string
|
||||||
|
readonly status: "running" | "exited" | "timeout" | "killed"
|
||||||
|
readonly command: string
|
||||||
|
readonly cwd: string
|
||||||
|
readonly shell: string
|
||||||
|
readonly file: string
|
||||||
|
readonly pid?: number
|
||||||
|
readonly exit?: number | "Infinity" | "-Infinity" | "NaN"
|
||||||
|
readonly metadata: { readonly [x: string]: JsonValue }
|
||||||
|
readonly time: {
|
||||||
|
readonly started: number | "Infinity" | "-Infinity" | "NaN"
|
||||||
|
readonly completed?: number | "Infinity" | "-Infinity" | "NaN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ServerShellOutputInput = {
|
||||||
|
readonly id: { readonly id: string }["id"]
|
||||||
|
readonly location?: {
|
||||||
|
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||||
|
readonly cursor?: number | undefined
|
||||||
|
readonly limit?: number | undefined
|
||||||
|
}["location"]
|
||||||
|
readonly cursor?: {
|
||||||
|
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||||
|
readonly cursor?: number | undefined
|
||||||
|
readonly limit?: number | undefined
|
||||||
|
}["cursor"]
|
||||||
|
readonly limit?: {
|
||||||
|
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||||
|
readonly cursor?: number | undefined
|
||||||
|
readonly limit?: number | undefined
|
||||||
|
}["limit"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ServerShellOutputOutput = {
|
||||||
|
readonly location: {
|
||||||
|
readonly directory: string
|
||||||
|
readonly workspaceID?: string
|
||||||
|
readonly project: { readonly id: string; readonly directory: string }
|
||||||
|
}
|
||||||
|
readonly data: {
|
||||||
|
readonly output: string
|
||||||
|
readonly cursor: number
|
||||||
|
readonly size: number
|
||||||
|
readonly truncated: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ServerShellRemoveInput = {
|
||||||
|
readonly id: { readonly id: string }["id"]
|
||||||
|
readonly location?: {
|
||||||
|
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||||
|
}["location"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ServerShellRemoveOutput = void
|
||||||
|
|
||||||
export type QuestionsListRequestsInput = {
|
export type QuestionsListRequestsInput = {
|
||||||
readonly location?: {
|
readonly location?: {
|
||||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from "./generated/index"
|
export * from "./generated/index"
|
||||||
export type { EventsSubscribeOutput as OpenCodeEvent } from "./generated/types"
|
export type { EventsSubscribeOutput as OpenCodeEvent } from "./generated/types"
|
||||||
|
export type OpenCodeClient = ReturnType<typeof import("./generated/client").make>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Schema } from "effect"
|
|||||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||||
import { Location as CoreLocation } from "@opencode-ai/core/location"
|
import { Location as CoreLocation } from "@opencode-ai/core/location"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
|
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { SessionInput as CoreSessionInput } from "@opencode-ai/core/session/input"
|
import { SessionInput as CoreSessionInput } from "@opencode-ai/core/session/input"
|
||||||
import { SessionMessage as CoreSessionMessage } from "@opencode-ai/core/session/message"
|
import { SessionMessage as CoreSessionMessage } from "@opencode-ai/core/session/message"
|
||||||
@@ -26,10 +27,14 @@ test("Core and Server reuse the authoritative Schema and Protocol values", () =>
|
|||||||
expect(CoreLocation.Ref).toBe(Location.Ref)
|
expect(CoreLocation.Ref).toBe(Location.Ref)
|
||||||
expect(ModelV2.Ref).toBe(Model.Ref)
|
expect(ModelV2.Ref).toBe(Model.Ref)
|
||||||
expect(SessionV2.Info).toBe(Session.Info)
|
expect(SessionV2.Info).toBe(Session.Info)
|
||||||
|
expect(ProjectV2.Current).toBe(Project.Current)
|
||||||
|
expect(ProjectV2.Directory).toBe(Project.Directory)
|
||||||
|
expect(ProjectV2.Directories).toBe(Project.Directories)
|
||||||
expect(CoreSessionInput.Admitted).toBe(SessionInput.Admitted)
|
expect(CoreSessionInput.Admitted).toBe(SessionInput.Admitted)
|
||||||
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(Api.groups["server.project"].identifier).toBe("server.project")
|
||||||
expect(Object.keys(ClientApi.groups)).toEqual(Object.keys(Api.groups))
|
expect(Object.keys(ClientApi.groups)).toEqual(Object.keys(Api.groups))
|
||||||
expect(Session.ID.create()).toStartWith("ses_")
|
expect(Session.ID.create()).toStartWith("ses_")
|
||||||
expect(Project.ID.global).toBe("global")
|
expect(Project.ID.global).toBe("global")
|
||||||
|
|||||||
@@ -11,15 +11,18 @@ test("exposes every standard HTTP API group", () => {
|
|||||||
"sessions",
|
"sessions",
|
||||||
"messages",
|
"messages",
|
||||||
"models",
|
"models",
|
||||||
|
"generate",
|
||||||
"providers",
|
"providers",
|
||||||
"integrations",
|
"integrations",
|
||||||
"credentials",
|
"credentials",
|
||||||
|
"project",
|
||||||
"permissions",
|
"permissions",
|
||||||
"files",
|
"files",
|
||||||
"commands",
|
"commands",
|
||||||
"skills",
|
"skills",
|
||||||
"events",
|
"events",
|
||||||
"ptys",
|
"ptys",
|
||||||
|
"server.shell",
|
||||||
"questions",
|
"questions",
|
||||||
"references",
|
"references",
|
||||||
"projectCopies",
|
"projectCopies",
|
||||||
@@ -36,6 +39,33 @@ test("exposes every standard HTTP API group", () => {
|
|||||||
])
|
])
|
||||||
expect(Object.keys(client.files)).toEqual(["list", "find"])
|
expect(Object.keys(client.files)).toEqual(["list", "find"])
|
||||||
expect(Object.keys(client.ptys)).toEqual(["list", "create", "get", "update", "remove"])
|
expect(Object.keys(client.ptys)).toEqual(["list", "create", "get", "update", "remove"])
|
||||||
|
expect(Object.keys(client.project)).toEqual(["current", "directories"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("project methods use the public HTTP contract", async () => {
|
||||||
|
const requests: string[] = []
|
||||||
|
const client = OpenCode.make({
|
||||||
|
baseUrl: "http://localhost:3000",
|
||||||
|
fetch: async (input) => {
|
||||||
|
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url
|
||||||
|
requests.push(url)
|
||||||
|
if (url.includes("/directories")) return Response.json([])
|
||||||
|
return Response.json({ id: "proj_test", directory: "/tmp/project" })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const current = await client.project.current({ location: { workspace: "wrk_test" } })
|
||||||
|
const directories = await client.project.directories({
|
||||||
|
projectID: current.id,
|
||||||
|
location: { directory: current.directory },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(current).toEqual({ id: "proj_test", directory: "/tmp/project" })
|
||||||
|
expect(directories).toEqual([])
|
||||||
|
expect(requests).toEqual([
|
||||||
|
"http://localhost:3000/api/project/current?location%5Bworkspace%5D=wrk_test",
|
||||||
|
"http://localhost:3000/api/project/proj_test/directories?location%5Bdirectory%5D=%2Ftmp%2Fproject",
|
||||||
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("sessions.get returns the wire projection", async () => {
|
test("sessions.get returns the wire projection", async () => {
|
||||||
|
|||||||
@@ -369,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}}",
|
||||||
|
|
||||||
@@ -648,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":
|
||||||
|
|||||||
@@ -377,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}}",
|
||||||
|
|
||||||
@@ -658,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":
|
||||||
|
|||||||
@@ -373,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}}",
|
||||||
|
|
||||||
@@ -654,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":
|
||||||
|
|||||||
@@ -376,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}}",
|
||||||
|
|
||||||
@@ -657,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":
|
||||||
|
|||||||
@@ -370,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}}",
|
||||||
|
|
||||||
@@ -651,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":
|
||||||
|
|||||||
@@ -377,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}}",
|
||||||
|
|
||||||
@@ -658,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":
|
||||||
|
|||||||
@@ -377,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}}",
|
||||||
|
|
||||||
@@ -664,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":
|
||||||
|
|||||||
@@ -373,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}}",
|
||||||
|
|
||||||
@@ -656,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":
|
||||||
|
|||||||
@@ -374,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}}",
|
||||||
|
|
||||||
@@ -656,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":
|
||||||
|
|||||||
@@ -368,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}}",
|
||||||
|
|
||||||
@@ -648,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":
|
||||||
|
|||||||
@@ -374,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}}",
|
||||||
|
|
||||||
@@ -655,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":
|
||||||
|
|||||||
@@ -375,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}}",
|
||||||
|
|
||||||
@@ -656,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":
|
||||||
|
|||||||
@@ -379,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}}",
|
||||||
|
|
||||||
@@ -662,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":
|
||||||
|
|||||||
@@ -370,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}}",
|
||||||
|
|
||||||
@@ -651,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":
|
||||||
|
|||||||
@@ -377,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}}",
|
||||||
|
|
||||||
@@ -658,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":
|
||||||
|
|||||||
@@ -374,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}}",
|
||||||
|
|
||||||
@@ -654,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.",
|
||||||
|
|||||||
@@ -356,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 | 访问全球顶尖编程模型",
|
||||||
@@ -632,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":
|
||||||
|
|||||||
@@ -356,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 | 存取全球最佳編碼模型",
|
||||||
@@ -632,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
|
|
||||||
}
|
|
||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ export class CreditsError extends Error {}
|
|||||||
export class MonthlyLimitError extends Error {}
|
export class MonthlyLimitError extends Error {}
|
||||||
export class UserLimitError extends Error {}
|
export class UserLimitError extends Error {}
|
||||||
export class ModelError extends Error {}
|
export class ModelError extends Error {}
|
||||||
export class RegionError extends Error {}
|
|
||||||
|
|
||||||
class LimitError extends Error {
|
class LimitError extends Error {
|
||||||
retryAfter?: number
|
retryAfter?: number
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
MonthlyLimitError,
|
MonthlyLimitError,
|
||||||
UserLimitError,
|
UserLimitError,
|
||||||
ModelError,
|
ModelError,
|
||||||
RegionError,
|
|
||||||
RateLimitError,
|
RateLimitError,
|
||||||
FreeUsageLimitError,
|
FreeUsageLimitError,
|
||||||
GoUsageLimitError,
|
GoUsageLimitError,
|
||||||
@@ -50,8 +49,6 @@ import { createModelTpmLimiter } from "./modelTpmLimiter"
|
|||||||
import { createModelTpsLimiter } from "./modelTpsLimiter"
|
import { createModelTpsLimiter } from "./modelTpsLimiter"
|
||||||
import { createProviderBudgetTracker } from "./providerBudgetTracker"
|
import { createProviderBudgetTracker } from "./providerBudgetTracker"
|
||||||
import { accumulateUsage, HOT_WORKSPACES } from "./usageBatcher"
|
import { accumulateUsage, HOT_WORKSPACES } from "./usageBatcher"
|
||||||
import { Workspace } from "@opencode-ai/console-core/workspace.js"
|
|
||||||
import { countryFromRequest } from "~/lib/request-country"
|
|
||||||
|
|
||||||
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
|
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
|
||||||
type RetryOptions = {
|
type RetryOptions = {
|
||||||
@@ -128,24 +125,6 @@ export async function handler(
|
|||||||
: createKeyRateLimiter(modelInfo.id, modelInfo.rateLimit, zenApiKey, input.request)
|
: createKeyRateLimiter(modelInfo.id, modelInfo.rateLimit, zenApiKey, input.request)
|
||||||
await rateLimiter?.check()
|
await rateLimiter?.check()
|
||||||
const authInfo = await authenticate(modelInfo, zenApiKey)
|
const authInfo = await authenticate(modelInfo, zenApiKey)
|
||||||
const allowedRegions = authInfo?.region
|
|
||||||
? authInfo.region
|
|
||||||
: await (async () => {
|
|
||||||
if (!authInfo) return
|
|
||||||
return Actor.provide("system", { workspaceID: authInfo.workspaceID }, () =>
|
|
||||||
Workspace.setDefaultRegion({ country: countryFromRequest(input.request) }),
|
|
||||||
)
|
|
||||||
})()
|
|
||||||
/*
|
|
||||||
if (true) {
|
|
||||||
if (!allowedRegions?.includes("unavailable"))
|
|
||||||
throw new RegionError(
|
|
||||||
t("zen.api.error.regionNotAllowed", {
|
|
||||||
consoleGoUrl: `https://opencode.ai/workspace/${authInfo.workspaceID}/go`,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const stickyId = sessionId ? sessionId : (authInfo?.workspaceID ?? ip)
|
const stickyId = sessionId ? sessionId : (authInfo?.workspaceID ?? ip)
|
||||||
const stickyTracker = createStickyTracker(modelInfo.id, modelInfo.stickyProvider, stickyId)
|
const stickyTracker = createStickyTracker(modelInfo.id, modelInfo.stickyProvider, stickyId)
|
||||||
const stickyProvider = await stickyTracker?.get()
|
const stickyProvider = await stickyTracker?.get()
|
||||||
@@ -158,7 +137,7 @@ export async function handler(
|
|||||||
const providerBudgetTracker = createProviderBudgetTracker(
|
const providerBudgetTracker = createProviderBudgetTracker(
|
||||||
modelInfo.providers.map((provider) => ({ ...zenData.providers[provider.id], ...provider })),
|
modelInfo.providers.map((provider) => ({ ...zenData.providers[provider.id], ...provider })),
|
||||||
)
|
)
|
||||||
const providerBudget = await providerBudgetTracker?.check()
|
const providerBudgetUsage = await providerBudgetTracker?.check()
|
||||||
|
|
||||||
const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
|
const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
|
||||||
const providerInfo = selectProvider(
|
const providerInfo = selectProvider(
|
||||||
@@ -172,7 +151,7 @@ export async function handler(
|
|||||||
stickyProvider,
|
stickyProvider,
|
||||||
modelTpmLimits,
|
modelTpmLimits,
|
||||||
modelTpsLimits,
|
modelTpsLimits,
|
||||||
providerBudget,
|
providerBudgetUsage,
|
||||||
)
|
)
|
||||||
validateModelSettings(billingSource, authInfo)
|
validateModelSettings(billingSource, authInfo)
|
||||||
updateProviderKey(authInfo, providerInfo)
|
updateProviderKey(authInfo, providerInfo)
|
||||||
@@ -222,10 +201,7 @@ export async function handler(
|
|||||||
if (v === "$model") return headers.set(k, model)
|
if (v === "$model") return headers.set(k, model)
|
||||||
if (v === "$request") return headers.set(k, requestId)
|
if (v === "$request") return headers.set(k, requestId)
|
||||||
if (v === "$project") return headers.set(k, projectId)
|
if (v === "$project") return headers.set(k, projectId)
|
||||||
if (v === "$workspace") {
|
if (v === "$workspace" && authInfo?.workspaceID) return headers.set(k, authInfo.workspaceID)
|
||||||
if (authInfo?.workspaceID) headers.set(k, authInfo.workspaceID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
headers.set(k, v)
|
headers.set(k, v)
|
||||||
})
|
})
|
||||||
headers.delete("host")
|
headers.delete("host")
|
||||||
@@ -237,9 +213,6 @@ export async function handler(
|
|||||||
return headers
|
return headers
|
||||||
})(),
|
})(),
|
||||||
body: reqBody,
|
body: reqBody,
|
||||||
// Propagate caller disconnects to the upstream provider request so
|
|
||||||
// abandoned Console requests do not leave orphaned inference work open.
|
|
||||||
signal: input.request.signal,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (providerInfo.id.startsWith("console.")) {
|
if (providerInfo.id.startsWith("console.")) {
|
||||||
@@ -308,7 +281,7 @@ export async function handler(
|
|||||||
const costInfo = calculateCost(modelInfo, usageInfo)
|
const costInfo = calculateCost(modelInfo, usageInfo)
|
||||||
await trialLimiter?.track(usageInfo)
|
await trialLimiter?.track(usageInfo)
|
||||||
await modelTpmLimiter?.track(providerInfo.id, providerInfo.model, usageInfo)
|
await modelTpmLimiter?.track(providerInfo.id, providerInfo.model, usageInfo)
|
||||||
await providerBudgetTracker?.track(providerInfo.id, providerInfo.budgetPriority, costInfo.totalCostInCent)
|
await providerBudgetTracker?.track(providerInfo.id, costInfo.totalCostInCent)
|
||||||
await trackUsage(sessionId, billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo)
|
await trackUsage(sessionId, billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo)
|
||||||
await reload(billingSource, authInfo, costInfo)
|
await reload(billingSource, authInfo, costInfo)
|
||||||
json.cost = calculateOccurredCost(billingSource, costInfo)
|
json.cost = calculateOccurredCost(billingSource, costInfo)
|
||||||
@@ -335,10 +308,9 @@ export async function handler(
|
|||||||
const streamConverter = createStreamPartConverter(providerInfo.format, opts.format)
|
const streamConverter = createStreamPartConverter(providerInfo.format, opts.format)
|
||||||
const usageParser = providerInfo.createUsageParser()
|
const usageParser = providerInfo.createUsageParser()
|
||||||
const binaryDecoder = providerInfo.createBinaryStreamDecoder()
|
const binaryDecoder = providerInfo.createBinaryStreamDecoder()
|
||||||
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined
|
|
||||||
const stream = new ReadableStream({
|
const stream = new ReadableStream({
|
||||||
start(c) {
|
start(c) {
|
||||||
reader = res.body?.getReader()
|
const reader = res.body?.getReader()
|
||||||
const decoder = new TextDecoder()
|
const decoder = new TextDecoder()
|
||||||
const encoder = new TextEncoder()
|
const encoder = new TextEncoder()
|
||||||
|
|
||||||
@@ -370,11 +342,7 @@ export async function handler(
|
|||||||
timestampLastByte,
|
timestampLastByte,
|
||||||
usageInfo,
|
usageInfo,
|
||||||
)
|
)
|
||||||
await providerBudgetTracker?.track(
|
await providerBudgetTracker?.track(providerInfo.id, costInfo.totalCostInCent)
|
||||||
providerInfo.id,
|
|
||||||
providerInfo.budgetPriority,
|
|
||||||
costInfo.totalCostInCent,
|
|
||||||
)
|
|
||||||
await trackUsage(sessionId, billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo)
|
await trackUsage(sessionId, billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo)
|
||||||
await reload(billingSource, authInfo, costInfo)
|
await reload(billingSource, authInfo, costInfo)
|
||||||
const cost = calculateOccurredCost(billingSource, costInfo)
|
const cost = calculateOccurredCost(billingSource, costInfo)
|
||||||
@@ -424,11 +392,6 @@ export async function handler(
|
|||||||
|
|
||||||
return pump()
|
return pump()
|
||||||
},
|
},
|
||||||
cancel() {
|
|
||||||
// When the downstream caller stops reading, release the upstream
|
|
||||||
// response body instead of keeping the provider/inference stream alive.
|
|
||||||
return reader?.cancel()
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
return new Response(stream, {
|
return new Response(stream, {
|
||||||
status: resStatus,
|
status: resStatus,
|
||||||
@@ -436,15 +399,6 @@ export async function handler(
|
|||||||
headers: resHeaders,
|
headers: resHeaders,
|
||||||
})
|
})
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// The caller disconnected before we finished. Because the outbound provider
|
|
||||||
// request shares input.request.signal, an aborted caller surfaces here as an
|
|
||||||
// AbortError. There is no client left to receive a body, so skip the error
|
|
||||||
// metric and 500 and return a quiet client-closed response.
|
|
||||||
if (input.request.signal.aborted || error?.name === "AbortError") {
|
|
||||||
logger.debug("REQUEST ABORTED BY CALLER")
|
|
||||||
return new Response(null, { status: 499 })
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.metric({
|
logger.metric({
|
||||||
"error.type": error.constructor.name,
|
"error.type": error.constructor.name,
|
||||||
"error.message": error.message,
|
"error.message": error.message,
|
||||||
@@ -458,15 +412,6 @@ export async function handler(
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error instanceof RegionError)
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
type: "error",
|
|
||||||
error: { type: error.constructor.name, message: error.message },
|
|
||||||
}),
|
|
||||||
{ status: 403 },
|
|
||||||
)
|
|
||||||
|
|
||||||
// Note: both top level "type" and "error.type" fields are used by the @ai-sdk/anthropic client to render the error message.
|
// Note: both top level "type" and "error.type" fields are used by the @ai-sdk/anthropic client to render the error message.
|
||||||
if (
|
if (
|
||||||
error instanceof AuthError ||
|
error instanceof AuthError ||
|
||||||
@@ -564,12 +509,7 @@ export async function handler(
|
|||||||
stickyProviderId: string | undefined,
|
stickyProviderId: string | undefined,
|
||||||
modelTpmLimits: Record<string, number> | undefined,
|
modelTpmLimits: Record<string, number> | undefined,
|
||||||
modelTpsLimits: Record<string, { qualify: number; unqualify: number }> | undefined,
|
modelTpsLimits: Record<string, { qualify: number; unqualify: number }> | undefined,
|
||||||
providerBudget:
|
providerBudgetUsage: Record<string, number> | undefined,
|
||||||
| {
|
|
||||||
qualify: (providerId: string, priority: number) => boolean
|
|
||||||
prefer: (providerId: string, priority: number) => boolean
|
|
||||||
}
|
|
||||||
| undefined,
|
|
||||||
) {
|
) {
|
||||||
const modelProvider = (() => {
|
const modelProvider = (() => {
|
||||||
// Byok is top priority b/c if user set their own API key, we should use it
|
// Byok is top priority b/c if user set their own API key, we should use it
|
||||||
@@ -587,69 +527,67 @@ export async function handler(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use fallback provider if max retries reached
|
if (retry.retryCount !== MAX_FAILOVER_RETRIES) {
|
||||||
const fallbackProvider = allProviders.find((provider) => provider.id === modelInfo.fallbackProvider)
|
let topPriority = Infinity
|
||||||
if (retry.retryCount === MAX_FAILOVER_RETRIES) return fallbackProvider
|
const providers = allProviders
|
||||||
|
.filter((provider) => provider.weight !== 0)
|
||||||
|
.filter((provider) => !retry.excludeProviders.includes(provider.id))
|
||||||
|
.filter((provider) => {
|
||||||
|
if (provider.budgetMode !== "fill") return true
|
||||||
|
const budget = zenData.providers[provider.id]?.budget
|
||||||
|
if (budget === undefined) return false
|
||||||
|
return (providerBudgetUsage?.[provider.id] ?? 0) < centsToMicroCents(budget * 100)
|
||||||
|
})
|
||||||
|
.filter((provider) => {
|
||||||
|
if (!provider.tpmLimit) return true
|
||||||
|
const usage = modelTpmLimits?.[`${provider.id}/${provider.model}`] ?? 0
|
||||||
|
return usage < provider.tpmLimit * 1_000_000
|
||||||
|
})
|
||||||
|
.filter((provider) => {
|
||||||
|
if (!provider.tpsGoal) return true
|
||||||
|
const tps = modelTpsLimits?.[`${provider.id}/${provider.model}/${provider.tpsGoal}`] ?? {
|
||||||
|
qualify: 0,
|
||||||
|
unqualify: 0,
|
||||||
|
}
|
||||||
|
const isLowTps = tps.qualify + tps.unqualify > 10 && tps.qualify < tps.unqualify
|
||||||
|
return !isLowTps
|
||||||
|
})
|
||||||
|
.map((provider) => {
|
||||||
|
topPriority = Math.min(topPriority, provider.priority)
|
||||||
|
return provider
|
||||||
|
})
|
||||||
|
.filter((p) => p.priority <= topPriority)
|
||||||
|
.flatMap((provider) => Array<typeof provider>(provider.weight).fill(provider))
|
||||||
|
|
||||||
let topPriority = Infinity
|
// Use the last 4 characters of session ID to select a provider
|
||||||
const providers = allProviders
|
let h = 0
|
||||||
.filter((provider) => provider.weight !== 0)
|
const l = stickyId.length
|
||||||
.filter((provider) => !retry.excludeProviders.includes(provider.id))
|
for (let i = l - 4; i < l; i++) {
|
||||||
.filter((provider) => {
|
h = (h * 31 + stickyId.charCodeAt(i)) | 0 // 32-bit int
|
||||||
if (provider.budgetPriority === undefined) return true
|
}
|
||||||
if (!providerBudget) return true
|
const index = (h >>> 0) % providers.length // make unsigned + range 0..length-1
|
||||||
return providerBudget.qualify(provider.id, provider.budgetPriority)
|
const provider = providers[index || 0]
|
||||||
})
|
|
||||||
.filter((provider) => {
|
|
||||||
if (!provider.tpmLimit) return true
|
|
||||||
const usage = modelTpmLimits?.[`${provider.id}/${provider.model}`] ?? 0
|
|
||||||
return usage < provider.tpmLimit * 1_000_000
|
|
||||||
})
|
|
||||||
.filter((provider) => {
|
|
||||||
if (!provider.tpsGoal) return true
|
|
||||||
const tps = modelTpsLimits?.[`${provider.id}/${provider.model}/${provider.tpsGoal}`] ?? {
|
|
||||||
qualify: 0,
|
|
||||||
unqualify: 0,
|
|
||||||
}
|
|
||||||
const isLowTps = tps.qualify + tps.unqualify > 10 && tps.qualify < tps.unqualify
|
|
||||||
return !isLowTps
|
|
||||||
})
|
|
||||||
.map((provider) => {
|
|
||||||
topPriority = Math.min(topPriority, provider.priority)
|
|
||||||
return provider
|
|
||||||
})
|
|
||||||
.filter((p) => p.priority <= topPriority)
|
|
||||||
.flatMap((provider) => Array<typeof provider>(provider.weight).fill(provider))
|
|
||||||
|
|
||||||
// Use the last 4 characters of session ID to select a provider
|
// sticky provider does not exist => use selected provider
|
||||||
let h = 0
|
if (!stickyProviderId) return provider
|
||||||
const l = stickyId.length
|
const stickProvider = allProviders.find((provider) => provider.id === stickyProviderId)
|
||||||
for (let i = l - 4; i < l; i++) {
|
if (!stickProvider) return provider
|
||||||
h = (h * 31 + stickyId.charCodeAt(i)) | 0 // 32-bit int
|
|
||||||
}
|
|
||||||
const index = (h >>> 0) % providers.length // make unsigned + range 0..length-1
|
|
||||||
const provider = providers[index || 0] ?? fallbackProvider
|
|
||||||
|
|
||||||
// sticky provider does not exist => use selected provider
|
// stick provider exists + selected provider is API type => use sticky provider
|
||||||
if (!stickyProviderId) return provider
|
if (!provider.tpsGoal) return stickProvider
|
||||||
const stickProvider = allProviders.find((provider) => provider.id === stickyProviderId)
|
|
||||||
if (!stickProvider) return provider
|
|
||||||
|
|
||||||
const preferBudgetProvider =
|
// stick provier exists + selected provider is GPU type + GPU not idle => use selected provider
|
||||||
provider.budgetPriority !== undefined && providerBudget?.prefer(provider.id, provider.budgetPriority)
|
|
||||||
|
|
||||||
const preferTpsProvider = (() => {
|
|
||||||
if (!provider.tpsGoal) return false
|
|
||||||
const tps = modelTpsLimits?.[`${provider.id}/${provider.model}/${provider.tpsGoal}`] ?? {
|
const tps = modelTpsLimits?.[`${provider.id}/${provider.model}/${provider.tpsGoal}`] ?? {
|
||||||
qualify: 0,
|
qualify: 0,
|
||||||
unqualify: 0,
|
unqualify: 0,
|
||||||
}
|
}
|
||||||
return tps.qualify > tps.unqualify * 3
|
if (tps.qualify <= tps.unqualify * 3) return stickProvider
|
||||||
})()
|
|
||||||
|
|
||||||
if (!preferBudgetProvider && !preferTpsProvider) return stickProvider
|
return provider
|
||||||
|
}
|
||||||
|
|
||||||
return provider
|
// fallback provider
|
||||||
|
return allProviders.find((provider) => provider.id === modelInfo.fallbackProvider)
|
||||||
})()
|
})()
|
||||||
|
|
||||||
if (!modelProvider) throw new ModelError(t("zen.api.error.noProviderAvailable"))
|
if (!modelProvider) throw new ModelError(t("zen.api.error.noProviderAvailable"))
|
||||||
@@ -686,10 +624,7 @@ export async function handler(
|
|||||||
tx
|
tx
|
||||||
.select({
|
.select({
|
||||||
apiKey: KeyTable.id,
|
apiKey: KeyTable.id,
|
||||||
workspace: {
|
workspaceID: KeyTable.workspaceID,
|
||||||
id: WorkspaceTable.id,
|
|
||||||
region: WorkspaceTable.region,
|
|
||||||
},
|
|
||||||
billing: {
|
billing: {
|
||||||
balance: BillingTable.balance,
|
balance: BillingTable.balance,
|
||||||
paymentMethodID: BillingTable.paymentMethodID,
|
paymentMethodID: BillingTable.paymentMethodID,
|
||||||
@@ -767,13 +702,13 @@ export async function handler(
|
|||||||
if (
|
if (
|
||||||
modelInfo.id.startsWith("alpha-") &&
|
modelInfo.id.startsWith("alpha-") &&
|
||||||
Resource.App.stage === "production" &&
|
Resource.App.stage === "production" &&
|
||||||
!ADMIN_WORKSPACES.includes(data.workspace.id)
|
!ADMIN_WORKSPACES.includes(data.workspaceID)
|
||||||
)
|
)
|
||||||
throw new AuthError(t("zen.api.error.modelNotSupported", { model: modelInfo.id }))
|
throw new AuthError(t("zen.api.error.modelNotSupported", { model: modelInfo.id }))
|
||||||
|
|
||||||
logger.metric({
|
logger.metric({
|
||||||
api_key: data.apiKey,
|
api_key: data.apiKey,
|
||||||
workspace: data.workspace.id,
|
workspace: data.workspaceID,
|
||||||
user_id: data.user.id,
|
user_id: data.user.id,
|
||||||
...(() => {
|
...(() => {
|
||||||
if (data.billing.subscription)
|
if (data.billing.subscription)
|
||||||
@@ -790,14 +725,13 @@ export async function handler(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
apiKeyId: data.apiKey,
|
apiKeyId: data.apiKey,
|
||||||
workspaceID: data.workspace.id,
|
workspaceID: data.workspaceID,
|
||||||
region: data.workspace.region,
|
|
||||||
billing: data.billing,
|
billing: data.billing,
|
||||||
user: data.user,
|
user: data.user,
|
||||||
black: data.black,
|
black: data.black,
|
||||||
lite: data.lite,
|
lite: data.lite,
|
||||||
provider: data.provider,
|
provider: data.provider,
|
||||||
isFree: ADMIN_WORKSPACES.includes(data.workspace.id),
|
isFree: ADMIN_WORKSPACES.includes(data.workspaceID),
|
||||||
isDisabled: !!data.timeDisabled,
|
isDisabled: !!data.timeDisabled,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export function createModelTpsLimiter(providers: { id: string; model: string; tp
|
|||||||
)
|
)
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const currInterval = toInterval(new Date(now))
|
const currInterval = toInterval(new Date(now))
|
||||||
const prevInterval = toInterval(new Date(now - 60_000))
|
const prevInterval = toInterval(new Date(now - 60 * 1000))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
check: async () => {
|
check: async () => {
|
||||||
|
|||||||
@@ -2,148 +2,50 @@ import { centsToMicroCents } from "@opencode-ai/console-core/util/price.js"
|
|||||||
import { buildRateLimitKey, getRedis } from "./redis"
|
import { buildRateLimitKey, getRedis } from "./redis"
|
||||||
import { logger } from "./logger"
|
import { logger } from "./logger"
|
||||||
|
|
||||||
// Per-provider, per-minute budget with priorities. The budget belongs to a
|
|
||||||
// provider and is shared across every model that routes to it. Each model's
|
|
||||||
// provider entry carries a `budgetPriority`: priority 1 ("always") routes
|
|
||||||
// unconditionally, while higher priorities ("fill") only route while the provider's
|
|
||||||
// current-minute spend through that priority is still under budget.
|
|
||||||
//
|
|
||||||
// Spend is tracked per (provider, priority, minute) so a fill priority can yield its
|
|
||||||
// leftover headroom to the next priority down. The previous minute is also read so
|
|
||||||
// higher priorities can reserve the next minute's budget first.
|
|
||||||
export function createProviderBudgetTracker(
|
export function createProviderBudgetTracker(
|
||||||
providers: {
|
providers: {
|
||||||
id: string
|
id: string
|
||||||
budget?: number
|
budget?: number
|
||||||
budgetContribution?: number
|
budgetContribution?: number
|
||||||
budgetPriority?: number
|
budgetMode?: "always" | "fill"
|
||||||
}[],
|
}[],
|
||||||
) {
|
) {
|
||||||
const tracked = providers.filter(
|
const tracked = providers.filter(
|
||||||
(provider) =>
|
(provider) => provider.budget !== undefined && provider.budgetContribution !== undefined,
|
||||||
provider.budget !== undefined &&
|
|
||||||
provider.budgetContribution !== undefined &&
|
|
||||||
provider.budgetPriority !== undefined,
|
|
||||||
)
|
)
|
||||||
if (tracked.length === 0) return undefined
|
if (tracked.length === 0) return undefined
|
||||||
|
|
||||||
const intervalAt = (date: Date) =>
|
const interval = new Date()
|
||||||
date
|
.toISOString()
|
||||||
.toISOString()
|
.replace(/[^0-9]/g, "")
|
||||||
.replace(/[^0-9]/g, "")
|
.substring(0, 12)
|
||||||
.substring(0, 12)
|
|
||||||
const now = new Date()
|
|
||||||
const currInterval = intervalAt(now)
|
|
||||||
const prevInterval = intervalAt(new Date(now.getTime() - 60_000))
|
|
||||||
|
|
||||||
const redis = getRedis()
|
const redis = getRedis()
|
||||||
const key = (providerId: string, priority: number, withInterval: string) =>
|
const keys = Object.fromEntries(
|
||||||
buildRateLimitKey("provider-budget", `${providerId}:${priority}`, withInterval)
|
tracked.map((provider) => [provider.id, buildRateLimitKey("provider-budget", provider.id, interval)]),
|
||||||
|
)
|
||||||
const budgetByProvider = tracked.reduce<Record<string, number>>((acc, provider) => {
|
let budgetUsage: Record<string, number> = {}
|
||||||
acc[provider.id] = provider.budget!
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
|
|
||||||
const maxPriorityByProvider = tracked.reduce<Record<string, number>>((acc, provider) => {
|
|
||||||
acc[provider.id] = Math.max(acc[provider.id] ?? 0, provider.budgetPriority!)
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
|
|
||||||
// Effective budget in micro-cents per provider/priority, computed in check()
|
|
||||||
// from the configured budget minus previous-minute usage from higher priorities.
|
|
||||||
let effectiveBudget: Record<string, Record<number, number>> = {}
|
|
||||||
// Cumulative current-minute spend through each priority, per provider.
|
|
||||||
let spentThroughPriority: Record<string, Record<number, number>> = {}
|
|
||||||
let previousSpentThroughPriority: Record<string, Record<number, number>> = {}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Returns whether a provider at a given priority still has budget headroom.
|
|
||||||
// Priority 1 always qualifies; higher priorities qualify only while everything through
|
|
||||||
// the current priority hasn't already filled the previous-minute adjusted
|
|
||||||
// budget.
|
|
||||||
check: async () => {
|
check: async () => {
|
||||||
const reads = Object.entries(maxPriorityByProvider).flatMap(([providerId, maxPriority]) =>
|
const ids = tracked.map((provider) => provider.id)
|
||||||
Array.from({ length: maxPriority }, (_, index) => index + 1).flatMap((priority) => [
|
if (ids.length === 0) return {}
|
||||||
{ providerId, priority, interval: currInterval, prev: false },
|
const values = await redis.mget<(string | number | null)[]>(ids.map((id) => keys[id]))
|
||||||
{ providerId, priority, interval: prevInterval, prev: true },
|
budgetUsage = Object.fromEntries(ids.map((id, index) => [id, Number(values[index] ?? 0)]))
|
||||||
]),
|
return budgetUsage
|
||||||
)
|
|
||||||
const values = await redis.mget<(string | number | null)[]>(
|
|
||||||
reads.map((r) => key(r.providerId, r.priority, r.interval)),
|
|
||||||
)
|
|
||||||
|
|
||||||
const current: Record<string, Record<number, number>> = {}
|
|
||||||
const previous: Record<string, Record<number, number>> = {}
|
|
||||||
reads.forEach((r, index) => {
|
|
||||||
const amount = Number(values[index] ?? 0)
|
|
||||||
if (r.prev) {
|
|
||||||
previous[r.providerId] ??= {}
|
|
||||||
previous[r.providerId][r.priority] = amount
|
|
||||||
return
|
|
||||||
}
|
|
||||||
current[r.providerId] ??= {}
|
|
||||||
current[r.providerId][r.priority] = amount
|
|
||||||
})
|
|
||||||
|
|
||||||
effectiveBudget = {}
|
|
||||||
spentThroughPriority = {}
|
|
||||||
previousSpentThroughPriority = {}
|
|
||||||
Object.entries(maxPriorityByProvider).forEach(([providerId, maxPriority]) => {
|
|
||||||
const providerBudget = budgetByProvider[providerId]
|
|
||||||
if (providerBudget === undefined) return
|
|
||||||
const budget = centsToMicroCents(providerBudget * 100)
|
|
||||||
|
|
||||||
let currentRunning = 0
|
|
||||||
let previousRunning = 0
|
|
||||||
effectiveBudget[providerId] = {}
|
|
||||||
spentThroughPriority[providerId] = {}
|
|
||||||
previousSpentThroughPriority[providerId] = {}
|
|
||||||
Array.from({ length: maxPriority }, (_, index) => index + 1).forEach((priority) => {
|
|
||||||
currentRunning += current[providerId]?.[priority] ?? 0
|
|
||||||
effectiveBudget[providerId][priority] = Math.max(0, budget - previousRunning)
|
|
||||||
previousRunning += previous[providerId]?.[priority] ?? 0
|
|
||||||
spentThroughPriority[providerId][priority] = currentRunning
|
|
||||||
previousSpentThroughPriority[providerId][priority] = previousRunning
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
// Priority 1 is unconditional. Higher priorities gate on the spend through
|
|
||||||
// the current priority against the effective budget.
|
|
||||||
qualify: (providerId: string, priority: number) => {
|
|
||||||
if (priority <= 1) return true
|
|
||||||
const budget = effectiveBudget[providerId]?.[priority]
|
|
||||||
if (budget === undefined) return false
|
|
||||||
const spentThroughCurrentPriority = spentThroughPriority[providerId]?.[priority] ?? 0
|
|
||||||
return spentThroughCurrentPriority < budget
|
|
||||||
},
|
|
||||||
prefer: (providerId: string, priority: number) => {
|
|
||||||
const providerBudget = budgetByProvider[providerId]
|
|
||||||
if (providerBudget === undefined) return false
|
|
||||||
const budget = centsToMicroCents(providerBudget * 100)
|
|
||||||
const previousUsage = previousSpentThroughPriority[providerId]?.[priority]
|
|
||||||
if (previousUsage === undefined) return false
|
|
||||||
return previousUsage < budget * 0.8
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
track: async (provider: string, priority: number | undefined, costInCent: number) => {
|
track: async (provider: string, costInCent: number) => {
|
||||||
if (priority === undefined) return
|
const config = tracked.find((item) => item.id === provider)
|
||||||
const config = tracked.find((item) => item.id === provider && item.budgetPriority === priority)
|
|
||||||
if (!config) return
|
if (!config) return
|
||||||
if (config.budgetContribution === undefined) return
|
if (config.budgetContribution === undefined) return
|
||||||
const cost = centsToMicroCents(costInCent * config.budgetContribution)
|
const cost = centsToMicroCents(costInCent * config.budgetContribution)
|
||||||
if (cost <= 0) return
|
if (cost <= 0) return
|
||||||
const redisKey = key(provider, priority, currInterval)
|
|
||||||
const pipeline = redis.pipeline()
|
const pipeline = redis.pipeline()
|
||||||
pipeline.incrby(redisKey, cost)
|
pipeline.incrby(keys[provider], cost)
|
||||||
// Keep two minutes so the previous interval is readable for budget adjustment.
|
pipeline.expire(keys[provider], 120)
|
||||||
pipeline.expire(redisKey, 120)
|
|
||||||
await pipeline.exec()
|
await pipeline.exec()
|
||||||
logger.metric({
|
logger.metric({
|
||||||
"provider.budget_usage": cost,
|
"provider.budget_usage": budgetUsage[provider] + cost,
|
||||||
"provider.budget_priority": priority,
|
"model.budget_usage": cost,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE `workspace` ADD `region` json;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -37,7 +37,7 @@ export namespace ZenData {
|
|||||||
priority: z.number().optional(),
|
priority: z.number().optional(),
|
||||||
tpmLimit: z.number().optional(),
|
tpmLimit: z.number().optional(),
|
||||||
tpsGoal: z.number().optional(),
|
tpsGoal: z.number().optional(),
|
||||||
budgetPriority: z.number().optional(),
|
budgetMode: z.enum(["always", "fill"]).optional(),
|
||||||
budgetContribution: z.number().optional(),
|
budgetContribution: z.number().optional(),
|
||||||
weight: z.number().optional(),
|
weight: z.number().optional(),
|
||||||
disabled: z.boolean().optional(),
|
disabled: z.boolean().optional(),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { json, primaryKey, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
|
import { primaryKey, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
|
||||||
import { timestamps, ulid } from "../drizzle/types"
|
import { timestamps, ulid } from "../drizzle/types"
|
||||||
|
|
||||||
export const WorkspaceTable = mysqlTable(
|
export const WorkspaceTable = mysqlTable(
|
||||||
@@ -7,7 +7,6 @@ export const WorkspaceTable = mysqlTable(
|
|||||||
id: ulid("id").notNull().primaryKey(),
|
id: ulid("id").notNull().primaryKey(),
|
||||||
slug: varchar("slug", { length: 255 }),
|
slug: varchar("slug", { length: 255 }),
|
||||||
name: varchar("name", { length: 255 }).notNull(),
|
name: varchar("name", { length: 255 }).notNull(),
|
||||||
region: json("region").$type<("us" | "eu" | "sg" | "cn")[]>(),
|
|
||||||
...timestamps,
|
...timestamps,
|
||||||
},
|
},
|
||||||
(table) => [uniqueIndex("slug").on(table.slug)],
|
(table) => [uniqueIndex("slug").on(table.slug)],
|
||||||
|
|||||||
@@ -11,9 +11,6 @@ import { Key } from "./key"
|
|||||||
import { and, eq, isNull, sql } from "drizzle-orm"
|
import { and, eq, isNull, sql } from "drizzle-orm"
|
||||||
|
|
||||||
export namespace Workspace {
|
export namespace Workspace {
|
||||||
export const Region = z.enum(["us", "eu", "sg", "cn"])
|
|
||||||
export type Region = z.infer<typeof Region>
|
|
||||||
|
|
||||||
export const create = fn(
|
export const create = fn(
|
||||||
z.object({
|
z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
@@ -60,41 +57,22 @@ export namespace Workspace {
|
|||||||
|
|
||||||
export const update = fn(
|
export const update = fn(
|
||||||
z.object({
|
z.object({
|
||||||
name: z.string().min(1).max(255).optional(),
|
name: z.string().min(1).max(255),
|
||||||
region: z.array(Region).min(1).optional(),
|
|
||||||
}),
|
}),
|
||||||
async (input) => {
|
async ({ name }) => {
|
||||||
Actor.assertAdmin()
|
Actor.assertAdmin()
|
||||||
const workspaceID = Actor.workspace()
|
const workspaceID = Actor.workspace()
|
||||||
return await Database.use((tx) =>
|
return await Database.use((tx) =>
|
||||||
tx
|
tx
|
||||||
.update(WorkspaceTable)
|
.update(WorkspaceTable)
|
||||||
.set({
|
.set({
|
||||||
...("name" in input ? { name: input.name } : {}),
|
name,
|
||||||
...("region" in input ? { region: input.region } : {}),
|
|
||||||
})
|
})
|
||||||
.where(eq(WorkspaceTable.id, workspaceID)),
|
.where(eq(WorkspaceTable.id, workspaceID)),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
export const setDefaultRegion = fn(
|
|
||||||
z.object({
|
|
||||||
country: z.string().optional(),
|
|
||||||
}),
|
|
||||||
async (input) => {
|
|
||||||
const region: Workspace.Region[] =
|
|
||||||
input.country?.toUpperCase() === "CN" ? ["us", "eu", "sg", "cn"] : ["us", "eu", "sg"]
|
|
||||||
await Database.use((tx) =>
|
|
||||||
tx
|
|
||||||
.update(WorkspaceTable)
|
|
||||||
.set({ region })
|
|
||||||
.where(and(eq(WorkspaceTable.id, Actor.workspace()), isNull(WorkspaceTable.region))),
|
|
||||||
)
|
|
||||||
return region
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
export const remove = fn(z.void(), async () => {
|
export const remove = fn(z.void(), async () => {
|
||||||
await Database.use((tx) =>
|
await Database.use((tx) =>
|
||||||
tx
|
tx
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
export * as MoveSession from "./move-session"
|
export * as MoveSession from "./move-session"
|
||||||
|
|
||||||
import { Context, DateTime, Effect, Layer, Schema } from "effect"
|
import { Context, DateTime, Effect, Layer, Schema } from "effect"
|
||||||
import { makeGlobalNode } from "../effect/app-node"
|
|
||||||
import { EventV2 } from "../event"
|
import { EventV2 } from "../event"
|
||||||
import { Git } from "../git"
|
import { Git } from "../git"
|
||||||
import { Location } from "../location"
|
import { Location } from "../location"
|
||||||
@@ -147,9 +146,3 @@ export const defaultLayer = layer.pipe(
|
|||||||
Layer.provide(ProjectV2.defaultLayer),
|
Layer.provide(ProjectV2.defaultLayer),
|
||||||
Layer.provide(SessionStore.defaultLayer),
|
Layer.provide(SessionStore.defaultLayer),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const node = makeGlobalNode({
|
|
||||||
service: Service,
|
|
||||||
layer,
|
|
||||||
deps: [Git.node, EventV2.node, ProjectV2.node, SessionStore.node],
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
|
import { Layer } from "effect"
|
||||||
import { buildLocationServiceMap } from "../location-services"
|
import { buildLocationServiceMap } from "../location-services"
|
||||||
import { LocationServiceMap } from "../location-service-map"
|
import { LocationServiceMap } from "../location-service-map"
|
||||||
import { LayerNode } from "./layer-node"
|
import { LayerNode } from "./layer-node"
|
||||||
import { makeGlobalNode } from "./app-node"
|
import { makeGlobalNode } from "./app-node"
|
||||||
|
|
||||||
export function build<A, E>(root: LayerNode.Node<A, E, any>, replacements: LayerNode.Replacements = []) {
|
export function build<A, E>(root: LayerNode.Node<A, E, any>, replacements?: readonly LayerNode.Replacement[]) {
|
||||||
let allReplacements = replacements
|
const replacementMap = new Map(replacements?.map((item) => [item.source, item.replacement]))
|
||||||
|
|
||||||
// Only build the location service map if it's actually needed
|
if (!LayerNode.hasUnbound(root, LocationServiceMap.node)) {
|
||||||
if (LayerNode.hasUnbound(root, LocationServiceMap.node) && !hasReplacement(replacements, LocationServiceMap.node)) {
|
// If the location service map is not needed, we shouldn't pull it
|
||||||
const locationMap = buildLocationServiceMap(replacements)
|
// in. Compile the graph normally
|
||||||
const locationMapNode = makeGlobalNode({ service: LocationServiceMap.Service, layer: locationMap, deps: [] })
|
return LayerNode.compile(root, replacementMap)
|
||||||
allReplacements = replacements.concat([[LocationServiceMap.node, locationMapNode]])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return LayerNode.compile(root, allReplacements)
|
const locationMap = buildLocationServiceMap(replacementMap)
|
||||||
}
|
const locationMapNode = makeGlobalNode({ service: LocationServiceMap.Service, layer: locationMap, deps: [] })
|
||||||
|
|
||||||
function hasReplacement(replacements: LayerNode.Replacements, node: LayerNode.Node<unknown, unknown, any>) {
|
const app = LayerNode.bind(root, LocationServiceMap.node, locationMapNode)
|
||||||
return replacements.some(([source]) => source.name === node.name)
|
|
||||||
|
return LayerNode.compile(app, replacementMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
export * as AppNodeBuilder from "./app-node-builder"
|
export * as AppNodeBuilder from "./app-node-builder"
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
File to save in: ~/.local/share/opencode/worktree/012780/location-layer-tiers/packages/core/src/effect/
|
|
||||||
@@ -111,52 +111,20 @@ export function group<const Items extends readonly AnyNode[]>(
|
|||||||
return { kind: "group", name: "group", dependencies }
|
return { kind: "group", name: "group", dependencies }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Replacement = readonly [source: AnyNode, replacement: AnyNode | Layer.Any]
|
export type Replacement = {
|
||||||
export type Replacements = readonly Replacement[]
|
readonly source: Layer.Any
|
||||||
|
readonly replacement: Layer.Any
|
||||||
|
}
|
||||||
|
|
||||||
type CheckReplacementErrors<SourceError, ReplacementError> = [Exclude<ReplacementError, SourceError>] extends [never]
|
type CheckReplacementErrors<SourceError, ReplacementError> = [Exclude<ReplacementError, SourceError>] extends [never]
|
||||||
? unknown
|
? unknown
|
||||||
: { readonly "New replacement errors": Exclude<ReplacementError, SourceError> }
|
: { readonly "New replacement errors": Exclude<ReplacementError, SourceError> }
|
||||||
|
|
||||||
type CheckReplacement<Item> = Item extends readonly [Node<infer A, infer E, infer T>, infer Replacement]
|
export function replace<A, E, R, E2>(
|
||||||
? Replacement extends Node<NoInfer<A>, infer E2, T>
|
source: Layer.Layer<A, E, R>,
|
||||||
? CheckReplacementErrors<E, NoInfer<E2>>
|
replacement: Layer.Layer<NoInfer<A>, E2, never> & CheckReplacementErrors<E, NoInfer<E2>>,
|
||||||
: Replacement extends Layer.Layer<NoInfer<A>, infer E2, never>
|
): Replacement {
|
||||||
? CheckReplacementErrors<E, NoInfer<E2>>
|
return { source, replacement }
|
||||||
: { readonly "Invalid replacement": Replacement }
|
|
||||||
: { readonly "Invalid replacement": Item }
|
|
||||||
|
|
||||||
type CheckReplacements<Items extends Replacements> = {
|
|
||||||
readonly [K in keyof Items]: CheckReplacement<Items[K]>
|
|
||||||
}
|
|
||||||
|
|
||||||
type ValidReplacements<Items extends Replacements> = Items & CheckReplacements<Items>
|
|
||||||
|
|
||||||
function replacementNode(source: AnyNode, replacement: AnyNode | Layer.Any) {
|
|
||||||
const replacementNode = isNode(replacement)
|
|
||||||
? replacement
|
|
||||||
: make({
|
|
||||||
...nodeMakeIdentity(source),
|
|
||||||
layer: replacement as Layer.Layer<unknown, unknown>,
|
|
||||||
deps: [],
|
|
||||||
tag: source.tag,
|
|
||||||
})
|
|
||||||
if (source.name !== replacementNode.name) {
|
|
||||||
throw new Error(`Cannot replace ${source.name} with ${replacementNode.name}`)
|
|
||||||
}
|
|
||||||
if (source.tag !== replacementNode.tag) {
|
|
||||||
throw new Error(`Cannot replace ${source.name} across tags`)
|
|
||||||
}
|
|
||||||
return replacementNode
|
|
||||||
}
|
|
||||||
|
|
||||||
function nodeMakeIdentity(node: AnyNode): NodeIdentity {
|
|
||||||
if (node.service !== undefined) return { service: node.service }
|
|
||||||
return { name: node.name }
|
|
||||||
}
|
|
||||||
|
|
||||||
function isNode(input: Layer.Any | AnyNode): input is AnyNode {
|
|
||||||
return "kind" in input && "dependencies" in input
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tree -----------------------------------------------------------------------
|
// Tree -----------------------------------------------------------------------
|
||||||
@@ -208,38 +176,32 @@ function walk<Result>(
|
|||||||
return recur(root)
|
return recur(root)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hoist<A, E, T extends Tag, const Items extends Replacements = readonly []>(
|
export function hoist<A, E, T extends Tag>(
|
||||||
root: Node<A, E, any>,
|
root: Node<A, E, any>,
|
||||||
tag: T,
|
tag: T,
|
||||||
replacements?: ValidReplacements<Items>,
|
|
||||||
): {
|
): {
|
||||||
readonly node: Node<A, E>
|
readonly node: Node<A, E>
|
||||||
readonly hoisted: Node<unknown, E>
|
readonly hoisted: Node<unknown, E>
|
||||||
} {
|
} {
|
||||||
const hoisted = new Map<string, AnyNode>()
|
const hoisted = new Map<string, AnyNode>()
|
||||||
const replacementMap = replacementMapFrom(replacements)
|
|
||||||
|
|
||||||
const node = walk<AnyNode>(
|
const node = walk<AnyNode>(root, (node, context) => {
|
||||||
root,
|
if (node.kind === "group") {
|
||||||
(node, context) => {
|
|
||||||
if (node.kind === "group") {
|
|
||||||
return { ...node, dependencies: node.dependencies.map(context.visit) }
|
|
||||||
}
|
|
||||||
if (node.tag === tag) {
|
|
||||||
const existing = hoisted.get(node.name)
|
|
||||||
if (existing && existing !== node) {
|
|
||||||
throw new Error(`Tag ${tag} has conflicting implementations for ${node.name}`)
|
|
||||||
}
|
|
||||||
hoisted.set(node.name, node)
|
|
||||||
return group([])
|
|
||||||
}
|
|
||||||
if (node.kind === "unbound") {
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
return { ...node, dependencies: node.dependencies.map(context.visit) }
|
return { ...node, dependencies: node.dependencies.map(context.visit) }
|
||||||
},
|
}
|
||||||
{ resolve: (node) => replacementMap.get(node.name) ?? node },
|
if (node.tag === tag) {
|
||||||
)
|
const existing = hoisted.get(node.name)
|
||||||
|
if (existing && existing !== node) {
|
||||||
|
throw new Error(`Tag ${tag} has conflicting implementations for ${node.name}`)
|
||||||
|
}
|
||||||
|
hoisted.set(node.name, node)
|
||||||
|
return group([])
|
||||||
|
}
|
||||||
|
if (node.kind === "unbound") {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
return { ...node, dependencies: node.dependencies.map(context.visit) }
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
node: node as Node<A, E>,
|
node: node as Node<A, E>,
|
||||||
@@ -247,11 +209,10 @@ export function hoist<A, E, T extends Tag, const Items extends Replacements = re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function compile<A, E, const Items extends Replacements = readonly []>(
|
export function compile<A, E>(
|
||||||
root: Node<A, E, any>,
|
root: Node<A, E, any>,
|
||||||
replacements?: ValidReplacements<Items>,
|
replacements?: ReadonlyMap<Layer.Any, Layer.Any>,
|
||||||
): Layer.Layer<A, E> {
|
): Layer.Layer<A, E> {
|
||||||
const replacementMap = replacementMapFrom(replacements)
|
|
||||||
const cache = new Map<AnyNode, RuntimeLayer>()
|
const cache = new Map<AnyNode, RuntimeLayer>()
|
||||||
const compileNode = (node: AnyNode) =>
|
const compileNode = (node: AnyNode) =>
|
||||||
walk<RuntimeLayer>(
|
walk<RuntimeLayer>(
|
||||||
@@ -259,65 +220,18 @@ export function compile<A, E, const Items extends Replacements = readonly []>(
|
|||||||
(node, context) => {
|
(node, context) => {
|
||||||
if (node.kind === "unbound") throw new Error(`Unbound layer node: ${node.name}`)
|
if (node.kind === "unbound") throw new Error(`Unbound layer node: ${node.name}`)
|
||||||
const dependencies = node.dependencies.flatMap(flatten).map(context.visit)
|
const dependencies = node.dependencies.flatMap(flatten).map(context.visit)
|
||||||
const implementation = node.implementation! as RuntimeLayer
|
const implementation = (replacements?.get(node.implementation!) ?? node.implementation!) as RuntimeLayer
|
||||||
return dependencies.length === 0
|
return dependencies.length === 0
|
||||||
? implementation
|
? implementation
|
||||||
: implementation.pipe(Layer.provide(dependencies as [RuntimeLayer, ...RuntimeLayer[]]))
|
: implementation.pipe(Layer.provide(dependencies as [RuntimeLayer, ...RuntimeLayer[]]))
|
||||||
},
|
},
|
||||||
{ cache, resolve: (node) => replacementMap.get(node.name) ?? node },
|
{ cache },
|
||||||
)
|
)
|
||||||
const layers = flatten(root).map((node) => compileNode(node))
|
const layers = flatten(root).map((node) => compileNode(node))
|
||||||
const layer = layers.reduce<RuntimeLayer>((result, layer) => layer.pipe(Layer.provideMerge(result)), Layer.empty)
|
const layer = layers.reduce<RuntimeLayer>((result, layer) => layer.pipe(Layer.provideMerge(result)), Layer.empty)
|
||||||
return layer as Layer.Layer<A, E>
|
return layer as Layer.Layer<A, E>
|
||||||
}
|
}
|
||||||
|
|
||||||
function replacementMapFrom(replacements?: Replacements) {
|
|
||||||
return (
|
|
||||||
replacements?.reduce((map, [source, replacement]) => {
|
|
||||||
const normalized = rewriteReplacementDependencies(replacementNode(source, replacement), map)
|
|
||||||
const current = new Map([[source.name, normalized]])
|
|
||||||
for (const [name, node] of map) map.set(name, rewriteReplacementDependencies(node, current))
|
|
||||||
map.set(source.name, normalized)
|
|
||||||
return map
|
|
||||||
}, new Map<string, AnyNode>()) ?? new Map<string, AnyNode>()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function rewriteReplacementDependencies(root: AnyNode, replacements: ReadonlyMap<string, AnyNode>) {
|
|
||||||
if (replacements.size === 0) return root
|
|
||||||
const cache = new Map<AnyNode, AnyNode>()
|
|
||||||
const visiting = new Set<AnyNode>()
|
|
||||||
const stack: AnyNode[] = []
|
|
||||||
|
|
||||||
const recur = (node: AnyNode, isRoot = false): AnyNode => {
|
|
||||||
const target = isRoot ? node : (replacements.get(node.name) ?? node)
|
|
||||||
const cached = cache.get(target)
|
|
||||||
if (cached !== undefined || cache.has(target)) return cached!
|
|
||||||
if (visiting.has(target)) {
|
|
||||||
const start = stack.indexOf(target)
|
|
||||||
throw new Error(
|
|
||||||
`Cycle detected in layer tree: ${[...stack.slice(start), target].map((item) => item.name).join(" -> ")}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
visiting.add(target)
|
|
||||||
stack.push(target)
|
|
||||||
try {
|
|
||||||
const dependencies = target.dependencies.map((dependency) => recur(dependency))
|
|
||||||
const result = dependencies.every((dependency, index) => dependency === target.dependencies[index])
|
|
||||||
? target
|
|
||||||
: { ...target, dependencies }
|
|
||||||
cache.set(target, result)
|
|
||||||
return result
|
|
||||||
} finally {
|
|
||||||
stack.pop()
|
|
||||||
visiting.delete(target)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return recur(root, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hasUnbound(root: Node<unknown, unknown, any>, source: AnyNode): boolean {
|
export function hasUnbound(root: Node<unknown, unknown, any>, source: AnyNode): boolean {
|
||||||
if (source.kind !== "unbound") throw new Error(`Cannot check non-unbound layer node: ${source.name}`)
|
if (source.kind !== "unbound") throw new Error(`Cannot check non-unbound layer node: ${source.name}`)
|
||||||
return walk<boolean>(root, (node, context) => {
|
return walk<boolean>(root, (node, context) => {
|
||||||
@@ -326,6 +240,32 @@ export function hasUnbound(root: Node<unknown, unknown, any>, source: AnyNode):
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function bind<A, E, T extends Tag | undefined>(
|
||||||
|
root: Node<A, E, T>,
|
||||||
|
source: AnyNode,
|
||||||
|
replacement: AnyNode,
|
||||||
|
): Node<A, E, T> {
|
||||||
|
if (source.kind !== "unbound") throw new Error(`Cannot bind non-unbound layer node: ${source.name}`)
|
||||||
|
if (source.name !== replacement.name) {
|
||||||
|
throw new Error(`Cannot bind ${source.name} to ${replacement.name}`)
|
||||||
|
}
|
||||||
|
if (source.tag !== replacement.tag) {
|
||||||
|
throw new Error(`Cannot bind ${source.name} across tags`)
|
||||||
|
}
|
||||||
|
return walk<AnyNode>(
|
||||||
|
root,
|
||||||
|
(target, context) => {
|
||||||
|
if (target.kind === "unbound") return target
|
||||||
|
const dependencies: AnyNode[] = []
|
||||||
|
const clone = { ...target, dependencies }
|
||||||
|
context.cache.set(target, clone)
|
||||||
|
dependencies.push(...target.dependencies.map(context.visit))
|
||||||
|
return clone
|
||||||
|
},
|
||||||
|
{ detectCycles: false, resolve: (node) => (node === source ? replacement : node) },
|
||||||
|
) as Node<A, E, T>
|
||||||
|
}
|
||||||
|
|
||||||
function flatten(node: AnyNode): readonly AnyNode[] {
|
function flatten(node: AnyNode): readonly AnyNode[] {
|
||||||
return node.kind === "group" ? node.dependencies.flatMap(flatten) : [node]
|
return node.kind === "group" ? node.dependencies.flatMap(flatten) : [node]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ export * as EventV2 from "./event"
|
|||||||
import { Cause, Context, Effect, Layer, Option, PubSub, Queue, Schema, Stream } from "effect"
|
import { Cause, Context, Effect, Layer, Option, PubSub, Queue, Schema, Stream } from "effect"
|
||||||
import { Event } from "@opencode-ai/schema/event"
|
import { Event } from "@opencode-ai/schema/event"
|
||||||
import type { Data, Definition, Payload } from "@opencode-ai/schema/event"
|
import type { Data, Definition, Payload } from "@opencode-ai/schema/event"
|
||||||
import { and, asc, eq, gt, inArray } from "drizzle-orm"
|
import { and, asc, eq, gt, inArray, sql } from "drizzle-orm"
|
||||||
import { Database } from "./database/database"
|
import { Database } from "./database/database"
|
||||||
import { EventSequenceTable, EventTable } from "./event/sql"
|
import { EventSequenceTable, EventTable } from "./event/sql"
|
||||||
import { Location } from "./location"
|
import { Location } from "./location"
|
||||||
@@ -31,6 +31,22 @@ export const latestSequence = Effect.fn("EventV2.latestSequence")(function* (
|
|||||||
return row?.seq ?? -1
|
return row?.seq ?? -1
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const reserveSequence = Effect.fn("EventV2.reserveSequence")(function* (
|
||||||
|
db: Database.Interface["db"],
|
||||||
|
aggregateID: string,
|
||||||
|
seq: number,
|
||||||
|
) {
|
||||||
|
yield* db
|
||||||
|
.insert(EventSequenceTable)
|
||||||
|
.values([{ aggregate_id: aggregateID, seq }])
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: EventSequenceTable.aggregate_id,
|
||||||
|
set: { seq: sql`max(${EventSequenceTable.seq}, ${seq})` },
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
|
})
|
||||||
|
|
||||||
export type SerializedEvent = {
|
export type SerializedEvent = {
|
||||||
readonly id: ID
|
readonly id: ID
|
||||||
readonly type: string
|
readonly type: string
|
||||||
@@ -327,7 +343,7 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: EventSequenceTable.aggregate_id,
|
target: EventSequenceTable.aggregate_id,
|
||||||
set: {
|
set: {
|
||||||
seq,
|
seq: sql`max(${EventSequenceTable.seq}, ${seq})`,
|
||||||
...(input?.ownerID && row?.ownerID == null ? { owner_id: input.ownerID } : {}),
|
...(input?.ownerID && row?.ownerID == null ? { owner_id: input.ownerID } : {}),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user